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