cleanup phases 6

This commit is contained in:
hamid
2026-08-02 18:48:32 +03:30
parent e2db97392a
commit 51e86a1e5f
239 changed files with 118 additions and 70 deletions
@@ -0,0 +1,296 @@
# 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.