# UI Phase 7 — Nurse Daily Ops — Report (2026-07-19) ## What was built **The nurse "امروز" dashboard** (`nurse/page.tsx` → `NurseDashboardScreen.tsx`, new) — replaces the `PlaceholderScreen` with pure assembly over already-cached queries: - Greeting header (name off `useMe`) + own `TrustBadge` (`ownBadgeState`, one cached `useVerificationStatus` query, not two). - `NextVisitCard` — first actionable session from `useTodaySessions`, a display-only "starts in" line (`formatRelativeTime`), a check-in shortcut to `/nurse/visits`; a calm «امروز ویزیتی ندارید» empty state. - `RequestsStrip` — the most time-critical widget (sorts above earnings): pending count, the most urgent request's urgency-tinted countdown, an inline open into the request detail. - `EarningsSnapshotCard` — a compact two-stat row (signed net payable + eligible) via `useNurseEarningsBalance` + ``; never clamps a negative balance. - `DashboardActivationSlot` (new, exported, page-local) — the **named composition point** for Phase 8. Filled for now with only the existing verification-status banner when not yet approved; renders nothing once approved. **Phase 8: extend `client/src/app/[locale]/(private-routes)/nurse/DashboardActivationSlot.tsx` in place — don't add a second slot.** - `NotificationsEntryRow` — unread count (`useUnreadCount`) linking to `ROUTES.NURSE_NOTIFICATIONS`. - Every widget: skeleton → error-with-retry → empty → data, in that order. **Visits day surface** (`visits/page.tsx`): - Shamsi «امروز، {day month}» date anchor via `PageHeader` (replaces the static title). - `useTodaySessions` gained a 60s `refetchInterval` (`TODAY_SESSIONS_REFETCH_MS`) so a same-day schedule change surfaces without re-navigation; the EVV-mutation invalidation is untouched. - `SessionCard` gained an optional `serviceLabel` prop, rendered under the title; the day surface passes `item.variantLabel` (REQ-052, mock-tolerant — `undefined` on the real path just omits the line). - A real error state (`ErrorState` + retry) replaces the old two-field `{ data, isLoading }` destructure that rendered "no visits today" on a failed query. **`SessionCard` EVV hero** (shared, both roles — gated on `showEvvControls`): - Check-in/check-out are now the **full-width, ≥48px** primary action (`width:'100%', minHeight:48`), replacing the small `alignSelf:'flex-start'` button visually equal to the "view booking" text link beside it. - Check-out now goes through the new shared `CheckOutConfirmButton` (`components/booking/`, tested) — a lightweight `ConfirmDialog` before firing («اتمام ویزیت؟» — it ends the visit and starts the payout clock). Reused by `BookingDetailView`'s in-visit banner so both entry points confirm the same way. **Visit detail workspace** (`BookingDetailView.tsx`, shared both roles): - **Address card** (new, standalone — replaces the old inline hero address line): renders the frozen `addressSnapshotJson` when present (title/city/district/line, best-effort parse — REQ-045 still open), a `geo:{lat},{lng}` map deep-link + a Neshan web-map fallback link when the snapshot carries coordinates (REQ-051), and a quiet nurse-only "available after confirmation" note when masked. Never sourced from the b8 request stage. - **In-visit mode** (new, nurse-only): when a session is `checked_in`, an `AccentCard` state header — «در حال ویزیت» + elapsed on-site time (`formatElapsed` against the server `checkInAt`, never a guessed start) + the check-out CTA **promoted to the top**, via the same `CheckOutConfirmButton`. - **Contact affordance — already delivered, verified, no changes needed.** `BookingSupportEntry`/ `EmergencyBanner` (messaging composites, already mounted on `visits/[id]/page.tsx`) already surface a `tel:` click-to-call from the gated care-instructions read for the nurse on a confirmed+ booking. The `phone`/`navigate` icons the phase doc flagged as missing are also already registered (`AppIcon/config.ts`) — both audit findings were stale against current code. - Notes placement: `NurseVisitNotesPanel`/`BookingSupportEntry`/`BookingDetailView` were already each `maxWidth:640, mx:'auto', width:'100%'` and stacked under one `gap:3` — verified consistent, no change needed. **Request inbox redesign** (`requests/page.tsx`, `requests/[id]/page.tsx`): - Decision-first `InboxCard`: service + price headline when the list item carries `variantLabel`/ `variantPrice` (REQ-050, mock-tolerant — degrades to the patient-name headline on the real path), patient/ time/gender as secondary facts. The whole card is now a tappable link (`AppLink`), matching the "fully tappable AccentCard row" convention from the customer bookings screen. - Urgency-tinted countdown pill: `CountdownTimer`'s existing v2 tier API (`warnThresholdSeconds`/ `urgentThresholdSeconds`) at teal >2h / amber <2h / terracotta <30min, a label, and `coarseLabel` (which opts into `aria-live="polite"` humanized copy) — no extension to `CountdownTimer` was needed, its ui-phase-1 API already covers this. - Tabs + pager: «در انتظار» (`pending_nurse_response`) / «پاسخ‌داده» (client-merged `accepted_awaiting_payment` + `converted` + `rejected_by_nurse`, **page-1-only — documented limitation**, filed as part of REQ-050's status-group-filter ask) / «منقضی» (`expired_no_response`). `useNurseRequestInbox` gained an optional `enabled` param so the four non-active tab queries don't poll in the background. - Accept now requires a `ConfirmDialog` («با پذیرش، خانواده برای پرداخت دعوت می‌شود؛ پس از پرداخت، رزرو قطعی می‌شود.») before firing — the payment-window duration is never hard-coded; after acceptance the detail page renders a `CountdownTimer` against the server `paymentDeadlineAt`. - The inbox's error handling (`isError` → `ErrorState`) was **already correct** in the current code — the phase doc's cited line numbers were stale (a prior phase had already fixed the false-empty defect). **Earnings clarity** (`earnings/page.tsx`, `PayoutHistoryRow`, payout detail page): - «برداشت بعدی» `ForecastLine` above the tabs — server-served only (`nextPayoutDate`/ `nextPayoutEligibleAmountIrr`, both optional on `NurseEarningsSummary`), renders nothing until served (REQ-053, never computed client-side). - Failure-reason mapping (`services/payouts/failureReasons.ts`, new): known `failureReason` codes (today: `invalid_sheba`) render a mapped Persian/English label as the headline; unknown codes get a generic message; the raw code is always demoted to a secondary `dir="ltr"` caption — never the raw vendor string as the headline. Applied to both `PayoutHistoryRow` and the payout detail page. - `ExplainerCard` a11y: the bare `onClick` `Stack` is now a real `ButtonBase` (`aria-expanded`, `aria-controls`) and the eye icons (`visibilityon`/`visibilityoff`) are replaced by the registered `expand` chevron (rotates 180° when open). - Width normalization: adopted one page-level convention — `CONTENT_MAX_WIDTH` (800) + `mx:'auto'` — across every page this phase touches (`requests/page.tsx`, `requests/[id]/page.tsx`, `visits/page.tsx`, `earnings/page.tsx`, `earnings/payouts/page.tsx`, `earnings/payouts/[id]/page.tsx`). The dashboard uses a wider `DASHBOARD_MAX_WIDTH` (960) per the phase's "the dashboard may go wider" allowance. `BookingDetailView` (640) is a component-level width, untouched — a separate decision from the page-level convention. **New shared component:** `Pager` (`components/common/Pager/`, tested) — the prev/next "page X of Y" control, replacing three near-identical inline pagers (earnings, payout history, and the new inbox tabs). New `common` namespace i18n keys (`page_prev`/`page_next`/`page_indicator`) so it doesn't depend on the `payouts` namespace's copies (left in place, unused by the new code, but not deleted — out of scope to chase down every call site). **REQ-039…043 renumbered to REQ-050…054** — the phase doc was written assuming REQ-001…038 were taken, but ui-phase-3 through ui-phase-6 have since filed REQ-039…049. Filed as REQ-050 (inbox decision data + status-group filter), REQ-051 (nurse-view address post-confirmation), REQ-052 (today-feed service label), REQ-053 (payout forecast), REQ-054 (web-push, deferred/non-blocking) — see `for-backend.md`. ## What is now testable (and exactly how) 1. Log in as the seeded verified nurse (refinement-phase-1 demo accounts) → `/nurse` shows a real dashboard: greeting + trust badge, next visit (or the calm empty state), a pending-requests strip, an earnings snapshot, and (if not yet approved) the activation banner. 2. `/nurse/visits` (mobile, `/fa`): a Shamsi «امروز، …» header; check-in is a full-width primary CTA; check-out opens a confirm dialog first. Deny browser GPS → check-in still succeeds with the advisory warning toast. 3. `/nurse/requests`: three tabs; a pending card leads with service + price **only when `USE_BOOKING_REQUESTS_MOCK=true`** (flip the flag in `services/bookingRequests/constants.ts` — the real path degrades to the patient-name headline); the countdown pill is teal/amber/terracotta by remaining time (seed/adjust a request's deadline to see the escalation). Open a request → «پذیرش» → confirm dialog → accept → the detail shows a payment-window countdown. 4. Stop the API (or force a query error) on `/nurse/requests` or `/nurse/visits` → an `ErrorState` panel with retry, never the empty state. 5. `/nurse/visits/[id]` with **`USE_BOOKINGS_MOCK=true`** on a confirmed booking (e.g. id `5001` or the completed `5005`) → an address card with a working `geo:`/Neshan-web map link (booking 5001's seed now carries lat/lng matching the EVV reference point); on the real (masked) path → the quiet "available after confirmation" note, no crash. Check in on a today session, then open its booking detail → the «در حال ویزیت» banner with elapsed time + a promoted check-out CTA. 6. `/nurse/earnings`: the forecast line always appears (payouts stays mock-primary, `USE_PAYOUTS_MOCK=true` by default); a failed payout (seeded `invalid_sheba`) shows a mapped Persian/ English label as the headline with the raw code as a small LTR caption; the explainer header opens with Enter/Space and reports `aria-expanded`. 7. Repeat 1–6 on `/en` and dark mode — no stock-MUI colors, no Latin digits in `fa` timers. **Verification performed:** `npm run check` (tsc + eslint) is green; `npm run test:ci` is green — 106 suites / 455 tests, including the 2 new suites (`Pager`, `CheckOutConfirmButton`) and every touched shared component's existing suite (`SessionCard`, `BookingDetailView`, `PayoutHistoryRow`). A dev-server smoke pass confirmed every touched route (`/nurse`, `/nurse/requests[/1]`, `/nurse/visits[/5001]`, `/nurse/earnings[/payouts[/9001]]`, `/en/nurse`) compiles and responds 200 with no server errors. **Not performed:** a full authenticated click-through in a browser (no browser-automation tool available in this session, and exercising the OTP login flow end-to-end needs the backend API running) — the "How to test" steps above are written for a human to run that pass manually. ## What is mocked / waiting on a real service - `services/bookingRequests` — **real** by default (`USE_BOOKING_REQUESTS_MOCK=false`); the mock's `toListItem` now also stamps `variantLabel`/`variantPrice`/`variantPriceUnit` (REQ-050) so the redesigned inbox card is demonstrable when the flag is flipped for local testing. No registry row (bookingRequests was already de-mocked in refinement-phase-4 and has none — consistent with its siblings). - `services/bookings` — **real** by default (`USE_BOOKINGS_MOCK=false`); mock changes: `forViewer` now unmasks `addressSnapshotJson` for the nurse once `isBookingConfirmedOrBeyond` (simulating REQ-051 ahead of the real endpoint), `addr5001` gained matching `latitude`/`longitude`, and `listTodaySessions` stamps `variantLabel` (REQ-052). See the updated `mocks-registry.md` row. - `services/payouts` — **mock-primary** (`USE_PAYOUTS_MOCK=true`, unchanged — REQ-025 is still the root gap); `buildSummary()` now also serves `nextPayoutDate`/`nextPayoutEligibleAmountIrr` (REQ-053). See the updated `mocks-registry.md` row. - No new seams introduced this phase — every change extends an existing `services/{domain}` mock behind its existing seam. ## Contracts - Consumed: `dev/contracts/domains/{booking-requests,bookings-evv,payouts}.md` (unchanged this phase — no contract landed to consume). - Filed to `for-backend.md`: **REQ-050** (nurse inbox `variantLabel`/`variantPrice`/`variantPriceUnit` + an `answered` status-group filter), **REQ-051** (nurse-view address on confirmed+ bookings), **REQ-052** (service label on the today feed), **REQ-053** (payout forecast), **REQ-054** (web-push, deferred). ## Docs updated - `client/CLAUDE.md` "Project Structure": the nurse route tree (`page.tsx` → `NurseDashboardScreen.tsx` + `DashboardActivationSlot.tsx`, `requests/`, `visits/`, `earnings/`) and the shared component tree (`components/common/Pager/`, `components/booking/`'s `CheckOutConfirmButton` + the `BookingDetailView`/ `SessionCard` additions) updated in the same change. - `dev/shared-working-context/reports/mocks-registry.md`: `BookingsApi` and `PayoutsApi` rows updated in place with this phase's mock additions (also corrected `BookingsApi`'s stale "default `true`" config-flag note — it's `false`, real-primary, since refinement-phase-4). ## Follow-ups for later phases - **Phase 8** owns `DashboardActivationSlot`'s content — the fuller "go live" checklist (profile/services/ coverage/bank all done), per the phase doc's own hand-off note. - REQ-050's status-group filter (`status=answered`) would collapse the inbox's three-query «پاسخ‌داده» merge into one real paginated query — currently page-1-only, documented in the REQ and in code. - REQ-051 (nurse-view address) is the highest-value of this phase's REQs — until delivered, the address card is demoable only via the mock; REQ-045 (a typed address-snapshot shape) is still open from ui-phase-5 and would let the address card + `.ics` export drop their best-effort JSON-key parsing. - The `payouts` namespace's now-unused `failure_reason_label`/`page_prev`/`page_next`/`page_indicator` i18n keys were left in place (superseded by `failure_code_*` and the shared `common.page_*` keys respectively) — a future cleanup pass could remove them if nothing else references them. - Web-push (REQ-054) remains genuinely deferred — the 15s poll is the only freshness mechanism for new requests until a service-worker + backend push rail is built.