# 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.
