# UI Phase 9 — Customer account & care circle

> **Mission:** the customer account area is functionally complete but emotionally wrong for a home-care
> product: a flat settings form with no sign-out, care recipients as clinical rows behind an invisible tap
> target, a whole-list free-text care-record edit mode that loses drafts, and an address "map" that is a
> blank coordinate grid feeding the EVV proximity check. Reframe the area around **the people being cared
> for** — an account hub, a care circle with faces, per-item structured record editing, a real Neshan map —
> and fix the real defects along the way (silent profile-load error, error→false-empty, cramped dialogs).
>
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
> **Unlocks:** the account area feels like caring for people, not filling forms
>
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md)
> and invoke the frontend-designer skill — both are mandatory.**

## 1. Context — where this sits

Balinyaar's customer is a **family member arranging care for someone they love** — a parent, a spouse,
sometimes themselves. The account area (`client/src/app/[locale]/(private-routes)/(customer)/` — `profile/`,
`patients/`, `patients/[id]/record/`, `addresses/`) is where that relationship lives, and today it reads
like an admin table. Every problem below is verified in code:

- **Profile is a flat form, and a failed load silently blanks it.** `profile/page.tsx:16–18` handles only
  `isLoading` from `useCustomerProfile()`; on error the form renders with `initial={profile ?? null}` — a
  save from that blank state **overwrites server truth with empty fields**. No identity header, no phone,
  no sign-out; the only status affordance is a color-only completeness line (lines 75–77).
- **Query errors collapse into false-empty.** `patients/page.tsx:83` — `const isEmpty = !isLoading &&
  patients.length === 0;` never reads `isError`, so a failed query tells a family «هنوز بیماری ثبت نشده».
  Identical bug in `addresses/page.tsx:91`.
- **The care record is undiscoverable and error-prone to edit.** `PatientCard.tsx:66–85` renders the
  tap-to-open area as an unstyled `component="button"` (`background: 'none', border: 'none'`) — nothing
  signals the richest screen in the area exists. Inside, `record/page.tsx` edits each tab as a whole-list
  mode with per-tab `useState` drafts (lines 197/275/346) **destroyed on tab switch**; dose/frequency/
  time-of-day are plain free text (`routine_time`, line 321); سوابق is a flat card list with bare
  «قبلی/بعدی» paging (lines 439–445).
- **The map is not a map.** `components/geography/AddressMapPicker.tsx:28–35` documents itself as "NOT a
  real map (no Neshan/Google tiles), only a bounded canvas" and surfaces raw lat/lng captions — yet the pin
  it produces feeds nurse arrival and the EVV proximity check.
- **Forms are crammed into `maxWidth="sm"` dialogs** on a phone-first shell with no `fullScreen` and no
  dirty-state guard (`patients/page.tsx:160`, `addresses/page.tsx:170`); backdrop-click discards work.
- **No avatar/photo concept exists in the customer identity system** — `CustomerProfile`
  (`services/profiles/types.ts:44–50`) and `Patient` are text-only; `avatarUrl` is nurse-only.

**What already exists (do not rebuild):**

- [Phase 0](ui-phase-0-design-language.md) theme/brand, [Phase 1](ui-phase-1-primitives-and-states.md)
  primitives (ErrorState/EmptyState kit, PageHeader, card kit, skeleton twins), [Phase 2](ui-phase-2-shells-and-navigation.md)
  customer shell + the **sign-out affordance** — this phase gives sign-out its *home*, not its first existence.
- The `services/{domain}` layer: `profiles`, `patients` (+ `age.ts`), `addresses`, `patientRecords` are
  wired and (except `patientRecords`, REQ-027) **real**. This phase is presentation + defect fixes.
- `PatientHeader` (shared by E1 card + E2 record), `PatientForm`, `PatientCard`, `CascadingRegionSelect`,
  `AddressForm`, `AddressCard`, `AddressMapPicker` — all tested; restyle, don't fork.
- The care record's **non-leaking access gate**: `useRecordAccess` before any clinical fetch,
  `usePatient(patientId, { enabled: canView })` (`record/page.tsx:50–52`), access-denied card at 59–74,
  ownership banner at 108–116. Preserve verbatim.
- Soft-archive semantics + copy, the dashed-border empty states («اولین آدرس را اضافه کنید تا پرستار بداند
  کجا بیاید»), content-shaped skeletons, inline per-field validation with error-clearing.

## 2. Required reading (do this first)

- [audit/customer-account.md](audit/customer-account.md) — the 16 problems + 10 opportunities + the
  keep-list this phase executes; every file/line above comes from it.
