Skip to content

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 > 0 before 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; tagged Overdue Xh if past-due.
  • Correction note — last row from the CommunicationsLog keyed 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:

  1. Sales creates a booking (bookings.create).
  2. Finance approves the booking's financial side: POST /finance/bookings/:id/finance-approve (finance.bookings.approve_finance).
  3. Ops approves the booking's operational side via this page (approvals.approve).
  4. 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.