15 KiB
UI Phase 5 — Booking Lifecycle — Report (2026-07-18)
What was built
C4 request form — bookings/request/page.tsx:
- A sticky nurse-identity bar (
NurseIdentityBar, page-local) pinned to the top of the scroll: avatar, name, rating + review count, tappableTrustBadge, gender chip — off the already-fetcheduseNurseProfile. The family always sees who they're inviting home before filling anything in. - A «چه اتفاقی میافتد؟» strip reusing C5's own
StepperHeaderthree-step labels (step_submitted/step_awaiting/step_payment) atactiveStep={0}— no new one-off stepper, and the copy stays in sync with the tracker the family lands on next. - The native
type="date"field replaced by the new sharedJalaliDateIntentPicker(extracted from C1's local date-intent widget — see below) and the free start/end time fields replaced by tappable morning/afternoon/evening window chips + a «زمان دلخواه» custom option that reveals the time fields — kills the end≤start error class for the common case. - Fixed the dead-validation defect:
attempted(a single flag only ever set inside the submit handler the disabled button could never reach) replaced with a per-fieldtouchedmap setonBlur; inline errors are now reachable. The submit button stays disabled while required fields are missing (unchanged behavior) but a caption underneath now lists exactly what's missing («برای ادامه: انتخاب بیمار، تاریخ»), built fromcta_missing_*keys. - The fake-map preview (
AddressMapPickerwrapped inpointerEvents:'none') replaced with a compact address row (icon + "title · city · district" + street line) and a «تغییر» affordance that swaps the row back for the select; choosing a new address collapses it back to the compact row automatically. - Negative-margin stitching (
mt: -1.5,mt: -2) removed — price-under-select and counter-under-notes are now grouped with realStackcontainers. - C4 now also reads
patient_id/address_idquery params (extending the existingnurse_id/variant_id/required_genderhandoff from C3) so C5's terminal-state "request again" can reopen it fully prefilled.
New shared component — components/common/JalaliDateIntentPicker/: extracted from C1's local
DateIntentFilter (near-day chip strip + a calendar-icon Popover entry into the full Jalali grid) so C4's
real, required date field could reuse the identical widget instead of forking a second copy. C1's
SearchScreen.tsx was refactored to delegate to it (behavior byte-identical — verified via npm run test:ci); the new component ships its own co-located test.
C5 tracker — bookings/request/[id]/page.tsx:
- The response countdown now renders
CountdownTimer's progress ring (windowStart={request.createdAt}, the exact server-frozen pair — no client-side deadline math) with a humanized coarse label above the 10-minute threshold («حدود ۳ ساعت» / «حدود ۲۵ دقیقه», newcountdown_about_hours/countdown_about_minuteskeys) that switches to the ticking clock in the final minutes. A one-line «نتیجه را به شما اطلاع میدهیم» note sits under it. The payment countdown is unchanged (no accuratewindowStartexists on the DTO for it — never fabricated client-side). - Fixed the cancel-dialog label defect: the hand-rolled
Dialogreplaced with the sharedConfirmDialog. Dismiss now reads «نه، نگه دار» (cancel_confirm_keep, neutral text button); the destructive action reads «بله، انصراف از درخواست» (cancel_confirm_destructive, error/contained) — the dismiss button no longer carries the destructive action's own label. The now-unusedcancel_confirm_yeskey was removed from both message files. - Terminal-state recovery for
rejected_by_nurse/expired_no_response: a «درخواست دوباره با زمان دیگر» button reopens C4 prefilled with the same nurse/variant/patient/address, and a «پرستاران مشابه» button opens/searchcarrying the request's city/district/gender. For rejections, the same-nurse retry is suppressed whennurseRejectionReason(freeform text — no structured code exists, REQ-044) matches a gender/coverage keyword heuristic (fa+en), in which case only "similar nurses" is offered. Other terminal states (payment_deadline_expired,cancelled_by_customer,converted) are unchanged.
Bookings list — bookings/page.tsx + new BookingsScreen.tsx:
- Three segmented tabs: «در انتظار پاسخ» / «فعال» / «گذشته». The pending tab wires the
previously-exported-but-unused
useCustomerRequests(filtered topending_nurse_response+accepted_awaiting_payment), with a live miniCountdownTimerper row deep-linking to C5 — the "orphaned request" defect (money-adjacent deadline the customer could no longer find) is fixed. A badge on the tab label shows the pending count. - Active/past split
useBookingList('customer')client-side by status over one query with a growingpageSize("load more", the exact C2 results pattern) — booking #21+ is reachable via the button. - Rows are
AccentCards with a status-tonedborderInlineStart+ a softStatusChip,role="button"+ keyboard-activatable, fully tappable (not just a small nested button). - A completed/closed row without a review shows a compact star-strip CTA (
RatingInputread-only decor + «ثبت نظر» text) via a gateduseReviewEligibility(bookingId, { enabled: isCompleted })— no extra query fires for non-completed rows.
Booking detail — BookingDetailView.tsx:
- The header is now a hero: a next-upcoming-session headline («ویزیت ۲ · فردا ۰۹:۰۰», via
formatRelativeTime+ a Shamsi fallback past 7 days, bidi-isolated clock digits), the frozen visit address (best-effort parsed offaddressSnapshotJson— see REQ-045), a nurse-identity row, and a client-side.icsadd-to-calendar download (newcomponents/booking/ics.ts, no backend seam — Gregorian UTC in the file, Shamsi in the UI). - An EVV presence headline («پرستار در محل است · ورود ۰۹:۰۲», success-toned) renders above the address
whenever a session is currently checked in — elevates the existing advisory EVV data instead of leaving
it buried per-session. (The equivalent compact list-row indicator from the phase brief was not
built:
BookingListItemDtocarries no per-row EVV/session state, and fetching it via an extra query per in-progress row would be an N+1 anti-pattern — filed as a note, not a REQ, since no clean single-field addition was obvious; flagged for a human product/API call.) - The vertical
StatusTimelineswap the phase asked for (in place ofStepperHeader) was already done in ui-phase-1 (BookingStatusTimelinealready rendersStatusTimeline) — verified, not re-built; the stale in-code comment claiming otherwise is fixed in this pass. - Fixed the
unnamed_nursekey-misuse defect: a newbd_nurse_label("پرستار") key is now the nurse header-fact label;unnamed_nursestays exactly the no-name fallback value it was written for. SessionCard's outerPaperswapped forSurfaceCard(identicalpadding="sm"visual, now on the shared radius token) — the "align to phase-1 card anatomy" ask.
Cancel flow — bookings/[id]/cancel/page.tsx:
- Two off-ramps above the disclosure: «تغییر زمان» and «گفتگو با پشتیبانی», both opening the existing
ContactSupportDialog(pre-linked to the booking, categoriescoordination/supportrespectively) plus a one-line nurse-impact note. Real rescheduling stays DEFERRED (product decision + backend, per the phase brief — no REQ filed, it was already a known/flagged gap). - The reason
useStateno longer pre-defaults to'changed_mind'— it starts empty with a disabled placeholderMenuItem, and the continue CTA stays disabled until a reason is chosen. CancellationPolicyDisclosureis untouched (byte-identical behavior, per the keep-list).
Review flow — bookings/[id]/review/page.tsx + the list row:
- A context-recap card (service name off the variant snapshot, nurse avatar + name, Shamsi visit date —
all off the already-cached
useBookingDetail, no new fetch) renders above both the eligible-form and the already-reviewed states. - The moderation-expectation note («نظر شما پس از بررسی منتشر میشود») now renders before submit, not only in the post-submit "under review" state.
- Fixed the ungated-hook defect:
useMyReviewForBooking(bookingId)now passes{ enabled: reviewable }(booking?.status === 'completed' || 'closed'), matching the exact gate the booking-detail page already used for the same hook. - The list-row star-strip CTA (above) is the "post-completion review nudge" half of this deliverable.
Misc verified defects:
BookingRequestSummaryCard'swhenLabelbidi-isolated: the date·time-range now wraps the clock-digits segment in adir="ltr"tabular-numsspan, matchingSessionCard.tsx's existing precedent exactly. A new co-located test asserts the isolation.- The «ادامه پرداخت ←» arrow-in-string CTA (
booking.continue_payment) was not touched — confirmed it's phase 12's catalog-wide sweep, per the phase brief's own scope note; only strings genuinely edited this phase had their arrows reconsidered (none needed it).
What is now testable (and exactly how)
- 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 submit lists what's still missing.
- Pick a date from the Jalali chip strip (or the calendar-icon popover) and tap «صبح ۸–۱۲» → the time fields fill silently; tap «زمان دلخواه» → the free time fields appear. The address shows a compact text row with a «تغییر» link, never a grid-canvas stand-in.
- Submit → C5 shows the countdown ring with «حدود …» framing (switches to ticking digits under 10 min). Tap «انصراف از درخواست» → the dialog's neutral button reads «نه، نگه دار» and keeps the request; the red «بله، انصراف از درخواست» button cancels it.
- Leave C5 →
/bookings«در انتظار پاسخ» tab shows the pending request with a live mini-countdown and a tab badge; the row deep-links back to C5. Reject a request with a non-gender/coverage reason (dev nurse inbox) → C5's terminal card offers both «درخواست دوباره با زمان دیگر» (C4 reopens fully prefilled, including patient/address) and «پرستاران مشابه» (search prefilled with the same city/gender). - Seed >20 bookings → the active/past tabs load 20, then «نمایش بیشتر» reveals the rest; every row is keyboard-reachable (Tab + Enter) and carries a status-colored accent stripe.
- Open an active booking with a session scheduled → the hero reads «ویزیت N · <relative/Shamsi>
- Start a cancellation → the reason select is empty (continue disabled until chosen); «تغییر زمان»/«گفتگو با پشتیبانی» open the support dialog pre-linked to the booking; completing the flow shows the unchanged policy disclosure.
- Open a completed booking's
/bookingsrow → a star-strip CTA (only when un-reviewed) deep-links to the review page, which shows the service/nurse/date recap and the moderation note before any input. npm run checkandnpm run test:ciare green (101/101 suites, 437/437 tests, including the newJalaliDateIntentPickerandBookingRequestSummaryCardbidi tests).
What is mocked / waiting on a real service
Nothing new mocked — bookingRequests/bookings/reviews/refunds/tickets all run real, unchanged
from refinement-phase-4, per the phase's own "None introduced" note. Every deliverable is client-side over
existing seams; the .ics file is generated entirely in the browser.
Contracts
- Consumed:
services/bookingRequests(b8),services/bookings(b9),services/refunds(b11),services/reviews(b14),services/tickets(b15) — all unchanged. - Requested (
dev/shared-working-context/frontend/requests/for-backend.md, next free numbers 044–045):- REQ-044 — a structured
nurseRejectionReasonCodeonBookingRequestDto. C5's same-nurse-retry gate currently approximates this with a keyword heuristic over the freeformnurseRejectionReasontext (documented in-code,rejectionAllowsSameNurseRetry()). - REQ-045 — a typed
addressSnapshot/variantSnapshotshape onBookingDetailDtoin place of the opaque*SnapshotJsonstrings, whose field names were confirmed to drift across the seed fixtures (city/cityName/cityNameFa,line/addressLine). The client'saddressSnapshotLabel()tries every candidate key defensively; no user-facing defect today, just an unenforced contract.
- REQ-044 — a structured
Docs updated
client/CLAUDE.md"Project Structure": the/bookingstree (tabs + wireduseCustomerRequests+ load-more, C4/C5 redesign notes, the cancel/review off-ramp and gating fixes), the newJalaliDateIntentPickerentry, thebooking/composite line (hero,.ics, EVV presence, theStatusTimeline-not-StepperHeadercorrection — the prior line was already stale before this phase),CountdownTimer's ring now having a live consumer, andBookingRequestSummaryCard's bidi fix.
Follow-ups for later phases
- List-row EVV presence indicator (phase brief §3.4) was not built —
BookingListItemDtohas no per-row EVV/session field and an N+1 per-row fetch would be the wrong shape; needs a product/API decision (a lightweightcurrentlyCheckedInboolean on the list DTO, or accept the gap) before a future phase attempts it. - REQ-044/045 (rejection-reason code, snapshot typing) — no phase currently owns picking these up; flagged for whichever backend phase next touches b8/b9.
- The arrow-in-string
continue_paymentCTA sweep stays phase 12's, as scoped.
Memory
Saved a project-type memory (ui_phase_5_booking_lifecycle.md, indexed in MEMORY.md) covering: the
lifecycle tabs model (client-side status bucketing over one growing-pageSize query, not per-tab server
filters — a deliberate trade-off since the list API takes only a single status), the terminal-state
recovery decision tree (same-nurse retry vs similar-nurses, gated by a keyword heuristic pending REQ-044),
the confirm-dialog labeling convention (ConfirmDialog cancelLabel=keep/neutral,
confirmLabel=destructive/error — apply to every future confirm dialog), the presence-headline pattern, and
what phase 6 (checkout) should know about the accepted-request → payment handoff surfaces touched here (C5's
payment countdown + continue_payment CTA, unchanged in shape).