# UI Phase 7 — Nurse daily ops

> **Mission:** give the nurse side a home and make the daily loop phone-first. Today the nurse's landing
> page is literally a `PlaceholderScreen` (`nurse/page.tsx:7`), the day-of flow has **no address, no
> contact, no navigation affordance** anywhere (`addressSnapshotJson` has zero render sites in
> `client/src`), and the request inbox hides the decision-critical facts. Every data hook a real
> dashboard needs already exists and is cached; this phase is mostly composition plus surgical fixes
> and a handful of backend REQs.
>
> **Track:** frontend · **Depends on:** [Phases 0–2](ui-phase-2-shells-and-navigation.md) ·
> **Unlocks:** the nurse side finally has a home; field work is phone-first; [Phase 8](ui-phase-8-nurse-business-and-verification.md) plugs its activation checklist into the dashboard slot this phase leaves.
> **Before you start, read [../../phases/_shared/agent-operating-rules.md](../../phases/_shared/agent-operating-rules.md) and invoke the frontend-designer skill — both are mandatory.**

## 1. Context — where this sits

Nurses are the supply side of a trust-first marketplace, and they live this product **on a phone,
on shift, at a doorstep**. The functional layer under `client/src/app/[locale]/(private-routes)/nurse/`
is competent — but composed like a settings area, not an operational tool. Diagnosed, code-verified:

1. **The landing page is empty.** `nurse/page.tsx` renders `PlaceholderScreen` (line 7) while
   `useNurseRequestInbox`, `useTodaySessions`, `useNurseEarningsBalance`, `useVerificationStatus`,
   `useNurseTrustBadge`, and `useUnreadCount` all exist as cached queries — the dashboard is pure assembly.
