# UI Phase 4 — Customer storefront

> **Mission:** turn home → search → results → nurse profile from functional screens into a **storefront that
> answers "would I let this person into my mother's home?"**. The data layer already fetches the trust signals
> (gender, completed visits, verification badge, ratings) — the UI drops most of them on the floor, ships a dead
> search bar, a fake sort control, a Gregorian date picker in a Shamsi product, and a primary CTA buried under an
> infinite review list. This phase fixes the funnel's honesty defects and builds the reusable trust-presentation
> components (`VerificationPanel`, the tappable `TrustBadge` explainer) that phase 8's public-profile preview reuses.
>
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
> **Unlocks:** [Phase 5 (booking funnel)](ui-phase-5-booking-lifecycle.md) + [Phase 8](ui-phase-8-nurse-business-and-verification.md) (reuses the trust components built here)
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**

## 1. Context — where this sits

The storefront is the four customer discovery screens: `client/src/app/[locale]/(private-routes)/(customer)/page.tsx`
(A5 home), `search/page.tsx` (C1 filters), `search/results/page.tsx` (C2 results), and
`search/nurse/[nurseId]/page.tsx` (C3 profile). Phases 0–2 already de-startered the theme, primitives, and chrome —
this phase redesigns the route tree itself. Diagnosed defects (all verified in code, full evidence in
[audit/customer-storefront.md](audit/customer-storefront.md)):

1. **The home search bar is a dead affordance.** `HomeSearchBar` pushes `?q=<query>` (page.tsx L130) but
   `SearchFilterScreen` reads only `category_id` (search/page.tsx L49–50) and silently discards `q`. The placeholder
   promises «جستجوی خدمت یا پرستار…» and the input does nothing.
2. **A failed patients query bricks the home forever.** The gate `if (data == null || isEmpty) return <AppLoading />`
   (page.tsx L66–68) has no `isError`/retry branch — a transient API failure leaves the app's front door on a spinner.
3. **The patient-record nudge renders unconditionally forever** (page.tsx L96–102), unlike the profile nudge which is
   gated on `hasCustomerProfile` (L103).
4. **The C1 visit-date filter is a native Gregorian `type="date"`** (search/page.tsx L104–110) in a product where
   every displayed date is Shamsi; the gender facet re-implements an inline `ToggleButtonGroup` (L86–100) instead of
   the shared `GenderToggle`; the live-count CTA is the last child of a long scrolling form (L130–140).
5. **C2 ships a fake sort** — a `TextField select` with one hard-coded `MenuItem`, `value="rating"`, no `onChange`
   (results/page.tsx L67–69) — and no filter recap: `backToFilters` renders only inside the EmptyState (L58/L88), so
   editing filters from a populated list means browser-back.
6. **`NurseResultCard` is information-thin.** The result row IS a bookable variant, but the card never names the
   service — three variants of one nurse render as identical cards differing only in price. `nurseGender` and
   `totalCompletedBookings` are **served by the real backend today** (REQ-012 delivered them onto the index row — see
   `services/search/apis/clientApi.ts` `NurseSearchResultDto`) and never rendered. The variant display name is a
   genuine wire gap: neither `NurseSearchResult` (types.ts) nor the DTO carries it.
7. **C3 doesn't read as a dossier.** The «درخواست رزرو» CTA is the last page child (nurse/[nurseId]/page.tsx L92–101)
   — below the infinite reviews list when that tab is open — and not sticky. `totalCompletedBookings` is fetched and
   never shown; `TrustBadge` is a static chip with no affordance to learn *what* was verified; and the profile DTO
   does **not** serve gender (`clientApi.ts` stubs `nurseGender: 'female'` with an "unused" comment — never render it).
8. **The empty-results copy is nonsense**: `search.empty_suggest_city` = «شهر نزدیک دیگری مانند مشهد، اصفهان یا شیراز
   را امتحان کنید» (fa.json L361) — a family cannot "try Shiraz"; the patient lives where they live.

**What already exists (do not rebuild):**

- The search architecture: **the filter object IS the URL IS the React Query cache key**
  (`services/search/keys.ts` + `filterParams.ts`), `keepPreviousData`, debounced Toman price inputs, live count on C1.
- The **real** search backend (`USE_SEARCH_MOCK = false` since refinement-phase-4): the index row serves
  `nurseName`/`avatarUrl`/`distanceKm`/`nurseGender`/`totalCompletedBookings`; `GET nurses/{id}/profile` serves
  bio/years/services/latestReview.
