Files
baya-monorepo/archive/build-chain/working-context/reports/ui-phase-9-report.md
T
2026-08-02 18:48:32 +03:30

14 KiB

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 (ConfirmDialoguseLogout(), 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-057lastVisitAt (+ 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.