Files
baya-monorepo/archive/post-phase/ui/audit/nurse-trust-ops.md
T
2026-08-02 18:48:32 +03:30

74 lines
20 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.
# Nurse trust & operations — verification, request inbox, visits/EVV, earnings & payouts
## Current state
The nurse side lives under client/src/app/[locale]/(private-routes)/nurse/ inside NurseLayout (client/src/layout/NurseLayout.tsx), a 10-item flat sidebar + fixed TopBar shell inherited from the starter (TopBarAndSideBarLayout.tsx). Verification is a hub-and-spoke: verification/page.tsx (B3 hub) renders VerificationChecklist.tsx (an "X از Y" LinearProgress meter + data-driven step rows via verificationSteps.ts, reusing the shared StatusChip) with a single "continue" CTA; identity/page.tsx (B4) collects national ID + two local DocumentUpload captures; credentials/page.tsx (B5) renders one DocumentUpload per manual step plus INO number, specialty Chips and native type="date" registry dates; review/page.tsx (B6) is a second view of the same cached query. B4/B5/B6 carry a bare default-MUI StepperHeader (3 macro steps) alongside the hub's 7-step checklist. DocumentUpload owns a full idle→uploading(progress %)→success(preview)→error state machine plus a rejected variant with reason + re-upload.
The request inbox (requests/page.tsx) polls every 15s and lists pending-only cards (patient name, Shamsi time, required-gender chip, notes preview, per-card CountdownTimer) with an "open detail" button; requests/[id]/page.tsx shows the masked city·district location, stage-1 notes only, and 50/50 accept / reject-with-reason-dialog buttons plus 409-stale handling. Visits (visits/page.tsx) is a "today's sessions" list of shared SessionCard components with terracotta EVV check-in/out buttons driven by useEvvController (GPS via an ILocationProvider seam that never rejects — denied GPS still checks in, advisory) and EvvStatusBanner (success/warning/info tokens, mismatch is never an error); visits/[id]/page.tsx composes the both-roles BookingDetailView (timeline, sessions with EVV, money summary, gated CareInstructionsCard) + NurseVisitNotesPanel (append-only note + task checklist). Earnings (earnings/page.tsx) shows EarningsBalanceHeader (signed net balance with an explicit "owed back" negative state + four token-coded buckets), a collapsible explainer, state-filter Tabs and EarningsRow items (PriceBreakdown grosscommission=payout, per-state affordances, dispute-window countdown); payouts/page.tsx and payouts/[id]/page.tsx render PayoutHistoryRow / batch reconciliation with masked IBAN, transfer reference, and read-only failure banners. Styling throughout is flat bordered Paper cards with borderInlineStart accent strips, --bal-* CSS variables (mirrored dark scheme in src/theme/tokens.css), Persian digits via Intl fa-IR, and Shamsi dates via formatShamsiDate. Notably, the nurse landing page /nurse (nurse/page.tsx) is still a PlaceholderScreen.
## Problems (19)
- **[high]** `client/src/app/[locale]/(private-routes)/nurse/page.tsx` — The nurse landing page after login is a bare PlaceholderScreen — there is no operational home tying together today's visits, pending requests (with deadlines), verification progress, and earnings. Every session starts at a dead end and the sidebar is the only wayfinding.
- evidence: line 7: `return <PlaceholderScreen icon="dashboard" title={t('dashboard')} ... />`
- **[high]** `client/src/components/booking/BookingDetailView/BookingDetailView.tsx` — The nurse day-of flow contains no service address, no family/patient contact, and no navigate/call affordance anywhere — not on the visits day list, not on the booking detail. The header renders only patient + nurse names; the DTO's addressSnapshotJson is never rendered, and the bookings mock even nulls it for the nurse view (mockApi.ts:358). A field nurse cannot find where to go or reach the family from the app, even on a confirmed (post-payment, stage-2) booking.
- evidence: lines 90-93 render only HeaderFact(patient) + HeaderFact(nurse); `addressSnapshotJson` has zero render-site references in src/
- **[high]** `client/src/components/DocumentUpload/DocumentUpload.tsx` — Rejected-step recovery loses all upload feedback: the `rejected` branch of the render ternary takes precedence over `state === 'uploading'`, and the prop only flips after the status query invalidates. So when a nurse re-uploads a rejected document, the card stays frozen on the red 'rejected' state for the whole upload (no progress bar, no success flash) and the re-upload button stays enabled mid-flight, inviting double submissions on the single most anxiety-laden path.
- evidence: line 155 `{rejected ? (` … precedes line 193 `: state === 'uploading' ? (`; re-upload AppButton (lines 182-191) is only disabled by the `disabled` prop
- **[high]** `client/src/components/booking/SessionCard/SessionCard.tsx` — The EVV check-in/check-out CTA — the most important tap of a nurse's day, done on a phone at a doorstep — is a small start-aligned button (`alignSelf: 'flex-start', py: 1`) visually equal to tertiary links around it. No full-width layout, no large touch target, no sticky positioning, no visual weight distinguishing it from 'view booking'.
- evidence: lines 117-141: both EVV buttons use `sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}`
- **[high]** `client/src/app/[locale]/(private-routes)/nurse/verification/credentials/page.tsx` — The credentials form does not survive re-entry: INO number, specialties and registry fields start empty every visit (never hydrated from server status), and submit is disabled unless a document was uploaded in this session (`anyUploaded` reads only local `uploadedSteps` state) — so a returning nurse whose docs are already in_review sees blank fields and a dead submit button with no explanation.
- evidence: line 106 `const anyUploaded = Object.values(uploadedSteps).some(Boolean)` + line 255 `disabled={submitCredentials.isPending || !anyUploaded}`; state initialised to '' / [] at lines 37-44
- **[high]** `client/src/app/[locale]/(private-routes)/nurse/verification/credentials/page.tsx` — License issue/expiry dates use native Gregorian `type="date"` inputs in a Persian-default UI — Iranian nurses read their license dates in Shamsi; forcing a Gregorian browser picker on a trust-critical form invites wrong dates (which feed credential-expiry logic).
- evidence: lines 220-235: two `<TextField type="date" ...>` for issued_at / expires_at
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/requests/page.tsx` — Inbox cards omit the requested service/variant and its price — the decision-critical facts. A nurse sees patient name, time, gender chip and a notes preview but must open each detail to learn what job is being requested and what it pays.
- evidence: InboxCard (lines 57-115) renders counterpartyName, whenLabel, gender chip, customerNotes only; variantLabel/variantPrice appear only in [id]/page.tsx
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/requests/page.tsx` — Pending-only, page-1-only inbox: the page hardcodes the hook's default status ('pending_nurse_response') with no tabs for answered/expired history (no way to review past decisions or learn from expirations), and no pagination UI even though the API pages at 20 — a 21st pending request is unreachable.
- evidence: line 19 `useNurseRequestInbox()` called with no status/page args; no Pager rendered; BOOKING_REQUEST_PAGE_SIZE = 20
- **[medium]** `client/src/components/CountdownTimer/CountdownTimer.tsx` — No urgency escalation: the response-deadline countdown stays calm teal from 24h down to 00:00:01 (the `urgent` prop is static and unused by the inbox), and on inbox cards it renders without a label — bare ticking digits floating at the card corner. Also ambiguous formats: under an hour it drops to MM:SS which reads like HH:MM, and multi-day dispute windows render as raw hour counts like ۱۲۶:۴۴:۰۲.
- evidence: line 66 `const accent = urgent ? ... : 'var(--bal-primary)'`; lines 88-90 drop the hours segment when 0; requests/page.tsx:79 passes no `label`
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/verification/page.tsx` — Two competing progress metaphors in one journey: the B3 hub counts 7 granular checklist steps ("X از Y" + LinearProgress, inflated by the synthetic mobile step) while B4/B5/B6 show an unrelated default-MUI 3-step Stepper — the nurse gets two different answers to 'how far along am I'. StepperHeader itself is an unstyled starter Stepper (default numbered circles).
- evidence: VerificationChecklist ProgressMeter vs StepperHeader.tsx lines 20-28 (bare `<Stepper>` wrap); verificationSteps.ts MOBILE_STEP id 0 always 'passed'
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/page.tsx` — ExplainerCard's collapse header is a clickable Stack with no button semantics (no role, tabIndex, aria-expanded — keyboard users can't open the 'how payouts work' copy), and it uses eye icons (visibilityon/visibilityoff) as an expand/collapse affordance instead of a chevron, even though an 'expand' icon exists in the registry.
- evidence: lines 127-139: `<Stack ... sx={{ cursor: 'pointer' }} onClick={onToggle}>` + `<AppIcon icon={open ? 'visibilityoff' : 'visibilityon'} .../>`
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/payouts/[id]/page.tsx` — Raw vendor strings shown to nurses: failed payouts print the bank rail's `failureReason` verbatim (LTR English/bank codes) in a Persian UI, in both the payout detail and PayoutHistoryRow; likewise VerificationChecklist falls back to the raw snake_case failure code (e.g. 'blurry_scan') when an i18n key is missing.
- evidence: payouts/[id]/page.tsx lines 127-131 `{t('failure_reason_label')}: {data.failureReason}` dir="ltr"; PayoutHistoryRow.tsx 101-105; VerificationChecklist.tsx 75-79 `: step.failureReason`
- **[medium]** `client/src/layout/TopBarAndSideBarLayout.tsx` — Starter-grade shell hazards directly affecting these flows: physical `paddingLeft`/`paddingRight` + physically 'left'-anchored desktop drawer put the nurse nav on the trailing side in RTL fa (mobile anchor is 'right' — inconsistent, leftover starter comments in config.ts); the logo icon doubles as the sidebar opener with a hard-coded English 'Open Sidebar' tooltip; the main content gutter is a fixed 8px at all breakpoints.
- evidence: lines 53-58 physical paddingLeft/Right keyed on `anchor?.includes('left')`; line 71 `title={... : 'Open Sidebar'}`; line 102 `paddingLeft: 1, paddingRight: 1`; config.ts `SIDE_BAR_DESKTOP_ANCHOR = 'left'; // 'right';`
- **[medium]** `client/src/app/[locale]/(private-routes)/nurse/earnings/page.tsx` — Page-width chaos across adjacent nurse screens: verification (620) / requests / visits (640) cap maxWidth without mx:'auto' so content hugs the start edge beside a vast empty area on desktop; earnings and payout history have no maxWidth so money rows stretch the full viewport; BookingDetailView centers with mx:'auto'. Three different page shapes in one shell.
- evidence: earnings/page.tsx:48 `sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}` (no cap) vs verification/page.tsx:47 `maxWidth: 620` (no mx) vs BookingDetailView.tsx:62 `maxWidth: 640, mx: 'auto'`
- **[low]** `client/src/app/[locale]/(private-routes)/nurse/requests/[id]/page.tsx` — Accept fires on a single tap with no confirmation, summary of consequence ('the family will be asked to pay; a booking will be created'), or undo — while sitting flex:1 directly beside Reject at equal width. A mis-tap is materially consequential and irreversible from this UI.
- evidence: lines 198-219: accept/reject both `sx={{ m: 0, flex: 1, py: 1.25 }}`, `onClick={handleAccept}` mutates immediately
- **[low]** `client/src/components/common/AppButton/AppButton.tsx` — AppButton ships a starter default `margin: 1`, so virtually every call site in this area fights it with `sx={{ m: 0 }}` (30+ occurrences across the audited pages); any forgotten override produces phantom spacing.
- evidence: lines 9-11 `DEFAULT_SX_VALUES = { margin: 1, ... }`
- **[low]** `client/src/components/booking/SessionCard/SessionCard.tsx` — Terracotta — the brand's 'single sparing accent' — is spread across the nurse surface: EVV contained+outlined buttons, the per-session payout amount text, the 'نمای پرستار' chip, and the notes-panel border/icon/submit all use secondary at once, diluting its urgency value.
- evidence: SessionCard line 112 `color: 'var(--bal-secondary-dark)'` + lines 119/133 `color="secondary"`; BookingDetailView 71/83; NurseVisitNotesPanel 73/77/120
- **[low]** `client/src/app/[locale]/(private-routes)/nurse/visits/page.tsx` — The day surface has no date anchor (no 'امروز، ۲۴ تیر' header), the cards don't say which service the visit is for (patient name + session index only), and useTodaySessions has no refetchInterval — a same-day schedule change won't appear without re-navigation, unlike the polled inbox.
- evidence: page title is static `t('evv_visits_title')`; SessionCard receives no service name; useTodaySessions.ts sets only staleTime
- **[low]** `client/src/app/[locale]/(private-routes)/nurse/verification/review/page.tsx` — The under-review screen gives a text ETA but no submitted-timestamp or 'what happens next' timeline, and unlike its sibling pages it has no page h1/subtitle above the stepper — the heading lives inside the status card, breaking the header rhythm established by every other nurse page.
- evidence: lines 26-29: page opens directly with StepperHeader; h1 is the Typography inside the Paper (line 47)
## Opportunities (9)
- **Nurse 'Today' home — replace the placeholder dashboard** (impact: high, effort: medium) — Build /nurse as an operational hub: (1) next visit card with a check-in shortcut and time-until, (2) pending requests strip with the most urgent countdown and an inline accept path, (3) a verification-progress card (reusing the cached status query) until approved, (4) this-week earnings snapshot with the next-payout date. All four data sources already exist as cached queries — this is composition, not new plumbing, and it is the single highest-leverage screen for making the product feel alive and trustworthy to nurses.
- **Visit workspace: address, contact, and an in-visit mode** (impact: high, effort: large) — On a confirmed booking, surface the stage-2 disclosure the product already promises: render addressSnapshotJson as an address card with a map deep-link (Neshan/Balad/Google via geo: URI) and a tel: 'call family' action; make check-in a full-width sticky-bottom hero button; after check-in switch the screen into an 'in-visit' mode (elapsed timer, task checklist promoted from the notes panel, check-out CTA); after check-out show a payout confirmation moment ('این ویزیت X تومان به درآمد شما اضافه شد'). This is the flow nurses live in daily and today it's a stack of generic cards.
- **Mobile bottom navigation for the nurse shell** (impact: high, effort: medium) — Field nurses are on phones; the current pattern (tap the logo to open a right-anchored drawer with 10 flat items) hides everything. A 4-5 item bottom nav (امروز / درخواست‌ها with unread-count badge / درآمد / پروفایل) plus overflow drawer would transform day-of usability. The BottomBar component already exists in the layout folder, disabled by starter config.
- **Decision-first inbox card redesign + urgency system** (impact: high, effort: medium) — Reshape InboxCard around the accept decision: service name + price as the headline, an urgency-tinted countdown pill (teal >2h → amber <2h → terracotta <30min, aria-live), patient/time/gender as secondary facts, and inline accept/decline directly on the card (dialog only for reject reason). Add tabs (در انتظار / پاسخ‌داده / منقضی), a pager, and a sidebar badge for pending count. Pair with a 'fast responses win more bookings' trust nudge in the empty state.
- **Unified verification journey with the trust payoff visible** (impact: high, effort: medium) — Merge the two progress metaphors into one vertical journey page: a hero showing a live TrustBadge preview ('این نشان را خانواده‌ها می‌بینند') that fills in as steps pass, grouped step cards (identity / credentials / bank) replacing both the 3-step Stepper and the flat 7-row list, per-step ETA chips for in_review items (the 24-48h promise, today only on B6), a submitted-at timestamp, and a celebratory approved state that leads into publish. Verification is the product's core trust ritual and currently reads as a settings checklist.
- **Shamsi date picker component** (impact: medium, effort: medium) — A reusable Jalali date field (wheel or calendar) to replace native type="date" in credentials — and later everywhere dates are entered. Directly removes a correctness risk on license expiry data that feeds the credential-expiry sweep.
- **'Next payout' forecast on earnings** (impact: medium, effort: small) — Add a single server-provided line above the tabs: next batch date (holiday-shifted) + the eligible amount expected in it ('پرداخت بعدی: شنبه — ۲٬۴۵۰٬۰۰۰ تومان'). Converts the four abstract buckets into the one answer nurses actually seek ('when do I get paid, how much'), and replace the raw HH:MM:SS dispute-window countdown with day-granular copy ('۲ روز تا آزادسازی').
- **New-request notifications beyond the 15s poll** (impact: high, effort: large) — The 2h response deadline is only survivable if the nurse happens to have the tab open. Add a web-push opt-in banner on the inbox (service worker + the existing notifications service), falling back to SMS via the backend's Kavenegar rail for accepted-critical events. Deadline-driven marketplaces live or die on this.
- **Upload polish: fix rejected-state machine and add capture guidance** (impact: medium, effort: small) — Beyond the precedence bug fix, elevate DocumentUpload for the trust flow: show a frame overlay/illustration for the ID-card capture, image-too-dark/blurry client hints, and keep the rejected reason visible above (not instead of) the progress UI during re-upload so nurses see their recovery succeeding.
## Keep (do not regress)
- Semantic token discipline: every audited component colors via --bal-* CSS variables (StatusChip, TrustBadge, EvvStatusBanner, EarningsBalanceHeader) with explicit 'never hard-code a hex' comments and a mirrored dark scheme in src/theme/tokens.css — no raw hexes found in this area.
- Complete loading/empty/error coverage: every list and detail screen (verification hub, inbox, request detail, visits, earnings, payout history, payout detail) has a real skeleton, a designed dashed-border empty state with icon+copy, and an error panel with retry.
- The EVV advisory philosophy in code: GPS denial/timeout never blocks a check-in (locationProvider never rejects; controller submits null coords with a warning toast), out-of-range renders warning-toned never error (EvvStatusBanner), and per-session busy state isolates the acting card.
- CountdownTimer correctness: server-frozen deadlines only, an isolated 1s tick that re-renders nothing around it, Persian digits forced dir=ltr so HH:MM:SS order survives RTL, and a single onElapsed refetch hook.
- Money honesty in EarningsBalanceHeader/EarningsRow/PriceBreakdown: negative net renders as an explicit error-toned 'owed back' card (never a bare minus), gross commission = payout is reconciled visually, clawbacks get their own explanatory breakdown, and all money is BigInt-safe display-only strings.
- Two-stage disclosure enforced in the UI layer: request detail renders only customerNotes + coarse city·district with an explanatory disclosure note; the care-instructions query is enabled only for the nurse on a confirmed+ booking (never fired for customers).
- Rejected-step recovery paths exist everywhere and are data-driven: checklist rows surface the failure reason with a 'fix' CTA routed by step code, DocumentUpload has a dedicated rejected variant with reason + re-upload, and the Shahkar shared-SIM failure gets deliberately non-accusatory warning copy.
- RTL-aware physical CSS throughout content components: borderInlineStart accent strips, textAlign 'start'/'end' (no left/right), and dir="ltr" islands on IBANs, transfer references, national-ID input, and clock strings.
- Locale-correct numerals and dates: Intl NumberFormat fa-IR for all digits (progress counts, pagers, countdown) and formatShamsiDate for every date — no Gregorian date strings leak into the fa UI (except the native date-input problem flagged separately).
- The 'honesty constraint' pattern in verification: only genuinely automated checks advertise استعلام خودکار, manual-review copy never claims an authority check, and TrustBadge 'verified' renders only from the approved aggregate with expired visually distinct from unverified.