- The public trust-badge read: `useNurseTrustBadge(nurseId)` → `GET nurses/{id}/trust_badge` →
  `{ isVerified, approvedAt, credentialTypes[] }` (`services/verification`) — real data for the explainer.
- Phase 0's theme/brand/icon registry; phase 1's primitives (EmptyState/ErrorState kit, PageHeader, `<Money>`,
  **Jalali date picker**, skeleton twins pattern, RatingInput v2 with honest fractional stars); phase 2's shells,
  safe-area-aware `BottomBar`, and contextual customer header. Consume them; never fork local variants.
- All four data states on C1/C2/C3, `ProfileSkeleton`, the honest «کل شهر» district semantics, `PriceDisplay` money
  rules, the gender facet's humane hint copy.

## 2. Required reading (do this first)

- [audit/customer-storefront.md](audit/customer-storefront.md) — the 21 problems + keep-list for this route tree.
- [audit/feature-components.md](audit/feature-components.md) — `NurseResultCard`/`TrustBadge` findings + the
  component-layer keep-list (presentational purity, caller-owned i18n, token discipline).
- The four pages under `client/src/app/[locale]/(private-routes)/(customer)/` listed above, plus
  `search/useSearchFilters.ts` and `services/search/{types.ts,filterParams.ts,apis/clientApi.ts}` — know exactly what
  the wire serves before deciding what needs a REQ.
- `client/src/components/{NurseResultCard,TrustBadge,GenderToggle,ServicePriceRow,PriceDisplay}/` — the components
  this phase owns or extends.
- `client/src/services/verification/types.ts` (`TrustBadge` wire type, `publicBadgeState`) and
  `hooks/useNurseTrustBadge.ts` — the explainer's data source.
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) — invoke it;
  and skim `client/CLAUDE.md` "Golden rules".
- Product rules: [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) (the
  four ground truths), [../../../product/business/04-search-and-matching.md](../../../product/business/04-search-and-matching.md)
  (verified-only, same-gender, variant-is-the-unit), and
  [../../../product/business/02-nurse-verification.md](../../../product/business/02-nurse-verification.md) (what the
  pipeline actually verifies — the explainer must narrate this truthfully).

## 3. Scope — build this

### 3.1 Customer home — a front door that works

