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

24 KiB
Raw Blame History

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 · Unlocks: Phase 5 (booking funnel) + Phase 8 (reuses the trust components built here) Before you start, read ../../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):

  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)

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) — 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 counttotalCompletedBookings 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'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); 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.mdREQ-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 searchq param on GET search/nurses matching nurse/variant/category names (the home bar's upgrade path, 3.1).
  2. Index-row enrichmentvariantDisplayName (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:

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