Files
baya-monorepo/dev/post-phase/ui/ui-phase-5-booking-lifecycle.md
T
2026-07-17 13:22:04 +03:30

21 KiB
Raw Blame History

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 02; 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):

  1. The cancel-request dialog is mislabeled. In bookings/request/[id]/page.tsx lines 223225 the dismiss button renders t('cancel_request') («انصراف از درخواست») — the destructive action's own label — next to the real destructive cancel_confirm_yes; users who want to cancel click the button that keeps the request.
  2. C4's inline errors are unreachable. setAttempted(true) runs only inside handleSubmit (line 128) while submit is disabled={!requiredChosen || …} (line 439), so error={attempted && …} (line 230 etc.) never renders; the only feedback for an incomplete form is a silently disabled button.
  3. A pending request is unreachable once you leave C5. useCustomerRequests is exported from services/bookingRequests and consumed by zero pages (grep: only index.ts + its own hook file); bookings/page.tsx lists only post-payment bookings; the bottom nav has no requests entry.
  4. 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 unused useCustomerRequests), services/bookings (paginated useBookingList, sessions/EVV), services/refunds, services/reviews (useReviewEligibility, useMyReviewForBooking with an enabled option), 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 (TrustBadge patterns, 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/ (the dir="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 fetched useNurseProfile (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 315327, pointerEvents: 'none' around AddressMapPicker) 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.5 line 272, mt: -2 line 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 223225): 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 useBookingList already accepts { page, pageSize } and returns total. 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-colored borderInlineStart accent per row so a page of bookings ranks visually without reading every chip.
  • Rows fully tappable — the whole row navigates (keyboard-focusable, role/aria correct), 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 .ics download for the next session (no backend; Gregorian UTC in the file, Shamsi in the UI).
  • Adopt phase 1's vertical StatusTimeline in place of the horizontal StepperHeader usage — 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 the unnamed_nurse key misuse (line 92) — add a proper bd_nurse_label key and leave unnamed_nurse as 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 untouchedCancellationPolicyDisclosure (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-categorized coordination; 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 } to useMyReviewForBooking on 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's whenLabel (line 60/123): wrap the time-range segment in a dir="ltr" span with tabular-nums, exactly matching SessionCard.tsx line 99. This is a shared, tested component — update BookingRequestSummaryCard.test.tsx accordingly.
  • 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.mdREQ-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):

  1. Server-truth discipline. CountdownTimer never 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.
  2. Two-stage disclosure is a hard UI gate. The customer's care-instructions query never fires (useCareInstructions stays enabled-gated to the nurse on confirmed+); keep the BookingDetailView test proving it.
  3. 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 710-day ETA are product-mandated.
  4. Gender preference stays first-class. Never silently defaulted; the inline mismatch block stays; the culturally-tuned hint copy is untouched.
  5. 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.
  6. 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 check green; npm run test:ci green including updated tests for every touched shared component (BookingRequestSummaryCard, booking composites, new list-row components); en.json/fa.json in 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 + .ics download; 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; CancellationPolicyDisclosure byte-identical in behavior.
  • Review page: context recap + up-front moderation note; useMyReviewForBooking gated with enabled; 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 (/fa mobile first: the primary user).

7. How to test (what a human can verify after this phase)

  1. 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.
  2. 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.
  3. Submit → C5 shows the countdown progress ring with «حدود …» framing. Tap «انصراف از درخواست» → the dialog's keep-button reads «نه، نگه دار» and keeps the request; «بله، انصراف از درخواست» cancels it.
  4. 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 «پرستاران مشابه».
  5. 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.
  6. Open an active booking → hero shows «ویزیت … · », the visit address, nurse avatar, support entry, a working .ics download, and a vertical timeline. Check a nurse in (dev EVV) → detail and list row show «پرستار در محل است · ورود …».
  7. 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.
  8. Open a completed booking's /bookings row → star-strip CTA → review page shows the service/date/nurse recap and «نظر شما پس از بررسی منتشر می‌شود» before submit. Repeat 18 spot-wise on /en (LTR) and in dark mode: BookingRequestSummaryCard time ranges read startend both directions; no stock-MUI colors appear.

8. Hand off & document (close the phase)

  • Update client/CLAUDE.md Project Structure: the /bookings entry (tabs + wired useCustomerRequests + pagination), the C4/C5 descriptions, and any new shared components under components/(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.