Files
baya-monorepo/dev/post-phase/ui/ui-phase-7-nurse-daily-ops.md
T
2026-07-17 13:22:04 +03:30

22 KiB
Raw Blame History

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 02 · Unlocks: the nurse side finally has a home; field work is phone-first; Phase 8 plugs its activation checklist into the dashboard slot this phase leaves. Before you start, read ../../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:132133; 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:209217).
  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 117141) — visually equal to the "view booking" text link beside them.
  4. The inbox hides the decision. InboxCard (requests/page.tsx:57115) 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's CountdownTimer v2, EmptyState/ErrorState kit, PageHeader, <Money>, and skeleton twins.
  • The nurse shell chrome — grouped sidebar, bottom nav, identity card — is Phase 2's property (client/src/layout/). Do not touch it.

2. Required reading (do this first)

  • audit/nurse-trust-ops.md and 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, bookings-evv.md, payouts.md.
  • Product ground truth: data-model/index.md (Principle 6 — two-stage disclosure is a hard rule), 06-evv-and-service-delivery.md (address-match is advisory, never a block), 05-booking-and-scheduling.md, 10-payouts.md, and 12-messaging-and-emergencies.md (no nurse↔customer chat channel — contact is tel: + coordination tickets).
  • The REQ tracker: 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), 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:173183). 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:198219). 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.

Backend gaps become REQ entries appended to 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 everywhereIntl 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:

  • 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: 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.
  • 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.