Files
baya-monorepo/dev/post-phase/ui/ui-phase-9-customer-account-and-care-circle.md
T
2026-07-17 13:22:04 +03:30

281 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 02](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:1618` 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 7577).
- **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:6685` 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 439445).
- **The map is not a map.** `components/geography/AddressMapPicker.tsx:2835` 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:4450`) 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:5052`), access-denied card at 5974,
ownership banner at 108116. 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:7981`) 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:3238` 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 5759). 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.