ui phase 9
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
# UI Phase 9 — Customer account & care circle — Report (2026-07-19)
|
||||
|
||||
## What was built
|
||||
|
||||
### Account hub (`profile/page.tsx`)
|
||||
- Rebuilt from a flat settings form into an account hub: `ProfileSummary` identity header (avatar/initials
|
||||
via the new `initialsFallback` prop + name + server-masked phone), grouped tappable rows (اطلاعات شخصی /
|
||||
مخاطب اضطراری / نشانیها / زبان / اعلانها / پشتیبانی / خروج), an emergency-contact status card (complete →
|
||||
success check + `tel:`-only link; incomplete → a warm nudge), and a working خروج row (`ConfirmDialog` →
|
||||
`useLogout()`, the single logout path).
|
||||
- `/profile` stays the single route — اطلاعات شخصی/زبان/مخاطب اضطراری open `FormDialogShell` sheets, not
|
||||
sub-routes. زبان's sheet hosts the phase-2 `LocaleSwitcher` (the actual UI locale switch) alongside the
|
||||
server-stored `preferredLanguage` field — two distinct concerns, no second locale mechanism built.
|
||||
- The silent-load-error/blank-form defect (§3.1.4 of the phase doc) was **already fixed** before this phase
|
||||
started (confirmed against the current code, not the audit's snapshot — see "Docs updated" below); this
|
||||
phase's profile work is the hub redesign, not that fix.
|
||||
- `ActorSwitcher` preserved verbatim (renders nothing for a single-role session).
|
||||
|
||||
### Care-circle reframe (`patients/page.tsx`, `PatientCard`, `PatientHeader`)
|
||||
- **Naming decision:** «بیماران» → **«حلقهٔ مراقبت»** for the list/page title, nav label, and empty
|
||||
states (the safer default per the audit, given the «خودم» self-care relation). Individual-action copy
|
||||
(the add button, archive confirm) uses the neutral **«فرد»** ("person") rather than «عزیز» ("loved one"),
|
||||
which reads oddly for self-care. This is a **copy-only** rename — the `/patients` route, `services/patients`
|
||||
module, and every `patients.*`/`nav.patients` i18n **key name** are unchanged; only the translated
|
||||
**values** changed (both `en.json` and `fa.json`). Applied consistently: page title, nav tab, empty
|
||||
states, and the Home screen's `nudge_patient_*`/`patients_error` copy.
|
||||
- **Avatars:** a new `InitialsAvatar` component (warm auto-colored initials, deterministic per-name hash →
|
||||
one of 6 new `--bal-avatar-*` token pairs added to `tokens.css`, both scheme blocks) is now a slot on the
|
||||
shared `PatientHeader`, so both the E1 card and the E2 record viewer got a face in one change.
|
||||
`ProfileSummary` also gained an `initialsFallback` prop (same util) for the account hub's identity header.
|
||||
- **Visible record affordance:** `PatientCard`'s invisible `component="button"` area is now a pressable
|
||||
`ButtonBase` surface (hover/press + a trailing «forward» chevron) with an optional `lastVisitLabel` prop —
|
||||
**never populated** (the cached customer bookings list, `BookingListItemDto`, carries no `patientId` at
|
||||
all, so no client-side join is possible without an N+1 fetch per row; filed as REQ-057).
|
||||
- Soft-archive semantics + copy kept (generalized wording, same behavior); `isError`/empty-state handling
|
||||
was already correct (see note above) and is unchanged.
|
||||
|
||||
### Care-record editing (`patients/[id]/record/page.tsx`)
|
||||
- Replaced the whole-list free-text edit mode with **per-item bottom sheets** (`Drawer anchor="bottom"` on
|
||||
mobile, `Dialog` on desktop, each with its own dirty-gated discard-confirm): medications get name +
|
||||
structured **dose amount + unit** (قرص/کپسول/قطره/سیسی/واحد) + **frequency presets**
|
||||
(روزی ۱/۲/۳ بار، هر ۸ ساعت، در صورت نیاز + a free-text fallback when no preset fits) + **time-of-day
|
||||
chips** (stable codes `morning|noon|evening|night`); routine items get the same time-of-day chip row
|
||||
instead of the old free-text `routine_time`; tasks get a per-item sheet too (label + done), with a quick
|
||||
inline checkbox for toggling done without opening a sheet. Killing the whole-list mode removes the
|
||||
silent tab-switch draft loss by construction (each sheet is its own short-lived mount).
|
||||
- **REQ-027 addendum** recorded in `requests/for-backend.md` (not a new REQ number): the structured shape
|
||||
(dose amount/unit, frequency preset codes, time-of-day codes) so the eventual real table matches what the
|
||||
UI now collects. `services/patientRecords/types.ts` + `apis/mockApi.ts` updated to the new shape.
|
||||
- **سوابق (history)** is now a Shamsi month-grouped timeline (a subtle rail), each `VisitNoteCard` gains a
|
||||
done/undone task-summary line (`{done} از {total} انجام شد`) and, when `VisitNote.bookingId` is non-null, a
|
||||
«مشاهده رزرو» link to `/bookings/[id]` — the service name is shown only when best-effort derivable from an
|
||||
**already-cached** booking snapshot (`queryClient.getQueryData`, never fetched per-note — that would be an
|
||||
N+1). Paging kept (grouped within a page).
|
||||
- The access gate (`useRecordAccess` before any clinical fetch, the access-denied card, the ownership
|
||||
banner) is untouched, verbatim.
|
||||
|
||||
### Addresses + the real map (`addresses/page.tsx`, `components/geography/`)
|
||||
- `isError` was **not** handled before this phase (confirmed against current code) — fixed: `ErrorState` +
|
||||
retry, no more false-empty.
|
||||
- **Real Neshan map behind the existing `AddressMapPicker` boundary.** `{ latitude, longitude }` in/out is
|
||||
unchanged, so `AddressForm` needed zero changes. `AddressMapPicker` now branches on `NESHAN_WEB_KEY`
|
||||
(`@/config`, from `NEXT_PUBLIC_NESHAN_KEY`):
|
||||
- **Set:** dynamically imports (`next/dynamic`, `ssr:false`) a new `NeshanMap` component — Leaflet
|
||||
(`leaflet` + `@types/leaflet`, newly added `npm` dependencies) with a Neshan raster tile layer, an
|
||||
address search box (`services/geography/neshan.ts`'s `searchNeshan`), a locate-me button (browser
|
||||
Geolocation), a draggable/tappable pin (a brand-colored `L.divIcon`, never Leaflet's default marker
|
||||
images), and a reverse-geocoded preview line (`reverseGeocodeNeshan`) — raw coordinates are never
|
||||
rendered. The container stays `dir="ltr"` (same RTL hazard `AddressMapPicker`'s own doc comment already
|
||||
called out for the grid stand-in's marker).
|
||||
- **Unset:** falls back to the original bounded-canvas grid stand-in, **kept verbatim** (including its
|
||||
raw lat/lng captions — this is the preserved dev/CI/jsdom path, not the primary experience) except the
|
||||
hard-coded `rgba()` pin drop-shadow is now the new `--bal-pin-shadow` token.
|
||||
- **Honest flag (operating-rules §9 — external unknown, not stalled on):** the Neshan tile URL template
|
||||
(`services/geography/constants.ts`'s `NESHAN_TILE_URL_TEMPLATE`) and the Search/Reverse response field
|
||||
names (`services/geography/neshan.ts`) were set from Neshan's public documentation **without live
|
||||
verification** — this sandbox has no network route to `platform.neshan.org`/`api.neshan.org` for a
|
||||
fetch tool, only for `WebSearch`. Endpoints (`api.neshan.org/v1/search`, `api.neshan.org/v5/reverse`)
|
||||
and the `Api-Key` header are confirmed via search-result snippets; the exact JSON field names
|
||||
(`items[].location.x/y`, `formatted_address`) and the raster tile host/path are best-effort. Every read
|
||||
is defensive (optional-chained, try/catch), so a shape drift degrades to "no results"/"no tiles" rather
|
||||
than a crash. **Re-verify against the current Neshan developer portal before relying on this with a
|
||||
real key.**
|
||||
- `AddressCard` gained the pin-quality cue: «پین ثبت شده» / «پین ندارد» (warm warning tone) from
|
||||
`Address.latitude`/`longitude` being non-null.
|
||||
- Fixed the `line_hint` fa copy bug (`messages/fa.json`) — «برای یافتن در نیاز دارد» → «برای یافتن درِ منزل
|
||||
نیاز دارد» — flagging it here so phase 12's global copy sweep doesn't double-edit it.
|
||||
- Patient (`patients/page.tsx`) and address (`addresses/page.tsx`) forms now open in the new shared
|
||||
`FormDialogShell` (`components/common/FormDialogShell/`): full-screen below the `sm` breakpoint (a single
|
||||
scroll region, fixing the old double-scroll-against-keyboard defect), an app-bar header (title + close),
|
||||
and a dirty-gated discard-confirm on close/backdrop/escape — `PatientForm`/`AddressForm` report `dirty`
|
||||
via a new `onDirtyChange` prop (compares current fields to the `initial` prop).
|
||||
|
||||
### Form quality
|
||||
- `PatientForm`: the single guessed full-name field is now **نام** / **نام خانوادگی** (structured, the wire
|
||||
already carries `firstName`/`lastName`); `lastName` falls back to `firstName` only when left blank (the
|
||||
wire's `lastName` is a required string) — never a client-side guess-split.
|
||||
- Verified no surface renders the fabricated Jan-1 `birthDate` — every display path already went through
|
||||
`birthDateToAge` before this phase; age-only, as required.
|
||||
- `GenderToggle` gained `width: '100%'` so its `flex: 1` children actually split evenly.
|
||||
- `RelationSelect`'s selected state gained a check icon (+ a soft-tint background), no longer color-only
|
||||
(WCAG 1.4.1).
|
||||
- Profile's `AppLoading` full-page spinner replaced with a form-shaped `Skeleton` (identity header + row
|
||||
placeholders), matching the loading language of patients/addresses/record.
|
||||
|
||||
## What is now testable (and exactly how)
|
||||
|
||||
1. Log in as a seeded customer (`0912000000x`) on a mobile viewport at `/fa`. Open the Profile tab → identity
|
||||
header (initials + name + masked LTR phone), rows for اطلاعات شخصی/مخاطب اضطراری/نشانیها/زبان/اعلانها/
|
||||
پشتیبانی/خروج. Tap خروج → confirm → lands on `/login`, session revoked.
|
||||
2. Clear the emergency contact (via the row's edit) → the incomplete-state warm nudge; fill it in → a
|
||||
success check + `tel:` link.
|
||||
3. Stop the API, reload `/profile`, `/patients`, `/addresses` → each shows `ErrorState` with retry (no
|
||||
blank form, no false-empty). Start the API, tap retry → data returns without a full reload.
|
||||
4. Open the care circle (`/patients`) → every person has a colored-initials avatar and a visible chevron
|
||||
affordance; tap the card body → the record opens; the renamed «حلقهٔ مراقبت» title appears in the tab
|
||||
bar and the page header.
|
||||
5. In the record, tap "افزودن دارو" → the bottom sheet (mobile) with dose amount + unit, frequency preset
|
||||
chips (+ free-text fallback when none is chosen), and صبح/ظهر/عصر/شب chips; save → the row shows the
|
||||
structured summary. Switch tabs mid-edit and come back → nothing lost (the sheet only exists while open).
|
||||
Try closing a dirty sheet → discard-confirm.
|
||||
6. Open سوابق → notes grouped by Shamsi month on a rail; a note whose `bookingId` is set shows «مشاهده رزرو»
|
||||
linking to `/bookings/[id]`.
|
||||
7. Add an address on mobile → the form opens full-screen with an app-bar header. Without
|
||||
`NEXT_PUBLIC_NESHAN_KEY` set (the default in this environment), the grid stand-in still works exactly as
|
||||
before (raw lat/lng shown — the documented fallback). With a real key set, the real map (search box,
|
||||
locate-me, draggable pin, reverse-geocoded preview) should appear instead — **not verified end-to-end
|
||||
in this sandbox** (no real Neshan key available; see the honest flag above). Back on the list, an
|
||||
address without a pin shows «پین ندارد»; one with a pin shows «پین ثبت شده».
|
||||
8. Repeat 1, 4, 5, 7 on `/en` (LTR) and in dark mode.
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
|
||||
- `patientRecords` stays **mock-primary** for the family-owned record (REQ-027 — unchanged domain
|
||||
boundary); this phase only changed the **shape** collected/persisted by the mock (see the REQ-027
|
||||
addendum above), not the mock/real seam itself.
|
||||
- The Neshan web map is **client config**, not a backend seam (per the phase doc: "not a backend seam —
|
||||
with a grid-stand-in fallback when unset") — no `mocks-registry.md` entry filed; the fallback grid *is*
|
||||
the registered dev/CI behavior, already documented in `AddressMapPicker`'s own doc comment.
|
||||
- `services/geography/neshan.ts`'s tile URL and response-shape assumptions are unverified against live
|
||||
Neshan documentation (see the honest flag above) — this is a **content-accuracy risk to re-check before
|
||||
production use with a real key**, not a mock.
|
||||
|
||||
## Contracts
|
||||
|
||||
- Consumed: no new backend contract this phase (the touched domains — profiles/patients/addresses/
|
||||
patientRecords — already had their contracts from earlier phases).
|
||||
- Filed to `dev/shared-working-context/frontend/requests/for-backend.md`:
|
||||
- **REQ-027 addendum** (not a new number) — the structured medication/routine field shape.
|
||||
- **REQ-057** — `lastVisitAt` (+ optional `visitCount`) on the patient read model (`PatientDto`).
|
||||
- **REQ-058** (deferred, non-blocking) — optional patient photo upload; the shipped solution is
|
||||
initials-only and needs no backend.
|
||||
|
||||
## Docs updated
|
||||
|
||||
- `client/CLAUDE.md` "Project Structure": `profile/`, `patients/`, `patients/[id]/record/`, `addresses/`
|
||||
route notes; the `components/common/` list (new `InitialsAvatar/`, `FormDialogShell/`); `PatientForm`,
|
||||
`PatientCard`, `PatientHeader`, `ProfileSummary`, and `geography/` component bullets.
|
||||
- `client/.env.sample`: documented `NEXT_PUBLIC_NESHAN_KEY` (the client web key — separate from the
|
||||
server's `NeshanGeocoder` key, which lives in server config and is never touched here).
|
||||
- **Correction to the phase's own premise:** the audit (`dev/post-phase/ui/audit/customer-account.md`,
|
||||
authored 2026-07-17 13:22) predates a same-day `ui phase 2` commit (2026-07-17 19:05) that already added
|
||||
the `isError`/`ErrorState` handling the audit describes as missing in `profile/page.tsx` and
|
||||
`patients/page.tsx`. Verified against the current code (not re-derived from the stale audit text) before
|
||||
starting — `addresses/page.tsx` genuinely still lacked it and was fixed in this phase. Flagging this so a
|
||||
future phase doesn't re-"fix" an already-fixed defect based on the audit's original wording.
|
||||
|
||||
## Follow-ups for later phases
|
||||
|
||||
- Re-verify `services/geography/neshan.ts` + `NESHAN_TILE_URL_TEMPLATE` against the current
|
||||
`platform.neshan.org` developer docs once a real web key is available to test against.
|
||||
- REQ-057 (`lastVisitAt`) — once served, wire `PatientCard`'s existing `lastVisitLabel` prop in
|
||||
`patients/page.tsx`.
|
||||
- Phase 12's global copy sweep: the `address.line_hint` fa fix already landed here — don't re-edit it.
|
||||
- A read-mode "daily schedule" view (meds grouped صبح/ظهر/شب) and an `AddressCard` static-map thumbnail are
|
||||
both explicitly deferred per the phase doc (§3.3.3, §3.4.3) — not started.
|
||||
Reference in New Issue
Block a user