add build development phases
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
# Frontend Phase 4 — Catalog browse & nurse service builder
|
||||
|
||||
> **Mission:** light up the two faces of the configurable service catalog. For the **family/customer**
|
||||
> this is the **Home (A5)** screen — greeting, the search bar, the four-tile service-category grid
|
||||
> (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی), and the complete-patient-record nudge —
|
||||
> the front door of the whole app. For the **nurse** this is the **"add a service" builder (B7 services)**:
|
||||
> a stepper that walks pick-category → answer required/optional option groups → set price + price unit,
|
||||
> producing a priced **variant** (the atomic bookable unit), plus the list/edit/deactivate surface for a
|
||||
> nurse's own offerings. This is what makes nurses *appear* in search (f6) and what a customer browses, so
|
||||
> it must be exactly right about money, required-option validation, and reference-data caching.
|
||||
>
|
||||
> **Track:** frontend · **Depends on:** [`frontend-phase-3-b4.md`](./frontend-phase-3-b4.md) (addresses & geo, cascading province/city/district, nurse coverage editor) + backend **phase b5** contract (`catalog`) · **Unlocks:** search & discovery ([`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))
|
||||
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.
|
||||
|
||||
---
|
||||
|
||||
## 1. Context — where this sits
|
||||
|
||||
We are at the hinge between *identity/geo* (f1–f3, done) and *discovery/booking* (f6+). A nurse cannot
|
||||
be found until she has at least one **active, priced variant**; a customer has nowhere to start until the
|
||||
**Home category grid** exists. This phase builds both, against the backend's **Catalog & pricing** domain
|
||||
(b5): admin-seeded `service_categories` → `service_option_groups` → `service_option_values`, and the nurse
|
||||
layers `nurse_service_variants` → `nurse_service_variant_options`. The bookable unit is the **variant**, never
|
||||
the nurse and never the category — search, booking, and pricing all operate on variants downstream.
|
||||
|
||||
The product framing: transparent, **nurse-set** pricing per variant is a deliberate differentiator versus the
|
||||
opaque "توافقی/negotiable" incumbents — so the price the nurse enters and the way we *display* it (price +
|
||||
unit, with session count) is brand-load-bearing, not a detail.
|
||||
|
||||
**What already exists (do not rebuild) — built by prior frontend phases:**
|
||||
- **f0** ([`frontend-phase-0.md`](./frontend-phase-0.md)) — the three actor app **shells** and route groups
|
||||
(customer mobile shell with the **5-tab bottom nav** خانه/رزروها/بیماران/کیفپول/پروفایل · nurse shell ·
|
||||
admin shell); the **`services/{domain}` + TanStack Query** reference pattern (`types.ts` / `keys.ts` /
|
||||
`apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`); the **types-from-contract** convention; the
|
||||
**shared composite components** including the **stepper/progress header**, **status chip**, OTP input,
|
||||
phone field; the **money/format util** (`formatIrrToToman`, integer-safe IRR parse, Shamsi date display)
|
||||
in `src/utils/`; the i18n namespace conventions in both `messages/en.json` and `messages/fa.json`; the
|
||||
RTL baseline and `tokens.css` brand colours. **Reuse all of it — do not re-create the shell, the stepper,
|
||||
the money util, or the services pattern.**
|
||||
- **f1-b2** — phone-OTP login, the role router, and roles in `AuthContext`. You read the current role to
|
||||
decide *customer Home* vs *nurse builder* chrome; you do **not** touch auth.
|
||||
- **f2-b3** — onboarding, patient CRUD, customer & nurse profiles, nurse bank-account settings. The
|
||||
**patient list/record state** is what the Home "complete patient record" nudge points at; the **nurse
|
||||
profile** is the parent the variant builder hangs off (B7 = "تکمیل پروفایل و خدمات"; this phase owns the
|
||||
**services part**, the profile-bio/photo part is already in f2).
|
||||
- **f3-b4** — address book + map picker + cascading **province → city → district** dropdowns, and the **nurse
|
||||
coverage-area editor** (`nurse_service_areas`). You do **not** rebuild geo; the *service areas* a nurse
|
||||
declares there are what fan the variant into search (f6) — out of scope here, just don't regress it.
|
||||
|
||||
> The variant builder produces *pricing*; the coverage editor (f3) produces *geography*; **search (f6)**
|
||||
> joins them. This phase ships neither search nor the index — wiring the Home search bar to results is f6.
|
||||
|
||||
## 2. Required reading (do this first)
|
||||
|
||||
**Product / domain (business truth — read before designing any screen):**
|
||||
- [`../../../product/business/03-service-catalog-and-pricing.md`](../../../product/business/03-service-catalog-and-pricing.md)
|
||||
— the catalog model in plain language: admin defines categories + configurable option groups/values; each
|
||||
**nurse defines variants** (category + chosen option values + own `price` + `price_unit`); the five price
|
||||
units (`per_hour`/`per_session`/`per_half_day`/`per_day`/`per_24h`); `display_name` auto-generates from
|
||||
option labels but is nurse-editable; **deactivate not delete**; catalog snapshotted onto bookings. This is
|
||||
the *why* behind every validation rule below.
|
||||
- [`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — the visual baseline.
|
||||
Study **A5 (خانه / Home)**: greeting + avatar, search bar (`جستجوی خدمت یا پرستار…`), the **service-category
|
||||
grid** (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی), the **complete-patient-record nudge card**,
|
||||
and the **bottom tab nav** (Home active). And **B7 (تکمیل پروفایل و خدمات)**: the services-and-prices list
|
||||
(`مراقبت سالمند — ساعتی ۲۸۰٬۰۰۰ تومان/ساعت`, `+ افزودن خدمت`) — this phase builds the *services* half of B7.
|
||||
Note the legend (green=verified, amber=pending) and the deep-green brand / cream surface / Vazirmatn font.
|
||||
|
||||
**Contract to consume (the source of truth for shapes — do not guess):**
|
||||
- [`../../contracts/domains/catalog.md`](../../contracts/domains/catalog.md) — written by **backend-phase-5**.
|
||||
This is where the real routes, request/response payloads, enums, and pagination live. Read it end-to-end
|
||||
before writing a single type. If it is not yet published or a needed shape is missing, follow the seam
|
||||
procedure in §4 (mock behind `services/catalog` and append a request for backend).
|
||||
- [`../../contracts/conventions/api-conventions.md`](../../contracts/conventions/api-conventions.md) — the
|
||||
envelope (`OperationResult`/`ApiResult`, already unwrapped by `clientFetch`), `snake_case` routes/JSON,
|
||||
status codes, **mandatory pagination** (`page`/`page_size` → `items`+`total`) on the variant list,
|
||||
`name_fa`/`name_en` reference-data localisation.
|
||||
- [`../../contracts/conventions/money-and-types.md`](../../contracts/conventions/money-and-types.md) —
|
||||
**money is IRR Rials as an integer string on the wire**, parsed integer-safe and rendered via the f0
|
||||
money util; **Toman is display-only**; enums cross as stable string codes (`per_hour`/`per_session`/
|
||||
`per_half_day`/`per_day`/`per_24h`) mirrored as string-literal unions with **i18n labels, never a label
|
||||
hardcoded off the code**.
|
||||
|
||||
**Engineering & design rules:**
|
||||
- [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md) and
|
||||
[`../../../client/CLAUDE.md`](../../../client/CLAUDE.md) — RSC/client boundary, `services/{domain}` +
|
||||
Query caching + invalidate-on-mutation, one-hook-per-file, minimal re-renders, MUI v9 primitives reused,
|
||||
both locales in sync, tokens-based colours, RTL.
|
||||
- **Invoke the `frontend-designer` skill** for *all* visual work here (Home, the category grid/tiles, the
|
||||
builder stepper UI, the variant list/cards, every empty/loading/error/success state). It is the brand/
|
||||
design contract — palette, tokens, typography, the `App*` library, layout shells, the hard UI rules. Do
|
||||
not hand-craft visuals outside it.
|
||||
- The existing **f0 `services/auth/*`** and the **f3 `services/geo` + `services/address`** services — copy
|
||||
their exact structure for the new `services/catalog`. Reuse the f0 **stepper** and **status chip**; do not
|
||||
fork them.
|
||||
|
||||
## 3. Scope — build this
|
||||
|
||||
Two surfaces (customer Home, nurse builder) + the supporting `services/catalog` domain. Build every state
|
||||
(loading / empty / error / validation / success) the digest's "Notes for UI" calls for.
|
||||
|
||||
### 3.1 `services/catalog` domain (the data layer the screens consume)
|
||||
Create `client/src/services/catalog/` mirroring the f0 pattern:
|
||||
- `types.ts` — string-literal unions + DTOs derived from [`catalog.md`](../../contracts/domains/catalog.md):
|
||||
`PriceUnit = 'per_hour' | 'per_session' | 'per_half_day' | 'per_day' | 'per_24h'`; `ServiceCategory`
|
||||
(`id`, `name_fa`, `name_en`, `sort_order`, optional `icon`/`slug`); `ServiceOptionGroup` (`id`,
|
||||
`service_category_id` nullable = cross-category, `name_fa`/`name_en`, **`is_required`**, `sort_order`);
|
||||
`ServiceOptionValue` (`id`, `option_group_id`, `name_fa`/`name_en`, `sort_order`); `NurseServiceVariant`
|
||||
(`id`, `service_category_id`, `price` as **IRR digit-string**, `price_unit`, `session_count` nullable,
|
||||
`display_name`, `is_active`, the chosen `options: { option_group_id, option_value_id }[]`). Mirror the
|
||||
exact casing/nullability from the published swagger — do not invent.
|
||||
- `keys.ts` — a query-key factory: `catalogKeys.categories()`, `catalogKeys.categoryOptionGroups(categoryId)`,
|
||||
`catalogKeys.myVariants()` (nurse), `catalogKeys.variant(id)`.
|
||||
- `apis/clientApi.ts` — wrappers over `clientFetch` for each route in the contract (see §3.4). Add
|
||||
`apis/serverApi.ts` (`serverFetch`) **only** if the Home category grid is prefetched in an RSC (see 3.2).
|
||||
- `hooks/` (one hook per file): `useServiceCategories.ts`, `useCategoryOptionGroups.ts` (query),
|
||||
`useMyVariants.ts` (paginated nurse list), `useCreateVariant.ts`, `useUpdateVariant.ts`,
|
||||
`useDeactivateVariant.ts` (mutations). **Reference data (categories, option groups/values) is cached with
|
||||
a long `staleTime`/`gcTime`** — it changes rarely; do not refetch it on every screen. Variant mutations
|
||||
**invalidate `catalogKeys.myVariants()`** (and `setQueryData` the edited row where it avoids a refetch).
|
||||
- `index.ts` — barrel export.
|
||||
|
||||
### 3.2 Customer **Home (A5)** — `app/[locale]/(private-routes)/<customer>/home`
|
||||
The primary landing screen, inside the customer shell with the **bottom tab nav (Home active)**:
|
||||
- **Greeting + avatar** — `سلام، {firstName}` from `AuthContext`/`useCurrentUser` (f1); avatar from the
|
||||
customer profile (f2). RSC where it cleanly removes a round-trip; no `next/headers` in client components.
|
||||
- **Search bar** — `جستجوی خدمت یا پرستار…`. **Render the input here, but search execution is f6** — tapping
|
||||
it navigates toward the (future) search route / sets the query; do **not** implement results, the index,
|
||||
or filters in this phase. Tag results/filters **(DEFERRED → [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))**.
|
||||
- **Service-category grid** — a tile per `service_category` from `useServiceCategories()`, ordered by
|
||||
`sort_order`, label by locale (`name_fa`/`name_en`), seed-matching the wireframe's four
|
||||
(مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی) but **data-driven** (never hardcode the category
|
||||
list — EAV configurability is load-bearing). Tapping a tile carries the chosen `service_category_id` into
|
||||
the (future) search flow. Build **loading skeleton tiles**, **empty** (no categories seeded → friendly
|
||||
message) and **error/retry** states.
|
||||
- **Complete-patient-record nudge card** — shown when the signed-in customer has no patient *or* an incomplete
|
||||
record (derive from the f2 patient state already in cache — do **not** add a new fetch if f2's query
|
||||
already holds it); CTA routes to add/complete a patient (f2 screens). Hide when the record is complete.
|
||||
- A small reusable **`CategoryTile`** and **`HomeNudgeCard`** composite live at the shared level if reused;
|
||||
page-only composition stays in the page.
|
||||
|
||||
### 3.3 Nurse **"add a service" builder + offerings list** — `app/[locale]/(private-routes)/<nurse>/services`
|
||||
The services half of **B7**, inside the nurse shell:
|
||||
|
||||
**(a) Offerings list (`ListMyVariants`)** — `useMyVariants()` (paginated):
|
||||
- Each row/card shows `display_name`, the **price rendered via the f0 money util** as
|
||||
`{formatIrrToToman(price)} تومان {unitLabel}` (unit label is an i18n key off `price_unit`), and an
|
||||
**active vs deactivated** visual distinction (reuse the f0 **status chip**) with a "deactivated can't be
|
||||
booked" hint on inactive rows. Row actions: **Edit**, **Deactivate** (with a confirm).
|
||||
- States: **loading** (skeleton rows), **empty** (no offerings yet → prominent `+ افزودن خدمت` CTA), populated.
|
||||
|
||||
**(b) Create/Edit variant builder (`CreateVariant` / `UpdateVariant`)** — a **stepper** (reuse the f0
|
||||
stepper/progress header), launched by `+ افزودن خدمت` or Edit:
|
||||
1. **Step 1 — category.** Single-select from `useServiceCategories()`. On select, fetch that category's
|
||||
option groups via `useCategoryOptionGroups(categoryId)` (cached). (On *edit*, category is fixed — changing
|
||||
it would change identity; lock it and explain.)
|
||||
2. **Step 2 — options.** Render each `service_option_group` for the category (plus any cross-category group
|
||||
where `service_category_id` is null) as a single-select of its `service_option_value`s. **Mark required
|
||||
groups** (`is_required`) and **block advancing until every required group is answered** (one value per
|
||||
group — `UNIQUE(variant_id, option_group_id)` is enforced server-side; the UI enforces single-select).
|
||||
Optional groups may be left unanswered.
|
||||
3. **Step 3 — price + unit (+ duration).** A **price** field (Toman input → store/submit as IRR digit-string
|
||||
via the f0 integer-safe parse; **never a float**), a **`price_unit`** select (the five units, i18n
|
||||
labels), and an optional **`session_count`/duration**. Show a **live estimated total** computed from
|
||||
**price + unit + session_count together** — e.g. for `per_hour` with a duration, surface the
|
||||
`formatIrrToToman(price × hours)` estimate — **do not compute or display a total from price alone**. Show
|
||||
the auto-generated **`display_name`** (from the chosen option labels) as an editable field.
|
||||
- **Submit:** `useCreateVariant()` / `useUpdateVariant()`. On the **duplicate-listing conflict** (server `409`
|
||||
on `(nurse_id, category, option-set)`), show a **friendly inline warning** ("شما قبلاً خدمتی با همین مشخصات
|
||||
دارید") and let the nurse adjust — don't silently fail or generic-toast it. Success → invalidate
|
||||
`myVariants`, route back to the list with the new/edited row visible.
|
||||
- States to build: loading (fetching catalog), per-step validation errors (missing required option, missing/
|
||||
invalid price), the duplicate warning, success.
|
||||
|
||||
**(c) Deactivate (`DeactivateVariant`)** — `useDeactivateVariant()`; a confirm dialog explaining the variant
|
||||
becomes **unbookable and drops out of search**; **soft only — never a hard delete**. On success, flip the
|
||||
row to the deactivated visual state (`setQueryData`/invalidate).
|
||||
|
||||
### 3.4 Catalog browse (categories) — the read surface
|
||||
The category-browse query the Home grid and the (future) search filters both consume: `ListCategories` and
|
||||
`GetCategoryOptionGroups` via `services/catalog`. Build the **catalog-browse view** as the data-driven grid
|
||||
in 3.2 (Home) reusing `CategoryTile`; a standalone "all categories" browse screen is optional and may be
|
||||
**(DEFERRED → f6)** if the Home grid + search cover it — your call, but if you build it, reuse the same
|
||||
hooks/components.
|
||||
|
||||
**Endpoints consumed (final names from [`catalog.md`](../../contracts/domains/catalog.md) — these mirror the
|
||||
b5 capabilities; use the contract's exact `snake_case` routes):**
|
||||
- `GET api/v1/catalog/categories` → categories (reference data, cached).
|
||||
- `GET api/v1/catalog/categories/{id}/option_groups` (with values) → the skeleton the builder renders.
|
||||
- `POST api/v1/nurse/variants` → CreateVariant.
|
||||
- `PUT api/v1/nurse/variants/{id}` → UpdateVariant / EditDisplayName.
|
||||
- `POST api/v1/nurse/variants/{id}/deactivate` → Deactivate (soft).
|
||||
- `GET api/v1/nurse/variants` (paginated) → ListMyVariants (active + inactive).
|
||||
|
||||
**Out of scope (tag explicitly):**
|
||||
- Search results / filters / the nurse-result cards / the search index — **(DEFERRED → [`frontend-phase-6-b7.md`](./frontend-phase-6-b7.md))**.
|
||||
- The **admin catalog manager** (category/option-group/value CRUD) — **(DEFERRED → admin console [`frontend-phase-15-b15.md`](./frontend-phase-15-b15.md))**; the admin seeds the catalog server-side for now.
|
||||
- Nurse **availability** slots/calendar — **(DEFERRED**, soft-constraint, not on this path).
|
||||
- The **public nurse profile** services rows a customer sees — **(DEFERRED → f6 C3)**.
|
||||
- Holiday/surge pricing, companionship tier, per-category commission — **(DEFERRED** per product doc).
|
||||
|
||||
## 4. Mocks & seams in this phase
|
||||
|
||||
This is a **frontend** phase: the only seam is the data seam behind `services/catalog`.
|
||||
- **Reuse the `services/{domain}` seam pattern from f0.** Every catalog call goes through
|
||||
`services/catalog/apis/clientApi.ts` (over `clientFetch`) — never a raw `fetch()`.
|
||||
- **If the b5 `catalog` contract is published and merged**, derive `types.ts` from it and call the real
|
||||
endpoints — no mock.
|
||||
- **If a needed shape is missing or the contract isn't live yet**, build a **mock `clientApi`** behind the
|
||||
same `services/catalog` seam (returning realistically-shaped data: a few seeded categories, an option
|
||||
group with `is_required` true/false, a couple of variants across price units, and a `409` path to exercise
|
||||
the duplicate warning), **and**:
|
||||
- append the gap to [`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
|
||||
(per operating-rules §6 — you request, backend delivers; never edit backend files), and
|
||||
- record the mock in [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
|
||||
+ your phase report (per operating-rules §7) with exactly how f-next swaps it for the real endpoint.
|
||||
- No new external-service seam is introduced here (Elasticsearch / `INurseSearch` belongs to **search**,
|
||||
f6/b7 — do not pull it forward).
|
||||
|
||||
## 5. Critical rules you must not get wrong
|
||||
|
||||
- **Money correctness — IRR is integer, never a float.** Money is **IRR Rials as an integer string on the
|
||||
wire**; parse it integer-safe and render it **only** through the f0 money util (`formatIrrToToman`). Toman
|
||||
is **display-only**; convert Toman input → IRR digit-string at the field boundary. **No floating-point**
|
||||
anywhere on the price path (input, state, or submit) — float coercion is a defect.
|
||||
- **price + unit + session_count drive the displayed total — never compute the total from price alone.** The
|
||||
estimated total = `price` interpreted by `price_unit` combined with `session_count`/duration. A bare
|
||||
`price` is a unit rate, not an engagement total. Render the unit label from an **i18n key off the
|
||||
`price_unit` code**, never a label hardcoded in the component.
|
||||
- **Reference data is cached.** Categories and option groups/values are admin-seeded reference data — fetch
|
||||
once with a long `staleTime`/`gcTime` and reuse from cache; do not refetch them per screen or per step.
|
||||
Variant mutations invalidate `myVariants` (and `setQueryData` the edited row) so you never needlessly
|
||||
refetch the list.
|
||||
- **Validate every required option group.** The builder must not submit until **every `is_required` group has
|
||||
exactly one value chosen**; optional groups may be empty; one value per group (single-select mirrors the
|
||||
server's `UNIQUE(variant_id, option_group_id)`).
|
||||
- **Deactivate is soft — never hard-delete.** A deactivated variant must read as unbookable and is understood
|
||||
to drop out of search; there is no delete affordance.
|
||||
- **Duplicate-listing is a friendly conflict, not a crash.** Surface the server `409` on
|
||||
`(nurse_id, category, option-set)` as inline, actionable copy.
|
||||
- **Data-driven catalog (no hardcoded enums).** Categories, option groups, and option values come from the
|
||||
API and render by `sort_order` + locale label — **never** hardcode the category/option list as constants.
|
||||
The **only** closed enum is `price_unit` (the five units).
|
||||
- **RTL-first, both locales.** `fa` is default and RTL; every user-visible string is a key in **both**
|
||||
`en.json` and `fa.json`, in sync. Persian unit labels (ساعتی / روزانه / شبانهروزی, …) must read correctly.
|
||||
- **RSC/client boundary & re-renders.** No `next/headers`/`next-intl/server` in client components; keep
|
||||
builder step state colocated (low) so typing in the price field doesn't re-render the whole stepper;
|
||||
stable references where it pays.
|
||||
- **MUI primitives stay MUI; reuse the shared stepper & status chip** from f0 — do not fork a new root
|
||||
primitive or a second stepper.
|
||||
- **Tenancy is server-enforced, but don't leak it in UI:** the nurse only ever sees/edits *her own* variants
|
||||
(`GET api/v1/nurse/variants` is self-scoped) — never build a UI that lists or edits another nurse's
|
||||
offerings.
|
||||
|
||||
## 6. Definition of Done
|
||||
|
||||
The shared [definition-of-done.md](../_shared/definition-of-done.md), plus these phase specifics:
|
||||
- [ ] `services/catalog` exists (types/keys/apis/hooks/index) mirroring the f0 pattern; reference data
|
||||
cached with deliberate `staleTime`/`gcTime`; variant mutations invalidate/`setQueryData` `myVariants`.
|
||||
- [ ] **Home (A5)** renders inside the customer shell with the bottom nav: greeting + avatar, the search bar
|
||||
(navigates toward f6, results not built here), the **data-driven category grid** (loading/empty/error
|
||||
states), and the complete-patient-record nudge (derived from cached f2 state, hidden when complete).
|
||||
- [ ] The **nurse builder** (stepper category → required/optional options → price+unit+duration) enforces all
|
||||
required groups, shows the live unit-aware estimated total, auto-generates an editable `display_name`,
|
||||
submits as IRR digit-string, and handles the **duplicate `409`** with friendly inline copy.
|
||||
- [ ] The **offerings list** shows active vs deactivated distinctly, supports **edit** and **soft
|
||||
deactivate** (confirm dialog), with empty/loading states.
|
||||
- [ ] All money rendered via the f0 money util; the **`price_unit`** labels are i18n keys in **both** locales,
|
||||
RTL-correct; `en.json`/`fa.json` in sync.
|
||||
- [ ] Types derive from [`catalog.md`](../../contracts/domains/catalog.md); any gap is logged in
|
||||
`for-backend.md` and mocked behind the `services/catalog` seam (recorded in the mock registry).
|
||||
- [ ] `npm run check` green; `npm run test:ci` green for any shared component added (e.g. `CategoryTile`,
|
||||
a price-unit display, the variant card) with co-located `*.test.tsx`.
|
||||
- [ ] `client/CLAUDE.md` *Project Structure* updated for the new route segments (customer `home`, nurse
|
||||
`services`) and the `services/catalog` domain + any new shared component.
|
||||
|
||||
## 7. How to test (what a human can verify after this phase)
|
||||
|
||||
Run `npm run dev` (and ensure the b5 endpoints are reachable, or the `services/catalog` mock is active).
|
||||
|
||||
1. **Home renders.** Sign in as a customer → the Home screen shows the greeting, avatar, search bar, and the
|
||||
**category grid** (مراقبت سالمند / پرستار کودک / تزریقات و سرم / فیزیوتراپی) ordered by `sort_order`. With
|
||||
no patient on file, the **complete-patient-record nudge** is visible; after completing a patient (f2) it
|
||||
disappears (no extra refetch — verify in React Query Devtools that the patient query is reused).
|
||||
2. **Locale + RTL.** Switch `fa`↔`en` → labels translate, `dir` flips, the grid and tiles mirror correctly;
|
||||
Persian unit labels read right.
|
||||
3. **Build a variant.** Sign in as a nurse → `+ افزودن خدمت` → step through: pick a category; in the options
|
||||
step, try to advance **without** answering a required group → **blocked** with a clear message; answer it →
|
||||
advance; set a **price (in Toman)** and a **unit** (e.g. ساعتی) with a duration → the **estimated total**
|
||||
updates from price × duration (not from price alone); edit the auto-generated `display_name`; submit →
|
||||
the new variant appears in the offerings list, price shown as `… تومان ساعتی`.
|
||||
4. **Duplicate warning.** Create a second variant with the **same category + same option set** → the builder
|
||||
shows the friendly duplicate-listing warning (server `409`) and lets you change it, without a crash or a
|
||||
generic error toast.
|
||||
5. **Edit + deactivate.** Edit a variant's price/`display_name` → list reflects it without a full refetch
|
||||
(Devtools: `setQueryData`/single invalidation). Deactivate a variant → it flips to the deactivated visual
|
||||
state with the "can't be booked" hint; there is **no delete** option.
|
||||
6. **Caching.** In React Query Devtools confirm `catalogKeys.categories()` / `categoryOptionGroups` are
|
||||
served from cache across Home and the builder (no repeated network calls per step); a variant mutation
|
||||
invalidates only `myVariants`.
|
||||
7. **Gate.** `npm run check` and `npm run test:ci` pass.
|
||||
|
||||
## 8. Hand off & document (close the phase)
|
||||
|
||||
- **Docs to update (same change):**
|
||||
- [`client/CLAUDE.md`](../../../client/CLAUDE.md) *Project Structure* — add the customer `home` and nurse
|
||||
`services` route segments, the `services/catalog` domain, and any new shared component (`CategoryTile`,
|
||||
price-unit display, variant card). Note the reference-data caching convention if it's the first long-lived
|
||||
cached domain.
|
||||
- If you discover or decide a catalog/pricing rule the product docs don't capture (e.g. how the estimated
|
||||
total is presented for each unit), record it in
|
||||
[`../../../product/business/03-service-catalog-and-pricing.md`](../../../product/business/03-service-catalog-and-pricing.md)
|
||||
— don't invent rules; record decisions, and flag any open question in your report.
|
||||
- **Contract to consume:** [`../../contracts/domains/catalog.md`](../../contracts/domains/catalog.md) (b5) —
|
||||
types/services derive from it; do not guess shapes. Any missing/ambiguous shape → append to
|
||||
[`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
|
||||
(you request; backend delivers — never edit backend files). The frontend produces no contract.
|
||||
- **Handoff & report (per operating-rules §6–§7):**
|
||||
- Append your phase summary to
|
||||
[`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md).
|
||||
- Write `dev/shared-working-context/reports/frontend-phase-4-report.md`: what shipped (Home A5, the nurse
|
||||
variant builder + offerings list, `services/catalog`), **what is now testable and exactly how** (the §7
|
||||
steps), what is mocked vs live behind the catalog seam and **how f6 swaps it**, the contract consumed,
|
||||
and the follow-ups handed to **f6** (the Home search bar now hands a `service_category_id` to search; the
|
||||
variant builder is what populates the index f6 reads).
|
||||
- Update [`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
|
||||
if you mocked any catalog endpoint (seam, what's faked, config, how to make it real).
|
||||
- **Memory (per operating-rules §8):** save a `project`-type memory note for the non-obvious decisions —
|
||||
the price/unit/session_count display rule (total never from price alone), the IRR-string-in/Toman-display
|
||||
money handling on the builder, and the reference-data caching choice — with a one-line `MEMORY.md` pointer.
|
||||
Don't record what the code/docs already make obvious.
|
||||
Reference in New Issue
Block a user