ui phase 5

This commit is contained in:
hamid
2026-07-18 09:51:03 +03:30
parent 53b4e1b0a4
commit 4c70d8e424
42 changed files with 2834 additions and 548 deletions
@@ -0,0 +1,189 @@
# 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, tappable `TrustBadge`, gender chip — off the already-fetched
`useNurseProfile`. The family always sees who they're inviting home before filling anything in.
- A «چه اتفاقی می‌افتد؟» strip reusing C5's own `StepperHeader` three-step labels (`step_submitted` /
`step_awaiting` / `step_payment`) at `activeStep={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 shared `JalaliDateIntentPicker` (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-field `touched` map set `onBlur`; 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 from `cta_missing_*` keys.
- The fake-map preview (`AddressMapPicker` wrapped in `pointerEvents:'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 real `Stack` containers.
- C4 now also reads `patient_id`/`address_id` query params (extending the existing `nurse_id`/`variant_id`/
`required_gender` handoff 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 («حدود ۳ ساعت» / «حدود ۲۵ دقیقه», new `countdown_about_hours`/`countdown_about_minutes`
keys) that switches to the ticking clock in the final minutes. A one-line «نتیجه را به شما اطلاع می‌دهیم»
note sits under it. The payment countdown is unchanged (no accurate `windowStart` exists on the DTO for
it — never fabricated client-side).
- **Fixed the cancel-dialog label defect**: the hand-rolled `Dialog` replaced with the shared
`ConfirmDialog`. 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-unused `cancel_confirm_yes`
key 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 `/search` carrying the request's city/district/gender. For rejections, the same-nurse retry
is suppressed when `nurseRejectionReason` (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 to `pending_nurse_response` +
`accepted_awaiting_payment`), with a live mini `CountdownTimer` per 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 growing
`pageSize` ("load more", the exact C2 results pattern) — booking #21+ is reachable via the button.
- Rows are `AccentCard`s with a status-toned `borderInlineStart` + a soft `StatusChip`, `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 (`RatingInput` read-only decor +
«ثبت نظر» text) via a gated `useReviewEligibility(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 off `addressSnapshotJson` — see REQ-045), a nurse-identity row, and a
client-side **`.ics` add-to-calendar download** (new `components/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: `BookingListItemDto` carries 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 `StatusTimeline` swap the phase asked for (in place of `StepperHeader`) was **already done**
in ui-phase-1 (`BookingStatusTimeline` already renders `StatusTimeline`) — verified, not re-built; the
stale in-code comment claiming otherwise is fixed in this pass.
- Fixed the `unnamed_nurse` key-misuse defect: a new `bd_nurse_label` ("پرستار") key is now the nurse
header-fact **label**; `unnamed_nurse` stays exactly the no-name **fallback value** it was written for.
- `SessionCard`'s outer `Paper` swapped for `SurfaceCard` (identical `padding="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, categories `coordination`/`support` respectively) 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 `useState` no longer pre-defaults to `'changed_mind'` — it starts empty with a disabled
placeholder `MenuItem`, and the continue CTA stays disabled until a reason is chosen.
- `CancellationPolicyDisclosure` is 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`'s `whenLabel` bidi-isolated: the date·time-range now wraps the clock-digits
segment in a `dir="ltr"` `tabular-nums` span, matching `SessionCard.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)
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 submit lists what's still missing.
2. 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.
3. 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.
4. 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).
5. 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.
6. Open an active booking with a session scheduled → the hero reads «ویزیت N · <relative/Shamsi> <time>»,
the frozen address, a nurse avatar, and a working «افزودن به تقویم» `.ics` download. Check a nurse in
(dev EVV sim) → the hero shows «پرستار در محل است · ورود …».
7. 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.
8. Open a completed booking's `/bookings` row → 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.
9. `npm run check` and `npm run test:ci` are green (101/101 suites, 437/437 tests, including the new
`JalaliDateIntentPicker` and `BookingRequestSummaryCard` bidi 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 044045):
- **REQ-044** — a structured `nurseRejectionReasonCode` on `BookingRequestDto`. C5's same-nurse-retry
gate currently approximates this with a keyword heuristic over the freeform `nurseRejectionReason`
text (documented in-code, `rejectionAllowsSameNurseRetry()`).
- **REQ-045** — a typed `addressSnapshot`/`variantSnapshot` shape on `BookingDetailDto` in place of the
opaque `*SnapshotJson` strings, whose field names were confirmed to drift across the seed fixtures
(`city`/`cityName`/`cityNameFa`, `line`/`addressLine`). The client's `addressSnapshotLabel()` tries
every candidate key defensively; no user-facing defect today, just an unenforced contract.
## Docs updated
- `client/CLAUDE.md` "Project Structure": the `/bookings` tree (tabs + wired `useCustomerRequests` +
load-more, C4/C5 redesign notes, the cancel/review off-ramp and gating fixes), the new
`JalaliDateIntentPicker` entry, the `booking/` composite line (hero, `.ics`, EVV presence, the
`StatusTimeline`-not-`StepperHeader` correction — the prior line was already stale before this phase),
`CountdownTimer`'s ring now having a live consumer, and `BookingRequestSummaryCard`'s bidi fix.
## Follow-ups for later phases
- **List-row EVV presence indicator** (phase brief §3.4) was not built — `BookingListItemDto` has no
per-row EVV/session field and an N+1 per-row fetch would be the wrong shape; needs a product/API
decision (a lightweight `currentlyCheckedIn` boolean 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_payment` CTA 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).