frontend phase 4: catalog browse (Home A5) & nurse service builder (B7)
Light up the two faces of the configurable service catalog over a new cached services/catalog domain (consumes the b5 contract; unlocks f6 search). - services/catalog: types/keys/constants/apis(client+mock+seam)/hooks/index + names.ts. Categories & option groups are session-cached reference data (Infinity staleTime, like geography); variant mutations invalidate myVariantsLists() and setQueryData the edited row. Mock-primary (USE_CATALOG_MOCK), one-line swap; mock reproduces the 400 missing-required and (nurse,category,option-set) 409 duplicate rules. - Customer Home (A5): greeting+avatar, search bar (navigates toward f6), data-driven category grid (loading/empty/error), patient nudge from the cached f2 query. Deferred /search placeholder stub. - Nurse Services & prices (B7) at /nurse/services: offerings list (active vs deactivated, edit, soft-deactivate w/ confirm, reactivate, no delete) and a 3-step variant builder (category -> required/optional options -> price+unit+ duration). Required-group gate; Toman->IRR digit-string at the field boundary (no float); live unit-aware estimated total (never from price alone); editable auto display_name; inline 409 duplicate warning; locked category edit form. - Shared, tested components: CategoryTile, PriceDisplay, VariantCard. Money util: tomanToRial + multiplyIrr (integer-safe) + tests. - i18n: catalog/services/search namespaces + home additions + nav.services (both locales, in sync). Icons, routes (SEARCH, NURSE_SERVICES), nurse nav. Gate: npm run check green; npm run test:ci green (147 tests, +18 across 4 suites); npm run build green with NEXT_PUBLIC_API_URL set. Docs: client/CLAUDE.md (Project Structure, caching note, namespaces), STATUS, for-backend REQ-010 (pagination param casing), phase report, mocks registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+15
-3
@@ -118,7 +118,8 @@ client/
|
||||
│ │ ├── select-role/page.tsx # /select-role — first-use role picker (no public role yet); role router lands here
|
||||
│ │ ├── (customer)/ # Customer (family) app — mobile-first, bottom-tab nav; no URL segment
|
||||
│ │ │ ├── layout.tsx # 'use client' — wraps CustomerLayout
|
||||
│ │ │ ├── page.tsx # / (A5 home — 'use client'; first-login onboarding gate + record/profile nudges)
|
||||
│ │ │ ├── page.tsx # / (A5 home — 'use client'; greeting+avatar, search bar, data-driven category grid, first-login onboarding gate + record/profile nudges)
|
||||
│ │ │ ├── search/page.tsx # /search — DEFERRED→f6 stub (PlaceholderScreen; Home search bar + category tiles land here carrying q/category_id)
|
||||
│ │ │ ├── onboarding/page.tsx # /onboarding — A3→A4 wizard (relation → first patient)
|
||||
│ │ │ ├── bookings/page.tsx # /bookings
|
||||
│ │ │ ├── patients/page.tsx # /patients — E1 list/CRUD (add/edit dialog reusing PatientForm, soft-archive)
|
||||
@@ -129,6 +130,7 @@ client/
|
||||
│ │ │ ├── layout.tsx # 'use client' — wraps NurseLayout
|
||||
│ │ │ ├── page.tsx # /nurse (dashboard)
|
||||
│ │ │ ├── profile/page.tsx # /nurse/profile — B7 profile bootstrap (avatar+bio+years; unverified placeholder)
|
||||
│ │ │ ├── services/ # /nurse/services — B7 services half: offerings list ↔ variant builder (page.tsx switches mode; MyServicesList + VariantBuilder co-located)
|
||||
│ │ │ ├── coverage/page.tsx # /nurse/coverage — F3 coverage-area editor (whole-city/district areas, dup-blocked)
|
||||
│ │ │ ├── bank/page.tsx # /nurse/bank — payout IBAN + ownership states (pending/verified/mismatch)
|
||||
│ │ │ ├── verification/page.tsx # /nurse/verification
|
||||
@@ -153,6 +155,9 @@ client/
|
||||
│ ├── PatientForm/ # A4 patient form (name/age/gender/conditions/relation) — reused create+edit
|
||||
│ ├── PatientCard/ # E1 patient summary card + edit/archive actions
|
||||
│ ├── BankStatusPanel/ # Nurse bank-account ownership state (pending/verified/mismatch), masked IBAN
|
||||
│ ├── CategoryTile/ # f4 tappable service-category tile (icon+label; `selected` state for the builder) — Home grid + builder step 1 (tested)
|
||||
│ ├── PriceDisplay/ # f4 price renderer: money-util Toman + i18n unit label + unit-aware estimated total (never a total from price alone) (tested)
|
||||
│ ├── VariantCard/ # f4 nurse offering card: display_name, PriceDisplay, active/deactivated distinction, edit/deactivate (no delete) (tested)
|
||||
│ ├── geography/ # F3 geo composites: CascadingRegionSelect, AddressMapPicker (map-pin stand-in), AddressForm, AddressCard (each tested)
|
||||
│ └── auth/ # Auth-flow composites: LoginFlow, PhoneStep, OtpStep, RoleRouter, SelectRole, AuthCard, BrandMark, AuthSplash, useCountdown
|
||||
├── i18n/
|
||||
@@ -201,6 +206,7 @@ client/
|
||||
│ ├── geography/ # F3 cached province→city→district reference lookups (Infinity staleTime, shared geographyKeys; reused by addresses, coverage & later search)
|
||||
│ ├── addresses/ # F3 customer address book CRUD + set-primary (single-primary invariant; invalidate-on-mutation)
|
||||
│ ├── serviceAreas/ # F3 nurse coverage areas add/remove (areaExists dup-guard; districtId=null = whole city)
|
||||
│ ├── catalog/ # F4 catalog skeleton + nurse pricing variants (b5). Reference data (categories, category option groups) cached session-long like geography (Infinity staleTime); myVariants invalidated on mutation. useServiceCategories/useCategoryOptionGroups/useMyVariants/useCreateVariant/useUpdateVariant/useSetVariantActive; seam+mock+client; names.ts locale-label helper
|
||||
│ └── {domain}/
|
||||
│ ├── types.ts # Request/response types + the domain's Api interface (the seam)
|
||||
│ ├── keys.ts # React Query key factory (hierarchical)
|
||||
@@ -284,13 +290,16 @@ async function MyServerComponent() {
|
||||
- `'shell'` — actor-shell titles + the not-yet-built placeholder body
|
||||
- `'patients'` — the E1 patient list/CRUD (list, card, add/edit dialog, archive)
|
||||
- `'onboarding'` — the A3→A4 wizard + the shared enum labels (relation/condition/gender codes → labels)
|
||||
- `'home'` — the A5 family home (greeting + record/profile nudges)
|
||||
- `'home'` — the A5 family home (greeting + avatar, search bar, category grid, record/profile nudges)
|
||||
- `'profile'` — the customer profile + emergency contact
|
||||
- `'nurseProfile'` — the nurse B7 profile bootstrap (photo/bio/years + unverified placeholder)
|
||||
- `'bank'` — the nurse payout bank settings (IBAN form + the three ownership states)
|
||||
- `'geo'` — the shared cascading province→city→district dropdowns (`CascadingRegionSelect`: level labels, "whole city", cascade hints)
|
||||
- `'address'` — the customer address book + add/edit form (title/street, map-pin helper, set-primary, empty/delete states) + the profile-hub link
|
||||
- `'coverage'` — the nurse coverage-area editor (whole-city/specific-district scope, chips, duplicate + "won't appear in search" warnings)
|
||||
- `'catalog'` — **shared** catalog vocabulary: the five `price_unit` labels + count nouns + the estimated-total label (read by `PriceDisplay`; f6 reuses it customer-side)
|
||||
- `'services'` — the f4 nurse Services & prices surface (offerings list, the variant builder steps/fields/validation, the duplicate-listing warning, deactivate confirm)
|
||||
- `'search'` — the f4 deferred `/search` placeholder (title + "arrives next phase" + query/category echo); f6 fills it out
|
||||
- `'auth'` — the phone-OTP login flow, role router, and SelectRole screen (`common.brand`/`brand_tagline` for the wordmark)
|
||||
|
||||
**Namespace conventions for the phases to come** (seed each when its feature lands, in both locale
|
||||
@@ -532,7 +541,10 @@ Every domain follows the same shape: `types.ts` (wire types + the domain's `Api`
|
||||
level is fetched **once** and served from cache across every consumer (the address form, the coverage editor,
|
||||
and later search) — never refetched on a dropdown open. Contrast with mutable lists (addresses, coverage
|
||||
areas) which invalidate on every mutation. See `services/geography/*`. Reuse this pattern for future
|
||||
reference data; do not reinvent per-consumer fetching.
|
||||
reference data; do not reinvent per-consumer fetching. **`services/catalog` (f4) is the second long-lived
|
||||
cached reference domain:** admin-seeded categories + a category's option groups/values use the same Infinite
|
||||
`staleTime`/`gcTime` (`CATALOG_REFERENCE_*`) so the Home grid and every builder step read them from cache;
|
||||
the nurse's own **variant list** is the mutable side — mutations invalidate `catalogKeys.myVariantsLists()`.
|
||||
- **Mock behind a seam:** when the backend endpoint isn't live, implement the domain's `Api` interface
|
||||
twice — a real `clientApi.ts` and an in-memory `mockApi.ts` — and select in `apis/index.ts` by a config
|
||||
flag (`USE_{DOMAIN}_MOCK`). Hooks import the selected `api`; the swap is one line. Record every mock in
|
||||
|
||||
Reference in New Issue
Block a user