Skip to content

12 · One customer journey and the Customer 360 view

Every screen — staff, partner portal, customer portal, tour-leader app — shows the same journey, with the same stage names and colours. The journey is derived from the records (booking, passengers, visa, tickets, payments, itinerary, incidents); nobody sets it by hand.

The universal journey

JRN-001 · One journey model

Status: DECIDED 2026-09-17 (owner: "it should be universal") · Owner: Management

flowchart LR
  E[Enquiry<br/>lead] --> Q[Quotation]
  Q -->|accepted| B[Booking draft]
  E -->|direct booking| B
  B --> A[Awaiting approval<br/>ops → finance]
  A -->|sent back| B
  A -->|rejected| X1([Closed — rejected])
  A -->|approved| C[Confirmed]
  C --> P[Preparing to travel<br/>documents · visa · tickets · payments]
  P -->|all ready| R[Ready to travel]
  R -->|departure date| T[In travel]
  T -->|return date| H[Returned]
  H -->|closeout done| Z([Completed])

  B & A & C & P & R -->|cancellation| K([Cancelled])
  P -->|visa rejected| VR{Visa rejected}
  VR -->|re-apply| P
  VR -->|cancel| K
  C & P & R -->|transfer| TR[Transferred to another group] --> C
  R -->|did not travel| NS([No-show])
  T -->|incident| I{{Emergency / incident}}
  I -->|resolved| T
  I -->|hospitalised| HS[Hospitalised] --> T
  I -->|deceased| D([Deceased — see file 13])

The journey exists at two levels: - Booking journey — the stages above. - Traveller journey — the same stages per passenger, plus traveller-only states: Hospitalised, Deceased, No-show, Transferred out, Cancelled. A booking with mixed travellers shows its most urgent traveller state (priority: Deceased > Emergency > Hospitalised > In travel > …).

Enforced by: 20260919120000_journey_customer_360.sql — get_booking_journey, get_booking_journeys, get_group_journey, get_journey_board compute stage and branch per booking and per traveller from the records; branch priority is jrn_branch_rank (deceased > missing > emergency > hospitalised > cancelled > rejected > visa rejected > no-show > transferred) and a booking shows its most urgent active traveller. Incident-derived states come from the extension point journey_incident_state(passengerId) (the incidents module replaces it). No-show is read from cancelled/no-show flight and hotel rows; Transferred from the audit trail (booking group move or passenger moved between bookings). Enquiry/Quotation appear on Customer 360 from open quotations. Tests: supabase/tests/journey.sql, src/lib/journey.test.ts.

JRN-002 · Stages are computed, not typed

Status: PROPOSED · Owner: Operations The stage is calculated from underlying facts (LC-001 status, passenger status, readiness items, dates, open incidents). Changing a stage means changing the fact (approve, pay, issue visa, check in, record incident) — through the normal confirmed action (UX-001). Enforced by: 20260919120000_journey_customer_360.sql — nothing stores a stage; the functions are STABLE reads that derive it from Booking.status, group departure/return dates, passenger rows, visa cases, tickets, verified payments (booking_finance_cleared) and rooming. IN_TRAVEL / RETURNED are computed from the group dates until the LC-040 daily job exists; COMPLETED waits for the LC-041 closeout.

JRN-003 · Readiness checklist

Status: PROPOSED · Owner: Operations Preparing to travel becomes Ready to travel only when, for every active traveller: passport valid ≥ 6 months beyond return (existing 185-day rule); visa ISSUED or COLLECTED; ticket ISSUED; hotel and room assigned; transport assigned; required payments received per schedule (PRC-010); guardian/mahram requirements met (PAX-020..022); vaccination and insurance recorded (file 14). Each item is shown with owner and due date. Enforced by: jrn_travellers in 20260919120000_journey_customer_360.sql returns one item per traveller as {key, label, state ok|missing|blocked|na, detail, ruleId, link}: passport validity (185 days after return), passport scan, guardian (PAX-011/020), payments (booking_finance_cleared), visa (VISA-010/011), ticket, rooming, group transport. Emergency contact (INC-005), mahram (PAX-022), vaccination (HLT-001) and insurance (HLT-002) are returned as n/a with the rule that will add them — those facts are not recorded yet. Shown by src/components/journey/ReadinessChecklist.tsx, rolled up per group on /operations/journey. Owner and due date per item are not modelled yet.

