# 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`, ``, 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 `` 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 `` (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.