22 KiB
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 (addressSnapshotJsonhas zero render sites inclient/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 · 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:
- The landing page is empty.
nurse/page.tsxrendersPlaceholderScreen(line 7) whileuseNurseRequestInbox,useTodaySessions,useNurseEarningsBalance,useVerificationStatus,useNurseTrustBadge, anduseUnreadCountall exist as cached queries — the dashboard is pure assembly. - The day-of flow is blind.
BookingDetailViewrenders only patient + nurse names;addressSnapshotJsonis never rendered anywhere — and per the b9 contract it isnullin the nurse view by design (services/bookings/types.ts:132–133;bookings-evv.md: "The nurse view omitsaddressSnapshotJson"), so serving it needs a REQ (§3.3). The contact affordance needs no REQ:CareInstructionsDtoalready carriesemergencyContactName/emergencyContactPhonein the gated post-confirmation read (types.ts:209–217). - The most important tap of a nurse's day is a tertiary button. Both EVV CTAs in
SessionCard.tsxrendersx={{ m: 0, alignSelf: 'flex-start', py: 1 }}(lines 117–141) — visually equal to the "view booking" text link beside them. - The inbox hides the decision.
InboxCard(requests/page.tsx:57–115) shows patient, time, gender chip, notes — no service, no price;BookingRequestListItemcarries 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:19destructures only{ data, isLoading }) — paid work silently missed. - Earnings answers every question except the one nurses ask ("when do I get paid, how much"),
and failed payouts print the bank rail's
failureReasonverbatim — LTR English bank codes in a Persian UI (earnings/payouts/[id]/page.tsxfailure 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'sCountdownTimerv2, 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/
mequery — verify the field) +TrustBadge(viauseNurseTrustBadge/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 1CountdownTimerwith 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— reuseEarningsBalanceHeadercompact 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
useUnreadCountlinking toROUTES.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 1PageHeader), so the page reads as today, not a generic list. - Service name on session cards: rows show patient name + session index only —
BookingSessionListItemDtohas 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
refetchIntervaltouseTodaySessions(e.g. 60s — same-day schedule changes currently never appear without re-navigation; the hook sets onlystaleTime). Keep the EVV-mutation invalidation untouched. - EVV CTA as the hero: in
SessionCard, whenshowEvvControlsis 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).SessionCardis shared both-roles — gate every change onshowEvvControlsso 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/emergencyContactPhoneare already inCareInstructionsDto, gated to the assigned nurse post-confirmation.tel:only (no VoIP, per product);BookingSupportEntryremains the coordination path. Registercall/navigationicons inAppIcon/config.ts— the registry haslocation/gpsbut 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 existingEvvStatusBanner/formatElapsed. - Notes placement polish: keep
NurseVisitNotesPanelbelow 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
CountdownTimertiers — 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 (
useNurseRequestInboxalready acceptsstatusandpage; 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 thepaymentDeadlineAtcountdown 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:20destructures 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
failureReasoncodes to Persian labels (i18n keys) in the payout detail andPayoutHistoryRow; unknown codes get a generic Persian message with the raw code as a secondarydir="ltr"caption — never the raw vendor string as the headline. - ExplainerCard a11y: real button semantics on the collapse header (today a
cursor: pointerStack — no role, no keyboard path),aria-expanded, and the registeredexpandchevron instead of thevisibilityon/visibilityoffeye 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 nursebooking_requests/listrow; 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 whetherrecipientPhonejoins the post-confirmation nurse view. - REQ-041 — Service label on the today feed: variant display name on
booking_sessions/todayrows. - 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
- 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. - 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;
useCareInstructionsstays 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.
- coarse city·district. The address card (§3.3) exists only on the confirmed booking from the
b9 read;
- 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.
- 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 adir="ltr"island. - Locale digits + Shamsi everywhere —
Intlfa-IR digits,formatShamsiDate, Toman at the display boundary via the shared money utils;dir="ltr"islands for clocks/IBANs/phone numbers. - Ownership boundaries:
layout/is Phase 2's; shared primitives are Phase 1's — extend minimally there if a gap bites, never fork locally.SessionCard/BookingDetailViewchanges must keep the customer view pixel-compatible (gate onshowEvvControls/viewerRole). - 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.
- 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,
EarningsBalanceHeadermoney honesty, non-accusatory failure copy, the 15s inbox poll +onElapsedrefetch.
6. Definition of Done
On top of the shared definition-of-done.md:
npm run checkgreen;npm run test:cigreen for every touched shared component (SessionCard,BookingDetailView,CountdownTimerif extended, new shared widgets).en.json/fa.jsonin sync — no orphan keys either way./nurserenders 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 (/famobile first: this is the phone-first phase).
7. How to test (what a human can verify after this phase)
- Log in as the seeded verified nurse (refinement phase 1 demo accounts) →
/nurseshows a real dashboard: greeting + trust badge, next visit, pending-requests strip counting down, earnings snapshot in Toman. - As a customer, create a request targeting that nurse → within the poll interval the dashboard
strip and
/nurse/requestsshow it; the card leads with service + price (REQ pending: gracefully headline-less); the countdown pill escalates teal → amber → terracotta (adjust a mock deadline). - Open the request → «پذیرش» → a confirm dialog summarizes the consequence; confirm → status flips; a stale second accept still 409s into the refetch path.
- Force the inbox query to fail (stop the API) → an error panel with retry — not «درخواستی ندارید».
- 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. - Open a confirmed visit's detail with
USE_BOOKINGS_MOCK = true(mock serves an address snapshot) → address card + map link openinggeo:/Neshan;tel:dials the care-instructions emergency contact. On the real (masked) path → the quiet "available after confirmation" note, no crash. - 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. - Repeat 1, 2, and 5 on
/enand 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.