JRN-004 · Same labels and colours everywhere

Status: PROPOSED · Owner: Management Stage names and colours come from one shared definition used by staff pages, portals, messages and reports. Colour is never the only signal (icon + label). Enforced by: src/lib/journey.ts (stage order, labels, icons, tones) + src/lib/statusTones.ts; every badge and stepper renders icon + label, colour only as reinforcement. The database returns the same stage/branch keys and item labels so portals, messages and reports can use them. Tests: src/lib/journey.test.ts, src/components/journey/JourneyStepper.test.tsx. src/lib/statusTones.ts (StatusTone, statusTokens, getStatusToken) + src/components/common/StatusBadge.tsx (icon + label, never colour alone); tone colours are CSS variables in src/index.css with hex mirrors for print/e-mail; src/lib/statusTones.test.ts checks WCAG AA for every tone in light and dark.

Customer 360

C360-001 · One page per customer

Status: DECIDED 2026-09-17 · Owner: Sales & Operations The customer profile shows, in this order: 1. Now — for any trip in progress or upcoming: journey stage (JRN-001), readiness (JRN-003), and any open emergency (file 13) at the very top in red. 2. Where they are today — city, hotel, room and roommates for today's date from the itinerary; latest tour-leader check-in (file 14) with time; next movement (e.g. "Madinah → Makkah bus 07:00 tomorrow"). 3. Itinerary — day-by-day plan: flights with PNR and times, hotel stays by city, transport, ziyarat, meeting points. 4. Trips — all bookings (past, current, future) with stage, travellers, totals. 5. Travellers & family — linked family members, guardians/mahram, traveller category (PAX-001). 6. Documents — passport (masked number), visa, tickets, insurance, vaccination, with expiry warnings. 7. Money — paid, due, schedule, refunds, credit notes (from the ledger, FIN-033). 8. Visa & tickets — status per traveller. 9. Requests & messages — support requests, messages sent (email/WhatsApp) and consent (AUD-022). 10. Health & special needs — wheelchair, medical notes (restricted view), emergency contacts. 11. Activity — the audit timeline.

Enforced by: get_customer_360 (20260919120000_journey_customer_360.sql) and /customers/:id (src/pages/customers/Customer360.tsx), in that order: open incident banner, Now (stage, readiness, next best action), where they are today (planned city/hotel/room/roommates/next movement — tour-leader check-ins arrive with FLD-002), itinerary, trips, travellers & family, documents, money (from verified payments and group invoices, never Booking.balanceAmount), visa & tickets, requests & messages with consent, health (restriction notice — no data yet), activity (ActivityTimeline). Old /sales/customers?id= links redirect there.

C360-002 · Same facts in the customer portal

Status: PROPOSED · Owner: Management The customer portal shows the same Now / Itinerary / Travellers / Documents / Money / Visa sections for the customer's own trips (without internal notes, costs or restricted medical detail). Enforced by: the same functions are callable by a customer for their own bookings (jrn_visible_booking_ids uses auth_customer_booking_ids / auth_agent_booking_ids), tested in supabase/tests/journey.sql. The portal UI is Wave 3.

C360-003 · Who sees what

Status: PROPOSED · Owner: IT Medical details and incident details are visible only to users with the relevant permission (e.g. incidents.view, health.view); others see that a restriction exists ("Medical note — restricted"). Enforced by: get_customer_360 returns each section only to a caller with its permission and names the rest in restricted; the page hides the same sections (canSeeSection, src/lib/customer360.ts). The journey carries only the traveller state (hospitalised/deceased/missing) — never incident detail — so incident content stays behind incidents.view. Health data is not recorded yet (HLT-003).