Bookings
The Bookings module is the operational heart of the ERP. Every paying customer that travels with Al Huda — whether directly or through a B2B partner — is attached to a Booking row. A booking ties together a customer (the primary traveller / contact), a travel group (the package they joined), one or more passengers, a rate breakdown, and a payment lifecycle that is tracked independently for Ops and Finance.
Where it lives
- List view:
src/pages/sales/Bookings.tsx:68 - Detail view:
src/pages/sales/BookingDetail.tsx:177 - Create wizard page:
src/pages/sales/BookingWizard.tsx:4 - Import dialog:
src/components/bookings/BookingImportDialog.tsx - List math + filter helpers:
src/pages/sales/bookingsMath.ts - State machine (canonical):
docs/ALHUDA_ERP_STATE_MACHINES.md:17
1. What a booking is
A booking represents a paid or in-progress sale for a single departure. It owns:
| Slot | Where | Purpose |
|---|---|---|
customerId |
primary field | Who the booking is for (always a Customer row — wizard creates one if the passport is new) |
payerId |
optional | Different billing party (e.g. a family head paying for multiple relatives) |
groupId |
required for grouped sales | Which travel group this booking joined |
agentId + source='agent' |
partner bookings | Invoiced on account against the partner ledger |
passengers[] |
Booking Passenger rows | Actual travellers (≥ 1, usually equal to passengerCount) |
adultRate / childRate / infantRate |
money per tier | Rate per head for the three age tiers |
ticketRate / visaRate / hotelRate / mealsRate / groundRate |
breakdown | Component split of the adult rate (for reporting, invoice printing, and P&L) |
totalAmount, paidAmount, balanceAmount, gstAmount, grossAmount |
ledger-backed | Money state; mirrored into the General Ledger via finance vouchers |
status |
lifecycle | Ops-facing status (draft, confirmed, on_hold, cancelled, needs_correction) |
financeApprovalStatus / opsApprovalStatus |
twin-track | Two independent approval tracks — see the state machine below |
2. List view (/sales/bookings)
src/pages/sales/Bookings.tsx:68 renders the full bookings queue. It is a virtualized table (VirtualTable, src/pages/sales/Bookings.tsx:1777) with per-column filters and a React-Query cache keyed by ['bookings'] (src/pages/sales/Bookings.tsx:65).
Columns
| Column | Source | Notes |
|---|---|---|
| Booking # | b.bookingNo or fallback #<last-6-of-id> |
Mono, tracking-wide (Bookings.tsx:1788) |
| Customer | formatPersonName(b.customer) |
Name + email |
| Group | b.group?.name |
Shows Assigned or Unassigned pill |
| Amount | grossAmount + balance |
Colour-coded: green when fully paid, amber when due |
| Pax | b.passengerCount |
Center-aligned |
| Approvals | finance + ops dots | See approval statuses below |
| Status | b.status |
Pill: Confirmed / Cancelled / On Hold / Draft (Bookings.tsx:1876) |
Filter bar
Located at the top of the page. Filters are client-side predicates through buildBookingFilterPredicate (src/pages/sales/bookingsMath.ts:79):
- Search — matches booking number, customer name, email, phone
- Status — all / confirmed / cancelled / on hold / draft / needs correction
- Finance — payment state
- Group — filter to one group
- Trip Type — Umrah / Haj / Ziyarat / Umrah & Ziyarat / Haj & Ziyarat / Domestic / International (
Bookings.tsx:1714)
Statuses
The canonical state machine lives at docs/ALHUDA_ERP_STATE_MACHINES.md:17. Summary:
status |
Meaning | Set by |
|---|---|---|
draft |
Wizard just created it; not yet routed | Wizard submit |
confirmed / approved |
Finance + Ops have both approved | Approvals workflow |
on_hold |
Deliberately paused (e.g. waiting on passport) | Approvals |
cancelled |
Soft-cancelled; journals reversed | Approvals / delete |
needs_correction |
Sent back for a fix by Ops or Finance | Send Back dialog (Bookings.tsx:2181) |
Approvals are twin-tracked: financeApprovalStatus and opsApprovalStatus are each pending / approved / rejected / needs_correction. Both must end up approved before the booking is live. The list column renders two coloured dots summarising both tracks (Bookings.tsx:1849).
3. Detail view (/sales/bookings/:id)
src/pages/sales/BookingDetail.tsx:177 is the full record screen. It pulls three datasets in parallel: the booking itself, the Journal Entries referencing it, and the Audit log of every write against it.
Tabs (via Tabs from @/components/ui/tabs):
- Overview — customer card, rate breakdown, KPI cards (gross / paid / balance)
- Passengers — list of
BookingPassengerrows with expandable flight + hotel panels (PassengerFlightsPanel,PassengerHotelsPanel) - Finance — linked journal entries, with drill-down to the voucher (
VoucherDetailDialog) - Audit — every mutation, with a
UserChippopover showing actor + role (BookingDetail.tsx:85)
Drill-down helpers:
openCustomerLedger(booking.customer)opens the party ledger dialog.PartyLedgerDialogreusesfetchCustomerLedgerfromsrc/services/ledgerService.ts.
4. Key actions
All actions are gated by <PermissionGate>. Source references below.
| Action | UI entry | Permission | Path |
|---|---|---|---|
| Create booking | "New Booking" button | bookings.create |
Bookings.tsx:1753 → /sales/bookings/new |
| Import bookings | "Import" button (CSV/XLSX) | bookings.create |
Bookings.tsx:1755 → BookingImportDialog |
| Edit booking | Pencil / detail view | bookings.edit |
Bookings.tsx:2072 |
| Add passengers | "Add Passengers" | bookings.edit |
Bookings.tsx:2080 → AddPassengersDialog |
| Send message | "Send Message" | bookings.view |
Bookings.tsx:2092 → SendMessageDialog |
| Green WhatsApp button | bookings.view |
Bookings.tsx:2103 → sendWhatsAppText |
|
| Print invoice | Printer icon | bookings.view |
Bookings.tsx:2125 → GET /sales/bookings/:id/invoice + printInvoice |
| Add payment | "Add Payment" | finance.create |
Bookings.tsx:2141 → POST /finance/payments |
| Request visa case | "Request Visa" | visa.create |
Bookings.tsx:2154 → POST /visa |
| Send back | "Send Back" (to Ops) | approvals.approve |
Bookings.tsx:2181 |
| Resubmit to Finance | visible when status==='needs_correction' |
bookings.edit |
Bookings.tsx:2187 |
| Delete booking | Trash button | bookings.delete |
Bookings.tsx:2192 |
| Customer ledger | "Customer Ledger" button | customers.view |
Bookings.tsx:1942 |
Deletion is soft by default
DELETE /sales/bookings/:id soft-deletes the row and reverses any posted journals. A hard delete is only possible when the booking has zero vouchers attached.
5. Wizard entry point
Clicking New Booking navigates to /sales/bookings/new (src/App.tsx:161), which renders BookingWizard → WizardShell.
See Booking Wizard for the full walkthrough.
6. Rate breakdown
The booking carries a component breakdown of the adult rate so that finance can report revenue per service:
The wizard auto-recomputes adultRate whenever any of the five components change (src/components/bookings/wizard/WizardShell.tsx:83). The edit dialog on the detail page follows the same invariant.
childRate and infantRate are independent overrides — if left blank they inherit the adult rate.
GST is a separate line: gstEnabled, gstRate, gstAmount. When enabled, the booking is printed as a tax invoice with CGST/SGST or IGST as configured globally (admin.currency/admin.config.edit). Per-booking override is allowed; empty values fall back to the server-side default (WizardShell.tsx:383).
7. Payment tracking
Payments are recorded separately in the Payment table and posted to the General Ledger via finance vouchers. The booking itself carries only the running totals:
paidAmount— rollup of verified receipts allocated to this bookingbalanceAmount—grossAmount - paidAmountgstAmount,grossAmount— only populated whengstEnabled
The "Add Payment" button (Bookings.tsx:2141) opens a dialog that posts POST /finance/payments with { bookingId, amount, method, reference, status: 'verified' }. Once verified, the booking's paidAmount increments (via applyBookingPaymentIncrement — bookingsMath.ts). The wizard's first payment uses the same endpoint (WizardShell.tsx:394).
Partner bookings are billed on account
When source === 'agent', paymentPolicy is forced to on_account (WizardShell.tsx:380). The partner's ledger carries the receivable; the customer never sees an invoice directly. No "first payment" is taken at booking creation.
8. Permissions
Single source of truth: docs/PERMISSIONS.md §6.4.
| Action | Permission |
|---|---|
| View list + detail | bookings.view |
| Create (wizard, import) | bookings.create |
| Edit, add passengers, resubmit | bookings.edit |
| Delete | bookings.delete |
| Print invoice, send message, WhatsApp | bookings.view |
| Add payment | finance.create |
| Request visa | visa.create |
| Send back | approvals.approve |
| Customer ledger | customers.view |
Roles that hold bookings.* in full: CEO, GM, IT_ADMIN, ADMIN_HR, SALES_MANAGER, SALES_EXEC, B2B_MANAGER, B2B_EXEC (see docs/PERMISSIONS.md §5). OPS_MANAGER, OPS_EXEC, FINANCE_MANAGER, ACCOUNTANT, CASHIER, TICKET_MANAGER, VISA_OFFICER, AUDITOR hold bookings.view only.
9. Lifecycle diagram
stateDiagram-v2
[*] --> draft: Wizard submits
draft --> pending_ops: Routed to Ops
pending_ops --> pending_finance: Ops approves
pending_ops --> needs_correction: Sent back by Ops
pending_finance --> confirmed: Finance approves
pending_finance --> needs_correction: Sent back by Finance
needs_correction --> pending_ops: Resubmitted
confirmed --> on_hold: Paused (e.g. visa wait)
on_hold --> pending_ops: Resume review
confirmed --> cancelled: Cancel (reverses journals)
draft --> cancelled: Cancel before routing
cancelled --> [*]