# Customer booking-request + booking lifecycle screens (C4 request form → C5 pending tracker → bookings list → booking detail/EVV → cancel → refund status → review)
## Current state
The flow is complete and functionally rich but visually generic. C4 (`client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx`) is a single long page of default MUI selects (patient, service variant, saved address, native date/time inputs, a 3-way gender ToggleButtonGroup, notes with counter), with per-field dashed-border empty states, a form skeleton, and a domain-code→message error mapper. C5 (`bookings/request/[id]/page.tsx`) polls the request and renders the shared `BookingRequestSummaryCard`, a raw-MUI `StepperHeader` 3-step tracker, `CountdownTimer` (server-frozen deadline, LTR-forced Persian digits), five distinct terminal cards, and a cancel confirm Dialog. The bookings list (`bookings/page.tsx`) is a flat stack of bordered Paper rows — counterparty name, Shamsi date, session count, `StatusChip`, Toman total, "view" button — with skeleton/error/empty branches but no tabs, filters, pagination, or row click.
## Problems (19)
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/[id]/page.tsx` — In the cancel-request confirmation dialog, the DISMISS button is labeled with the destructive action's own label: `t('cancel_request')` = "انصراف از درخواست". Clicking the button that says "Cancel request" actually keeps the request and closes the dialog; the real destructive button is `cancel_confirm_yes`. Users who want to cancel will click the dismiss button; users who want to keep it may click confirm. Dismiss must read "بازگشت"/"نگه داشتن درخواست".
- evidence: Lines 223–225: ` setConfirmCancel(false)}>{t('cancel_request')}` inside DialogActions, next to the contained error confirm button.
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — All required-field inline errors are unreachable dead code. The submit button is `disabled={!requiredChosen …}`, but `setAttempted(true)` only runs inside `handleSubmit` — which can never fire while a required field is missing. So `error={attempted && patientId === ''}` etc. never render, and the only feedback for an incomplete form is a silently disabled button with no explanation of what's missing.
- evidence: Line 439 `disabled={!requiredChosen || genderMismatch || createRequest.isPending}` vs line 128 `setAttempted(true)` (only in handleSubmit) and line 230 `error={attempted && patientId === ''}`.
- **[high]** `client/src/services/bookingRequests/hooks/useCustomerRequests.ts` — The customer has NO way back to a pending request. `useCustomerRequests` (the customer requests inbox hook) is exported but consumed by zero pages; the bottom nav (CustomerLayout: Home/Bookings/Patients/Wallet/Profile) has no requests entry, and `bookings/page.tsx` lists only post-payment bookings (`list_empty_body`: "پس از تایید پرستار و پرداخت…"). If the user leaves C5 or closes the app while a request is pending — or during the 30-minute payment window — the request is orphaned unless they remember the URL.
- evidence: Grep: `useCustomerRequests` appears only in services/bookingRequests/index.ts and its own hook file; CustomerLayout.tsx lines 33–37 show the five nav items with no requests route.
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — The request form never shows WHO the request is for. `useNurseProfile` is fetched (used only for the gender-mismatch check and hidden display-context), but no nurse name, avatar, rating, or verification badge renders anywhere on the page — the header is just "request_title" + a generic subtitle. In a trust-first nursing marketplace, asking a family to hand over patient + home address to an unnamed party is the single biggest trust failure in the flow.
- evidence: Lines 206–215 render only `t('request_title')`/`t('form_subtitle')`; `profile.nurseName` is referenced only inside the `context` object at line 144.
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — Date selection uses the browser-native Gregorian `type="date"` input while every displayed date in the product is Shamsi (`formatShamsiDate`). Persian users must mentally convert Jalali → Gregorian to book a visit, then see the confirmation back in Shamsi. Time inputs are fine; the date picker needs a Jalali calendar.
- evidence: Lines 335–347: `` with `slotProps={{ inputLabel: { shrink: true } }}`.
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — The address preview embeds the fake-map stand-in (`AddressMapPicker` with `pointerEvents: 'none'`) — a 220px grid-pattern canvas with a pin and lat/lng labels that the component's own doc admits "is NOT a real map". In a read-only confirmation context it communicates nothing a text line doesn't, looks like placeholder scaffolding, and eats a large chunk of the form's vertical space.
- evidence: Lines 315–327 wrap `` in ``; AddressMapPicker.tsx lines 29–35 document the stand-in nature.
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/page.tsx` — The bookings list has no status tabs/filters, no upcoming-vs-past grouping, and no pagination even though the service is paginated (BOOKINGS_PAGE_SIZE=20, `total` returned) — booking #21 is unreachable. Rows aren't clickable (only the small outlined button navigates), the error state has no retry, and the empty state has no CTA into search. Scannability is one-note: every status renders identically except the chip.
- evidence: Line 19 `useBookingList('customer')` with no page/status params; lines 39–54 error/empty branches with no action; row nav only via AppButton at lines 91–99.
- **[medium]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The booking detail header omits the facts a customer most needs: no visit address/location, no headline date/time (buried per-session), no nurse avatar or contact affordance, no total-at-a-glance. The header is service name + booking ref + two tiny label/value pairs — a customer opening "my booking" cannot answer where/when without scanning session cards.
- evidence: Lines 63–95: header Paper contains only `service ?? t('bd_title')`, `bd_ref`, and two `HeaderFact`s (patient, nurse name).
- **[medium]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — The countdown — the emotional core of C5 — is bare `HH:MM:SS` digits next to a generic 'pending' icon, ticking every second. There is no progress ring/bar showing how much of the response window remains, no humanized framing ("پاسخ معمولاً تا چند ساعت"), and for multi-hour windows a per-second ticker reads as anxiety-inducing rather than calm. The 'urgent' variant only swaps the color to terracotta.
- evidence: Lines 92–109: label caption + `AppIcon icon="pending"` + a 1.5rem tabular-nums span; no other presentation.
- **[medium]** `client/messages/en.json` — A directional arrow is hard-coded inside the translated CTA copy: fa "ادامه پرداخت ←" and en "Continue to payment ←" — in English (LTR) the forward arrow should be →, so the EN button points backwards; and the button already carries `endIcon="payment"`, duplicating the affordance. Directionality must come from layout/icons, never from string literals.
- evidence: fa.json:445 / en.json:445 `"continue_payment": "Continue to payment ←"`; consumed at bookings/request/[id]/page.tsx lines 182–191.
- **[medium]** `client/src/components/BookingRequestSummaryCard/BookingRequestSummaryCard.tsx` — The date·time-range label is not bidi-isolated: `whenLabel` concatenates Shamsi date, '·', and "start – end" times with no `dir="ltr"` wrapper (unlike SessionCard, which wraps its identical time range in a `dir="ltr"` span). With Persian (AN-class) digits in an RTL paragraph, the range can visually render end-before-start.
- evidence: Line 60 builds `whenLabel`; lines 121–125 render it in a plain Typography with `textAlign: 'end'` — compare SessionCard.tsx line 99's `{timeLabel}`.
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/review/page.tsx` — The review form lacks context and expectation-setting: no recap of the booking being reviewed (service/date), no up-front note that reviews are moderated before publishing (the user only learns post-submit via the 'under review' state), no visible character counter for the 2000-char body (input silently sliced), and no hover/selected labels on the stars (just 5 identical icons).
- evidence: Lines 135–185: heading is only nurse name; line 150 `onChange={(e) => setBody(e.target.value.slice(0, REVIEW_BODY_MAX))}` with no counter; RatingInput has no per-star labels.
- **[medium]** `client/src/components/StatusChip/StatusChip.tsx` — Every status in the entire flow renders as a solid, fully-saturated pill (solid success/error/warning/info backgrounds with cream text), all at equal visual weight — a cancelled booking shouts as loudly as a disputed one, and screens with several chips (list rows, sessions, refund card, review state) read as a loud, cold dashboard rather than the calm clinical-warm tone. Soft-tinted chips (bg-soft + strong fg) would fit the brand and let severity actually rank.
- evidence: Lines 15–22: `verified: { bg: 'var(--bal-success)', fg: 'var(--bal-success-contrast)' … }` — solid token fill for all six kinds.
- **[low]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The i18n fallback key `unnamed_nurse` ("پرستار", intended as the no-name placeholder) is repurposed as the field LABEL for the nurse in the header facts — semantically wrong key reuse that will break the moment the fallback copy changes.
- evidence: Line 92: ``.
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/request/page.tsx` — Negative-margin layout hacks stitch related elements together instead of composed grouping: the price display is pulled up under the service select with `mt: -1.5`, and the notes counter is pulled under its TextField with `mt: -2` — fragile spacing that breaks when helper text appears.
- evidence: Line 272 `` and line 424 `sx={{ …, textAlign: 'end', mt: -2 }}`.
- **[low]** `client/src/components/booking/BookingStatusTimeline/BookingStatusTimeline.tsx` — The cancelled terminal row uses the brand-teal info tint (`--bal-primary-soft`) as its background while the sibling StatusNote in BookingDetailView uses `--bal-divider` for the same cancelled state — two different 'neutral' treatments for one status on one screen, and teal reads as informational/brand, not terminated.
- evidence: Line 45 `bgcolor: 'var(--bal-primary-soft)'` vs BookingDetailView.tsx line 185 `tone === 'neutral' ? 'var(--bal-divider)'`.
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/cancel/page.tsx` — The cancellation reason select pre-defaults to 'changed_mind', so users can submit without ever choosing — biasing the reason analytics and skipping a moment of reflection the trust-first flow otherwise builds carefully.
- evidence: Line 58: `useState('changed_mind')` with no empty/placeholder option.
- **[low]** `client/src/components/StepperHeader/StepperHeader.tsx` — StepperHeader is a raw default-MUI Stepper (default numbered circles, default connectors, default typography) and it is the status-communication backbone of the whole lifecycle — C5 tracker, booking timeline, refund progress, cancel flow all render this unstyled starter component, which is the single most 'MUI beginner example' element in the flow.
- evidence: Lines 20–28: bare `` with zero styling beyond `py: 2`.
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/bookings/[id]/review/page.tsx` — `useMyReviewForBooking(bookingId)` fires unconditionally on the review page, while the detail page carefully gates the same hook with `{ enabled: reviewable }` — inconsistent pattern and a wasted request for non-eligible bookings.
- evidence: Line 43 `const myReview = useMyReviewForBooking(bookingId);` vs bookings/[id]/page.tsx line 52's gated call.
## Opportunities (10)
- **Customer requests inbox: merge requests + bookings under one tabbed surface** (impact: high, effort: medium) — Wire the already-built `useCustomerRequests` hook into a customer-facing surface: segmented tabs on /bookings (در انتظار پاسخ / فعال / گذشته). Pending-request rows carry a live mini-countdown chip and deep-link to C5; accepted-awaiting-payment rows surface the payment deadline as the row's primary CTA. This closes the orphaned-request hole (the highest-stakes UX gap: money-adjacent deadlines the user cannot find again) with zero new backend work.
- **Redesign C4 as a trust-anchored request flow** (impact: high, effort: medium) — Put a sticky nurse identity card at the top (avatar, name, rating + review count, verified badge, gender) so the family always sees who they're inviting home; add a 3-step 'what happens next' strip (درخواست → پاسخ پرستار → پرداخت) reinforcing the money-free promise already in form_subtitle. Replace the dead-end disabled submit with always-enabled submit + scroll-to-first-error. Optionally split into two steps: details → review-and-send with a compact BookingRequestSummaryCard preview, which doubles as the fix for the missing pre-submit recap.
- **Jalali date picker + time-window presets** (impact: high, effort: large) — Replace native type=date with a Shamsi calendar picker (weekend/holiday aware — the ops holiday table already exists server-side), and replace free start/end time fields with tappable window chips (صبح ۸–۱۲ / بعدازظهر ۱۲–۱۶ / عصر ۱۶–۲۰ + custom). This removes the Gregorian mental conversion, kills the end<=start error class for most users, and later becomes the seam for nurse-availability hints.
- **Booking detail 'next visit' hero + designed vertical timeline** (impact: high, effort: medium) — Rebuild the detail header as an actionable hero: next upcoming session ("ویزیت ۲ · فردا ۹:۰۰"), the visit address, nurse avatar with message/support entry, and add-to-calendar. Replace the generic horizontal Stepper with a designed vertical timeline (per-stage icons, timestamps where known, terracotta marker on the current stage, distinct terminal branch rendering) — this is where 'status via raw chips + default stepper' should become the product's signature trust visual.
- **Countdown as a calm progress ring** (impact: medium, effort: small) — Wrap the CountdownTimer digits in a circular progress ring fed by (deadline − createdAt) so users see the fraction of the window remaining, drop per-second ticking above 10 minutes remaining (show 'حدود ۳ ساعت'), switch to seconds only in the final minutes, and add a one-line 'we'll notify you' note tied to the notifications bell so users feel safe leaving the page.
- **Live 'nurse is on site' presence for in-progress bookings** (impact: medium, effort: small) — The customer already receives EVV banners per session — elevate this into a headline presence state on the detail (and list row): 'پرستار در محل است · ورود ۰۹:۰۲' while checked-in. It converts the EVV plumbing into the platform's most visceral trust cue for the family member who is not at home with the patient.
- **Cancellation off-ramps before the kill switch** (impact: medium, effort: medium) — Before the fee disclosure, offer alternatives: 'تغییر زمان' (reschedule request via support ticket until real rescheduling exists) and 'گفتگو با پشتیبانی'. Add a one-line human note about nurse impact. The existing disclosure is excellent; giving an exit that isn't destruction both reduces cancellations and reads as fair.
- **Post-completion review nudge on the list** (impact: medium, effort: small) — Completed bookings without a review should show a compact star-strip CTA directly on the bookings-list row (the eligibility + my-review hooks already exist), instead of relying on the user opening the detail and finding the button below the money summary.
- **Terminal-state cards with smarter recovery** (impact: medium, effort: medium) — C5's rejected/expired cards all funnel to generic search. Offer 'درخواست دوباره از همین پرستار با زمان دیگر' (prefilled C4) when the rejection reason isn't gender/coverage, and 'پرستاران مشابه' (same service + area) otherwise — recovering the booking intent rather than restarting discovery from zero.
- **Soft-tint status system + status-differentiated list rows** (impact: medium, effort: small) — Introduce a soft chip variant (token -soft backgrounds + strong text, reserving solid fills for EVV banners) and give list rows a status-colored inline-start border accent so the eye can rank a page of bookings without reading every chip — aligning the whole flow with the calm/warm brand direction.
## Keep (do not regress)
- Token discipline is genuinely excellent in this area: zero hard-coded hexes across all audited files — every color resolves through --bal-* semantic tokens with both light/dark schemes defined (StatusChip, EvvStatusBanner, RatingInput even document the rule in comments).
- Server-truth discipline: CountdownTimer never computes deadlines client-side (renders diff vs server-frozen instant, LTR-forced Persian digits); BookingStatusTimeline never advances a step client-side; money is display-only IRR digit-strings through formatIrrToToman — never summed or re-split in the UI (BookingMoneySummary doc + props contract).
- Two-stage disclosure as a hard UI gate: the customer's care-instructions query never fires (enabled only for nurse on confirmed+), replaced by a designed lock affordance with honest copy ('visible to your assigned nurse and support only') — BookingDetailView CareSection.
- The cancellation flow's trust architecture: full pre-submit disclosure of policy tier, refund %/fee %, concrete Toman split via PriceBreakdown, per-session refundable/locked breakdown, admin-approval explainer, and an explicit acknowledgement checkbox gating confirm (CancellationPolicyDisclosure + cancel page).
- Honest refund UX: a failed refund suppresses ALL success-framed progress/amount/ETA (explicit comment in RefundStatusCard), BNPL's ~7–10-business-day window is surfaced plainly in RefundEtaBanner, and retry is deliberately absent (admin-only).
- EVV advisory semantics done right: out-of-range is warning-toned (never error), GPS-unavailable is neutral, and a mismatch never blocks the flow — tri-state handled end-to-end (EvvStatusBanner styleFor + SessionCard).
- Every screen in the flow has real skeleton loading states shaped like the final layout (FormSkeleton, StatusSkeleton, DetailSkeleton, review skeletons) plus distinct empty/error branches — no spinner-only pages.
- RTL/Persian craft in most components: dir="ltr" isolation on countdown digits, session time ranges and refund references; borderInlineStart logical accents; textAlign:'end'; Shamsi dates via fa-IR-u-ca-persian and Persian digits via Intl everywhere; the AddressMapPicker's documented inline-style workaround against stylis RTL flipping.
- First-class caregiver-gender preference with culturally-informed hint copy, never silently defaulted, and gender-mismatch blocked inline before the round-trip with the server staying authoritative (request form lines 109–113).
- The single terracotta-accent rule holds: --bal-secondary appears only at money/urgency moments (payment countdown, payment CTA, payout rows, nurse-view chip) — exactly the sparing-accent brand intent.