2. **The day-of flow is blind.** `BookingDetailView` renders only patient + nurse names;
   `addressSnapshotJson` is never rendered anywhere — and per the b9 contract it is **`null` in the
   nurse view by design** (`services/bookings/types.ts:132–133`; `bookings-evv.md`: "The nurse view
   omits `addressSnapshotJson`"), so serving it needs a REQ (§3.3). The contact affordance needs
   **no** REQ: `CareInstructionsDto` already carries `emergencyContactName`/`emergencyContactPhone`
   in the gated post-confirmation read (`types.ts:209–217`).
3. **The most important tap of a nurse's day is a tertiary button.** Both EVV CTAs in
   `SessionCard.tsx` render `sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}` (lines 117–141) —
   visually equal to the "view booking" text link beside them.
4. **The inbox hides the decision.** `InboxCard` (`requests/page.tsx:57–115`) shows patient, time,
   gender chip, notes — no service, no price; `BookingRequestListItem` carries **no variant fields**
   (the detail DTO has them — REQ-013 delivered). It is pending-only page-1-only (hook defaults, no
   tabs, no pager, API pages at 20), and a failed query renders the *empty* state
   (`requests/page.tsx:19` destructures only `{ data, isLoading }`) — paid work silently missed.
5. **Earnings answers every question except the one nurses ask** ("when do I get paid, how much"),
   and failed payouts print the bank rail's `failureReason` verbatim — LTR English bank codes in a
   Persian UI (`earnings/payouts/[id]/page.tsx` failure box, `PayoutHistoryRow`).

**What already exists (do not rebuild):**

- The nurse route tree and all its pages: requests (+detail), visits (+detail with
  `BookingDetailView` + `BookingSupportEntry` + `NurseVisitNotesPanel`), earnings (+payout history
  and detail), profile/services/coverage/bank/verification.
- The data layer: `useNurseRequestInbox` (15s poll), `useTodaySessions`, `useSessionEvv`,
  `useEvvController` (advisory GPS via the never-rejecting location seam), `useNurseEarningsBalance`,
  `useNursePayoutHistory`/`useNursePayoutDetail`, `useVerificationStatus`, `useNurseTrustBadge`, `useUnreadCount`.
- Shared components: `SessionCard`, `EvvStatusBanner`, `EarningsBalanceHeader` (with test),
  `TrustBadge`, `StatusChip`, plus [Phase 1](ui-phase-1-primitives-and-states.md)'s `CountdownTimer`
  v2, EmptyState/ErrorState kit, `PageHeader`, `<Money>`, and skeleton twins.
- The nurse shell chrome — grouped sidebar, bottom nav, identity card — is
  [Phase 2](ui-phase-2-shells-and-navigation.md)'s property (`client/src/layout/`). **Do not touch it.**

## 2. Required reading (do this first)

- [audit/nurse-trust-ops.md](audit/nurse-trust-ops.md) and
  [audit/nurse-workspace.md](audit/nurse-workspace.md) — the full evidence + the keep-lists §5 folds in.
- The design contract: `.claude/skills/frontend-designer/SKILL.md` (invoke the skill).
- Code, in this order: the nurse route tree `client/src/app/[locale]/(private-routes)/nurse/`
  (`page.tsx`, `requests/**`, `visits/**`, `earnings/**`); `client/src/components/booking/`
  (`SessionCard`, `BookingDetailView`, `useEvvController`, `EvvStatusBanner`);
  `client/src/services/bookingRequests/types.ts` (the disclosure semantics live in its header
  comments), `client/src/services/bookings/types.ts`, `client/src/services/payouts/`;
  `client/src/constants/routes.ts` (NURSE_* routes).
- Contracts: [booking-requests.md](../../contracts/domains/booking-requests.md),
  [bookings-evv.md](../../contracts/domains/bookings-evv.md), [payouts.md](../../contracts/domains/payouts.md).
- Product ground truth: [data-model/index.md](../../../product/data-model/index.md) (Principle 6 —
  two-stage disclosure is a hard rule),
  [06-evv-and-service-delivery.md](../../../product/business/06-evv-and-service-delivery.md)
  (address-match is advisory, never a block),
  [05-booking-and-scheduling.md](../../../product/business/05-booking-and-scheduling.md),
  [10-payouts.md](../../../product/business/10-payouts.md), and
  [12-messaging-and-emergencies.md](../../../product/business/12-messaging-and-emergencies.md)
  (**no nurse↔customer chat channel** — contact is tel: + coordination tickets).
- The REQ tracker: [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md)
  — REQ-001…038 are taken; this phase files REQ-039 onward.

## 3. Scope — build this

### 3.1 The «امروز» dashboard (`nurse/page.tsx`)

Replace the `PlaceholderScreen` with the operational home. Pure assembly — every widget reads an
already-cached query (verify each hook's exact return shape before wiring):

- **Greeting header:** nurse name (from the cached profile/`me` query — verify the field) +
  `TrustBadge` (via `useNurseTrustBadge`/`useVerificationStatus` — one cached status query, not two).
- **Next-visit card:** first actionable session from `useTodaySessions` — patient name, time range,
  a "time until" line (display-only relative time, not a deadline computation), and a check-in
  shortcut deep-linking to `/nurse/visits`. Empty → a calm «امروز ویزیتی ندارید», not a warning.
- **«منتظر پاسخ شما» strip:** pending requests from `useNurseRequestInbox` — count, the most urgent
  request's countdown (Phase 1 `CountdownTimer` with urgency tiers), and an inline open into
  `/nurse/requests/{id}`. The most time-critical widget; it sorts above earnings.
- **Earnings snapshot:** compact stat row from `useNurseEarningsBalance` — reuse
  `EarningsBalanceHeader` compact or compose a two-stat row with the Phase 1 `<Money>` primitive;
  deep-link to `/nurse/earnings`. Signed values render signed (never clamp a negative).
- **Activation/verification banner slot:** a clearly named composition point (e.g.
  `DashboardActivationSlot`) filled for now with only the existing verification-status banner when
  not yet approved. The activation checklist itself is
  **(DEFERRED → [Phase 8](ui-phase-8-nurse-business-and-verification.md))**, which owns the slot's
  content — document the slot in your report so Phase 8 finds it.
- **Notifications entry:** unread count via `useUnreadCount` linking to `ROUTES.NURSE_NOTIFICATIONS`
  (the bell in the shell chrome is Phase 2's; this is just a dashboard row).

The page is currently a server component; keep `page.tsx` a thin composition rendering client
widgets. Four-state pattern on every widget: skeleton → error-with-retry (Phase 1 kit) → empty → data.

### 3.2 Visits day surface (`visits/page.tsx` + `SessionCard`)

- **Date anchor header:** replace the static title with «امروز، ۲۴ تیر»-style Shamsi date (via
  `formatShamsiDate`, Phase 1 `PageHeader`), so the page reads as *today*, not a generic list.
- **Service name on session cards:** rows show patient name + session index only —
  `BookingSessionListItemDto` has no service/variant field (verified, `types.ts:173–183`). File
  **REQ-041** (§4); meanwhile render it when present (mock-tolerant optional field) — never fetch
  per-row booking details to fake it (N+1).
- **Freshness:** add a modest `refetchInterval` to `useTodaySessions` (e.g. 60s — same-day schedule
  changes currently never appear without re-navigation; the hook sets only `staleTime`). Keep the
  EVV-mutation invalidation untouched.
- **EVV CTA as the hero:** in `SessionCard`, when `showEvvControls` is on, render check-in/check-out
  as the **full-width, thumb-reach primary action** — ≥48px touch target, full row width, the
  existing busy states, and a lightweight confirm on check-*out* (it ends the visit and starts the
  payout clock). `SessionCard` is shared both-roles — gate every change on `showEvvControls` so the
  customer's booking detail is untouched, and update its co-located test.

### 3.3 Visit detail workspace (`visits/[id]/page.tsx` + `BookingDetailView`)

- **Address card:** render `addressSnapshotJson` (title, address line, city/district) with a map
  deep-link built client-side from the snapshot's lat/lng — `geo:{lat},{lng}` URI with a web
  Neshan/Balad fallback; link only, no SDK, no API key. The nurse view is masked server-side
  (contract-level), so file **REQ-040** (§4) and build the card mock-tolerant: render when non-null,
  otherwise a quiet "address available after confirmation" note. Never source an address from the
  request-stage (b8) data.
- **Contact affordance:** a `tel:` action from the care-instructions read —
  `emergencyContactName`/`emergencyContactPhone` are already in `CareInstructionsDto`, gated to the
  assigned nurse post-confirmation. `tel:` only (no VoIP, per product); `BookingSupportEntry` remains
  the coordination path. Register `call`/`navigation` icons in `AppIcon/config.ts` — the registry
  has `location`/`gps` but no phone or directions glyph.
- **In-visit mode:** when the viewer is the nurse and a session is checked in, a state header —
  «در حال ویزیت» + elapsed time (from server `checkInAt`, never a client clock) + the check-out CTA
  promoted to the top. Compose from the existing `EvvStatusBanner`/`formatElapsed`.
- **Notes placement polish:** keep `NurseVisitNotesPanel` below the EVV surface (it already is) but
  give the page one visual rhythm — the detail currently reads as three unrelated stacks.

### 3.4 Request inbox redesign (`requests/page.tsx` + `requests/[id]/page.tsx`)

- **Decision-first cards:** service name + price as the headline; patient, time, gender chip as
  secondary facts. The list DTO has neither field → **REQ-039** (§4); render them when present,
  degrade to today's layout when absent (mock-tolerant). Price via `<Money>` (Toman display).
- **Urgency-tinted countdown pill** using Phase 1's `CountdownTimer` tiers — teal >2h → amber <2h →
  terracotta <30min, `aria-live="polite"`, with a label (the inbox currently renders bare unlabeled
  digits). If the Phase 1 component lacks the tier API, extend it minimally there (never fork a
  local variant) and note it in your report.
- **Tabs + pagination:** «در انتظار» / «پاسخ‌داده» / «منقضی» plus a pager (`useNurseRequestInbox`
  already accepts `status` and `page`; the API pages at 20 — a 21st pending request is unreachable
  today). The API filters by a *single* status: map «در انتظار» → `pending_nurse_response`, «منقضی»
  → `expired_no_response`; for «پاسخ‌داده» either merge the accepted/converted/rejected single-status
  queries (page-1, documented limitation) or wait on REQ-039's status-group filter — pick one and
  say so in the report.
- **Accept confirmation dialog:** accept currently mutates on a single tap sitting flex:1 beside
  reject (`requests/[id]/page.tsx:198–219`). Add a confirm dialog with a consequence summary —
  «با پذیرش، خانواده برای پرداخت دعوت می‌شود؛ پس از پرداخت، رزرو قطعی می‌شود.» Do **not** hard-code
  the payment-window duration into copy (config-owned policy number; after acceptance render the
  `paymentDeadlineAt` countdown from the server instant instead).
- **Error state:** a failed inbox query must render the Phase 1 ErrorState with retry — today it
  renders "no incoming requests" (income-critical false negative). Same fix on the day surface
  (`visits/page.tsx:20` destructures only `{ data, isLoading }` too).

### 3.5 Earnings clarity (`earnings/**`)

- **«برداشت بعدی» forecast line** above the tabs: next batch date (holiday-shifted) + expected
  eligible amount — **server-served only**, file **REQ-042** (§4); do NOT compute it client-side
  (holiday shifting, eligibility, clawback netting are backend truth). Render only when served.
- **Failure-reason mapping:** map known bank-rail `failureReason` codes to Persian labels (i18n
  keys) in the payout detail and `PayoutHistoryRow`; unknown codes get a generic Persian message
  with the raw code as a secondary `dir="ltr"` caption — never the raw vendor string as the headline.
- **ExplainerCard a11y:** real button semantics on the collapse header (today a `cursor: pointer`
  Stack — no role, no keyboard path), `aria-expanded`, and the registered `expand` chevron instead
  of the `visibilityon`/`visibilityoff` eye icons.
- **Width normalization:** nurse pages ship three shapes — 620/640 hugging the start edge, unbounded
  (earnings), 640+`mx:'auto'` (`BookingDetailView`). Adopt one **page-level** convention (a single
  width constant + `mx: 'auto'`; the dashboard may go wider) across the pages this phase touches.
  The shell gutter (`layout/`) is Phase 2's — do not edit it.

### 3.6 Web-push for new requests (DEFERRED)

The 2h response window vs a 15s poll that only works while the tab is open is a real tension, but
push infrastructure (service worker + backend push rail) is out of scope. File it as **REQ-043**
marked deferred/non-blocking (§4) so the need is on record; build nothing for it.

## 4. Mocks & seams in this phase

**No new mocks or seams.** Bookings and booking-requests already run **real**
(`USE_BOOKINGS_MOCK = false`, `USE_BOOKING_REQUESTS_MOCK = false`); the nurse payouts read is still
mock-primary (`USE_PAYOUTS_MOCK = true`). All new UI must therefore tolerate both worlds: optional
fields render when present, degrade quietly when absent. If you extend a mock (payouts forecast,
today-feed service label) to exercise the UI, keep it behind the existing `services/{domain}` seam
and record it in [mocks-registry.md](../../shared-working-context/reports/mocks-registry.md).

Backend gaps become REQ entries appended to
[for-backend.md](../../shared-working-context/frontend/requests/for-backend.md) (REQ-001…038 taken):

- **REQ-039 — Nurse inbox decision data:** `variantLabel` + `variantPrice` (+ unit) on the nurse
  `booking_requests/list` row; optionally a status-group filter (`answered`) for the inbox tabs.
- **REQ-040 — Nurse-view address on confirmed+ bookings:** serve `addressSnapshotJson` (or a
  nurse-shaped subset incl. lat/lng) to the *assigned nurse* once status ∈ confirmed/in_progress —
  a deliberate b9 contract change; include the open product question of whether `recipientPhone`
  joins the post-confirmation nurse view.
- **REQ-041 — Service label on the today feed:** variant display name on `booking_sessions/today` rows.
- **REQ-042 — Payout forecast:** server-computed «برداشت بعدی» (next batch date, holiday-shifted +
  expected eligible amount) on the nurse earnings read.
- **REQ-043 — Web-push for new requests** (deferred, non-blocking — see §3.6).

## 5. Critical rules you must not get wrong

1. **EVV is advisory, never a block.** GPS denial/timeout/out-of-range never disables check-in/out;
   mismatch renders warning-toned, never error (`EvvStatusBanner`); the location seam never rejects.
   Making the CTA bigger must not make it stricter.
2. **Two-stage disclosure stays pre-acceptance.** Inbox and request detail show only `customerNotes`
   + coarse city·district. The address card (§3.3) exists **only** on the confirmed booking from the
   b9 read; `useCareInstructions` stays gated (assigned nurse, confirmed+ — never fired for
   customers). REQ-039's price enrichment is fine (money isn't clinical); address/contact enrichment
   of the *request* stage is not.
3. **Money is display-only.** Payout math, eligibility, forecast, clawbacks — server truth. Signed
   net balance renders signed (the "owed back" negative state must survive the compact dashboard
   snapshot). BNPL commission never appears on nurse surfaces.
4. **CountdownTimer's server-frozen contract stays:** a server instant rendered against `Date.now()`,
   its own isolated 1s tick, never a recomputed deadline; digits locale-aware inside a `dir="ltr"` island.
5. **Locale digits + Shamsi everywhere** — `Intl` fa-IR digits, `formatShamsiDate`, Toman at the
   display boundary via the shared money utils; `dir="ltr"` islands for clocks/IBANs/phone numbers.
6. **Ownership boundaries:** `layout/` is Phase 2's; shared primitives are Phase 1's — extend
   minimally there if a gap bites, never fork locally. `SessionCard`/`BookingDetailView` changes must
   keep the customer view pixel-compatible (gate on `showEvvControls`/`viewerRole`).
7. **Design contract non-negotiables:** every string in both message catalogs; tokens/palette keys,
   never hexes; logical properties only (RTL); dark mode via tokens; MUI v9 API; the icon registry
   for new icons; co-located tests for touched shared components; fetch/cookies rules untouched.
8. **Do not regress the audit keep-lists** (both audits, "Keep" sections): the four-state pattern
   where it exists, dashed-border empty states, per-session busy isolation, `EarningsBalanceHeader`
   money honesty, non-accusatory failure copy, the 15s inbox poll + `onElapsed` refetch.

## 6. Definition of Done

On top of the shared [definition-of-done.md](../../phases/_shared/definition-of-done.md):

- [ ] `npm run check` green; `npm run test:ci` green for every touched shared component
      (`SessionCard`, `BookingDetailView`, `CountdownTimer` if extended, new shared widgets).
- [ ] `en.json`/`fa.json` in sync — no orphan keys either way.
- [ ] `/nurse` renders the real dashboard (greeting + TrustBadge, next visit, requests strip with
      countdown, earnings snapshot, notifications entry, Phase 8 activation slot), each widget with
      skeleton/error/empty/data states.
- [ ] Visits page: Shamsi date anchor, interval refresh, full-width EVV CTA with busy/confirm
      states; customer booking detail unchanged.
- [ ] Nurse booking detail: address card + map deep-link when the API serves the snapshot
      (mock-verified), `tel:` contact from care instructions, in-visit header with elapsed time.
- [ ] Inbox: cards lead with service + price when served; tabs + pager work; a failed query shows
      an error state with retry (not the empty state); accept requires a confirm dialog.
- [ ] Payout failure reasons render as Persian labels (raw code demoted to a secondary LTR line);
      ExplainerCard is keyboard-operable with `aria-expanded`.
- [ ] REQ-039…043 appended to the tracker with the exact DTO/route shapes proposed.
- [ ] Visual verification on all four axes — `/fa` + `/en` × light + dark — and mobile + desktop for
      dashboard, visits, inbox, and visit detail (`/fa` mobile first: this is the phone-first phase).

## 7. How to test (what a human can verify after this phase)

1. Log in as the seeded verified nurse (refinement phase 1 demo accounts) → `/nurse` shows a real
   dashboard: greeting + trust badge, next visit, pending-requests strip counting down, earnings
   snapshot in Toman.
2. As a customer, create a request targeting that nurse → within the poll interval the dashboard
   strip and `/nurse/requests` show it; the card leads with service + price (REQ pending: gracefully
   headline-less); the countdown pill escalates teal → amber → terracotta (adjust a mock deadline).
3. Open the request → «پذیرش» → a confirm dialog summarizes the consequence; confirm → status flips;
   a stale second accept still 409s into the refetch path.
4. Force the inbox query to fail (stop the API) → an error panel with retry — **not** «درخواستی ندارید».
5. On `/nurse/visits` (mobile, `/fa`): Shamsi «امروز …» header; check-in is a full-width primary CTA.
   Deny browser GPS → check-in still succeeds with the advisory warning banner. Check out → confirm
   prompt → elapsed duration renders from server timestamps.
6. Open a confirmed visit's detail with `USE_BOOKINGS_MOCK = true` (mock serves an address snapshot)
   → address card + map link opening `geo:`/Neshan; `tel:` dials the care-instructions emergency
   contact. On the real (masked) path → the quiet "available after confirmation" note, no crash.
7. On `/nurse/earnings`: the forecast line appears only when the (mock) API serves it; a failed
   payout shows a Persian failure label with the raw code as a small LTR caption; the explainer
   header opens with Enter.
8. Repeat 1, 2, and 5 on `/en` and dark mode — no stock-MUI colors, no Latin digits in fa timers.

## 8. Hand off & document (close the phase)

- Update `client/CLAUDE.md` "Project Structure" if you added dashboard widget components or new
  shared components (the nurse route tree itself doesn't change shape).
- Write the report at
  [ui-phase-7-report.md](../../shared-working-context/reports/ui-phase-7-report.md): what shipped
  per §3, the exact name/location of the Phase 8 activation slot, the inbox-tab strategy picked
  (§3.4), any minimal extensions to Phase 0/1 foundation files, and four-axes verification notes.
- List REQ-039…043 as filed (one-line status each) in the report and confirm they're appended to
  [for-backend.md](../../shared-working-context/frontend/requests/for-backend.md).
- Save a memory note per operating-rules §8: the nurse daily loop is now dashboard → visit →
  check-in/out → earnings; address/contact are REQ-gated (REQ-040) with mock-tolerant UI; EVV
  advisory and two-stage disclosure invariants unchanged.
