# 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 gross−commission=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.
