Files
baya-monorepo/archive/post-phase/ui/ui-phase-4-customer-storefront.md
T
2026-08-02 18:48:32 +03:30

281 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 02](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 02 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 L4950) 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 L6668) 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 L96102), 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 L104110) in a product where
every displayed date is Shamsi; the gender facet re-implements an inline `ToggleButtonGroup` (L86100) instead of
the shared `GenderToggle`; the live-count CTA is the last child of a long scrolling form (L130140).
5. **C2 ships a fake sort** — a `TextField select` with one hard-coded `MenuItem`, `value="rating"`, no `onChange`
(results/page.tsx L6769) — 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 L92101)
— 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 (56 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 15 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.