Admin
Written April 2026 — read this first
Still broadly right. Note that IT_ADMIN now holds system settings only — users, MFA reset, integrations, audit, currencies and cities — and no business approvals or finance write rights (ACC-012). Integration secrets are write-only: staff see a configured flag, never a value (AUD-021). Every access change asks for a reason, which is stored with it.
System administration console — users, roles, permissions, integrations, audit logs, currencies, cities, locations, and operational daily stats.
Scope
The Admin module is the back-office control centre. It does not include finance configuration (GL mapping, GST rate, invoice numbering) — those live under Finance → Settings.
Pages
src/pages/admin/Admin.tsx— the single-page tabbed admin console. Mounted at/admin.
Route guard (src/App.tsx:176):
<ProtectedRoute allowedRoles={['admin']} requiredPermissions={['admin.view']}>
<Admin />
</ProtectedRoute>
Double-gated route
/admin is gated by both an allowedRoles=['admin'] check and the admin.view permission. This is intentional so that a permission mis-grant cannot expose the admin console to a non-admin tier role. See docs/PERMISSIONS.md §6.16.
Tabs & surface area
The console is a flat <Tabs> component (src/pages/admin/Admin.tsx:154) with the following panes:
| Tab | Component | Purpose |
|---|---|---|
| Dashboard | inline + OnlineUsers |
Daily stats per module (bookings, payments, finance, customers, groups, visa) with a date picker. Right rail shows live online users. Hits GET /admin/daily-stats?date=YYYY-MM-DD. |
| Audit Logs | inline + Timeline |
The audit log table (time, user, action, entity, IP) one server page at a time (GET /audit?paged=1), newest first, with a search over action, record, record id and who, and a When date range (Indian days, both ends included), all in SQL; the paging bar under it gives rows per page, "1–25 of N entries" and first, previous, next and last page (AUD-030, UX-030). It is read only when the tab is open. A side-rail timeline shows the 10 newest on the page. |
| Currency Settings | CurrencySettings |
CRUD for the currency master. The Exchange Rates tab adds, edits and deletes rates, fetches automatic rates and clears today's manual ones — only with finance.rates.manage, through set_exchange_rate / delete_exchange_rate; a rate dated before today asks for a reason (FIN-046). admin.currency.* no longer changes rates. |
| City Settings | CitySettings |
CRUD for the service-cities master. |
| Locations | LocationSettings |
India locations hierarchy (state → district → taluk). |
| Settings | inline | Read-only snapshots of approval policies (finance approval required, ticket exception approval, name-update deadline) and system flags (default currency, audit logging, email notifications). Also hosts a Booking Number Decoder that parses the millis timestamp embedded in BK-<timestamp> IDs. |
| Permissions | PermissionsMatrix |
Interactive role × permission grid + per-user overrides. Writes via /permissions/... endpoints. A user's roles show read only: a role changes by a role request (ACC-077). |
| Integrations | IntegrationSettings |
Email provider (Resend), WhatsApp Cloud API, SMS (legacy), Google Drive OAuth. The WhatsApp card holds the sign-up form, the WhatsApp menu settings and ends with the WhatsApp delivery problems list. |
| Reminders | NotificationCategoriesCard, BookingRemindersCard, WhatsAppNoticesCard (lazy) |
The automatic e-mail categories and their switches (below); the booking reminder e-mails: on/off, each kind and its days, and the last 50 reminders; the automatic WhatsApp notices: on/off, template names and their state in Meta, the last 50 notices. Shown with admin.integrations.view; see Booking reminders and Automatic WhatsApp notices. |
Settings tab is informational
The tiles under the Settings tab read like configurable toggles but currently render status badges only. Live config for approval thresholds and email toggles is not yet editable from this screen.
Audience
Primary: CEO, GM, IT_ADMIN — seeded every permission (see supabase/migrations/20260416020000_remove_permission_bypass.sql).
Secondary: ADMIN_HR holds most admin grants. Functional managers hold admin.view only if explicitly granted.
Permissions
| Gate | Permission | Source |
|---|---|---|
Route /admin |
admin.view + role admin |
src/App.tsx:176 |
| Permissions matrix edits | admin.permissions.edit |
src/lib/api.ts:12550, :12597 |
| User CRUD | admin.users.create, admin.users.edit, admin.users.delete |
src/lib/api.ts:13022, :13063, :13201 |
| Request / approve / apply a role change | roles.request, roles.approve, roles.apply |
Role requests, docs/PERMISSIONS.md §4.7 |
| Audit log view | admin.view (legacy) → admin.audit.view (granular, seeded not yet wired) |
docs/PERMISSIONS.md §4.4 |
| Currency / cities / locations edits | admin.edit (legacy) → admin.currency.*, admin.cities.*, admin.locations.view |
docs/PERMISSIONS.md §4.4 |
| Exchange rates (add, edit, delete, fetch, clear) | finance.rates.manage (FIN-046) |
docs/PERMISSIONS.md §4.4, §6.16 |
| Integrations edits | admin.edit → admin.integrations.edit, admin.integrations.test |
docs/PERMISSIONS.md §4.4 |
Privilege escalation gate
admin.permissions.edit is the most sensitive permission in the system — holders can grant themselves any other permission. It is granted only to super-admins and audited.
API endpoints hit
GET /admin/daily-stats?date=...— aggregate daily stats for the dashboard tile.GET /audit?paged=1— viausePagedList(Admin) andauditLogService.fetchAuditLogPage()(People's recent activity: the 20 newest changes to customers, partners and leads, asked per record type).GET /users,POST /users,PATCH /users/:id,DELETE /users/:id— user management.GET /userscarriesaccessMode(ACC-070);PATCH /users/:idwith{ action: 'pause', reason }or{ action: 'resume' }pauses or resumes a login (admin.users.edit).GET /users/:id/profile,PATCH /users/:id/profile,DELETE /users/:id/documents/:docId— the employee profile and its documents (ACC-071;admin.users.view/admin.users.edit).PATCH /users/:id/profilealso takeshr.profiles.edit: HR edits the profile details of a staff login that is not an IT Admin or Super Admin, never the name, work phone or anything about access (ACC-084). The profile read andGET /users/:id/recordcarryprofileEdit(allowed,scopeallordetails,fields,reason).PATCH /users/:idwith{ action: 'change_email', email, reason }andPOST /users/workspace-domainwith{ dryRun, reason? }— change a staff email, or move every staff login to@alhudatravels.in(ACC-075;admin.users.edit).GET /permissions,POST /permissions/roles/:roleId/grants,POST /permissions/users/:userId/grants.GET /online-users— live presence feed rendered inOnlineUsers.
Related modules
- Finance settings →
docs/features/finance/index.md(GL mapping, GST, voucher prefixes). - Security settings →
docs/features/settings.md(2FA, push notifications, active sessions — per-user, not global). - People (employees) →
docs/features/people.mdfor the staff directory (route/people/employeesis also gated byadmin.view).
Profiles and pausing an account (2026-09-25)
<UserManagement> (on /people/employees, sidebar Employees & staff — /admin has no user list; layout in
People → Employees & staff) opens the employee record when a row is clicked
(admin.users.view, /admin/users/:id — see
People → The employee record), has Pause / Resume account in each row's ⋯ menu
(admin.users.edit), and shows Paused as the status. Pausing asks for a reason and puts the account in read-only mode: the
person signs in and reads as before, and the database refuses every write
(ACC-070). Resuming asks Yes/No. Both go through
PATCH /users/:id → admin_pause_user / admin_resume_user, which refuse pausing yourself, anyone
whose permissions you do not all hold, and a paused caller; the change is audited with the reason
and the person is told in the app (without the reason). Pause and resume are also on the record.
The partner side is the partner record, Partners → The partner record.
A paused person sees a banner on every desktop page ("Read-only: your account is paused. Contact
the office.") — src/components/layout/ReadOnlyBanner.tsx in MainLayout, from
User.accessMode. Buttons are not hidden: the screen does not pretend to be the boundary.
Everyone has their own My profile page at /me — the Settings menu in the header now lists
My profile above Security — where staff keep their contact and emergency details current
and file documents, and a partner sees their agency (People → My profile,
Partners → My profile).
Changing a staff email (2026-09-28)
Rule: ACC-075.
Both controls are in <UserManagement> (on /people/employees) behind
admin.users.edit. The dialogs (src/components/admin/StaffEmailDialogs.tsx) load when first opened.
- Change email (in a staff row's ⋯ menu; customer and partner logins are not on this page). Type the
new address — it must end with
@alhudatravels.inor@alhuda.co.in— and Save. You are asked for a reason and Yes/No. The person's sign-in and profile change together; their password, username and open sessions do not. They get a short email at the new address. - Move staff to @alhudatravels.in (under More in the page header). It first shows the list and changes
nothing: each staff login on
@alhuda.co.inwith its name, old → new address, and any conflict (someone else already has the new address, the login has no sign-in, or it holds permissions you do not). Move N logins asks for a reason and Yes/No, moves every login without a conflict, and then shows what changed: Moved, Not moved (with why) or Failed (with why). A login with a conflict is left as it is. When nobody is left on the old domain the list says so, and running it again changes nothing.
After a change the person signs in with their username, the new address or the old one — the two company domains are one person (ACC-061). Every change is an audit row with the reason. Customers and partners keep their own addresses.
Not built: the phone app has no email change. The employee's personal email on their profile is not touched. The Workspace mailbox itself is created by IT, not by this screen — create it before moving someone, or the notice email bounces.
WhatsApp menu (2026-10-01)
Integrations → WhatsApp → WhatsApp menu (src/components/admin/WhatsAppMenuCard.tsx,
loaded on demand). Reading needs admin.integrations.view; changing needs
admin.integrations.edit. Three settings, saved through PUT /admin/communication-settings
with a reason (audited):
| Setting | What it does |
|---|---|
| On / off | Off by default. On: a customer who writes to the business number gets the WhatsApp menu. Switching on asks for a confirmation |
| Office hours | Told to a customer who chooses Talk to the office (default Mon–Sat 10:00–18:00 IST; 80 characters) |
| Bank details | Sent as typed with Pay balance (600 characters). Empty: the customer is told to ask the office |
Rules: COMM-030 … COMM-036.
WhatsApp delivery problems (2026-10-02)
Rule: ACC-069.
At the foot of Integrations → WhatsApp, behind admin.integrations.view
(src/components/admin/WhatsAppDeliveryProblems.tsx, GET /admin/whatsapp-delivery-failures).
It lists every password reset code WhatsApp refused in the last 30 days, newest first, at most 50: when, whose account, the last two digits of the number it went to, Meta's error code and Meta's words. Any run of five or more digits in Meta's words is masked, so no phone number or code is shown. The common codes carry a hint: 131030 (the number is not on Meta's test list — the app is still in Development mode), 132001 (template name or language does not match an approved template), 131026 (the number is not on WhatsApp) and 190 (the access token expired). Refresh reloads the list.
Not built: only reset codes are listed. A staff message WhatsApp refuses is reported to the sender on the Communications screen when it is sent, and a finance PIN code is not recorded here. There is no alert: someone has to open the list.
WhatsApp message templates (2026-10-03)
Rule: AUD-024.
In Integrations → WhatsApp, above the delivery problems, behind admin.integrations.view
(src/components/admin/WhatsAppTemplatesCard.tsx, src/services/whatsappTemplateAdmin.ts).
WhatsApp only lets you message someone who hasn't written in 24 hours with a template Meta has approved. Templates are created in Meta, not here. The card shows:
- every template the system knows: name, language, category, status and when it was last synced. Usable in chat means staff will see it in the WhatsApp inbox and the Communications screen. Anything else says why not — pending, rejected, paused or disabled in Meta; an authentication (one-time code) template; a header picture, a header variable or a link button with a variable, which the chat cannot fill; or removed in Meta.
- Open Meta template manager — Meta's page for this WhatsApp account (the Business Account ID saved above).
- Sync from Meta (
admin.integrations.edit) — copies the templates from Meta now and shows what changed: added, updated, removed in Meta (kept, not usable), not usable in chat, and how many are ready to send. When Meta refuses — an expired token, a wrong account ID — Meta's reason is shown in plain words.
The sync also runs by itself every night at 03:00 IST once the dispatcher's Vault secrets are stored (operations).
Not built: templates cannot be created, edited or submitted for approval from this screen — that is done in Meta. A template with a header picture, a header variable or a link button that takes a value is listed but cannot be sent from chat yet.
Role requests
Page: src/pages/admin/RoleRequests.tsx at /admin/role-requests (sidebar Admin → Role requests). Rules: ACC-077 … ACC-080, ACC-082, ACC-083. API: Role requests.
Nobody changes a staff role directly — not HR, not the GM, not a CEO, and not the super admin. A change takes three people:
- Request. Someone with
roles.request(HR, the GM, a CEO, IT) asks to add or remove one role for one login and gives a reason. An administrative role only by IT Admin or Super Admin (see below). They start it from Employees & staff → ⋯ → Request role change on the person's row, or Request a role change on this page. The person is told a change has been asked for. - CEO approves. A CEO (
roles.approve) opens Waiting for the CEO and approves, or rejects with a note. A CEO never decides a request they made or one about themselves — the other CEO does. For when both are away, the super admin holdsroles.approvetoo and can approve. - Admin applies. Approval puts a job in the work inbox of every admin who may apply it (
roles.apply, IT), except the requester, the person and the approver. The job opens this page. The admin chooses Accept and apply — the role changes at that moment — or rejects with a note. The jobs close when one admin decides.
The requester and the person are told after each step; the approver is told the outcome. Mine lists what you asked for and what is about you; the requester can withdraw an open request with a reason. History keeps every applied, rejected and withdrawn request with who decided and when. Buttons show only where the database says this login may act; if someone else must decide, the card says so.
What a role request does not cover: customer, partner and tour-leader roles keep their own flows (sign-up, partner approval, appointing a tour leader on the departure — ACC-080). A new staff login starts with no role: a role chosen in Add employee becomes a request by the person who created the login (ACC-082).
Administrative roles are asked for only by an admin (ACC-083). "Admin" here is IT Admin or Super Admin — the only roles with roles.apply. A role is administrative when its permissions control users, access, settings or role changes; today that is Super Admin, IT Admin, HR Admin, General Manager and CEO. HR, the GM and a CEO do not see those roles in Request role change or in Add employee, for adding or for removing, and the dialog says why. If they try anyway, the database refuses: "Only IT Admin or Super Admin can ask for the CEO role." They still ask for every other role. When an admin asks for an administrative role, a CEO still approves it and a different admin applies it — the IT Admin applies the Super Admin's request and the reverse. If the requester is the only active admin, the request waits until there is another; there is no way round it.
The database refuses every other way of changing a staff role, including the old role picker and the edge function the office used before (ACC-079).
Not built: asking for a change and withdrawing one are web-only (the phone approves and applies). A role cannot be given for a limited time (ACC-043). Per-user permission overrides in Permissions still change in one step by the super admin.
Automatic e-mails by category (2026-10-02)
Rules: COMM-039. The first card on
the Reminders tab (src/components/admin/NotificationCategoriesCard.tsx, loaded with the tab;
src/services/notificationCategoryAdmin.ts).
- One row per category of automatic e-mail, with its switch: HR — leave e-mails (shown, not switched here: switched in Leave admin → Settings, by HR — LV-039) and Finance — payment receipts and payment claims not accepted (on by default — COMM-037, COMM-038). Work — jobs given to you, finished, due soon and overdue (on by default since 08/10/2026 — WRK-009): the e-mail copy of each work note. Off stops the e-mails only; the notes and the phone push still go.
- Switching a category off asks first; with
admin.integrations.editonly (a paused login cannot). Audited asnotification_category_changed. While off, nothing in it is queued; e-mails already queued still go.
Not built: a switch for the booking and visa e-mails (they keep their own rules), a switch per e-mail inside a category, a list of what each category sent.
Booking reminders (2026-09-30)
Rules: COMM-010 … COMM-014. The Reminders tab, behind
admin.integrations.view (src/components/admin/BookingRemindersCard.tsx, loaded only when the tab
is opened; src/services/bookingReminderAdmin.ts).
- Send reminder e-mails — the master switch. It ships off: nothing is sent until an administrator turns it on. Turning it on asks first, and says who will be e-mailed.
- Payment due, Payment overdue, Departure, Documents missing — each on or off, with its days. The numbers are bounded; a number out of bounds is refused with a sentence saying which.
- Save (
admin.integrations.edit; a paused login cannot) — audited asreminder_settings_changed. Without the right the settings are shown read-only. - Last 50 reminders — when, booking, kind, the date it is for, customer or partner, and the result: waiting to send, sent, failed, not sent (opted out of e-mail, no e-mail address, no longer needed). E-mail addresses are not shown.
What each reminder says and who gets it: Booking e-mails and reminders. Running it: Booking reminder e-mails.
Not built: a preview of the e-mail on this screen; a "send now" button (reminders go at the daily run only).
Automatic WhatsApp notices (2026-09-30)
Rules: COMM-020 … COMM-025. The second card on the
Reminders tab (src/components/admin/WhatsAppNoticesCard.tsx, loaded with the tab;
src/services/whatsappNoticeAdmin.ts).
- Booking confirmed and Payment received — each on or off. Both ship off. Turning one on asks first and says who will get a WhatsApp message.
- Template names — for each notice, the template with a Document header and the text-only one
(defaults
booking_confirmed/booking_confirmed_text,payment_receipt/payment_receipt_text). Next to each name: what Meta holds — Approved · document header, Approved · text only, Not synced from Meta yet, Not approved in Meta (…), or a wrong header. A name Meta cannot hold (capitals, spaces) is refused before saving. - Save (
admin.integrations.edit; a paused login cannot) — audited aswhatsapp_notice_settings_changed. Without the right the card is read-only. - Last 50 WhatsApp notices — when, booking, notice, customer or partner with the last two digits of the number, and the result: waiting, sent (with the attached file name), not sent (with the reason) or failed (with Meta's code and words).
What each notice says and who gets it: Booking WhatsApp notices. Creating the templates in Meta: WhatsApp Cloud API.
Not built: a "send again" button for a failed notice (the dispatcher retries only when Meta or the Drive did not answer); a preview of the message.