ui phase 5
This commit is contained in:
@@ -602,6 +602,61 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
- **Status:** open (partner login-routing deferred; `/partner` reachable by direct nav + the partnerCenter mock)
|
||||
- **Status:** open
|
||||
|
||||
## REQ-040 — Index-row enrichment: `variantDisplayName` + optional `topReviewTag` — filed by ui-phase-4 — 2026-07-18
|
||||
- **Need:** Two additions to `NurseSearchResultDto` (`GET search/nurses`):
|
||||
1. `variantDisplayName` (required, string) — the variant's display name, so the C2 result card can
|
||||
name the exact service being bought (the row **is** a variant).
|
||||
2. `topReviewTag` (optional, string | null) — a one-line top review tag (e.g. «منظم و دقیق») from the
|
||||
review-tag vocabulary, for an optional one-line trust cue on the card.
|
||||
- **Why:** `NurseResultCard` v2 (ui-phase-4) is the four-second decision unit. Today the index row carries
|
||||
no variant label, so a nurse offering three variants renders as three near-identical cards differing
|
||||
only by price; the card falls back to the row's **category** name (passed in by the page from the cached
|
||||
catalog reference data) until this lands — a genuine wire gap, not a display choice. The client type
|
||||
(`services/search/types.ts` `NurseSearchResult.topReviewTag`) and `NurseResultCard` already render the
|
||||
tag conditionally so this is a zero-code swap once served.
|
||||
- **Proposed shape:** `NurseSearchResultDto { …, variantDisplayName: string, topReviewTag?: string | null }`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-041 — Free-text search (`q`) over nurse/variant/category names — filed by ui-phase-4 — 2026-07-18
|
||||
- **Need:** A `q` query param on `GET search/nurses` matching against nurse display name, variant display
|
||||
name, and category name (case-insensitive substring or better).
|
||||
- **Why:** The Home search bar was a dead affordance — it pushed `?q=` to C1, which silently discarded it
|
||||
(C1 reads only `category_id`). ui-phase-4 replaced the free-text field with a tappable entry point straight
|
||||
to C1 (decision: the index has no text column, variant names aren't client-queryable, and the only
|
||||
matchable dataset client-side — 5–6 cached category names — is already better served by the category grid
|
||||
directly below; a half-working text field over-promises exactly where trust matters). This REQ is the
|
||||
upgrade path noted in the component's JSDoc: once `q` is served, the Home search bar can become a real
|
||||
typeahead without changing its i18n keys.
|
||||
- **Proposed shape:** `GET search/nurses?...&q=<string>` — matches folded into the existing filter set
|
||||
(AND'd with category/city/gender/price), same `Paginated<NurseSearchResultDto>` response shape.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-042 — `nurseGender` on `NursePublicProfileDto` — filed by ui-phase-4 — 2026-07-18
|
||||
- **Need:** Add `nurseGender` (`'male'|'female'`) to the `GET nurses/{id}/profile` response.
|
||||
- **Why:** C3's header would show a gender chip alongside years-of-experience/rating (same-gender
|
||||
matching is load-bearing throughout the product), but the public profile DTO doesn't serve it — the
|
||||
client's `nurseGender: 'female'` is a **placeholder stub** (explicitly commented "unused by the C3 page").
|
||||
ui-phase-4 keeps the C3 header gender chip **omitted** rather than render the stub as truth; the C2 card
|
||||
and the carried `required_gender` query param already cover the matching flow honestly in the meantime.
|
||||
- **Proposed shape:** `NursePublicProfileDto { …, nurseGender: 'male' | 'female' }`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-043 — Public per-step verification detail (step codes + decision dates) — filed by ui-phase-4 — 2026-07-18
|
||||
- **Need:** Extend the public `GET nurses/{id}/trust_badge` read (or a sibling public endpoint) with
|
||||
per-step passed-check detail: step `code`, `status`, and `decidedAt`/`passedAt` date — not just the
|
||||
aggregate `credentialTypes[]` + `approvedAt` it serves today.
|
||||
- **Why:** The new shared `VerificationPanel` component (ui-phase-4, `src/components/VerificationPanel/`)
|
||||
is the "what Balinyaar verified" explainer — reused by the C3 profile section, the tappable `TrustBadge`
|
||||
bottom-sheet/dialog, and (per the phase's cross-reference) phase 8's public-profile preview. Today it can
|
||||
only render one row per `credentialTypes[]` entry (identity/license/INO/background-check are folded into
|
||||
three generic credential-type codes) plus the single aggregate `approvedAt` — it cannot list identity
|
||||
check, Shahkar match, and license individually with their own dates. Until this lands the panel stays
|
||||
honest (renders only what's served: the credential-type rows + the one approval date — no invented steps
|
||||
or fake dates).
|
||||
- **Proposed shape:** `TrustBadgeDto { …, steps?: [{ code: string, status: 'passed', decidedAt: string }] }`
|
||||
(only `passed` steps are ever publicly listed — never a pending/failed step's detail).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-039 — WebOTP-conformant OTP SMS template — filed by ui-phase-3 — 2026-07-17
|
||||
- **Need:** the OTP SMS body must end with the origin-bound last line `@<domain> #<code>` (the
|
||||
[WebOTP](https://web.dev/articles/web-otp) / origin-bound one-time-code convention), e.g.:
|
||||
@@ -620,3 +675,38 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
- **Proposed shape:** append `\n@<domain> #<code>` as the SMS's last line, where `<domain>` is the site's own
|
||||
host (no scheme, no trailing slash) and `<code>` is the exact code the user must enter.
|
||||
- **Status:** open — client-side WebOTP wiring ships regardless and degrades gracefully until this lands.
|
||||
|
||||
## REQ-044 — Structured `nurseRejectionReasonCode` on `BookingRequestDto` — filed by ui-phase-5 — 2026-07-18
|
||||
- **Need:** A stable, enumerated `nurseRejectionReasonCode` (e.g. `gender_preference` | `outside_coverage` |
|
||||
`schedule_conflict` | `other`) alongside the existing freeform `nurseRejectionReason` text on
|
||||
`BookingRequestDto`.
|
||||
- **Why:** C5's terminal-state recovery (phase §3.2) offers "request again with the same nurse, a different
|
||||
time" only when the rejection reason **isn't** a hard gender/coverage block — otherwise only "similar
|
||||
nurses" is offered, since re-requesting the same nurse would just fail again. `nurseRejectionReason` is
|
||||
nurse-authored free text with no code, so the client can only approximate this with a keyword heuristic
|
||||
over the (possibly translated/paraphrased) string — implemented in
|
||||
`bookings/request/[id]/page.tsx`'s `rejectionAllowsSameNurseRetry()`, matching `gender`/`coverage`/`area`
|
||||
and their Persian equivalents. A real code would replace the heuristic with an exact check.
|
||||
- **Proposed shape:** `BookingRequestDto { …, nurseRejectionReasonCode: 'gender_preference' |
|
||||
'outside_coverage' | 'schedule_conflict' | 'other' | null }`, required whenever `nurseRejectionReason` is
|
||||
set. `reject_dialog`'s reason field would become a category select (+ optional free-text) instead of a
|
||||
single free-text field.
|
||||
- **Status:** open — the client ships the keyword heuristic in the meantime (documented as a known
|
||||
approximation in the phase report), never blocking the recovery UI.
|
||||
|
||||
## REQ-045 — Typed `addressSnapshot`/`variantSnapshot` on `BookingDetailDto` — filed by ui-phase-5 — 2026-07-18
|
||||
- **Need:** Replace (or supplement) `addressSnapshotJson`/`variantSnapshotJson` (opaque JSON strings) with
|
||||
typed fields: `addressSnapshot: { title, cityNameFa, cityNameEn, districtNameFa, districtNameEn,
|
||||
addressLine, postalCode } | null` and `variantSnapshot: { displayName, priceUnit }`.
|
||||
- **Why:** The booking-detail hero (phase §3.4) needed the frozen visit address for its "next visit" card,
|
||||
but the seed fixtures show the snapshot blob has **no stable schema** — field names drift across bookings
|
||||
(`city`/`cityName`/`cityNameFa`, `line`/`addressLine`; see `services/bookings/apis/mockApi.ts`'s `addr5001`
|
||||
vs the booking-5005 fixture). The client (`BookingDetailView.tsx`'s `addressSnapshotLabel()`) currently
|
||||
tries every candidate key and joins whatever resolves — a best-effort parse, not a contract. The nurse view
|
||||
already masks this field to `null` server-side (two-stage disclosure); only the shape within the customer's
|
||||
non-null value is the gap.
|
||||
- **Proposed shape:** as above — the same shape `BookingRequestDisplayContext`'s address fields already use
|
||||
elsewhere in the frontend, so the mapping is a straight carry-over from whatever snapshotting logic
|
||||
produces the JSON blob today.
|
||||
- **Status:** open — the client keeps the defensive multi-key parse until this lands; no user-facing defect
|
||||
today (the address still renders when any recognizable key is present), just an unenforced contract.
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# UI Phase 4 — Customer Storefront — Report (2026-07-18)
|
||||
|
||||
## What was built
|
||||
|
||||
**Home (A5)** — `HomeScreen.tsx`:
|
||||
- The dead free-text search bar (`?q=` silently discarded by C1) replaced with a tappable faux-input
|
||||
(`ButtonBase`) that routes straight to C1. Decision: the search index has no text column, variant names
|
||||
aren't client-queryable, and the only matchable dataset client-side (5–6 cached category names) is
|
||||
already better served by the category grid directly below — a half-working text field over-promises
|
||||
where trust matters most. The upgrade path is documented in the component's JSDoc and filed as REQ-041.
|
||||
- A compact one-line `TrustStrip` (escrow · verified nurses · support) under the greeting.
|
||||
- The patient-record `NudgeCard` is now gated on a derived completeness signal (a patient with no
|
||||
recorded conditions) instead of rendering forever, and is dismissible for the session (a module-scoped
|
||||
flag — survives client-side navigation, resets on a hard reload; deliberately not a cookie/localStorage
|
||||
write, since it's ephemeral UI state, not app/auth state).
|
||||
- A `RebookRow` — up to 2 "رزرو دوباره با …" cards sourced from `useBookingList('customer')`, deduplicated
|
||||
by nurse, each resolving its `nurseId` via a per-card `useBookingDetail` call (the list row doesn't carry
|
||||
`nurseId`) and deep-linking to the nurse's C3 profile. Renders nothing when there's no booking history.
|
||||
- The pre-existing `isError` retry branch on `usePatients()` was found **already correct** in the current
|
||||
code (the audit's finding predates it) — verified, not re-fixed.
|
||||
|
||||
**Search (C1)** — `SearchScreen.tsx` + `useSearchFilters.ts`:
|
||||
- The native `<input type="date">` replaced by a Jalali day-chip strip (`JalaliDatePicker` `chips` variant,
|
||||
extended with optional `todayLabel`/`tomorrowLabel` props for «امروز»/«فردا») plus a calendar-icon entry
|
||||
into the full Jalali grid (a `Popover`) for later dates — intent-only semantics unchanged.
|
||||
- The inline `ToggleButtonGroup` gender facet replaced by the shared `GenderToggle`, extended with an
|
||||
opt-in `allowAny` mode (a discriminated-union prop shape — the default booking-context contract is
|
||||
byte-for-byte unchanged for every other caller).
|
||||
- The live-count CTA is now a `StickyActionBar` (new shared primitive) — never a tappable "مشاهده ۰
|
||||
پرستار": at zero results it shows a non-CTA message with a relaxation hint instead of the button.
|
||||
- `useSearchFilters` now hydrates **every** field from the initial URL (`searchParamsToFilters`), not just
|
||||
`category_id` — a full filter set carried back from a C2 recap chip rehydrates C1 completely. A
|
||||
client-only `province_id` query param (not part of `NurseSearchFilters`/the search cache key) carries the
|
||||
province id so `CascadingRegionSelect`'s city dropdown can prefill without a server round trip.
|
||||
|
||||
**Results (C2)** — `results/page.tsx`:
|
||||
- A tappable filter-recap chip row (category · region · gender · price) under the count — every chip
|
||||
performs the same navigation: back to C1 with the **entire current query string** (C2's URL is already a
|
||||
superset of everything C1 set, including `province_id`/`date`), so hydration is exact.
|
||||
- The dead single-option sort `<TextField select>` replaced with a static "مرتبشده بر اساس امتیاز" caption.
|
||||
- Skeleton twins (`NurseResultCard.Skeleton`) were already wired in the current code; updated to match the
|
||||
v2 card anatomy.
|
||||
|
||||
**`NurseResultCard` v2** (this phase owns it) — rebuilt as the four-second decision unit:
|
||||
- Added: a service/variant label (falls back to the category name via a page-supplied `serviceLabel` prop
|
||||
until REQ-040's `variantDisplayName` lands), a quiet nurse-gender chip, the completed-visits count
|
||||
(«N ویزیت موفق», already-served data that was fetched and never rendered), an optional one-line top-review
|
||||
tag (`topReviewTag`, REQ-040), and a tappable `TrustBadge` (now passes `nurseId`).
|
||||
- `NurseResultCardSkeleton` updated to the new anatomy (an extra meta-row).
|
||||
|
||||
**Nurse profile (C3)** — `nurse/[nurseId]/page.tsx`:
|
||||
- Header now shows the completed-visits count (served, previously unrendered). No gender chip — the
|
||||
public profile DTO doesn't serve `nurseGender` (the client's `'female'` stub is explicitly a
|
||||
placeholder); filed as REQ-042 rather than rendering it.
|
||||
- New `VerificationSection` renders the shared `VerificationPanel`, fed by `useNurseTrustBadge(nurseId)`.
|
||||
- `TrustBadge` now passes `nurseId`, making the badge tappable everywhere it appears (C2 cards + C3).
|
||||
- The primary CTA is now a `StickyActionBar` (price-from beside "درخواست رزرو") — survives the infinite
|
||||
reviews list.
|
||||
- An optional latest-review snippet renders on the services tab when `profile.latestReview` is present.
|
||||
- The reviews tab's fractional `RatingInput` (no `Math.round`) was found **already correct** in the current
|
||||
code — verified, not re-fixed (phase 1's RatingInput v2 already replaced the rounding).
|
||||
|
||||
**New shared components** (both co-located-tested, both reused by phase 8 per the phase's cross-reference):
|
||||
- `VerificationPanel` (`src/components/VerificationPanel/`) — "what Balinyaar verified": one row per
|
||||
`TrustBadge.credentialTypes[]` (i18n off the `verification` namespace's `step_*` codes, never a raw wire
|
||||
value) + the approval date. Renders only what is served — no invented steps, no fake dates.
|
||||
- `TrustBadge`'s new opt-in `nurseId` prop — tappable, opens a bottom-sheet (mobile)/dialog (desktop)
|
||||
rendering `VerificationPanel`, fed by a **lazily-enabled** `useNurseTrustBadge` fetch (only queries once
|
||||
the explainer is actually opened). Implemented as an inner `InteractiveTrustBadge` subcomponent so the
|
||||
**default** (no `nurseId`) badge — used everywhere else in the app (`ProfileSummary`, etc.) — calls no
|
||||
query hook at all and needs no `QueryClientProvider` in its callers' tests.
|
||||
- `StickyActionBar` (`src/components/common/StickyActionBar/`) — the bottom-pinned action-bar shell shared
|
||||
by C1's live-count CTA and C3's booking CTA. Composes with the shell's existing `BottomBar` safe-area
|
||||
handling (a flex sibling below the scrolling `main`) rather than reimplementing
|
||||
`env(safe-area-inset-bottom)`.
|
||||
|
||||
**Copy**: `search.empty_suggest_city` («شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز») deleted from both
|
||||
`en.json`/`fa.json`, replaced with `empty_suggest_date` ("try a different date" — an honest relaxation the
|
||||
family can actually act on). Reused in the C1 zero-count sticky-bar hint too.
|
||||
|
||||
## What is now testable (and exactly how)
|
||||
|
||||
1. **Home**: log in as the seeded customer (`0912000000x`). Tap the search field on Home → lands on
|
||||
`/search` with no `?q=` in the URL. Stop the API and reload → an error card with «تلاش مجدد», not an
|
||||
eternal spinner. Restart → home recovers; the trust strip shows under the greeting; a customer with a
|
||||
completed booking sees a «رزرو دوباره با …» card that opens the nurse's profile.
|
||||
2. **C1**: `/search` — the date filter shows «امروز»/«فردا» + Shamsi day chips (no native browser
|
||||
calendar anywhere); tapping the calendar icon opens the full Jalali grid in a popover. Pick a category +
|
||||
city → the count CTA is pinned at the bottom while scrolling the page. Pick filters matching nothing
|
||||
(e.g. an unusually high min price) → the sticky bar shows the non-CTA "no matches" message, not a
|
||||
tappable button.
|
||||
3. **C2**: run a search → recap chips show category/region/gender/price; tap any chip → C1 opens with
|
||||
every filter pre-filled (category, region incl. province prefill, gender, price, date); browser-back
|
||||
returns to identical results with zero network (cache hit, unchanged from before this phase). The
|
||||
header reads «مرتبشده بر اساس امتیاز» as static text, not a dropdown.
|
||||
4. **Cards**: a nurse with multiple variants shows distinguishable cards (category/variant label + price);
|
||||
every card shows the gender chip and «N ویزیت موفق»; tapping the ✓ badge opens the verification
|
||||
bottom-sheet/dialog (fetches `nurses/{id}/trust_badge` lazily on open).
|
||||
5. **C3**: open a profile → header shows completed visits + rating; the `VerificationPanel` section lists
|
||||
the served credential types + approval date (Shamsi); open the reviews tab and scroll deep → «درخواست
|
||||
رزرو» stays pinned at the bottom; a 4.5 average renders as a fractional star row. Tap the CTA → the C4
|
||||
request form receives the same nurse/variant/gender/date params as before.
|
||||
6. Repeat 1–5 on `/en` (LTR) and in dark mode; on a mobile viewport confirm both sticky bars sit above the
|
||||
`BottomBar` and the home-indicator safe area (the `BottomBar` already owns
|
||||
`env(safe-area-inset-bottom)`; `StickyActionBar` is a separate flex sibling above it, inside the
|
||||
scrolling `main`, so it never overlaps).
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
|
||||
Nothing new mocked this phase — search stays real (`USE_SEARCH_MOCK = false`, unchanged) and verification
|
||||
stays mock-primary (`USE_VERIFICATION_MOCK`, unchanged) exactly as before; this phase only consumes the
|
||||
existing `useNurseTrustBadge` hook (previously built, never wired into a page) and adds no new fetch code
|
||||
outside existing service hooks. No mock registry entries added/changed.
|
||||
|
||||
## Contracts
|
||||
|
||||
- Consumed: `services/search` (b7 + REQ-012, unchanged), `services/verification`'s public trust-badge read
|
||||
(b6, unchanged — this phase is the first to actually call `useNurseTrustBadge` from a page/component).
|
||||
- Requested (`dev/shared-working-context/frontend/requests/for-backend.md`, next free numbers 040–043):
|
||||
- **REQ-040** — `variantDisplayName` (required) + optional `topReviewTag` on `NurseSearchResultDto`.
|
||||
- **REQ-041** — free-text `q` search over nurse/variant/category names on `GET search/nurses` (the
|
||||
Home search bar's upgrade path).
|
||||
- **REQ-042** — `nurseGender` on `NursePublicProfileDto` (the C3 header gender chip, currently omitted).
|
||||
- **REQ-043** — public per-step verification detail (step codes + decision dates) so `VerificationPanel`
|
||||
can list identity/Shahkar/license individually instead of folding to `credentialTypes[]`.
|
||||
|
||||
## Docs updated
|
||||
|
||||
- `client/CLAUDE.md` "Project Structure": the C1/C2/C3 line items (Jalali chips, sticky CTA, recap chips,
|
||||
dossier layout), the new `VerificationPanel`/`StickyActionBar` entries, and updated `TrustBadge`/
|
||||
`GenderToggle`/`NurseResultCard`/`JalaliDatePicker` lines noting their new modes.
|
||||
|
||||
## Follow-ups for later phases
|
||||
|
||||
- **Multi-variant collapse.** Collapsing a nurse's several variant rows into one card with a price range +
|
||||
"N خدمت" disclosure needs the `variantDisplayName` REQ (040) served first — noted as a design follow-up,
|
||||
not built this phase (the category-name fallback keeps rows distinguishable meanwhile).
|
||||
- **Public/guest storefront and landing page** — deferred to phase 13 (unchanged from the phase brief).
|
||||
- **Save/favorite/share nurses** — post-MVP product decision (unchanged from the phase brief).
|
||||
- **Full ICU zero-case copy sweep** (ratings/counts pluralization polish across the whole app) — deferred
|
||||
to phase 12 per the phase brief; this phase only ensured the C1 zero-count state is never a tappable CTA.
|
||||
- Phase 8's public-profile preview should reuse `VerificationPanel` and `TrustBadge`'s `nurseId` explainer
|
||||
mode unchanged, per the phase's explicit cross-reference.
|
||||
|
||||
## Memory
|
||||
|
||||
Saved a `project`-type memory (`ui_phase_4_customer_storefront.md`, indexed in `MEMORY.md`) covering: the
|
||||
`NurseResultCard`/`TrustBadge` ownership split with phase 8, the `VerificationPanel` shared-component
|
||||
contract, the `TrustBadge` split-component gotcha (query hooks must live in a conditionally-**mounted**
|
||||
subcomponent, not a conditionally-**enabled** hook call, to avoid forcing `QueryClientProvider` on every
|
||||
caller's tests), the C3 profile DTO's missing `nurseGender` (REQ-042, never render the placeholder stub),
|
||||
and the `StickyActionBar`/`province_id`-carry patterns for future sticky-CTA or region-prefill needs.
|
||||
@@ -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 044–045):
|
||||
- **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).
|
||||
Reference in New Issue
Block a user