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

69 lines
18 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.
# Customer account & care-circle management (profile, patients, patient care record, addresses + map picker)
## Current state
The area lives under `client/src/app/[locale]/(private-routes)/(customer)/` inside `CustomerLayout` (slim TopBar + 5-tab BottomBar, content column capped at CONTENT_MAX_WIDTH). `profile/page.tsx` is a single flat form: first/last name, a preferred-language select, an emergency-contact section (name + `PhoneNumberField`), one save button, and an outlined Paper card linking to the address book. `patients/page.tsx` is a header + `PatientCard` list with add/edit via `PatientForm` reused in a `Dialog maxWidth="sm"`, soft-archive with a confirm dialog, a 2-row Skeleton loader, and a dashed-border empty state with icon + CTA. `patients/[id]/record/page.tsx` is the care-record viewer: shared `PatientHeader`, a family-ownership banner on `--bal-primary-soft`, four scrollable Tabs (داروها/روتین/سوابق/وظایف); the three editable tabs use a whole-list "edit mode" (every row becomes small TextFields, save-all), history is read-only `VisitNoteCard`s with text prev/next pagination; access is gated by `useRecordAccess` with a non-leaking access-denied card. `addresses/page.tsx` mirrors the patients page: `AddressCard` list (primary badge via `StatusChip status="verified"`), add/edit dialog hosting `AddressForm` = title + `CascadingRegionSelect` (province→city→district with loading adornments and an explicit "whole city" option) + `AddressMapPicker` + multiline address line + set-primary switch.
Styling is disciplined and token-driven: everything is `elevation={0}` Paper with `border: 1px solid divider, borderRadius: 2`, colors come from `--bal-*` CSS variables (both schemes), text uses `text.secondary`, and RTL is handled with logical properties (`textAlign:'start'`, `marginInlineStart:'auto'`) — `AddressMapPicker` even pins itself `dir="ltr"` with inline styles and a comment explaining the stylis-RTL transform hazard. The weak points are structural rather than cosmetic: the "map" is a coordinate-grid stand-in (no tiles/search/geocoding, raw lat/lng shown), query errors collapse into empty/blank states, long forms are crammed into non-fullscreen modals on a mobile-first shell, there is no avatar/photo concept anywhere in the customer identity system, and the Profile tab lacks basic account affordances (no sign-out anywhere in the customer shell, no phone display, no locale switch).
## Problems (16)
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/patients/page.tsx` — Query errors collapse into the empty state: a failed usePatients() leaves data undefined so the page shows 'هنوز بیماری ثبت نشده' with an add CTA — telling a family their care recipients don't exist and inviting duplicate re-entry. No isError branch or retry exists. Identical bug in addresses/page.tsx line 91.
- evidence: line 83: `const isEmpty = !isLoading && patients.length === 0;` — isError is never read from the query
- **[high]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — Profile load error is silently swallowed: only isLoading is handled, so on a failed useCustomerProfile() the form renders blank (initial=null) and a save would overwrite server truth with empty fields. No isError state, no retry.
- evidence: lines 16-18: `const { data: profile, isLoading } = useCustomerProfile(); ... if (isLoading) return <AppLoading />;` then `initial={profile ?? null}`
- **[high]** `client/src/layout/CustomerLayout.tsx` — No sign-out affordance exists anywhere in the customer experience: CustomerLayout has only support/bell/dark-toggle chrome and the 5-tab BottomBar; logout lives only in SideBar.tsx, which the customer shell never renders, and the Profile tab (the natural home for it) has no account section, no phone-number display, and no sign-out. A logged-in customer literally cannot log out.
- evidence: CustomerLayout renders TopBar(startNode=support, endNode=bell+DarkModeToggleButton)+BottomBar only; grep for logout hits layout/components/SideBar.tsx but no (customer) file
- **[high]** `client/src/components/geography/AddressMapPicker.tsx` — The 'map pin picker' is a blank coordinate grid, not a map: no tiles, no address search, no geocode, no locate-me — a family user is asked to place a pin on a featureless 220px grid whose output feeds the later EVV proximity check, so a meaningless pin is near-guaranteed. It also surfaces raw latitude/longitude captions ('عرض: 35.71234') to consumers — developer-grade UI in the most trust-sensitive form of the account area.
- evidence: lines 30-34 doc: 'It is NOT a real map (no Neshan/Google tiles), only a bounded canvas'; lines 143-152 render `{latLabel}: {value.latitude.toFixed(5)}`
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/addresses/page.tsx` — The long address form (title + 3 cascading selects + 220px map + multiline line + switch + actions) is hosted in a Dialog maxWidth='sm' that is not fullScreen on mobile — on the phone-first customer shell this yields a cramped double-scroll (DialogContent + keyboard) modal; additionally backdrop-click/onClose silently discards a half-completed form with no dirty-state guard. Same pattern for PatientForm in patients/page.tsx line 160.
- evidence: line 170: `<Dialog open={formOpen} onClose={closeForm} fullWidth maxWidth="sm">` — no fullScreen={isMobile}, no discard confirmation
- **[medium]** `client/src/services/profiles/types.ts` — No avatar/photo exists anywhere in the customer identity system: avatarUrl is nurse-only (NurseProfile line 29-31), CustomerProfile has none, and PatientHeader/PatientCard render text-only with no Avatar/initials slot. For a marketplace where a nurse walks into a stranger's home, a photo (or at least a generated-initials avatar) of the care recipient and the account holder is an expected identification and warmth affordance — its absence makes the patients list read as a data table of names.
- evidence: CustomerProfile (lines 50-54) has firstName/lastName/preferredLanguage only; PatientHeader.tsx imports no Avatar
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/patients/[id]/record/page.tsx` — Care-record editing is a whole-list 'edit mode' per tab: tap edit → every medication becomes a stack of 4 free-text TextFields → save-all. There is no per-item add/edit sheet, dose/frequency/time-of-day are unstructured free text (routine_time line 321 is a plain TextField), and switching tabs while editing unmounts the tab component and silently destroys the draft (tab state lives in the parent, draft in the child) with no warning. For medication data this is an error-prone editing surface.
- evidence: lines 118-135 Tabs drive `<EditableTabs tab={tab}/>` which conditionally mounts MedicationsTab/RoutineTab/TasksTab; each holds `const [draft, setDraft] = useState(...)` lost on unmount
- **[medium]** `client/src/components/PatientCard/PatientCard.tsx` — The tappable identity area that opens the care record is an invisible affordance: an unstyled `component="button"` (background:none, border:none) with no chevron, no 'view record' label, no hover/pressed state — nothing signals that the card body is clickable, so the record viewer (the richest screen in the area) is undiscoverable; the only visible actions are edit/archive icons.
- evidence: lines 66-85: `<Box component="button" ... sx={{ background: 'none', border: 'none', ... }}>{header}</Box>` — no visual affordance styles
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/patients/[id]/record/page.tsx` — Visit-note history (سوابق) presents longitudinal clinical info as a flat, undifferentiated card list with bare text 'قبلی/بعدی' pagination — no grouping by date/booking, no link to the booking the note came from, no indication of which nurse/service, no filtering. For the one place a family reviews their loved one's care over time, the presentation carries no narrative or hierarchy.
- evidence: lines 431-448: `items.map((note) => <VisitNoteCard .../>)` followed by prev/next text buttons + 'page_of' caption
- **[medium]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — The Profile tab — one of only five bottom-nav destinations — is a flat settings form, not an account hub: no identity header, no avatar, no account phone, no links to notifications/support/bookings history/locale, and the 'profile completeness' cue is a single color-coded text line (color-only state signal). It reads as a leftover form page where users will expect the app's account center.
- evidence: lines 75-77: completeness is `<Typography sx={{ color: isComplete ? 'var(--bal-success)' : 'text.secondary' }}>` — the page's only status affordance
- **[low]** `client/messages/fa.json` — Persian copy bug in the address-line hint: 'پلاک، خیابان، واحد — جزئیاتی که پرستار برای یافتن در نیاز دارد.' — 'برای یافتن در' is grammatically broken (word dropped, likely 'یافتن درِ منزل' or just 'یافتن نشانی'). This text sits under the most-filled field of the address form.
- evidence: address.line_hint value in messages/fa.json
- **[low]** `client/src/components/GenderToggle/GenderToggle.tsx` — ToggleButtonGroup is left at its default inline-flex sizing so the `flex: 1` on child buttons has no room to distribute — the required gender toggle renders content-width and visually misaligned against the fullWidth TextFields above it in PatientForm; the intended equal-half layout never materializes.
- evidence: lines 43-49: sx sets `'& .MuiToggleButton-root': { flex: 1 }` but the group has no fullWidth/width:'100%'
- **[low]** `client/src/components/RelationSelect/RelationSelect.tsx` — Radio-card selection is communicated by border color alone — no check icon, no fill change, no focus-visible or hover styling on the Paper cards — a color-only state signal that is weak in dark mode and fails WCAG 1.4.1 use-of-color for the selected state.
- evidence: lines 53-55: `borderColor: selected ? 'primary.main' : 'divider'` is the entire selected treatment
- **[low]** `client/src/app/[locale]/(private-routes)/(customer)/profile/page.tsx` — Loading-state inconsistency within the same tab cluster: profile uses the full-page AppLoading spinner while patients/addresses/record all use content-shaped Skeletons — the account area flickers between two different loading languages.
- evidence: line 18: `if (isLoading) return <AppLoading />;` vs patients/page.tsx lines 103-108 Skeleton rows
- **[low]** `client/src/components/geography/AddressMapPicker.tsx` — Hard-coded rgba shadow on the pin (`drop-shadow(0 1px 2px rgba(0,0,0,0.35))`) instead of a token — the only non-token color in the audited area; slightly heavy against the light --bal-primary-soft canvas in dark mode.
- evidence: line 116: `filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.35))'`
- **[low]** `client/src/components/PatientForm/PatientForm.tsx` — The form collects only a whole-year age that is stored as a fabricated Jan-1 birthDate (age.ts), and a single full-name field naively split into first/last (splitName lines 32-38, lastName falls back to firstName). Acceptable MVP shortcuts, but the fabricated birth date will surface later (records, invoices) as a false precision the family never entered.
- evidence: age.ts line 10: `return `${year}-01-01`;`
## Opportunities (10)
- **Real map with search + locate-me for addresses** (impact: high, effort: large) — Replace the grid stand-in behind the existing AddressMapPicker component boundary with real Neshan tiles (a Neshan adapter already exists server-side from refinement phase 8): address search box, GPS locate-me button, reverse-geocode preview of the pin ('پین روی: خیابان ولیعصر، ...'), and drop the raw lat/lng captions. This is the single highest-trust upgrade in the area since the pin feeds nurse arrival and EVV.
- **Turn the Profile tab into an account hub** (impact: high, effort: medium) — Redesign profile/page.tsx as a settings hub: identity header (avatar/initials + name + masked phone from /me), then grouped tappable rows — personal info, emergency contact, addresses, preferred language, dark mode, support/tickets, about/terms — ending with a sign-out row. This fixes the missing-logout hole, gives the 5th nav tab the weight users expect, and creates a natural home for future items (payment methods, saved nurses).
- **Care-circle reframe with patient avatars** (impact: high, effort: medium) — Rename/reframe 'بیماران' toward a care-circle ('عزیزان شما' / حلقه مراقبت), add an Avatar slot to PatientHeader (photo upload or warm auto-colored initials per person), and let the card lead with the person, not the data. In a home-care product the emotional register of this list matters; today it reads like an admin table of patients.
- **Structured per-item care-record editing** (impact: high, effort: large) — Replace whole-list edit mode with per-item bottom sheets: an 'add medication' sheet with structured dose unit, frequency presets (روزی ۱ بار…), and time-of-day chips (صبح/ظهر/شب); routine items with a time-of-day chip row instead of free text; a draft-loss guard when leaving edit mode or switching tabs. Then a read-mode 'daily schedule' view (meds grouped by morning/noon/night) becomes possible — genuinely useful to both family and arriving nurse.
- **Full-screen mobile form flows** (impact: medium, effort: small) — Make the patient and address dialogs fullScreen below the sm breakpoint (MUI useMediaQuery pattern) with an app-bar-style header (title + close + save), and add a dirty-state confirm before discarding. Small change, removes the most-felt mobile papercut in the area.
- **Visit-note timeline with booking context** (impact: medium, effort: medium) — Group سوابق notes by month with a subtle timeline rail, show the service name and a link back to the booking each note came from, and add a done/undone task summary per note. Turns the flat card list into the 'story of care' — a differentiating trust surface no competitor form-clone will have.
- **Proper error states with retry** (impact: medium, effort: small) — Introduce a shared QueryErrorCard (icon + 'مشکلی پیش آمد' + retry button) and use it on patients/addresses/profile/record instead of collapsing to empty/blank states; distinguish offline from server error. Pairs with converting profile's AppLoading to a form-shaped skeleton for loading consistency.
- **Emergency-contact card with call affordance** (impact: medium, effort: small) — Present the emergency contact as its own status card (complete = green check + tel: link; incomplete = warm nudge explaining why nurses need it) rather than two bare fields, and surface incompleteness as a checklist item during booking. Reinforces the safety story that is the product's core promise.
- **Address card map thumbnail + pin quality cue** (impact: low, effort: medium) — Once real tiles exist, show a small static-map thumbnail on each AddressCard and a 'pin set / pin missing' status so families can see at a glance which addresses a nurse can actually navigate to.
- **Record affordance on PatientCard** (impact: medium, effort: small) — Add a visible 'مشاهده پرونده' chevron row (or make the whole card a hover/press-styled CardActionArea with the action icons overlaid) so the record viewer is discoverable; include last-visit date on the card as a teaser ('آخرین ویزیت: ۱۲ تیر').
## Keep (do not regress)
- Token discipline is genuinely good across the whole area: every color is a --bal-* CSS variable or theme key (StatusChip.tsx even documents 'Never hard-code a hex here'), so dark mode works by construction — do not regress this.
- AddressMapPicker's RTL engineering: dir="ltr" on the canvas plus inline-style marker positioning with an explicit comment on why the stylis RTL plugin would break `left`/`translate` — exemplary hazard handling; keep it when swapping in real tiles.
- The non-leaking access-denied card and family-ownership banner on the care record (record/page.tsx lines 59-74, 108-116) — privacy-correct (no partial clinical data on 403) and a real trust cue; preserve verbatim in any redesign.
- Consistent empty states on patients and addresses: dashed-border Paper, brand icon, warm two-line copy, CTA — the empty_body strings ('اولین آدرس را اضافه کنید تا پرستار بداند کجا بیاید') are the best copy in the app.
- PatientHeader shared between the E1 list card and the E2 record viewer — one identity block, rendered identically in both places; keep this single source when adding avatars.
- CascadingRegionSelect UX details: parent-gated enabling, per-level CircularProgress adornments, the explicit 'whole city' MenuItem (empty district as a real choice, never an error), and the out-of-range value guard for edit prefill.
- Soft-archive semantics and copy for patients ('از فهرست شما حذف می‌شود اما سوابق رزروهای گذشته حفظ می‌ماند') with an optimistic mutation plus an explanatory error toast when the card reappears — respectful and honest.
- GenderToggle's never-defaulted, non-deselectable required gender — load-bearing for same-gender caregiver matching; keep the constraint regardless of restyling.
- Skeleton loaders shaped like the content they replace on patients, addresses, and the record page (RecordSkeleton mirrors header/banner/tabs/card).
- Inline single-field validation with error-clearing on change (name/phone/city/pin) and dedicated Persian error strings per field — the validation pattern itself is sound, only the surfaces around it need work.