14 KiB
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:
ProfileSummaryidentity header (avatar/initials via the newinitialsFallbackprop + 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). /profilestays the single route — اطلاعات شخصی/زبان/مخاطب اضطراری openFormDialogShellsheets, not sub-routes. زبان's sheet hosts the phase-2LocaleSwitcher(the actual UI locale switch) alongside the server-storedpreferredLanguagefield — 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.
ActorSwitcherpreserved 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
/patientsroute,services/patientsmodule, and everypatients.*/nav.patientsi18n key name are unchanged; only the translated values changed (bothen.jsonandfa.json). Applied consistently: page title, nav tab, empty states, and the Home screen'snudge_patient_*/patients_errorcopy. - Avatars: a new
InitialsAvatarcomponent (warm auto-colored initials, deterministic per-name hash → one of 6 new--bal-avatar-*token pairs added totokens.css, both scheme blocks) is now a slot on the sharedPatientHeader, so both the E1 card and the E2 record viewer got a face in one change.ProfileSummaryalso gained aninitialsFallbackprop (same util) for the account hub's identity header. - Visible record affordance:
PatientCard's invisiblecomponent="button"area is now a pressableButtonBasesurface (hover/press + a trailing «forward» chevron) with an optionallastVisitLabelprop — never populated (the cached customer bookings list,BookingListItemDto, carries nopatientIdat 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,Dialogon 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 codesmorning|noon|evening|night); routine items get the same time-of-day chip row instead of the old free-textroutine_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.tsupdated to the new shape. - سوابق (history) is now a Shamsi month-grouped timeline (a subtle rail), each
VisitNoteCardgains a done/undone task-summary line ({done} از {total} انجام شد) and, whenVisitNote.bookingIdis 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 (
useRecordAccessbefore any clinical fetch, the access-denied card, the ownership banner) is untouched, verbatim.
Addresses + the real map (addresses/page.tsx, components/geography/)
isErrorwas not handled before this phase (confirmed against current code) — fixed:ErrorState+ retry, no more false-empty.- Real Neshan map behind the existing
AddressMapPickerboundary.{ latitude, longitude }in/out is unchanged, soAddressFormneeded zero changes.AddressMapPickernow branches onNESHAN_WEB_KEY(@/config, fromNEXT_PUBLIC_NESHAN_KEY):- Set: dynamically imports (
next/dynamic,ssr:false) a newNeshanMapcomponent — Leaflet (leaflet+@types/leaflet, newly addednpmdependencies) with a Neshan raster tile layer, an address search box (services/geography/neshan.ts'ssearchNeshan), a locate-me button (browser Geolocation), a draggable/tappable pin (a brand-coloredL.divIcon, never Leaflet's default marker images), and a reverse-geocoded preview line (reverseGeocodeNeshan) — raw coordinates are never rendered. The container staysdir="ltr"(same RTL hazardAddressMapPicker'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-shadowtoken. - Honest flag (operating-rules §9 — external unknown, not stalled on): the Neshan tile URL template
(
services/geography/constants.ts'sNESHAN_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 toplatform.neshan.org/api.neshan.orgfor a fetch tool, only forWebSearch. Endpoints (api.neshan.org/v1/search,api.neshan.org/v5/reverse) and theApi-Keyheader 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.
- Set: dynamically imports (
AddressCardgained the pin-quality cue: «پین ثبت شده» / «پین ندارد» (warm warning tone) fromAddress.latitude/longitudebeing non-null.- Fixed the
line_hintfa 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 sharedFormDialogShell(components/common/FormDialogShell/): full-screen below thesmbreakpoint (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/AddressFormreportdirtyvia a newonDirtyChangeprop (compares current fields to theinitialprop).
Form quality
PatientForm: the single guessed full-name field is now نام / نام خانوادگی (structured, the wire already carriesfirstName/lastName);lastNamefalls back tofirstNameonly when left blank (the wire'slastNameis a required string) — never a client-side guess-split.- Verified no surface renders the fabricated Jan-1
birthDate— every display path already went throughbirthDateToAgebefore this phase; age-only, as required. GenderTogglegainedwidth: '100%'so itsflex: 1children 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
AppLoadingfull-page spinner replaced with a form-shapedSkeleton(identity header + row placeholders), matching the loading language of patients/addresses/record.
What is now testable (and exactly how)
- 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. - Clear the emergency contact (via the row's edit) → the incomplete-state warm nudge; fill it in → a
success check +
tel:link. - Stop the API, reload
/profile,/patients,/addresses→ each showsErrorStatewith retry (no blank form, no false-empty). Start the API, tap retry → data returns without a full reload. - 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. - 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.
- Open سوابق → notes grouped by Shamsi month on a rail; a note whose
bookingIdis set shows «مشاهده رزرو» linking to/bookings/[id]. - Add an address on mobile → the form opens full-screen with an app-bar header. Without
NEXT_PUBLIC_NESHAN_KEYset (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 «پین ثبت شده». - Repeat 1, 4, 5, 7 on
/en(LTR) and in dark mode.
What is mocked / waiting on a real service
patientRecordsstays 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.mdentry filed; the fallback grid is the registered dev/CI behavior, already documented inAddressMapPicker'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(+ optionalvisitCount) 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; thecomponents/common/list (newInitialsAvatar/,FormDialogShell/);PatientForm,PatientCard,PatientHeader,ProfileSummary, andgeography/component bullets.client/.env.sample: documentedNEXT_PUBLIC_NESHAN_KEY(the client web key — separate from the server'sNeshanGeocoderkey, 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-dayui phase 2commit (2026-07-17 19:05) that already added theisError/ErrorStatehandling the audit describes as missing inprofile/page.tsxandpatients/page.tsx. Verified against the current code (not re-derived from the stale audit text) before starting —addresses/page.tsxgenuinely 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_TEMPLATEagainst the currentplatform.neshan.orgdeveloper docs once a real web key is available to test against. - REQ-057 (
lastVisitAt) — once served, wirePatientCard's existinglastVisitLabelprop inpatients/page.tsx. - Phase 12's global copy sweep: the
address.line_hintfa fix already landed here — don't re-edit it. - A read-mode "daily schedule" view (meds grouped صبح/ظهر/شب) and an
AddressCardstatic-map thumbnail are both explicitly deferred per the phase doc (§3.3.3, §3.4.3) — not started.