Customers and Customer 360
The customer directory, the one-page view of a customer, and the journey that runs through every screen.
Rules: 12 · Journey and Customer 360
(JRN, C360), AUD-020 (masking),
AUD-022 (consent).
1. Screens
| Route | What it is | Gate |
|---|---|---|
/sales/customers |
the directory — list, create, edit, import, export | customers.view |
/customers/:id |
Customer 360 — everything about one customer | customers.view |
/operations/journey |
the journey board — groups in travel or departing soon | bookings.view |
/customer/auth, /customer |
the customer's own portal | customer role |
Old /sales/customers?id= links redirect to /customers/:id.
The staff app has the directory and a lighter Customer 360 too (customers.view): the same
customers_list_page and customer_360_screen, with Call and WhatsApp. See
the app → Customers (PTY-007).
2. The journey
One journey model, used identically by staff screens, portals, messages and reports (JRN-001). Ten stages, in order:
enquiry → quotation → booking → approval → confirmed → preparing → ready → in_travel →
returned → completed
plus nine branches that take priority over the stage when they apply, most urgent first:
deceased, missing, emergency, hospitalised, cancelled, rejected,
visa_rejected, no_show, transferred.
The journey exists at two levels: per booking, and per traveller. A booking shows its most urgent active traveller's state.
A transferred traveller's transferred line names both ends from the transfer record:
"Transferred from BK-00012 (Umrah 29 Aug) to BK-00050 (Umrah 12 Sep) on 28 Sep 2026". A booking
whose travellers all left reads "All travellers transferred out to BK-00050"
(LC-031).
Nothing stores a stage
The stage is computed from the records — booking status, group dates, passenger rows, visa cases, tickets, verified payments, rooming, open incidents (JRN-002). Changing a stage means changing the fact behind it: approve, pay, issue a visa, record an incident. There is no "set stage" anywhere, and there should never be one.
Stage names, labels, icons and colours come from one shared definition, and every badge renders an icon and a label — colour is reinforcement, never the only signal. The tone colours are checked against WCAG AA in light and dark by a test (JRN-004).
Readiness
Preparing to travel becomes Ready to travel only when every active traveller clears the
checklist (JRN-003). Twelve items, each returned
as ok, missing, blocked or na, with the rule that raised it and a link to the fix:
passport validity (185 days beyond return) · passport scan · guardian · payments · visa · ticket · rooming · transport · emergency contact · mahram · vaccination · insurance.
The last four come back as not applicable, naming the rule that will add them — those facts are not recorded anywhere yet (INC-005, PAX-022, HLT-001, HLT-002). Owner and due date per item are not modelled.
3. Customer 360
get_customer_360 returns the page in this order
(C360-001):
- Open incident banner, in red, above everything.
- Now — journey stage, readiness, next best action.
- Where they are today — planned city, hotel, room and room-mates from the itinerary, and the next movement.
- Itinerary — day by day: flights with PNR and times, hotel stays, transport, and the departure's programme (ziyarat and other activities, planned on the group's Itinerary tab — INV-008).
- Trips — every booking, past, current and future.
- Travellers and family — linked family members, guardians, category.
- Documents — passport (masked), visa, tickets, with expiry warnings.
- Money — a booking whose travellers were all transferred reads "Transferred · money went with the
travellers", and the money carried with them counts on the booking they went to
(LC-032). From verified payments and group invoices, never from
Booking.balanceAmount. - Visa and tickets, per traveller.
- Requests and messages, with the consent state.
- Health — a restriction notice; no data is recorded yet.
- Activity — the audit timeline.
The function returns each section only to a caller who holds its permission, and names
the rest in a restricted list so the page can say a restriction exists rather than
pretending the section is empty (C360-003). The
journey carries only the traveller state, never incident content, so incident detail stays
behind incidents.view.
"Where they are today" shows the plan and the latest tour-leader check-in (FLD-002, FLD-003): kind, time, place, whether this traveller was present or missing, and who recorded it. A check-in city that differs from the planned city is highlighted. The leader's location is linked only when the booking agreed to it (FLD-005). See Tour leader app.
Sections with a cap
Requests (50) and messages (20) keep a fixed cap for good reason, so they return the total and a way to page the rest — nothing disappears without the reader knowing (PLT-057).
How fast it opens
The page is one request to the database, customer_360_screen
(PRF-010). It carries the
Customer 360, the tab counts and the first 100 entries of the activity card. Before, the
page made 12 requests, 7 of them one after another (about 1.7 s from Srinagar). The
answer is the same: the function checks customers.view and calls the same functions as
before, as you. "Load older" on the activity card is still its own request.
4. The directory
Every customer has a permanent customer code (CU-000123), shown beside the name in the
list, the profile sheet and the Customer 360 header; typing the exact code in the search box
finds the customer (PTY-001, PTY-006; Party codes).
Create, edit, import, export. When the customer has their own login, changing the phone on
the customer record changes the login's phone too, and a change on the login comes back to the
record; the form says so under the field. A new spelling of the same number (09906272405,
+91 99062 72405) is not a change, and a blank never clears the other side
(ACC-076). A customer a
partner booked is not the partner: their phone never touches the partner's login.
Passport numbers are masked to the last four characters
outside the customer's own record (AUD-020).
Passport scans and identity documents live on the company Shared Drive and open inside the
ERP: the customer's Documents tab and the Customer 360 Files card list them with an
Open button that shows the file in a viewer on the page (an image, or a PDF, with
Download). The drive-file function checks the viewer (customers.view for a customer
document, visa.view for a visa document, bookings.view for an issued e-ticket or voucher)
and hands out a link that lives ten minutes (AUD-021,
ACC-074). Staff need no Google account. Upload (customers.edit) goes through
upload-customer-doc, which files it under Customers / the customer's name and records it.
The Customer 360 Files card puts the customer's own documents, their visa cases'
documents and the live e-tickets and hotel vouchers of their bookings in one list, newest
first (GET /journey/customers/:id/files).
A passport page, a passport-size photo or a bank receipt the customer sent in the
WhatsApp menu is listed as Passport front, Passport back,
Passport-size photo or Payment receipt, marked From WhatsApp · for customers.edit presses OK or Reject
(with what is wrong). A rejected part is asked for again the next time the customer opens
Documents needed. Nothing is read from the photo: type the passport details on the
booking (COMM-035).
Lists page and search in the database, not in the browser (PLT-051) — see Lists, paging and search. The list narrows to when the customer was Added and sorts by added, first name or last name — from the toolbar or by clicking the Name and Added columns (UX-030). The paging bar under it shows rows per page (25, 50 or 100), "1–25 of N customers" and first, previous, next and last page, even when they all fit on one page.
The list opens with one request, customers_list_page
(PRF-010): the first page, the
total, and the pickers the dialogs use — partners (import), currencies (the customer form)
and active departures (assign to a group). Before, it made 12 requests, 4 one after another.
Each part is read as you, so you see the rows you saw before and no more; without
admin.currency.view the currency picker stays empty, as it did. A search is one request
per page and does not re-read the pickers.
Customer.passportNo is mandatory, which is why the public visa intake will not create a
customer without one (VISA-040).
Reading a passport
Three ways, on the customer screen and on the add-passenger screen. All three fill the same review dialog, which is shown before anything is saved.
Scan passport opens the camera and reads the machine-readable zone — the two lines of capitals and chevrons at the foot of the page. It reads frames until one decodes cleanly; there is no shutter button. The picture never leaves the device and no service is asked what it says: the zone is a fixed-width format (ICAO 9303) and is decoded, not recognised as meaning. The text recognition runs in the browser and downloads its language data the first time it is used on a device, so the first scan needs a connection.
The band marked on screen is exactly the strip that is read, and the line the camera is currently making out is shown underneath it — so a page it cannot read looks different from a camera that is not focused. Shapes the camera is known to confuse (O for 0, I for 1, S for 5) are put back using the alphabet each position must hold, and the check digits then decide whether that repair was right.
Only a read that adds up is accepted. The zone carries check digits for the passport number, the date of birth, the expiry and the line as a whole. A misread character does not satisfy them, so the scanner keeps looking instead of offering a wrong number — a wrong passport number on a visa application costs the pilgrim their trip. The same check runs on every route below.
Type the lines instead takes the two lines typed or pasted. A hand-held passport reader sends them as keystrokes, so one can be used here with no further change.
Extract from Passport uploads a photo or PDF as before. That route reads the same zone where it can and otherwise falls back to a vision model and then an OCR service, so unlike the two above it needs a connection and sends the image away.
A passport carrying a single legal name is kept as one name and never split (PAX-007). The zone does not carry the issue date or the father's name, so those are still typed.
5. Consent
Contact preference is per channel — email, WhatsApp, SMS — in CustomerContactPreference.
The rule the system applies today
(AUD-022, interim, pending a management
decision):
- a transactional message is allowed unless the customer opted out;
- a marketing or broadcast message needs an explicit opt-in.
A WhatsApp "STOP" or "START" reply updates it. A bulk send shows a consent summary first and sends only to allowed recipients. See Communications.
Nobody writes the preference table directly; set_contact_preference requires a source of at
least three characters — how the consent or opt-out was given.
6. Not built
- Customer-portal parity with these sections — the server functions allow it and are tested; the UI is Wave 3 (C360-002).
- Health and special needs data (HLT-003).
- Duplicate customer detection and merge (INT-101, Wave 4).
- Trip suggestions for returning pilgrims (CX-004, Wave 5).
- Automatic IN_TRAVEL / RETURNED / COMPLETED. Those stages are computed from the group dates; the daily job of LC-040 and the closeout of LC-041 are Wave 5.
7. Where to look
| Concern | Path |
|---|---|
| Journey and Customer 360 functions | supabase/migrations/20260919120000_journey_customer_360.sql |
| Paged tabs and board | supabase/migrations/20260920120300_list_paging_rpcs.sql |
| Shared stage definitions | src/lib/journey.ts, src/lib/statusTones.ts |
| Section permissions | src/lib/customer360.ts |
| Screens | src/pages/customers/Customer360.tsx, src/pages/customers/Customers.tsx, src/pages/operations/JourneyBoard.tsx |
| Components | src/components/journey/ |
| One-call reads (PRF-010) | supabase/migrations/20260930080000_customers_are_one_call.sql |
| Tests | supabase/tests/journey.sql, supabase/tests/customers_are_one_call.sql, src/lib/journey.test.ts, src/lib/customer360.test.ts, src/lib/perf.customers.test.ts |