# 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.