Skip to content

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 + dialog
  • src/pages/sales/Bookings.tsx:68 — Bookings list + detail peek
  • src/pages/sales/BookingDetail.tsx:177 — Booking detail page
  • src/pages/sales/BookingWizard.tsx:4 — New-booking wizard wrapper
  • src/pages/sales/Customers.tsx — Customers master
  • src/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, city
  • travelType — umrah, hajj, umrah_plus, custom (Leads.tsx:47)
  • groupSize
  • preferredMonth
  • message — 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, specialRemarks
    • passengers[] — same shape as booking passengers
  • totalAmount, discount, currency
  • validUntil — expiry date (ISO)
  • status — draft / sent / accepted / rejected / expired
  • notes

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 — Direct vs Through Business Partner (links to sourceAgentId).
  • 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 createQuotation or createBookingFromLead; both take the lead's captured fields and create a Customer row on the fly if the contact isn't already in the master.
  • Quotation → Booking uses createBookingFromQuotation, which copies passengers, rates, and payer onto a fresh Booking, then updates the quotation status to accepted (Quotations.tsx:515).
  • Every booking update that changes a customer-relevant field (e.g. phone) propagates back to the Customer row 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 by finance.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.