Customers
Customer master and self-service portal. Covers the admin-side customer directory (/sales/customers), the reusable create/edit dialog, and the customer-facing portal at /customer.
Scope
- Admin directory — staff-side list, create, edit, delete, import, export (
src/pages/sales/Customers.tsx). - Create/edit dialog — the canonical "Add Customer" form reused from bookings, quotations, and partner flows (
src/components/common/CustomerFormDialog.tsx). - Customer portal — logged-in customers view their bookings, invoices, visa status, requests (
src/pages/customer/CustomerPortal.tsx). - Customer auth — email/phone login + signup for the customer portal (
src/pages/customer/CustomerAuth.tsx).
The sub-directory src/pages/customer/ contains only the portal-facing screens — the admin-side customer management lives under src/pages/sales/.
Pages and components
Admin directory — src/pages/sales/Customers.tsx
Route: /sales/customers. Gated by customers.view (src/App.tsx:158).
Contains the searchable customer list, inline create form, profile dialog with ledger, document uploads to Google Drive, and CSV import/export. The inline "Add Customer" form in this page duplicates the field set from CustomerFormDialog (see src/pages/sales/Customers.tsx:1076-1141 for the inline DateInput usages). New flows should prefer the dialog component over re-implementing the form.
Reusable dialog — src/components/common/CustomerFormDialog.tsx
Shared across bookings wizard, quotations, partner portal customer creation, and anywhere else a customer record needs to be created on the fly. POST /sales/customers with the field set below.
Props (src/components/common/CustomerFormDialog.tsx:64-73):
open,onOpenChange— controlled dialog state.onCreated(customer)— callback with the created record; callers use this to auto-select the new customer.extraPayload— merged into the API payload. Used by partner portal to injectagentId, by wizards to tagsource, etc.title,description— override the dialog copy.
Customer portal — src/pages/customer/CustomerPortal.tsx
Route: /customer. Role-gated: allowedRoles={['customer']} (src/App.tsx:186). Permission matrix does not apply — access is enforced by ownership checks in API handlers (see docs/PERMISSIONS.md §4.5).
The portal surfaces: own bookings, payment history with receipt upload, visa case status with document download, invoice download, service-request threads, profile edit, active-session management. Data is fetched through src/services/customerPortalService.ts and src/services/customerRequestService.ts.
Customer auth — src/pages/customer/CustomerAuth.tsx
Route: /customer/auth. Public. Email or phone + password, Turnstile captcha, optional 2FA (src/pages/customer/CustomerAuth.tsx:73-80). Phone → email resolution goes through POST /auth/resolve-identifier (src/pages/customer/CustomerAuth.tsx:47-64). On success, role must be customer or the session is forcibly signed out (src/pages/customer/CustomerAuth.tsx:35-38).
src/pages/auth/Auth.tsx is the staff auth page, not the customer one. ResetPassword.tsx is shared.
Fields
Defined in CustomerFormData (src/components/common/CustomerFormDialog.tsx:20-40). Three sections:
Personal
| Field | Notes |
|---|---|
title |
MR/MRS/MS/MSTR/MISS/DR/PROF |
firstName, lastName |
Required. Auto-uppercased on change. |
dob |
Date. DateInput with max={today}. |
gender |
Male/Female |
fatherName |
Auto-uppercased. |
bloodGroup |
A+ through O- |
nationality |
Free text; defaults to INDIAN. |
Identity & documents
| Field | Notes |
|---|---|
passportNo |
Alphanumeric only, uppercased, max 8 chars (CustomerFormDialog.tsx:215). |
passportIssuedDate |
DateInput with max={today}. |
passportExpiry |
DateInput. Warns in-form at < 6 months; flags expired (CustomerFormDialog.tsx:240-247). |
gstNumber |
Optional. 15-char GSTIN, validated via isValidGstin / normalizeGstin from src/lib/gstin.ts. Invalid-but-non-empty shows inline error and disables Save (CustomerFormDialog.tsx:85-96, :288). |
GST number is a recent addition
gstNumber was added to the customer schema and dialog. It flows into invoice numbering and GST-summary reports. Empty is valid; a value must match the 15-character format. See CustomerFormDialog.tsx:226-237.
Contact & address
| Field | Notes |
|---|---|
email |
Lowercased on change. |
phone |
PhoneInput component (country-code aware). |
address |
Uppercased. |
state |
LocationCombo over INDIA_LOCATIONS.states. Defaults to JAMMU AND KASHMIR. |
district |
LocationCombo over INDIA_LOCATIONS.districts[state]. Cleared when state changes (CustomerFormDialog.tsx:268). |
country |
Defaults to INDIA. |
pinCode |
6 digits, numeric only. |
Date input behavior
All three date fields on this dialog (dob, passportIssuedDate, passportExpiry) use the shared DateInput component.
Chrome autofill was recently hardened
Chrome historically suggested saved dates and email-address values inside the DateInput text field, producing a flickering autocomplete overlay and occasionally clobbering user input. Two consecutive fixes landed on the fix/date-input-autocomplete branch:
21f841e— disables Chrome autofill suggestions onDateInput.38cc4a2— separates the calendar-icon trigger from the text input so picking a date no longer flickers the suggestion dropdown.
If you add a new date field, use DateInput from src/components/ui/date-input.tsx — do not render a bare <Input type="date"> or you will regress both of these fixes.
DateInput accepts user-typed dd/mm/yyyy with auto-slash insertion (date-input.tsx:60-74) and clamps against max / min (date-input.tsx:77-79). The calendar picker writes back in YYYY-MM-DD; display reformats through formatDateStamp.
Customer-facing auth flow
| Route | Purpose | Guard |
|---|---|---|
/customer/auth |
Sign in / sign up / OTP | Public |
/customer |
Portal home | allowedRoles={['customer']} |
/reset-password |
Shared password reset | Public (token-scoped) |
The staff auth page (src/pages/auth/Auth.tsx) is a separate flow for the /app admin console. A customer who signs in on the staff page is rejected; a staff user on /customer/auth is signed out on load (CustomerAuth.tsx:35-38). Partner/agent users have their own page at /partner/auth (see Partners).
Permissions
Canonical reference: docs/PERMISSIONS.md §4.1 and §6.1.
| Action | Permission | Enforced at |
|---|---|---|
View /sales/customers |
customers.view |
Route (App.tsx:158) |
| Create customer | customers.create |
<PermissionGate> + API |
| Edit customer | customers.edit |
<PermissionGate> + API |
| Delete customer | customers.delete |
<PermissionGate> + API |
| Upload / delete Drive doc | customers.edit |
Server-side |
| View customer ledger | customers.view |
Server-side scoping |
| Portal access | role = customer (no matrix permission) |
Route allowedRoles + API ownership |
Related
- DateInput component
- Partners — external agents who also create customers via the partner portal wizard.
- PERMISSIONS.md §6.1 — authoritative action matrix.