15 KiB
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) + ownTrustBadge(ownBadgeState, one cacheduseVerificationStatusquery, not two). NextVisitCard— first actionable session fromuseTodaySessions, 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) viauseNurseEarningsBalance<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: extendclient/src/app/[locale]/(private-routes)/nurse/DashboardActivationSlot.tsxin place — don't add a second slot.NotificationsEntryRow— unread count (useUnreadCount) linking toROUTES.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). useTodaySessionsgained a 60srefetchInterval(TODAY_SESSIONS_REFETCH_MS) so a same-day schedule change surfaces without re-navigation; the EVV-mutation invalidation is untouched.SessionCardgained an optionalserviceLabelprop, rendered under the title; the day surface passesitem.variantLabel(REQ-052, mock-tolerant —undefinedon 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 smallalignSelf:'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 lightweightConfirmDialogbefore firing («اتمام ویزیت؟» — it ends the visit and starts the payout clock). Reused byBookingDetailView'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
addressSnapshotJsonwhen present (title/city/district/line, best-effort parse — REQ-045 still open), ageo:{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, anAccentCardstate header — «در حال ویزیت» + elapsed on-site time (formatElapsedagainst the servercheckInAt, never a guessed start) + the check-out CTA promoted to the top, via the sameCheckOutConfirmButton. - Contact affordance — already delivered, verified, no changes needed.
BookingSupportEntry/EmergencyBanner(messaging composites, already mounted onvisits/[id]/page.tsx) already surface atel:click-to-call from the gated care-instructions read for the nurse on a confirmed+ booking. Thephone/navigateicons 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/BookingDetailViewwere already eachmaxWidth:640, mx:'auto', width:'100%'and stacked under onegap: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 carriesvariantLabel/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, andcoarseLabel(which opts intoaria-live="polite"humanized copy) — no extension toCountdownTimerwas needed, its ui-phase-1 API already covers this. - Tabs + pager: «در انتظار» (
pending_nurse_response) / «پاسخداده» (client-mergedaccepted_awaiting_paymentconverted+rejected_by_nurse, page-1-only — documented limitation, filed as part of REQ-050's status-group-filter ask) / «منقضی» (expired_no_response).useNurseRequestInboxgained an optionalenabledparam 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 aCountdownTimeragainst the serverpaymentDeadlineAt. - 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):
- «برداشت بعدی»
ForecastLineabove the tabs — server-served only (nextPayoutDate/nextPayoutEligibleAmountIrr, both optional onNurseEarningsSummary), renders nothing until served (REQ-053, never computed client-side). - Failure-reason mapping (
services/payouts/failureReasons.ts, new): knownfailureReasoncodes (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 secondarydir="ltr"caption — never the raw vendor string as the headline. Applied to bothPayoutHistoryRowand the payout detail page. ExplainerCarda11y: the bareonClickStackis now a realButtonBase(aria-expanded,aria-controls) and the eye icons (visibilityon/visibilityoff) are replaced by the registeredexpandchevron (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 widerDASHBOARD_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)
- Log in as the seeded verified nurse (refinement-phase-1 demo accounts) →
/nurseshows 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. /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./nurse/requests: three tabs; a pending card leads with service + price only whenUSE_BOOKING_REQUESTS_MOCK=true(flip the flag inservices/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.- Stop the API (or force a query error) on
/nurse/requestsor/nurse/visits→ anErrorStatepanel with retry, never the empty state. /nurse/visits/[id]withUSE_BOOKINGS_MOCK=trueon a confirmed booking (e.g. id5001or the completed5005) → an address card with a workinggeo:/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./nurse/earnings: the forecast line always appears (payouts stays mock-primary,USE_PAYOUTS_MOCK=trueby default); a failed payout (seededinvalid_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 reportsaria-expanded.- Repeat 1–6 on
/enand dark mode — no stock-MUI colors, no Latin digits infatimers.
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'stoListItemnow also stampsvariantLabel/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:forViewernow unmasksaddressSnapshotJsonfor the nurse onceisBookingConfirmedOrBeyond(simulating REQ-051 ahead of the real endpoint),addr5001gained matchinglatitude/longitude, andlistTodaySessionsstampsvariantLabel(REQ-052). See the updatedmocks-registry.mdrow.services/payouts— mock-primary (USE_PAYOUTS_MOCK=true, unchanged — REQ-025 is still the root gap);buildSummary()now also servesnextPayoutDate/nextPayoutEligibleAmountIrr(REQ-053). See the updatedmocks-registry.mdrow.- 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 inboxvariantLabel/variantPrice/variantPriceUnit+ anansweredstatus-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/'sCheckOutConfirmButton+ theBookingDetailView/SessionCardadditions) updated in the same change.dev/shared-working-context/reports/mocks-registry.md:BookingsApiandPayoutsApirows updated in place with this phase's mock additions (also correctedBookingsApi's stale "defaulttrue" config-flag note — it'sfalse, 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 +
.icsexport drop their best-effort JSON-key parsing. - The
payoutsnamespace's now-unusedfailure_reason_label/page_prev/page_next/page_indicatori18n keys were left in place (superseded byfailure_code_*and the sharedcommon.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.