# 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`
  + `<Money>`; 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.
