Approvals
Operations-tier approval queue for bookings. Second leg of the maker-checker handshake with Finance.
Scope
This page deals with the ops-approval step only. Finance-tier booking approval lives inside /finance?tab=approvals — see docs/features/finance/index.md. Journal entry maker-checker lives inside docs/features/finance/journals.md.
Pages
src/pages/approvals/Approvals.tsx— main queue at/approvals.src/pages/approvals/CorrectionsQueue.tsx— corrections triage at/corrections.
Route guards (src/App.tsx:165-166):
<Route path="/approvals" element={<ProtectedRoute requiredPermissions={['approvals.view']}><Approvals /></ProtectedRoute>} />
<Route path="/corrections" element={<ProtectedRoute requiredPermissions={['bookings.view']}><CorrectionsQueue /></ProtectedRoute>} />
Corrections queue permission drift
/corrections is currently gated by bookings.view rather than a dedicated approvals.view or corrections.view. Tracked as drift item #3 in docs/PERMISSIONS.md §8.
What gets queued
Approvals (/approvals)
Filters bookings to rows where opsApprovalStatus === 'pending' (Approvals.tsx:83). For each booking the page evaluates canApprove():
- Full-payment bookings require
financeApprovalStatus === 'approved'before ops can approve. - Partial-payment bookings require
financeApprovalStatus === 'approved'before ops can approve. - Advance-payment bookings require
paidAmount > 0before ops can approve.
Each row exposes four actions:
| Action | Endpoint | Permission |
|---|---|---|
| Approve | POST /operations/bookings/:id/ops-approve |
approvals.approve |
| Reject | POST /operations/bookings/:id/ops-reject |
approvals.approve |
| Send back | POST /operations/bookings/:id/ops-send-back |
approvals.approve |
| Log contact | POST /operations/communications |
(write) |
The Approve button is idempotent on the client: pendingAction state blocks double-clicks, and the server short-circuits repeat calls. See Approvals.tsx:114.
Corrections (/corrections)
Filters to bookings flagged with any of status === 'needs_correction', opsApprovalStatus === 'needs_correction', or financeApprovalStatus === 'needs_correction' (CorrectionsQueue.tsx:107).
Each row shows:
- From — Finance, Operations, or Sales (whoever flagged it).
- Owner — assignable staff member (fetched via
GET /users, filtered to non-CUSTOMER/AGENT roles). - SLA — countdown against
correctionDueAt; taggedOverdue Xhif past-due. - Correction note — last row from the
CommunicationsLogkeyed to the booking.
Actions per row:
| Action | Endpoint |
|---|---|
| Assign owner | POST /operations/bookings/:id/corrections/assign |
| View timeline | GET /operations/bookings/:id/corrections |
| Resubmit to finance | POST /operations/bookings/:id/resubmit |
Maker-checker tie-in
The approval chain is:
- Sales creates a booking (
bookings.create). - Finance approves the booking's financial side:
POST /finance/bookings/:id/finance-approve(finance.bookings.approve_finance). - Ops approves the booking's operational side via this page (
approvals.approve). - Booking moves into Tickets / Visa pipelines.
If either approver chooses Send back, the booking lands in the corrections queue for the originating team.
Related docs:
- Finance approval semantics →
docs/features/finance/index.md. - Journal-entry maker-checker →
docs/features/finance/journals.md. - State machines →
docs/ALHUDA_ERP_STATE_MACHINES.md.
Permissions
| Action | Permission | Enforcement |
|---|---|---|
View /approvals |
approvals.view |
Route guard |
View /corrections |
bookings.view |
Route guard (drift) |
| Approve / reject / send back | approvals.approve |
<PermissionGate> + requirePermission() in src/lib/api.ts:21671, :22023, :22073 |
| Assign correction owner / resubmit | Inherits route gate | |
| Log contact | admin.users.edit (drift — server gate in handleOperationsCommunications, src/lib/api.ts:13201) |
Should migrate to a dedicated communications.create permission |
Approval policy card
The footer on /approvals renders a read-only reminder of the three payment-policy approval rules. These are display-only — the actual gate is the canApprove() evaluator on the client and the server-side handler.