Sales
Written April 2026 — read this first
The screens below are still right; the controls around them are not. A booking's status cannot be edited, pricing on a rate-sheet group comes from the rate sheet, cancellation is a request someone else approves, and lists filter, search and count in the database rather than in the browser. Read Bookings and Lists, paging and search.
The Sales module is the customer-facing funnel: Leads → Quotations → Bookings → Customers. Everything a sales executive does before a customer becomes a paying traveller lives here.
Where it lives
All Sales pages sit under src/pages/sales/ and a few siblings:
src/pages/leads/Leads.tsx:65— Leads pipeline (routed at/sales/leads)src/pages/sales/Quotations.tsx— Quotations list + dialogsrc/pages/sales/Bookings.tsx:68— Bookings list + detail peeksrc/pages/sales/BookingDetail.tsx:177— Booking detail pagesrc/pages/sales/BookingWizard.tsx:4— New-booking wizard wrappersrc/pages/sales/Customers.tsx— Customers mastersrc/pages/sales/bookingsMath.ts,customersMath.ts,quotationsMath.ts— pure helpers (stats, filters, patch-merge)
Routes (all under /sales/*, see src/App.tsx:158):
| Route | Component | Permission |
|---|---|---|
/sales/customers |
Customers |
customers.view |
/sales/leads |
Leads |
leads.view |
/sales/quotations |
Quotations |
quotations.view |
/sales/bookings |
Bookings |
bookings.view |
/sales/bookings/new |
BookingWizard |
bookings.create |
/sales/bookings/:id |
BookingDetail |
bookings.view |
1. The sales pipeline
flowchart LR
Inquiry([Customer inquiry]) --> Lead[/Lead/]
Lead -->|Contact + qualify| Qual{Qualified?}
Qual -->|no| Closed[Close lead]
Qual -->|yes| Convert{Convert path}
Convert -->|Quote customer| Quote[/Quotation/]
Convert -->|Skip quote| Book[/Booking/]
Quote -->|Send to customer| Sent[sent]
Sent -->|Accept| Accepted
Sent -->|Expire / reject| Dead[rejected or expired]
Accepted --> Book
Book -->|Ops + Finance approve| Confirmed([Confirmed booking])
Closed --> End([End])
Dead --> End
Confirmed --> End
Each step is a distinct page with its own permission, its own API surface, and its own status machine.
2. Leads (/sales/leads)
src/pages/leads/Leads.tsx:65 is a lightweight CRM for inbound inquiries — customers who called, WhatsApp'd, filled a web form, or walked in.
Fields captured on create
defaultLeadForm (Leads.tsx:54):
fullName,phone,email,citytravelType—umrah,hajj,umrah_plus,custom(Leads.tsx:47)groupSizepreferredMonthmessage— freeform
Status pipeline
statusOptions (Leads.tsx:39):
| Status | Meaning |
|---|---|
new |
Just captured; no outbound contact yet |
contacted |
Rep has reached out |
qualified |
Budget / intent confirmed; ready to quote |
converted |
Became a quotation or booking |
closed |
Stopped working it (no-intent / duplicate / stale) |
lost |
Went nowhere — we lost the work (SAL-001) |
closed and lost both require a reason, which is stored on the lead and shown back on the
detail dialog with who and when. See Leads.
Actions
| Action | Permission | Path |
|---|---|---|
| View page | leads.view |
Route |
| Create lead | leads.edit |
Header button |
| Update status | leads.edit |
Detail dialog |
| Convert to quotation | quotations.create |
Detail dialog → calls convertLeadToQuotation |
| Convert to booking | bookings.create |
Detail dialog → calls convertLeadToBooking |
Services: src/services/leadService.ts — fetchLeads, createLead, updateLeadStatus, convertLeadToQuotation, convertLeadToBooking.
3. Quotations (/sales/quotations)
src/pages/sales/Quotations.tsx is the quote management workspace. A quotation is a non-binding price offer the customer accepts or rejects; accepted quotes convert to bookings.
The list searches the quotation number and the customer's name, filters by status (including converted), narrows to when the quotation was Created and sorts by created, last updated, quotation number or status (UX-030). The paging bar under it shows rows per page (25, 50 or 100), "1–25 of N quotations" and first, previous, next and last page, even when they all fit on one page.
Quotation form
Shape defined in QuotationFormData (Quotations.tsx:65):
customerId,groupId,agentId(if sold through a partner)packageDetails:flights[],hotels[],meals[]— inventory IDs + display labels (operators pick from dropdowns)visa,duration,roomType,requestedAmount,specialRemarkspassengers[]— same shape as booking passengers
totalAmount,discount,currencyvalidUntil— expiry date (ISO)status—draft/sent/accepted/rejected/expirednotes
Status lifecycle
stateDiagram-v2
[*] --> draft: Create
draft --> sent: Send to customer
sent --> accepted: Customer approves
sent --> rejected: Customer declines
sent --> expired: Past validUntil (auto)
accepted --> [*]: Convert to booking
rejected --> [*]
expired --> [*]
draft --> [*]: Delete
isQuotationExpired (src/pages/sales/quotationsMath.ts) is the client-side flag.
Losing a quotation (SAL-001)
Mark Rejected on the open quotation asks "Why did we lose it?" and the API refuses
rejected or expired without an answer of at least 3 characters. It is stored on the
quotation as rejectionReason, with rejectedAt and rejectedBy, and shown back on the
quotation under "Why we lost it" with the name and the time. Putting the quotation back in
play clears all three. Every status change is written to the audit trail.
rejected and expired are no longer choosable in the edit form's Status box — losing a
quotation is an action that asks why, not a value picked from a list with no reason attached.
An accepted quotation has no Mark Rejected button: it became a booking, which is cancelled,
not rejected.
Nothing sets expired automatically. The list marks a quote past its validUntil date in
the browser; the stored status stays as it was until somebody changes it.
Actions
| Action | Permission | Source |
|---|---|---|
| View list | quotations.view |
Route |
| Create | quotations.create |
Header button |
| Edit | quotations.create (treat-as-edit) |
Row action |
| Send to customer | quotations.create |
sendQuotation (Quotations.tsx:54, line 645) |
| Convert to booking | bookings.create + quotations.create |
createBookingFromQuotation (Quotations.tsx:58, line 515) — also flips status to accepted if not already (Quotations.tsx:561) |
| Mark rejected | quotations.create |
updateQuotationStatus — asks for the reason (SAL-001) |
| Delete | quotations.create |
Row action |
Quotations permissions are under-split
Edit / send / delete all currently gate on quotations.create. docs/PERMISSIONS.md §6.3 marks this as a future split into quotations.edit / quotations.delete. Do not introduce new finer checks without also updating the matrix and seed migration.
Services: src/services/quotationService.ts — fetchQuotations, createQuotation, updateQuotation, updateQuotationStatus, sendQuotation.
4. Bookings (/sales/bookings)
Full coverage: Bookings.
In short: the paying-customer record, created by the Booking Wizard (see Booking Wizard) or converted from a lead / quotation. Gated by bookings.* permissions.
5. Customers (/sales/customers)
src/pages/sales/Customers.tsx is the master list of every individual person Alhuda has ever transacted with. A customer row is the durable identity that multiple bookings and payments reference.
Key facets
- Customer code —
CU-000123, given by the system and permanent (PTY-001). The booking import can name an existing customer by it (PTY-005, Party codes). - Passport-based dedup — the primary natural key; the wizard and import dialog both use it to prevent duplicates.
- Profile fields — name, DOB, gender, title, nationality (default INDIAN), full Indian address block, phone, email.
- Hajj-specific fields — blood group, PAN, Aadhaar (populated from Hajj bookings, used on future bookings).
- Source —
DirectvsThrough Business Partner(links tosourceAgentId). - Documents — Drive-backed uploads (passport, visa, tickets) via
customers.edit.
Actions
| Action | Permission |
|---|---|
| View list + detail | customers.view |
| Create | customers.create |
| Edit | customers.edit |
| Delete | customers.delete |
| Upload / delete document | customers.edit |
| View ledger | customers.view |
| Export CSV | customers.view |
| Import CSV | customers.create |
Services: src/services/customerService.ts.
6. How the Sales pages talk to each other
flowchart LR
subgraph Leads page
A[Lead row] -- Convert to Quote --> Q[Quotation]
A -- Convert to Booking --> B[Booking]
end
subgraph Quotations page
Q -- Convert (accepted) --> B
end
subgraph Customers page
C[Customer master]
end
B --> C
Q --> C
A --> C
- Lead conversion calls
createQuotationorcreateBookingFromLead; both take the lead's captured fields and create aCustomerrow on the fly if the contact isn't already in the master. - Quotation → Booking uses
createBookingFromQuotation, which copies passengers, rates, and payer onto a freshBooking, then updates the quotation status toaccepted(Quotations.tsx:515). - Every booking update that changes a customer-relevant field (e.g. phone) propagates back to the
Customerrow so the next booking starts from fresh data.
7. Permissions summary
The canonical source is docs/PERMISSIONS.md §6.1 through §6.4. Condensed:
| Module | view |
create |
edit |
delete |
|---|---|---|---|---|
| Leads | leads.view |
— | leads.edit |
— |
| Quotations | quotations.view |
quotations.create |
quotations.create |
quotations.create |
| Bookings | bookings.view |
bookings.create |
bookings.edit |
bookings.delete |
| Customers | customers.view |
customers.create |
customers.edit |
customers.delete |
Role bundles that carry full sales-module access: CEO, GM, IT_ADMIN, ADMIN_HR, SALES_MANAGER, B2B_MANAGER. SALES_EXEC and B2B_EXEC lack customers.delete but hold everything else in the funnel (see docs/PERMISSIONS.md §5.2).
8. What the Sales module does NOT contain
For developer context, so you don't go hunting in the wrong folder:
- Finance (payments, vouchers, journals) lives under
/finance—src/pages/finance/Finance.tsx. Payments are triggered from the Bookings list via the "Add Payment" button (gated byfinance.create) but the posting handler lives in finance. - Approvals queue (for ops + finance to approve bookings) is under
/approvals—src/pages/approvals/Approvals.tsx. Sales routes them there via "Send Back" and "Resubmit". - Partners / agents master is under
/agents—src/pages/partners/Partners.tsx. Sales only selects an existing partner; creation / edit happens there. - Groups is a peer module, not a child of Sales —
/groups. See Groups.