Files
baya-monorepo/archive/build-chain/working-context/reports/frontend-phase-3-report.md
T
2026-08-02 18:48:32 +03:30

8.4 KiB

Frontend Phase 3 — Addresses, map picker & nurse coverage areas (b4)

Track: frontend · Consumes: dev/contracts/domains/geography-addresses.md (backend-phase-4) · Unlocks: f7 booking request (needs a chosen address) + f6 search (needs nurse coverage areas) · Date: 2026-07-03 · Gate: npm run check green · npm run test:ci green (129 tests, +17 across 5 new suites) · npm run build green (routes /[locale]/addresses, /[locale]/nurse/coverage generated)

What shipped

Both actors get their place on the map — pure geography, no money, no clinical data.

Three domain services (mirroring the patients/nurse template)

  • services/geography — the cached province → city → district reference lookups. types.ts (Province/City/District + the GeographyApi seam), keys.ts (geographyKeys.{provinces,cities,districts}), constants.ts (USE_GEOGRAPHY_MOCK, GEO_STALE_TIME = Infinity + GEO_GC_TIME, CITY_CENTROIDS + cityCentroid()), names.ts (pickRegionName), apis/{clientApi,mockApi,seed,index}.ts, hooks useProvinces/useCities(enabled on province)/useDistricts(enabled on city). Aggressively cached: each level is fetched once per session and shared across both editors (and later f6 search).
  • services/addresses — the customer address book. types.ts (CustomerAddress = wire CustomerAddressDto
    • client-augmented provinceId; CreateAddressInput; AddressesApi), keys.ts, constants.ts, full seam+mock+client, hooks useAddresses/useCreateAddress/useUpdateAddress/useDeleteAddress/ useSetPrimaryAddressevery mutation invalidates addressKeys.lists() (set-primary flips two rows, so invalidate, don't hand-patch).
  • services/serviceAreas — the nurse coverage areas. types.ts (NurseServiceArea; AddServiceAreaInput; ServiceAreasApi; the pure areaExists dup-guard), keys.ts, constants.ts, seam+mock+client, hooks useServiceAreas/useAddServiceArea/useRemoveServiceArea (add/remove invalidate the list).

Two shared composites + two form/card composites (src/components/geography/, each tested)

  • CascadingRegionSelect — province → city → district dependent MUI selects, driving the cached geography queries itself so both editors drop it in with only value/onChange. Each level enables on its parent, resets its children on change; a whole-city-only city surfaces the whole-city affordance; district is optional. Guards edit-prefill against out-of-range values before options load.
  • AddressMapPicker — the map-pin stand-in (see Mocks). A tappable/draggable marker canvas that emits real { latitude, longitude }; marker positioned with inline style (physical left/top) + dir="ltr" so the RTL stylis plugin can't mirror the pin off its click point. Centres on the chosen city's centroid.
  • AddressForm — the add/edit body: cascade + map pin + title + street + set-primary toggle; validates city required, pin required, title + street required, district optional. Emits CreateAddressInput.
  • AddressCard — presentational list card: title + primary badge (reuses the f0 StatusChip), region label, street line, edit/delete/set-primary; set-primary shows only on non-primary cards (never two primaries).

Screens

  • Customer address book (/addresses, reached from a profile-hub link) — cards with primary badge, add/edit dialog (cascade + map pin), set-primary, soft-delete confirm, empty + skeleton states.
  • Nurse coverage editor (/nurse/coverage, new sidebar tab) — area chips (whole-city shown explicitly), an add control (cascade + whole-city/specific-districts scope toggle), inline duplicate block (client areaExists fast path + the server 409 mapped to the same message), remove confirm, and the empty-state "won't appear in search" warning.

Wiring

  • constants/routes.ts: ADDRESSES, NURSE_COVERAGE. AppIcon registry: location, delete, coverage.
  • i18n: new geo/address/coverage namespaces + nav.coverage in both en.json and fa.json (identical key sets, RTL-first). Colours from tokens.css only.
  • client/CLAUDE.md Project Structure + i18n namespaces + the reference-data caching convention updated.

What is now testable, and exactly how

Run cd client && npm run dev, sign in (f1-b2 OTP). All three services default to their client mock, so the flows work without the backend running.

  1. Cascading dropdowns + caching. Customer → Profile → Manage addressesAdd address. Province → city → district cascade; a whole-city-only city (Mashhad/Isfahan/…) shows the whole-city affordance. Re-open Add address: the lists come from cache (React Query Devtools shows no refetch).
  2. Add with a map pin + set primary. Pick city (+ optional district), drop a pin, enter title + street, toggle primary, save → the card shows a primary badge. Add a second, set it primary → exactly one badge moves. Save without a city or pin → inline errors.
  3. Nurse coverage + duplicate block. Nurse → Coverage. Empty → the "won't appear in search" warning. Add a whole-city area (a chip); add a city + district area; add the same pair again → inline "already covered", no request fired; remove → the chip disappears.
  4. i18n / RTL. Flip faen: every label/empty/error/duplicate string translates; the cascade, chips, and map controls mirror correctly; colours match the brand tokens.
  5. npm run check, npm run test:ci, npm run build all pass.

What is mocked client-side (and how f-next swaps it)

All three services are behind a services/{domain} seam with a USE_*_MOCK flag (default true) and a real clientApi already wired to the contract routes — the swap is a one-line flag flip per service. The IGeocoder seam is backend-owned (backend-phase-4); the client only sends the picked coordinates. The AddressMapPicker is a stand-in (no Neshan/Google tiles) behind a component boundary — a real map drops in without touching AddressForm. See the mock registry for the exact rows + make-it-real steps.

Contract consumed + gaps filed

Consumed dev/contracts/domains/geography-addresses.md (camelCase wire, snake_case query params, action-style routes, 409 on duplicate coverage, single-primary address). Two gaps filed in for-backend.md:

  • REQ-008 — accept the client-picked latitude/longitude on address create/update (the contract geocodes server-side today; f3 requires the user's dropped pin for EVV precision). The client sends them + echoes locally.
  • REQ-009 — add provinceId to CustomerAddressDto so the edit form can prefill the province→city cascade (the city list is fetched per-province). Client-augmented behind the seam meanwhile.

Adversarial review (pre-merge)

Ran a 5-dimension multi-agent review (contract fidelity · conventions/i18n · single-primary · coverage dedup/cascade · map/RTL/caching) with an adversarial verify pass. 3 findings confirmed and fixed:

  1. (high, RTL) AddressMapPicker marker's centering transform was in sx, so stylis-plugin-rtl flipped its X on the default fa locale — the pin rendered ~one icon-width off the tap point. Moved the transform into the inline style alongside the left/top offsets.
  2. (med, contract) addresses/serviceAreas list sent page_size (snake_case), which the server's PageSize binder ignores (only the geo lookups are explicitly snake_cased) → truncated lists once the real endpoints are used. Changed both to pageSize, matching the patients template.
  3. (med, UX) The coverage "specific districts" scope dead-ended on a whole-city-only city (district-less) with contradictory no_districtsdistrict_required messages. The page now reads the cached districts to force whole-city for such cities (disables the "districts" toggle), so the add never dead-ends.

Follow-ups for later phases

  • f7 booking request consumes the chosen customer address (id + coordinates).
  • f6 search consumes nurse coverage areas (whole-city rows match every district; reuse the geography cache
    • geographyKeys, don't reinvent). The same-gender filter is f6, not here.
  • When REQ-008/REQ-009 land, flip USE_ADDRESSES_MOCK to false.
  • Swap the AddressMapPicker stand-in for a real map (mock registry row).