21 KiB
UI Phase 5 — Booking lifecycle
Mission: the request→track→booking→cancel→review flow is functionally rich but visually generic, and hides real defects: a confirm dialog whose dismiss button carries the destructive action's own label, required-field validation that is provably dead code, and a customer requests inbox (
useCustomerRequests) exported but wired to nothing — a pending, money-adjacent request is orphaned once the user leaves C5. Make the lifecycle legible, calm, and trustworthy: the family always sees who they're inviting home, always has a way back to a pending request, and every terminal state offers recovery.Track: frontend · Depends on: Phases 0–2; Phase 4 recommended (funnel order) · Unlocks: Phase 6 (checkout follows acceptance) Before you start, read ../../phases/_shared/agent-operating-rules.md and invoke the frontend-designer skill — both are mandatory.
1. Context — where this sits
The booking lifecycle is the emotional core of the product: a family hands a stranger their patient's details and home address, then waits on two countdowns (nurse response, then a 30-minute payment window). Every screen already works — services real or contract-shaped, deadlines server-frozen, money display-only — but the presentation is a default-MUI form stack that undersells the trust story and ships four verified defects (all re-confirmed in code, 2026-07-16):
- The cancel-request dialog is mislabeled. In
bookings/request/[id]/page.tsxlines 223–225 the dismiss button renderst('cancel_request')(«انصراف از درخواست») — the destructive action's own label — next to the real destructivecancel_confirm_yes; users who want to cancel click the button that keeps the request. - C4's inline errors are unreachable.
setAttempted(true)runs only insidehandleSubmit(line 128) while submit isdisabled={!requiredChosen || …}(line 439), soerror={attempted && …}(line 230 etc.) never renders; the only feedback for an incomplete form is a silently disabled button. - A pending request is unreachable once you leave C5.
useCustomerRequestsis exported fromservices/bookingRequestsand consumed by zero pages (grep: onlyindex.ts+ its own hook file);bookings/page.tsxlists only post-payment bookings; the bottom nav has no requests entry. - The family never sees who they're inviting home. C4 fetches
useNurseProfile(line 61) but renders no nurse name, avatar, rating, or badge — the profile is used only for the gender-mismatch check.
Also verified: the bookings list calls useBookingList('customer') with no page param though the hook
paginates — booking #21 is unreachable; C4 keeps a native Gregorian type="date" (line 336), a
pointerEvents: 'none' fake-map preview (line 317), and negative-margin stitching (lines 272/424);
BookingRequestSummaryCard.tsx builds whenLabel (line 60) with no bidi isolation while
SessionCard.tsx:99 wraps its identical range in dir="ltr"; the review page fires useMyReviewForBooking
ungated (line 43) though the hook accepts { enabled }; the cancel page pre-defaults its reason to
'changed_mind' (line 58); BookingDetailView.tsx:92 reuses the unnamed_nurse fallback key as the
nurse field label.
What already exists (do not rebuild):
- The full functional flow: C4 form, C5 tracker, bookings list + detail, cancel flow with
CancellationPolicyDisclosure, refund status, review page — and the service layer beneath it:services/bookingRequests(incl. the unuseduseCustomerRequests),services/bookings(paginateduseBookingList, sessions/EVV),services/refunds,services/reviews(useReviewEligibility,useMyReviewForBookingwith anenabledoption),services/tickets. - Phase 1's primitives: CountdownTimer v2 (progress ring), the vertical
StatusTimeline, StatusChip v2 (soft tints), the Jalali date picker,<Money>, skeleton twins, EmptyState/ErrorState. Consume these; never fork a local variant (README ownership rules). Phase 4's trust components (TrustBadgepatterns, NurseResultCard v2, rating display). - The do-not-regress architecture: server-frozen deadlines, two-stage disclosure gate, honest refund copy, advisory EVV, shaped skeletons (§5).
2. Required reading (do this first)
- audit/booking-lifecycle.md — the 19 problems, 10 opportunities, and keep-list this phase is built from, with file/line evidence.
- Code — read before touching, under
client/src/app/[locale]/(private-routes)/(customer)/bookings/:request/page.tsx(C4),request/[id]/page.tsx(C5),page.tsx(list),[id]/page.tsx,[id]/cancel/page.tsx,[id]/review/page.tsx. Components:booking/BookingDetailView/,BookingRequestSummaryCard/,booking/SessionCard/(thedir="ltr"precedent),CountdownTimer/. Services:services/bookingRequests/hooks/useCustomerRequests.ts,services/bookings/hooks/useBookingList.ts(it already paginates). - Phase 1's report (
dev/shared-working-context/reports/ui-phase-1-report.md) for the exact APIs of CountdownTimer v2, StatusTimeline, StatusChip v2, the Jalali picker. .claude/skills/frontend-designer/SKILL.md— the design contract (invoke the skill, don't just read it).- Product rules: product/business/05-booking-and-scheduling.md (request lifecycle, deadlines, two-stage disclosure), product/business/07-cancellation-and-refunds.md (policy tiers — the disclosure copy is product-mandated), product/business/11-reviews-trust-and-safety.md (moderation-before-publish), product/business/06-evv-and-service-delivery.md (EVV is advisory, never blocking).
3. Scope — build this
3.1 C4 request form — a trust-anchored request, not a form stack
bookings/request/page.tsx:
- Sticky nurse identity summary at the top: avatar, name, rating + review count,
TrustBadge, gender — all already available from the fetcheduseNurseProfile(line 61). The family must always see who they're inviting home. Compose from phase-4's card anatomy; compact and sticky on mobile scroll. - «چه اتفاقی میافتد؟» strip — a 3-step visual (درخواست → پاسخ پرستار → پرداخت امن) reinforcing the
money-free promise already in
form_subtitle; use phase 1's step primitives, no new one-off stepper. - Jalali picker + time-window chips replacing the native Gregorian
type="date"(line 336) and free time fields: phase 1's Jalali date picker plus tappable window presets (صبح ۸–۱۲ / بعدازظهر ۱۲–۱۶ / عصر ۱۶–۲۰) with a «زمان دلخواه» custom option that reveals the time fields — presets kill the end≤start error class for most users. Nurse-availability hints on the picker are (DEFERRED → needs a backend availability read; file a REQ only if you build the seam now). - Fix the dead validation: switch to touched-on-blur field errors plus a disabled-CTA explainer — a
one-line caption under the disabled submit listing what's missing («برای ادامه: انتخاب بیمار، تاریخ»).
Delete the unreachable
attempted-only branches. Keep the gender-mismatch inline block exactly as is. - Replace the fake-map preview (lines 315–327,
pointerEvents: 'none'aroundAddressMapPicker) with a compact address row: icon, title, one-line street text, an «تغییر» affordance back to the select — the grid-canvas stand-in communicates nothing and eats vertical space. - Remove the negative-margin hacks (
mt: -1.5line 272,mt: -2line 424) — group price-under-select and counter-under-notes with real composed containers (Stack spacing), so helper text can't collide.
3.2 C5 tracker — a calm wait with a way out
bookings/request/[id]/page.tsx:
- Countdown as the phase-1 progress ring with humanized framing: the ring shows the fraction of the response window remaining ((deadline − createdAt), server fields only — the client never recomputes the deadline, §5); above ~10 minutes remaining render «حدود ۳ ساعت» instead of per-second digits, switching to precise digits in the final minutes. Add a one-line «نتیجه را اطلاع میدهیم» note so users feel safe leaving.
- FIX the cancel-request dialog defect (lines 223–225): the dismiss button must never carry the destructive label. Adopt a clear confirm convention — destructive: «بله، انصراف از درخواست» (error, contained); dismiss: «نه، نگه دار» (text, neutral) — and apply it to every confirm dialog you touch.
- Terminal-state recovery: rejected/expired cards currently funnel to generic search. When the rejection reason permits (not gender/coverage), offer «درخواست دوباره با زمان دیگر» — C4 reopened prefilled with the same nurse/variant/patient/address (extend the query params C4 already accepts from C3) — plus a «پرستاران مشابه» entry (same service + area) into existing search. Recover the intent, not from zero.
3.3 Customer requests visibility — /bookings becomes the lifecycle home
bookings/page.tsx:
- Segmented tabs: «در انتظار پاسخ» / «فعال» / «گذشته». The pending tab finally wires the
exported-but-unused
useCustomerRequests— rows show the nurse name, requested Shamsi slot, and a live mini-countdown chip (compact CountdownTimer v2); accepted-awaiting-payment rows make the payment deadline the primary CTA. Rows deep-link to C5. - Pagination: the list renders page 1 only while
useBookingListalready accepts{ page, pageSize }and returnstotal. Add a pager (or load-more, matching the C2 results pattern) with locale digits. - Status-accent rows: a soft
StatusChip(phase 1's soft-tint system) plus a status-coloredborderInlineStartaccent per row so a page of bookings ranks visually without reading every chip. - Rows fully tappable — the whole row navigates (keyboard-focusable,
role/ariacorrect), not just the small button; give the error branch a retry and the empty state a CTA into search.
3.4 Booking detail — a hero that answers where/when/who
components/booking/BookingDetailView/BookingDetailView.tsx (+ detail page):
- Next-upcoming-session headline («ویزیت ۲ · فردا ۹:۰۰» — derived from the served sessions,
display-only), the visit address, nurse avatar with a support entry (reuse
BookingSupportEntry), and add-to-calendar — a client-side.icsdownload for the next session (no backend; Gregorian UTC in the file, Shamsi in the UI). - Adopt phase 1's vertical
StatusTimelinein place of the horizontalStepperHeaderusage — per-stage icons, timestamps where the server provides them, terracotta marker on the current stage, distinct terminal branch. The timeline remains server truth: never advance a step client-side. - EVV as a presence state: elevate the existing advisory EVV data into a headline on the detail («پرستار در محل است · ورود ۰۹:۰۲») and a compact indicator on the in-progress list row. Keep the tri-state semantics (in-range / out-of-range warning / no-GPS neutral) — never error-toned, never blocking.
- Session list polish: align session cards to the phase-1 card anatomy; keep SessionCard's
dir="ltr"isolation. Fix theunnamed_nursekey misuse (line 92) — add a properbd_nurse_labelkey and leaveunnamed_nurseas the fallback value it was written to be.
3.5 Cancel flow — off-ramps before the kill switch
bookings/[id]/cancel/page.tsx:
- Keep the policy disclosure untouched —
CancellationPolicyDisclosure(tier, refund %/fee %, reconciled Toman split, per-session refundable/locked, acknowledgement checkbox) is do-not-regress. - Add off-ramps above the disclosure: «تغییر زمان» — opens a support ticket via the existing
ContactSupportDialog/tickets service, pre-categorizedcoordination; real rescheduling is (DEFERRED → product decision + backend) — and «گفتگو با پشتیبانی», plus one human line about nurse impact. The destructive path stays fully available — these are exits, not obstacles. - Do not pre-default the reason: replace
useState<CancelReasonCategory>('changed_mind')(line 58) with an empty placeholder state; confirm stays disabled until a reason is chosen. Keeps the analytics honest.
3.6 Review flow — context and expectations up front
bookings/[id]/review/page.tsx + the bookings-list row:
- Context recap header: service name, Shamsi visit date, nurse name/avatar (from the cached booking detail — no new fetch) so the user knows exactly what they're reviewing.
- Moderation expectation note up front: «نظر شما پس از بررسی منتشر میشود» before submit, not only in the post-submit state — moderation-before-publish is a product rule the UI should disclose early.
- Post-completion star-strip CTA on the list row: a completed booking without a review shows a compact RatingInput-styled strip on its /bookings row deep-linking into the review page (the eligibility + my-review hooks exist; gate the extra queries to completed rows only).
- Fix the ungated hook call: pass
{ enabled }touseMyReviewForBookingon the review page (line 43) exactly as the detail page gates it — the hook already accepts the option.
3.7 Misc verified defects
- Bidi-isolate
BookingRequestSummaryCard'swhenLabel(line 60/123): wrap the time-range segment in adir="ltr"span with tabular-nums, exactly matchingSessionCard.tsxline 99. This is a shared, tested component — updateBookingRequestSummaryCard.test.tsxaccordingly. - The «ادامه پرداخت ←» arrow-in-string CTA (fa/en
continue_payment) is phase 12's catalog sweep — note it in your report; removing the arrow on strings you already touch is fine, but don't sweep copy-wide here.
4. Mocks & seams in this phase
None introduced. Every deliverable is client-side over existing seams: useCustomerRequests,
useBookingList pagination, and the review hooks already exist; the .ics file is generated in the browser;
the «تغییر زمان» off-ramp rides the existing tickets service. bookingRequests/bookings/reviews run
real (de-mocked in refinement-phase-4) — build against the real wire, UI mock-tolerant behind the seams.
REQ posture: if a deliverable surfaces a genuine backend gap (a rejection-reason code C5's recovery logic
needs, nurse-availability for the time chips), append a REQ to
for-backend.md — REQ-001…038 are taken
(tracker verified); number from REQ-039 — and ship degraded-gracefully. Never edit server/.
5. Critical rules you must not get wrong
From the audit keep-list (all currently true in code — regressions fail this phase):
- Server-truth discipline.
CountdownTimernever computes or extends a deadline — the ring is presentation over server-frozen instants; timelines never advance a step client-side; money stays display-only IRR digit-strings via the money utils — never summed or re-split. - Two-stage disclosure is a hard UI gate. The customer's care-instructions query never fires
(
useCareInstructionsstays enabled-gated to the nurse on confirmed+); keep the BookingDetailView test proving it. - Honest cancel/refund copy stays. The full pre-confirm disclosure, the acknowledgement checkbox, the failed-refund state that suppresses all success framing, and the BNPL 7–10-day ETA are product-mandated.
- Gender preference stays first-class. Never silently defaulted; the inline mismatch block stays; the culturally-tuned hint copy is untouched.
- EVV stays advisory. Out-of-range is warning-toned, no-GPS neutral, never blocking — the presence headline is a positive reframe, not a new gate.
- Skeletons stay shaped like content. Every redesigned layout updates its skeleton twin in the same change — no spinner-only regressions.
Design-contract non-negotiables that bite here: i18n keys in both catalogs (this phase adds many);
tokens/palette only, never hexes (soft chips use the phase-0/1 --bal-*-soft tokens); RTL logical props
(borderInlineStart accents, dir="ltr" islands for times/digits); dark mode on every new surface; MUI v9
API only; changed shared components keep/gain co-located tests; fetch/cookies rules untouched.
6. Definition of Done
On top of the shared definition-of-done.md:
npm run checkgreen;npm run test:cigreen including updated tests for every touched shared component (BookingRequestSummaryCard, booking composites, new list-row components);en.json/fa.jsonin sync.- Cancel-request dialog: dismiss reads «نه، نگه دار», destructive reads «بله، انصراف از درخواست» — the dismiss button never carries a destructive label anywhere in the flow.
- C4: nurse identity summary renders; blurred-empty required fields show inline errors; the disabled CTA
explains what's missing; no Gregorian
type="date", fake-map preview, or negative-margin stitching. - /bookings: three tabs; pending tab lists live requests with mini-countdowns deep-linking to C5; a list of >20 bookings is fully reachable via the pager; rows fully tappable with status accents.
- Booking detail: next-session hero with address +
.icsdownload; vertical StatusTimeline (no horizontal stepper); EVV presence headline while checked-in. - Cancel page: reason select starts empty (confirm disabled until chosen); both off-ramps present;
CancellationPolicyDisclosurebyte-identical in behavior. - Review page: context recap + up-front moderation note;
useMyReviewForBookinggated withenabled; completed list rows show the star-strip CTA. - Visual verification on all four axes —
/fa+/en× light + dark — and mobile + desktop for C4, C5, /bookings, booking detail (/famobile first: the primary user).
7. How to test (what a human can verify after this phase)
- From a nurse profile (C3) tap «درخواست رزرو» → C4 shows the sticky nurse card and the 3-step strip. Blur the empty patient select → inline error; the disabled CTA lists the missing fields.
- Pick a date from the Jalali picker and tap the «صبح» chip → times fill; choose «زمان دلخواه» → custom time fields appear. The address select shows a compact text row, not a grid canvas.
- Submit → C5 shows the countdown progress ring with «حدود …» framing. Tap «انصراف از درخواست» → the dialog's keep-button reads «نه، نگه دار» and keeps the request; «بله، انصراف از درخواست» cancels it.
- Leave C5 → /bookings «در انتظار پاسخ» tab shows the pending request with a live mini-countdown; the row returns to C5. Reject/expire a request (dev sim) → the terminal card offers «درخواست دوباره با زمان دیگر» (C4 opens prefilled) and «پرستاران مشابه».
- With >20 bookings seeded, page 2 is reachable and booking #21 opens. Rows are tappable end-to-end; each carries a soft status chip + matching inline-start accent.
- Open an active booking → hero shows «ویزیت … · », the visit address, nurse avatar, support entry,
a working
.icsdownload, and a vertical timeline. Check a nurse in (dev EVV) → detail and list row show «پرستار در محل است · ورود …». - Start a cancellation → reason select is empty and confirm disabled; the off-ramps open a support ticket / support chat; completing the flow shows the unchanged policy disclosure and acknowledgement gate.
- Open a completed booking's /bookings row → star-strip CTA → review page shows the service/date/nurse recap
and «نظر شما پس از بررسی منتشر میشود» before submit. Repeat 1–8 spot-wise on
/en(LTR) and in dark mode:BookingRequestSummaryCardtime ranges read start–end both directions; no stock-MUI colors appear.
8. Hand off & document (close the phase)
- Update
client/CLAUDE.mdProject Structure: the /bookings entry (tabs + wireduseCustomerRequests+ pagination), the C4/C5 descriptions, and any new shared components undercomponents/(booking/). - Write the report at
dev/shared-working-context/reports/ui-phase-5-report.md: what changed per scope item, the defects fixed (dialog labels, dead validation, orphaned inbox, ungated hook, bidi label), REQs filed (REQ-039+, appended to for-backend.md in the standard entry shape — or "none"), and the phase-12 note (arrow-in-string CTA sweep). - Save a memory note per operating-rules §8: the lifecycle redesign decisions (tabs model, recovery paths, presence state), the confirm-dialog labeling convention now in force, and what phase 6 (checkout) should know about the accepted-request → payment handoff surfaces you touched.