- Code, in this order: `profile/page.tsx`, `patients/page.tsx`, `patients/[id]/record/page.tsx`,
  `addresses/page.tsx` (all under `client/src/app/[locale]/(private-routes)/(customer)/`), then
  `client/src/components/{PatientCard,PatientHeader,PatientForm,RelationSelect,GenderToggle}/` and
  `client/src/components/geography/{AddressMapPicker,AddressForm,AddressCard,CascadingRegionSelect}.tsx`,
  then `client/src/services/{profiles,patients,addresses,patientRecords}/types.ts` and
  `client/src/services/patients/age.ts`.
- [../../../.claude/skills/frontend-designer/SKILL.md](../../../.claude/skills/frontend-designer/SKILL.md) —
  the design contract (invoke the skill, don't just read it).
- Product: [../../../product/business/01-actors-and-onboarding.md](../../../product/business/01-actors-and-onboarding.md)
  (who the "patient" actually is — relations include «خودم») and the Persian glossary in
  [../../../product/overview/platform-summary.md](../../../product/overview/platform-summary.md) — both feed
  the naming decision in 3.2; [../../../product/business/06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
  for why the address pin matters (advisory EVV proximity).
- The REQ tracker tail: [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
  — REQ-001…038 taken at audit time; earlier UI phases may have appended more. Take the next free number.
- `client/CLAUDE.md` — Golden rules + the `(customer)` part of Project Structure.

## 3. Scope — build this

### 3.1 Profile → account hub (`profile/page.tsx`)

Rebuild the Profile tab as the customer's account center:

1. **Identity header** — warm auto-colored initials (same util as 3.2) + full name + masked phone from
   `/me` (`useMe().phone` via `maskIranMobile` from `@/components/PhoneNumberField`, in a `dir="ltr"` span).
   The first place the customer sees *themselves* in the app.
2. **Grouped tappable rows** below the header — اطلاعات شخصی / مخاطب اضطراری / نشانی‌ها / زبان / اعلان‌ها /
   پشتیبانی / **خروج**. نشانی‌ها → `/addresses`, اعلان‌ها → `/notifications`, پشتیبانی → `/support/tickets`;
   اطلاعات شخصی and زبان open focused edit surfaces (prefer bottom sheets so `/profile` stays the single
   route; sub-routes require a `client/CLAUDE.md` Project Structure update). The زبان row owns the
   server-stored `preferredLanguage` and hands the actual locale switch to phase 2's switcher — don't build
   a second locale mechanism. The خروج row is the sign-out's home: confirm dialog → `useLogout()` (the
   single logout path — never hand-roll cookie clearing).
3. **Emergency contact as a status card**, not two bare fields: complete → success check + contact name +
   a `tel:` link (tel-only, per the f14 emergency rule); incomplete → a warm nudge explaining *why* nurses
   need it («پرستار باید بداند در شرایط اضطراری با چه کسی تماس بگیرد») + edit CTA.
4. **Fix the silent load error**: on `useCustomerProfile()` error render the phase-1 ErrorState with retry —
   **never** the editable form. The blank-form-overwrites-server-truth path must be impossible.

### 3.2 Care-circle reframe (`patients/page.tsx`, `PatientCard`, `PatientHeader`)

1. **Naming**: consult the product docs + glossary (§2) before renaming «بیماران». Candidates: «عزیزان شما»
   / «حلقه مراقبت». Mind the «خودم» relation — the term must not be absurd for self-care («حلقه مراقبت» is
   the safer default). **Record the decision in your report** and apply it consistently (page title, nav
   label, empty states, the A5 home nudge). **Copy-level rename only**: the `/patients` route,
   `services/patients`, and the `patients.*` i18n key names stay unchanged.
2. **Avatar slot on `PatientHeader`** — the patients API has no photo field, so ship warm auto-colored
   initials: a deterministic util (name hash → one of ~6 new `--bal-avatar-*` token pairs added to
   `tokens.css` **both scheme blocks**; note the token addition as a foundation extension in your report —
   phase 0 owns `theme/`). `PatientHeader` is shared by the E1 card and E2 record, so one change gives both
   a face; update its co-located test. Real photo upload is (DEFERRED → optional REQ, see §4).
3. **Visible record affordance on `PatientCard`** — replace the invisible button with a
   `CardActionArea`-style hover/press surface or an explicit «مشاهده پرونده» chevron row. Add a
   **last-visit meta line** («آخرین ویزیت: ۱۲ تیر») via an optional `lastVisitLabel` prop — sourced
   best-effort from the cached customer bookings list; the server-truth field is a REQ (§4). Omit when
   unknown; never fabricate.
4. **Fix error→false-empty**: `isError` branch with phase-1 ErrorState + retry on **both** `patients/page.tsx`
   and `addresses/page.tsx`. A failed query must never render «هنوز بیماری ثبت نشده» + add CTA.
5. **Keep** soft-archive semantics and its copy verbatim (do-not-regress).

### 3.3 Care record editing (`patients/[id]/record/page.tsx`)

1. **Per-item bottom sheets replace whole-list edit mode.** Each medication/routine/task row gets edit (and
   each tab an add CTA) opening a bottom sheet (`Drawer anchor="bottom"`; standard dialog on desktop).
   Medication sheet: name + **structured dose** (amount + unit select قرص/کپسول/قطره/سی‌سی/واحد) +
   **frequency presets** («روزی ۱ بار» … «هر ۸ ساعت» / «در صورت نیاز» + free-text fallback) + **time-of-day
   chips** (stable codes `morning|noon|evening|night` → صبح/ظهر/عصر/شب). Routine items get the same chip row
   instead of the free-text `routine_time`; tasks stay label + done. A dirty sheet gets a discard-confirm;
   killing the whole-list mode removes the silent tab-switch draft loss by construction.
2. **REQ posture**: the family-owned record is REQ-027 mock territory (no backend). Keep the UI seam-tolerant
   behind `services/patientRecords` — extend the client model + `mockApi` for the structured fields, and
   **append the structured shape as an addendum to REQ-027** in the tracker (dose amount/unit, frequency
   preset codes, time-of-day codes) so the eventual table matches what the UI collects.
3. **Visit-note history becomes a timeline**: group سوابق by Shamsi month with a subtle rail; each
   `VisitNoteCard` gains a done/undone task summary line; when `VisitNote.bookingId` is non-null
   (`services/patientRecords/types.ts:79–81`) link «مشاهده رزرو» → `/bookings/[id]`, showing the service
   name only when derivable from the cached booking — never block on it. Keep paging (grouped within pages
   is fine). A read-mode "daily schedule" view (meds grouped صبح/ظهر/شب) is (DEFERRED — not in this chain).
4. **Do not touch the access gate** — `useRecordAccess` before any clinical fetch, the access-denied card,
   and the ownership banner survive the redesign verbatim.

### 3.4 Addresses + the real map (`addresses/page.tsx`, `components/geography/`)

1. **Real Neshan web tiles behind the existing `AddressMapPicker` boundary** — the component's props/output
   (`{ latitude, longitude }`) don't change, so `AddressForm` is untouched. Client-side embed (official
   Neshan web SDK or Leaflet + Neshan tiles), **dynamically imported with `ssr: false`**. Key from
   `NEXT_PUBLIC_NESHAN_KEY` — the **web key is separate config** from the server's `NeshanGeocoder` adapter
   (refinement phase 8; lives under `server/src/Infrastructure/…/Seams/Real/` — do not touch it). Document
   the variable in `client/.env.sample` (the repo uses `.env.sample`, **not** `.env.example`). **When the
   key is unset, fall back to the current grid stand-in** so dev/CI/jsdom tests keep working — keep the
   fallback code, don't delete it.
2. **Map features**: address search box (Neshan geocode), locate-me (GPS) button, draggable pin, and a
   reverse-geocoded pin preview («پین روی: خیابان ولیعصر…») replacing the raw lat/lng captions — coordinates
   never render in the UI again. The pin still only *refines* coordinates; the bookable geography remains
   the `CascadingRegionSelect` choice. Tokenize the pin's hard-coded `rgba` drop-shadow (line 116) too.
3. **Pin-quality cue on `AddressCard`**: `Address.latitude` is nullable (`services/addresses/types.ts:38`) —
   show «پین ثبت شده» / «پین ندارد» (the latter with a warm fix-it nudge, since a missing pin degrades the
   nurse's arrival + EVV). A static map thumbnail per card is (DEFERRED — revisit once tiles are proven).
4. **Full-screen mobile form dialogs** for the patient form (`patients/page.tsx:160`) and the address form
   (`addresses/page.tsx:170`): `fullScreen` below the `sm` breakpoint with an app-bar header (title / close
   / save) and a dirty-state discard-confirm on close/backdrop. If phase 1 shipped a form-dialog primitive,
   use it; otherwise add shared `components/FormDialogShell/` (co-located test) and note the foundation
   extension in your report.
5. **Fix the fa copy bug** in `messages/fa.json:228`: «…جزئیاتی که پرستار برای یافتن در نیاز دارد.» → «…برای
   یافتن درِ منزل نیاز دارد.» (or «برای یافتن نشانی نیاز دارد.»). Phase 12 owns the global copy sweep — fix
   this one here since you touch the form, and flag it in your report so phase 12 doesn't double-edit.

### 3.5 Form quality (`PatientForm`, loading consistency)

1. **Structured name fields**: replace the single full-name field with نام / نام خانوادگی (the wire already
   takes `firstName`/`lastName`/`displayName`; today `splitName` at `PatientForm.tsx:32–38` guesses, with
   `lastName` falling back to `firstName`). `displayName` = the joined value. If the Patient *read* DTO
   lacks `firstName`/`lastName` for edit-prefill, fold that into the REQ from 3.2.
2. **Birth-year presentation honesty**: `age.ts:10` fabricates a Jan-1 `birthDate` from the collected age —
   that stays (wire mapping), but the UI must never render the fabricated full date anywhere; display
   age-only (`age_years`), and consider collecting سال تولد instead of سن if it reads warmer. Verify no
   surface prints raw `birthDate`.
3. **Loading consistency**: profile's full-page `AppLoading` (`profile/page.tsx:18`) → a form-shaped
   skeleton, matching the skeleton language of patients/addresses/record.
4. **Small a11y fixes from the audit** (check an earlier phase didn't already land them): `GenderToggle`
   gets `width: '100%'` so its `flex: 1` children actually split (today it renders content-width, misaligned
   against `fullWidth` fields); `RelationSelect` selected state gains a check icon + fill (border-color-only
   today — fails WCAG 1.4.1). Both are shared → update their co-located tests; **never** loosen
   GenderToggle's never-defaulted, non-deselectable constraint.

## 4. Mocks & seams in this phase

**No new mocks or seams.** The area's domains stay as they are: `profiles`/`patients`/`addresses` real,
`patientRecords` mock-primary behind its existing seam (REQ-027). The Neshan web embed is client config
(`NEXT_PUBLIC_NESHAN_KEY`), not a backend seam — with a grid-stand-in fallback when unset.

Backend gaps become REQ entries appended to [the tracker](../../shared-working-context/frontend/requests/for-backend.md)
— check its tail and number onward (REQ-001…038 taken at audit time). Expected filings:

1. **REQ-(next): patient care metadata** — `lastVisitAt` (optionally `visitCount`) on the patient read
   model, plus `firstName`/`lastName` on the read DTO if absent (3.2/3.5).
2. **REQ-027 addendum** (not a new number): the structured care-record field shape — medication
   `{ doseAmount, doseUnit, frequencyCode|frequencyText, timeOfDay[] }`, routine `timeOfDay` codes (3.3).
3. **REQ-(next), optional & product-gated: patient photo upload** — the UI ships initials-only either way.

## 5. Critical rules you must not get wrong

- **The non-leaking access-denied gate stays.** `useRecordAccess` gates **before** any clinical fetch
  (`usePatient` stays `enabled: canView`); a 403/denied renders the access-denied card with zero clinical
  data. Preserve the family-ownership banner. **Clinical text is never logged, never in localStorage, never
  in a query string** (`patientRecords` rule).
- **`AddressMapPicker`'s RTL engineering survives the tile swap**: the canvas stays `dir="ltr"`, marker
  positioning stays inline-`style` (the stylis RTL plugin flips `left` and `translate` — see the comment at
  lines 57–59). Real map containers get the same `dir="ltr"` island treatment.
- **`CascadingRegionSelect` architecture stays** — parent-gated enabling, per-level progress adornments, the
  explicit "whole city" MenuItem as a real choice. The map pin refines, never replaces, the region choice.
- **`GenderToggle` stays never-defaulted and non-deselectable** (same-gender matching). **Soft-archive copy
  + optimistic-with-explanatory-error stays**; the archive confirm's dismiss stays the safe/neutral button.
- **Sign-out goes through `useLogout()`** — the single logout path (server revoke + cookie clear + LOG_OUT +
  cache drop). No cookie handling in page code.
- Design-contract non-negotiables that bite here: i18n keys in **both** catalogs; tokens not hexes (new
  avatar colors = `--bal-*` pairs in both scheme blocks); RTL logical props (phone numbers get deliberate
  `dir="ltr"` islands); dark mode on every new surface; MUI v9 API only; shared components keep co-located
  tests; fetch/cookies only via `@/lib/api` + `@/lib/cookies`.

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green with updated tests for every touched shared component
      (`PatientHeader`/`PatientCard`/`PatientForm`/`GenderToggle`/`RelationSelect`/`AddressMapPicker`/
      `AddressCard`/any new `FormDialogShell`).
- [ ] `en.json`/`fa.json` in sync; the «بیماران» rename applied consistently in both; the `fa.json`
      line_hint bug fixed.
- [ ] Visual verification on the four axes (`/fa` + `/en` × light + dark), mobile **and** desktop — verify
      mobile at `/fa` first.
- [ ] With the API stopped: profile shows ErrorState + retry (no editable blank form); patients and
      addresses show ErrorState + retry (no false-empty). With the API back, retry recovers in place.
- [ ] `/profile` shows identity header (initials + name + masked LTR phone), grouped rows, emergency-contact
      status card, and a working خروج row (confirm → logged out → `/login`).
- [ ] Care circle: every person has a colored-initials avatar (stable across reloads), a visible
      «مشاهده پرونده» affordance, and archive/edit still work.
- [ ] Care record: add/edit medication via bottom sheet with structured dose/frequency/time-of-day; tab
      switches lose nothing; سوابق is a month-grouped timeline with booking links when `bookingId` exists.
- [ ] Addresses: with `NEXT_PUBLIC_NESHAN_KEY` set — real tiles, search, locate-me, draggable pin,
      reverse-geocoded preview, no raw lat/lng anywhere; with the key unset — the grid fallback still works
      and tests pass. `AddressCard` shows the pin-quality cue. `.env.sample` documents the variable.
- [ ] Patient + address forms are full-screen dialogs on mobile with app-bar header and dirty-state confirm.
      REQ entries filed per §4; REQ-027 addendum recorded.

## 7. How to test (what a human can verify after this phase)

1. Log in as a seeded customer (`0912000000x`) on a mobile viewport at `/fa`. Open the Profile tab → see
   your initials, name, and masked phone (digits LTR); rows for اطلاعات شخصی/مخاطب اضطراری/نشانی‌ها/زبان/
   اعلان‌ها/پشتیبانی/خروج. Tap خروج → confirm → you land on `/login`, session revoked.
2. Clear the emergency contact → the warm "why nurses need this" nudge; fill it → check + `tel:` link.
3. Stop the API, reload `/profile`, `/patients`, `/addresses` → each shows an error card with retry — no
   blank form, no «هنوز بیماری ثبت نشده». Start the API, tap retry → data returns without a full reload.
4. Open the care circle → each person has a colored-initials avatar and a visible «مشاهده پرونده» chevron;
   tap it → the record opens. The renamed title appears here and in the tab bar.
5. In the record, tap "add medication" → bottom sheet with dose amount + unit, frequency preset chips, and
   صبح/ظهر/عصر/شب chips; save → the row renders the structured summary. Start editing, switch tabs, come
   back → nothing lost. Close a dirty sheet → discard confirm.
6. Open سوابق → notes grouped by Shamsi month on a rail; a booking-linked note links to `/bookings/[id]`.
7. Add an address on mobile → the form opens full-screen with app-bar header; with a Neshan key set, search
   «ولیعصر», drag the pin, tap locate-me → the preview line shows the reverse-geocoded street, never raw
   coordinates. Back on the list, that address shows «پین ثبت شده»; an old pin-less address shows «پین ندارد».
   Tap close with unsaved changes → discard confirm; cancel keeps your draft.
8. Repeat 1, 4, 5, 7 on `/en` (LTR) and in dark mode — avatars, map island, timeline, and status cards all
   render correctly on all four axes.

## 8. Hand off & document (close the phase)

- Update `client/CLAUDE.md` "Project Structure" for anything added/renamed (new `components/` entries such
  as `FormDialogShell`, the avatar util, any profile sub-routes) and the `(customer)` route notes for the
  profile hub + the copy-level care-circle rename.
- Append the REQ entries + the REQ-027 addendum to
  [../../shared-working-context/frontend/requests/for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
- Write the frontend report at `dev/shared-working-context/reports/ui-phase-9-report.md`: the naming
  decision and why, the avatar-token additions (foundation extension), the Neshan embed choice + fallback
  behavior, the fa copy fix (flag it for phase 12's sweep), REQs filed, deferrals.
- Save a memory note per operating-rules §8: account-hub structure, the naming decision, the Neshan web-key
  config (`NEXT_PUBLIC_NESHAN_KEY`, fallback-to-grid), and the REQ-027 structured-fields addendum.
