Files
baya-monorepo/archive/post-phase/ui/audit/booking-lifecycle.md
T
2026-08-02 18:48:32 +03:30

73 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 223225: `<AppButton variant="text" onClick={() => setConfirmCancel(false)}>{t('cancel_request')}</AppButton>` 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 3337 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 206215 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 335347: `<TextField type="date" label={t('date_label')} …/>` 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 315327 wrap `<AddressMapPicker …/>` in `<Box sx={{ pointerEvents: 'none' }}>`; AddressMapPicker.tsx lines 2935 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 3954 error/empty branches with no action; row nav only via AppButton at lines 9199.
- **[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 6395: 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 92109: 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 182191.
- **[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 121125 render it in a plain Typography with `textAlign: 'end'` — compare SessionCard.tsx line 99's `<Typography component="span" dir="ltr" …>{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 135185: 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 1522: `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: `<HeaderFact label={t('unnamed_nurse')} value={booking.nurseName} />`.
- **[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 `<Box sx={{ mt: -1.5 }}>` 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<CancelReasonCategory>('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 2028: bare `<Stepper activeStep alternativeLabel>` 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 ~710-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 109113).
- 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.