Files
baya-monorepo/dev/shared-working-context/reports/frontend-phase-4-report.md
T
hamid 99ebf5d881 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>
2026-07-05 17:33:44 +03:30

9.3 KiB
Raw Blame History

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 from catalog.md (camelCase wire): ServiceCategory, ServiceOptionGroup (serviceCategoryId: number|null = cross-category, isRequired), ServiceOptionValue, NurseServiceVariant (price = IRR digit-string, priceUnit, sessionCount, options[]), CreateVariantInput/UpdateVariantInput, the CatalogApi seam, and helpers isGroupApplicable / optionSetSignature (the duplicate-listing signature). PriceUnit +the PRICE_UNITS array are the only closed enum — categories/groups/values are data-driven.
  • keys.tscatalogKeys: categories(), categoryOptionGroups(id), myVariants(params) / myVariantsLists() (invalidation prefix), variant(id).
  • constants.tsUSE_CATALOG_MOCK (default true); 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, pageSize pagination), 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 (setQueryData the row + invalidate), useSetVariantActive (deactivate/reactivate). index.ts barrels hooks.
  • names.tspickCatalogName(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; selected state for the builder; robust iconKey→icon fallback to a generic category icon).
  • PriceDisplay — renders {Toman} {unit} via the f0 money util + an i18n unit label off price_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-safe price × count for 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 (useMe firstName / initial), search bar (navigates toward /search?q=…; execution deferred to f6), data-driven category grid (useServiceCategories; loading skeletons / empty / error+retry; tiles carry service_category_id/search?category_id=), the complete-patient-record nudge (reuses the cached f2 usePatients — no new fetch), and the preserved first-login onboarding gate.
  • /search — deferred PlaceholderScreen stub 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.tsx switches between MyServicesList (offerings — active/inactive, edit, soft-deactivate w/ confirm, reactivate, empty/skeleton) and VariantBuilder (create/edit).

Builder (VariantBuilder.tsx)

  • Create = 3-step stepper (reuses the f0 StepperHeader): category (CategoryTile grid) → options (single-select ToggleButtonGroup per 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 via PriceDisplay; editable auto-generated display_name. The duplicate-listing 409 shows a friendly inline warning. Edit locks the category + option-set and edits only price/unit/duration/display.

Wiring

  • i18n: catalog / services / search namespaces + home additions + 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:

  1. 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: the patients query is reused, not refetched).
  2. Locale + RTL — toggle faen: labels translate, dir flips, tiles/grid mirror; Persian unit labels (ساعتی/روزانه/شبانه‌روزی) read correctly.
  3. 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 from price × duration; edit the auto display_name; submit → the card appears (… تومان ساعتی).
  4. Duplicate — create a second variant with the same category + same option-set → the friendly 409 duplicate warning shows inline; no crash / no generic toast.
  5. 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.
  6. Caching — Devtools: catalogKeys.categories() / categoryOptionGroups are served from cache across Home and every builder step (no per-step refetch); a variant mutation invalidates only myVariants.
  7. Gatenpm run check + npm run test:ci pass.

What is mocked / waiting on a real service

  • CatalogApiclient/src/services/catalog/apis/mockApi.ts (+ seed.ts), behind USE_CATALOG_MOCK (default true). Faithfully reproduces the b5 create validation (400 missing-required / bad-price) and the (nurse, category, option-set) duplicate 409; categories mirror the real seed; the variant store starts empty (so the empty state demos). See mocks-registry.md (CatalogApi row). Swap = one line (USE_CATALOG_MOCK = false); catalogClientApi is 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-010 in for-backend.md — confirm/align the list pagination query-param name (pageSize, per the proven b4 binding, vs the doc's page_size).
  • Produced: none (frontend produces no contract).

Docs updated

  • client/CLAUDE.mdProject Structure (customer search stub, nurse services route, services/catalog domain, 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_id to /search (today a placeholder); f6 builds the results, filters, and nurse cards, and can reuse CategoryTile, PriceDisplay, the cached useServiceCategories/useCategoryOptionGroups, and the catalog/search namespaces. 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 flip USE_CATALOG_MOCK=false.
  • Deferred (unchanged): admin catalog manager (→ f15), nurse availability slots, public nurse-profile service rows (→ f6 C3), holiday/surge pricing.