- **Fix the dead search bar — decision: convert it into an honest search entry point.** Replace the free-text
  `TextField` with a tappable search affordance (a faux-input `ButtonBase` styled as the search field) that routes to
  C1 (`/search`), optionally focusing the category grid. *Why not client-side text match:* the index has no text
  column, variant names are not queryable client-side, and the only matchable dataset (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. File the **text-search REQ** (`q` over nurse/variant/category names on `search/nurses`) so the bar
  can upgrade to a real typeahead when the backend serves it; note the upgrade path in the component's JSDoc.
- **Add the missing `isError` branch**: `usePatients()` failure renders the phase-1 `ErrorState` with retry — never an
  eternal spinner. Keep the onboarding redirect logic (`isEmpty` → onboarding) exactly as is.
- **Compact trust strip** under the greeting: three icon+label items — «پرداخت امن امانی» · «پرستاران تاییدشده» ·
  «پشتیبانی» — quiet, one row, tokens only. This is ambient reassurance, not a hero; keep it to one line on mobile.
- **Gate the patient-record NudgeCard on actual record incompleteness.** Derive a completeness signal from the cached
  patients data (e.g. a patient missing conditions/age — inspect `services/patients` types for what's derivable). If
  no honest signal is derivable client-side, show the nudge only when a patient list exists but a patient was never
  opened/completed — and make it dismissible (session-scoped) rather than permanent. Never a forever-nudge.
- **Rebook shortcut row**: source recent bookings from the existing `useBookingList('customer')` cache
  (`services/bookings`) and render up to 2 «رزرو دوباره با …» cards deep-linking to the nurse's C3 profile. Repeat
  care is the dominant pattern in home nursing. Render nothing (no empty state) when there are no past bookings.

### 3.2 Search (C1) — Persian-native filters, visible feedback

- **Replace the native `type="date"`** with the phase-1 Jalali controls: a horizontal day-chip strip — «امروز»،
  «فردا»، then day-name + Shamsi date chips for the next 7 days — plus an entry to the full Jalali picker for later
  dates. The value stays the same ISO string `dateIntent` the flow already carries (intent-only, never a hard filter
  — preserve that semantic and its hint copy).
- **Make the live-count CTA a sticky bottom bar**: «مشاهده N پرستار» pinned above the `BottomBar`, safe-area aware
  (compose with phase 2's safe-area handling — don't reimplement `env(safe-area-inset-bottom)` locally). The count is
  the screen's best feedback instrument; it must be visible while adjusting the upper filters.
- **Reuse the shared `GenderToggle`** instead of the divergent inline `ToggleButtonGroup`. `GenderToggle` is
  male/female-only by design (required, never defaulted — booking context); extend it with an **opt-in `allowAny`
  prop** (adds the «فرقی ندارد» option) rather than forking, keep the booking-context behavior unchanged, and update
  its co-located test for both modes.
- **Never render «مشاهده ۰ پرستار» as a tappable CTA.** When the live count is 0, the sticky bar shows a non-CTA
  message («پرستاری با این فیلترها یافت نشد») with the same relaxation hints as 3.6. Full ICU zero-case catalog sweep
  is (DEFERRED → [phase 12](ui-phase-12-copy-motion-and-polish.md)) — just don't ship the zero-CTA here.

### 3.3 Results (C2) — honest header, editable filters

- **Tappable filter-recap chip row** under the count: category · region (city/district or «کل شهر») · gender · price
  range — each chip deep-links back to C1 **with state preserved**. The filters already live in the URL
  (do-not-regress); C1 must initialize from the carried params, not just `category_id` — extend `useSearchFilters` to
  hydrate from a full `searchParamsToFilters` read.
- **Kill the fake sort.** Replace the single-option `TextField select` with a static caption «مرتب‌شده بر اساس
  امتیاز» (the contract's only MVP sort — `SearchSort = 'rating'`). Real sort options only if the API grows them
  (DEFERRED — do not file a REQ for sorts the product hasn't asked for).
- **Adopt skeleton twins**: replace the generic `Skeleton height={112}` rows with `NurseResultCard.Skeleton`
  (built in 3.4, following phase 1's twin pattern) so loading matches the card anatomy exactly.

### 3.4 `NurseResultCard` v2 — the decision card (this phase owns it)

Rebuild the card as the four-second decision unit, staying **presentational + memoized** with its co-located test:

- **Service/variant display name** — the row IS a variant; the card must name what is being bought. This field is
  missing from the wire (`NurseSearchResultDto` has no display name — verified): **file the REQ** to denormalize
  `variantDisplayName` onto the index row. Until it lands, render the row's category name (available via the cached
  catalog reference data, passed in by the page — the card stays data-agnostic) so multi-variant nurses are at least
  distinguishable by price row; switch to the served name when the REQ lands.
- **Nurse gender indicator** — load-bearing for same-gender matching; `nurseGender` is served today and never
  rendered. A quiet chip/glyph («خانم»/«آقا» styled per the design language), never color-only.
- **Completed-visits count** — `totalCompletedBookings` is served today; render «N ویزیت موفق» (fa digits). This
  carries more information than the uniform verified chip (every row is verified by invariant).
- **Optional top review tag** — one line, e.g. «منظم و دقیق», only if served. The index row carries no review tag
  (verified): file it as an optional field on the same index-row REQ; the card renders it conditionally.
- **`NurseResultCard.Skeleton`** static twin matching the new anatomy (avatar disc, name+badge row, meta row, price
  row).
- Keep: `memo`, stable `onSelect`, keyboard/focus a11y, `PriceDisplay` for money, `TrustBadge` for the badge.

### 3.5 Nurse profile (C3) — the trust dossier + the shared trust components

- **Header identity card**: large avatar, name, gender chip, years of experience, completed visits («N ویزیت موفق» —
  fetched today, never rendered), rating + count. The profile DTO does **not** serve gender (the client stub is a
  placeholder — never render it): **file the REQ** to add `nurseGender` to `NursePublicProfileDto`; until it lands,
  omit the gender chip on C3 (the C2 card and the carried `required_gender` param cover the matching flow honestly).
- **`VerificationPanel` — new shared component** (`src/components/VerificationPanel/`, barrel + test): "what
  Balinyaar verified" as a check-listed panel, fed by `useNurseTrustBadge(nurseId)` — verified state, `approvedAt`
  (Shamsi), and one row per `credentialTypes[]` entry (stable codes → i18n labels, never raw wire values). Render
  only what is served — no invented steps, no fake dates.
- **Tappable `TrustBadge` explainer**: give `TrustBadge` an opt-in `onClick`/expand affordance that opens a
  bottom-sheet (mobile) / dialog (desktop) narrating the verification story — «این پرستار این مراحل را گذرانده است…»
  — rendering the same `VerificationPanel` inside. The default (non-interactive) badge everywhere else is unchanged;
  update `TrustBadge.test.tsx`. Wire it up on C2 cards and C3. **File the REQ** for public per-step verification
  detail (step codes + decided dates) so the panel can list identity/Shahkar/license individually with dates; until
  then the panel shows the credential-type rows + the approval date — honest, served data only.
  These two are **REUSED by [phase 8](ui-phase-8-nurse-business-and-verification.md)'s public-profile preview — build
  them shared**, presentational, caller-fed.
- **Sticky «درخواست رزرو» bottom CTA** (same sticky-bar treatment as 3.2, price-from beside the button) so the CTA
  survives the infinite reviews list. Keep the carried variant/gender/date param handoff to f7 exactly as is.
- **Reviews tab polish**: aggregate header uses phase 1's RatingInput v2 (fractional — kill the `Math.round`
  overstatement at L260 by consuming the v2 API), review cards get the card-kit treatment. Rating-distribution bars
  are (DEFERRED → phase 12) unless trivially derivable from served data.
- Render the already-fetched `latestReview` snippet on the services tab if it improves the dossier — optional.

### 3.6 Empty-results copy — honest relaxation

Replace `empty_suggest_city` in **both** catalogs with honest relaxation suggestions the family can actually act on:
widen to the whole city («منطقه را خالی کنید تا کل شهر جست‌وجو شود» — keep/merge with the existing district line),
try other dates, remove the gender filter. Delete the «مشهد/اصفهان/شیراز» line. Reuse the same relaxation vocabulary
in the C1 zero-count bar (3.2). Final wording sweep is phase 12's; this phase removes the nonsense and lands
plausible copy in both `en.json`/`fa.json`.

**(DEFERRED)**: public/guest storefront and landing page (→ [phase 13](ui-phase-13-public-front-door.md));
save/favorite/share nurses (post-MVP, product decision); collapsing multi-variant nurses into one card (needs the
variant-name REQ served first — note it in the report as a follow-up); C1/C2 Suspense-fallback skeletons beyond what
phase 1's route-level `loading.tsx` already covers.

## 4. Mocks & seams in this phase

**None introduced.** Search and reviews are real (`USE_SEARCH_MOCK = false`); verification stays mock-primary but the
public `trust_badge` read flows through the existing `services/verification` seam either way — this phase only
consumes hooks. Backend gaps become REQ entries appended to
[../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
— **REQ-001…038 are taken; check the tracker for the next free number at execution time** (parallel UI phases also
file). Expected filings from this phase:

1. **Free-text search** — `q` param on `GET search/nurses` matching nurse/variant/category names (the home bar's
   upgrade path, 3.1).
2. **Index-row enrichment** — `variantDisplayName` (required) + optional `topReviewTag` on `NurseSearchResultDto`
   (3.4).
3. **`nurseGender` on `NursePublicProfileDto`** (3.5 header chip).
4. **Public verification-step detail** — per-step passed checks + decision dates on (or beside) the `trust_badge`
   payload (3.5 explainer depth).

The UI stays mock-tolerant: every new render is conditional on the field being served; nothing blocks on a REQ.

## 5. Critical rules you must not get wrong

- **Verified-only invariant.** Every returned row is verified by the search-index invariant; the UI **never
  re-filters or re-checks** verification and never fakes a badge state. The explainer *adds* narration to served
  truth — `TrustBadge`'s three honest states (verified/unverified/expired) and their server-driven derivation stay
  exactly as they are.
- **Never render placeholder data as truth.** The C3 profile's stubbed `nurseGender: 'female'` must not reach the
  screen; no invented verification steps or dates in the panel.
- **The filter-object-IS-the-URL-IS-the-query-key architecture stays.** Recap chips, C1 hydration, and the sticky CTA
  all read/write the same `filterParams` (de)serializer; deep-linking and back/forward cache hits must keep working.
- **Money rules**: amounts render only through `PriceDisplay`/the money util (BigInt IRR, Toman at the boundary);
  totals only ever price × sessionCount; never parse money to a float.
- **Same-gender facet care**: never defaulted, hint copy preserved, the chosen value carried into booking as before.
- **Design contract**: i18n keys in **both** catalogs; tokens not hexes (`--bal-*` / palette keys only); RTL logical
  props (`marginInlineStart`, `start`/`end`) — verify at `/fa` first; dark mode via tokens; MUI v9 API only; shared
  components (`VerificationPanel`, extended `TrustBadge`/`GenderToggle`/`NurseResultCard`) keep/get co-located tests;
  `clientFetch`/cookies/services rules untouched — this phase adds **no** fetch code outside existing hooks.
- **Ownership**: phase 0 owns `theme/`/`AppIcon`/`AppButton`; phase 1 owns the shared primitives (Jalali picker,
  RatingInput, state views, skeleton-twin pattern); phase 2 owns `layout/`. If a foundation gap blocks you (e.g. a
  missing icon or sticky-bar primitive), extend the foundation file **minimally** and note it in your report — never
  fork a local variant.

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green — including updated/new tests for `NurseResultCard` (+ its
      Skeleton), `TrustBadge` (interactive mode), `GenderToggle` (`allowAny`), and `VerificationPanel`.
- [ ] `en.json`/`fa.json` in sync; «مشهد/اصفهان/شیراز» is gone from both.
- [ ] The home search affordance routes to C1; a failed patients query shows retry (kill the API to prove it); the
      record nudge no longer renders for a complete record; a customer with past bookings sees the rebook row.
- [ ] C1: Jalali day-chips replace the Gregorian input; the count CTA is sticky and never tappable at count 0; the
      gender facet is the shared `GenderToggle`.
- [ ] C2: recap chips deep-link back to C1 with **all** filters preserved (deep-link a full URL to prove hydration);
      no interactive-looking dead sort; skeleton twins during load.
- [ ] Cards show variant/category label, gender, completed visits; C3 has the sticky CTA, completed visits in the
      header, `VerificationPanel`, and tappable badges opening the explainer; no gender chip on C3 until the REQ lands.
- [ ] Visual verification on the four axes — `/fa` + `/en` × light + dark — and mobile + desktop for home, C1, C2,
      C3 (sticky bars must clear the BottomBar and the home-indicator safe area on mobile).
- [ ] REQs filed in the tracker with the next free numbers; no edits outside `client/` and the tracker/report files.

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

1. **Home**: log in as the seeded customer → the search bar tap opens C1 (no dead `?q=`). Stop the API and reload →
   an error card with «تلاش مجدد», not an eternal spinner. Restart → home recovers; trust strip visible under the
   greeting; a customer with a completed booking sees «رزرو دوباره با …».
2. **C1**: the date filter shows «امروز/فردا» + Shamsi day chips (no Gregorian browser calendar anywhere). Pick
   filters → the count CTA stays pinned at the bottom while scrolling; set filters matching nothing → the bar shows
   the no-results message and is not tappable.
3. **C2**: run a search → recap chips show category/region/gender/price; tap one → C1 opens with every filter
   pre-filled; browser-back returns to identical results with zero network (cache hit). The header reads «مرتب‌شده
   بر اساس امتیاز» as text, not a dropdown.
4. **Cards**: a nurse with multiple variants shows distinguishable cards (service label + price); every card shows
   gender and «N ویزیت موفق»; tapping the ✓ badge opens the verification bottom-sheet.
5. **C3**: open a profile → header shows completed visits + rating; `VerificationPanel` lists the served credential
   types + approval date (Shamsi); open the reviews tab and scroll deep → «درخواست رزرو» stays pinned; a 4.5 average
   renders as a fractional star row, not five full stars. 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.

## 8. Hand off & document (close the phase)

- Update `client/CLAUDE.md` "Project Structure": the C1/C2/C3 line items (Jalali chips, sticky CTA, recap chips,
  dossier layout) and the new `VerificationPanel` component entry; note `TrustBadge`'s new interactive mode and
  `GenderToggle`'s `allowAny` on their lines.
- Write the report at `dev/shared-working-context/reports/ui-phase-4-report.md`: what changed per screen, the exact
  REQ numbers filed (text search, index-row enrichment, profile gender, public step detail), the home-search-bar
  decision + rationale, the multi-variant-collapse follow-up, and any foundation files you extended.
- Save a memory note per operating-rules §8: phase 4 owns `NurseResultCard`; `VerificationPanel` + the `TrustBadge`
  explainer are the shared trust components phase 8 consumes; the C3 profile DTO still lacks gender (placeholder
  field — never render it) until its REQ lands.
