99ebf5d881
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>
9.3 KiB
9.3 KiB
Frontend Phase 4 — Catalog browse (Home A5) & nurse service builder (B7) — Report (2026-07-05)
Lights up the two faces of the configurable catalog: the customer Home (A5) front door and the
nurse "add a service" builder + offerings list (B7 services half), over a new cached services/catalog
domain. Consumes the b5 catalog contract. Unlocks search & discovery (f6).
What was built
services/catalog domain (client/src/services/catalog/)
types.ts— DTOs mirrored fromcatalog.md(camelCase wire):ServiceCategory,ServiceOptionGroup(serviceCategoryId: number|null= cross-category,isRequired),ServiceOptionValue,NurseServiceVariant(price= IRR digit-string,priceUnit,sessionCount,options[]),CreateVariantInput/UpdateVariantInput, theCatalogApiseam, and helpersisGroupApplicable/optionSetSignature(the duplicate-listing signature).PriceUnit+thePRICE_UNITSarray are the only closed enum — categories/groups/values are data-driven.keys.ts—catalogKeys:categories(),categoryOptionGroups(id),myVariants(params)/myVariantsLists()(invalidation prefix),variant(id).constants.ts—USE_CATALOG_MOCK(defaulttrue);CATALOG_REFERENCE_STALE_TIME = Infinity+CATALOG_REFERENCE_GC_TIME(reference data cached session-long, like geography);MY_VARIANTS_STALE_TIME; page sizes.apis/—clientApi.ts(real, action-style routes, camelCase bodies,pageSizepagination),mockApi.ts+seed.ts(in-memory, behind the seam),index.ts(config-selected seam).hooks/(one per file) —useServiceCategories,useCategoryOptionGroups(cached reference data);useMyVariants(paginated, auth-gated, self-scoped);useCreateVariant,useUpdateVariant(setQueryDatathe row + invalidate),useSetVariantActive(deactivate/reactivate).index.tsbarrels hooks.names.ts—pickCatalogName(row, locale)locale-label helper (fa primary).
Shared components (client/src/components/, each with a co-located *.test.tsx)
CategoryTile— data-driven, tappable category tile (icon disc + localised label;selectedstate for the builder; robusticonKey→icon fallback to a genericcategoryicon).PriceDisplay— renders{Toman} {unit}via the f0 money util + an i18n unit label offprice_unit, and the unit-aware estimated total (price × sessionCount, integer-safe) only when a duration is present.VariantCard— a nurse offering:display_name,PriceDisplay, active/deactivated distinction (dimmed + neutral chip + "can't be booked" hint), Edit + Deactivate/Reactivate actions — no delete affordance.
Money util (client/src/utils/money.ts)
tomanToRial(value)— the sanctioned Toman→IRR digit-string conversion at the field boundary.multiplyIrr(value, count)— integer-safeprice × countfor the estimated total (never from price alone).- Both unit-tested (
money.test.ts).
Screens
- Customer Home (A5) —
app/[locale]/(private-routes)/(customer)/page.tsx(rewritten): greeting + avatar (useMefirstName / initial), search bar (navigates toward/search?q=…; execution deferred to f6), data-driven category grid (useServiceCategories; loading skeletons / empty / error+retry; tiles carryservice_category_id→/search?category_id=), the complete-patient-record nudge (reuses the cached f2usePatients— no new fetch), and the preserved first-login onboarding gate. /search— deferredPlaceholderScreenstub so the Home CTAs don't dead-end (echoes the q/category).- Nurse Services & prices (B7) —
app/[locale]/(private-routes)/nurse/services/(new sidebar tab).page.tsxswitches betweenMyServicesList(offerings — active/inactive, edit, soft-deactivate w/ confirm, reactivate, empty/skeleton) andVariantBuilder(create/edit).
Builder (VariantBuilder.tsx)
- Create = 3-step stepper (reuses the f0
StepperHeader): category (CategoryTile grid) → options (single-selectToggleButtonGroupper group; required badge; blocks advancing until every required group is answered, cross-category groups included) → price+unit+duration. Price entered in Toman → submitted as an IRR digit-string (tomanToRial, no float on the path); live unit-aware estimated total viaPriceDisplay; editable auto-generateddisplay_name. The duplicate-listing409shows a friendly inline warning. Edit locks the category + option-set and edits only price/unit/duration/display.
Wiring
- i18n:
catalog/services/searchnamespaces +homeadditions +nav.services(both locales, in sync, 313 keys). Icons:services,category,elderly,post_surgery,infant,chronic,companionship. Routes:SEARCH,NURSE_SERVICES. Nurse sidebar gains Services.
What is now testable (and exactly how)
Run npm run dev (mock is on by default — no backend needed). Follow the phase §7 steps:
- Home — sign in as a customer → greeting + avatar, search bar, and the category grid (5 seeded
categories ordered by
sortOrder); the patient nudge shows/hides off the cached patient state (React Query Devtools: thepatientsquery is reused, not refetched). - Locale + RTL — toggle
fa↔en: labels translate,dirflips, tiles/grid mirror; Persian unit labels (ساعتی/روزانه/شبانهروزی) read correctly. - Build a variant — sign in as a nurse →
/nurse/services→+ افزودن خدمت→ pick مراقبت از سالمند; in options, try Next without answering نوع شیفت (required) → blocked, the required badge turns red; answer it → Next; enter a Toman price + unit (ساعتی) + a duration → the estimated total updates fromprice × duration; edit the autodisplay_name; submit → the card appears (… تومان ساعتی). - Duplicate — create a second variant with the same category + same option-set → the friendly
409duplicate warning shows inline; no crash / no generic toast. - Edit + deactivate — edit a variant's price/name → the list reflects it without a full refetch (Devtools:
setQueryData+ single invalidation). Deactivate → confirm dialog → the row dims to the deactivated state with the "can't be booked" hint; no delete option. Reactivate from the inactive row. - Caching — Devtools:
catalogKeys.categories()/categoryOptionGroupsare served from cache across Home and every builder step (no per-step refetch); a variant mutation invalidates onlymyVariants. - Gate —
npm run check+npm run test:cipass.
What is mocked / waiting on a real service
CatalogApi—client/src/services/catalog/apis/mockApi.ts(+seed.ts), behindUSE_CATALOG_MOCK(defaulttrue). Faithfully reproduces the b5 create validation (400missing-required / bad-price) and the(nurse, category, option-set)duplicate409; categories mirror the real seed; the variant store starts empty (so the empty state demos). Seemocks-registry.md(CatalogApirow). Swap = one line (USE_CATALOG_MOCK = false);catalogClientApiis already wired to the action-style routes. Caveat recorded there: the mock seeds representative option groups the fresh backend does not (an admin authors them per category) — after the swap, categories have no groups until seeded server-side.
Contracts
- Consumed:
dev/contracts/domains/catalog.md(backend-phase-b5) — types/services derive from it; no shape guessed. - Requested:
REQ-010infor-backend.md— confirm/align the list pagination query-param name (pageSize, per the proven b4 binding, vs the doc'spage_size). - Produced: none (frontend produces no contract).
Docs updated
client/CLAUDE.md— Project Structure (customersearchstub, nurseservicesroute,services/catalogdomain,CategoryTile/PriceDisplay/VariantCard); the reference-data caching note (catalog = second long-lived cached domain); the i18n namespaces list (catalog/services/search,home).- This report +
STATUS.md+mocks-registry.md+for-backend.md(REQ-010). - No product-doc rule change was needed; the estimated-total presentation rule (total only from
price × session_count, never price alone) is captured here and enforced in
PriceDisplay/the builder.
Follow-ups for later phases
- f6 (search & discovery) — the Home search bar hands a
q/service_category_idto/search(today a placeholder); f6 builds the results, filters, and nurse cards, and can reuseCategoryTile,PriceDisplay, the cacheduseServiceCategories/useCategoryOptionGroups, and thecatalog/searchnamespaces. The variant builder is what populates the index f6 reads (a nurse must have ≥1 active variant + coverage to appear). - Backend — deliver
REQ-010(pagination param confirmation), then f6 can flipUSE_CATALOG_MOCK=false. - Deferred (unchanged): admin catalog manager (→ f15), nurse availability slots, public nurse-profile service rows (→ f6 C3), holiday/surge pricing.