# Frontend Phase 3 — Addresses, map picker & nurse coverage areas (b4) **Track:** frontend · **Consumes:** [`dev/contracts/domains/geography-addresses.md`](../../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`/ `useSetPrimaryAddress` — **every 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 addresses* → *Add 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 `fa`↔`en`: 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`](../frontend/requests/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_districts`↔`district_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).