Files
baya-monorepo/archive/post-phase/ui/ui-phase-7-nurse-daily-ops.md
T
2026-08-02 18:48:32 +03:30

297 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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: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](ui-phase-1-primitives-and-states.md)'s `CountdownTimer`
v2, EmptyState/ErrorState kit, `PageHeader`, `<Money>`, 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 `<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](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: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](../../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.