# Frontend Phase 8 (f8-b9) — Booking detail, sessions & nurse EVV — report **Date:** 2026-07-10 · **Lane:** frontend · **Consumes:** [bookings-evv.md](../../contracts/domains/bookings-evv.md) (b9) · **Unlocks:** f9 (checkout/pay), f13 (reviews & patient records) ## What was built The post-payment engagement — the hinge between "I asked for a nurse" and "a nurse is delivering care." ### Data layer — a **new** `services/bookings` domain The **sibling** of `services/bookingRequests` (b8), **not** a rename — a distinct b9 contract, distinct routes (`/api/v1/bookings/*` + `/api/v1/booking_sessions/*`), distinct shapes. Same `services/{domain}` shape as every other domain: - `types.ts` — derived 1:1 from the b9 swagger (camelCase): `BookingDetailDto`, `BookingSessionDto` (`BookingSessionSummaryDto`), `VisitVerificationDto`, `CareInstructionsDto`, `BookingListItemDto`, `BookingSessionListItemDto`, `CheckInVisitInput`/`CheckOutVisitInput`, the `BookingsApi` seam, the three enum unions (`BookingStatus`/`BookingSessionStatus`/`VisitVerificationStatus`), and pure helpers (`isBookingConfirmedOrBeyond`, `isBookingTerminalBranch`, `bookingTimelineActiveIndex`, `BOOKING_TIMELINE_ORDER`). - `keys.ts` — `bookingDetail(id)`, `bookingSessions(id)` (alias of `bookingDetail` — sessions are embedded), `today(params)`, `sessionEvv(id)`, `careInstructions(id)`, `list(params)`. - `apis/` — real `clientApi` (maps the routes 1:1), `mockApi` (the seeded state machine, **primary**), `serverApi.getBookingDetail` (the RSC-prefetch seam for the real path), and a config-selecting `index`. - `evv/locationProvider.ts` — the **`ILocationProvider`** GPS seam (real `navigator.geolocation` vs a canned mock; `getCurrentPosition()` resolves `null` on denial, never rejects). - `hooks/` (one per file): `useBookingDetail`, `useBookingSessions` (a `select` over the detail query, no second fetch), `useBookingList`, `useTodaySessions`, `useSessionEvv`, `useCareInstructions` (**enabled-gated**), `useCheckInVisit`, `useCheckOutVisit` (both invalidate detail+sessionEvv+today+list on success). ### Shared composites — `src/components/booking/` (each with a co-located `*.test.tsx`) - `BookingDetailView` — the both-roles smart container (role-conditioned EVV + gated care). - `BookingStatusTimeline` — the server-truth 7-status timeline over the f0 `StepperHeader` + status chip. - `SessionList` → `SessionCard` — per-session Shamsi schedule, status chip, EVV CTA, elapsed/payout. - `EvvStatusBanner` — advisory banner (in-range success / out-of-range warning / no-gps neutral). - `CareInstructionsCard` — the decrypted clinical read (conditions/meds/allergies/instructions/emergency). - `BookingMoneySummary` — gross / commission (کارمزد) / payout, display-only via the money util. - `useEvvController` — GPS-capture + check-in/out orchestration (one instance per surface, per-session busy). - `format.ts` (clock/duration) + `statusKind.ts` (status → StatusChip kind) helpers. ### Screens - **Customer:** `/bookings` (رزروها list) → `/bookings/[id]` (detail, customer view: timeline + sessions + money; the care record shows the "visible to your nurse only" affordance and the query never fires). - **Nurse:** `/nurse/visits` (ویزیت امروز — today's sessions with inline EVV check-in/out) → `/nurse/visits/[id]` (detail, nurse view: EVV controls + the gated care card). ### Cross-cutting - i18n `booking` namespace **extended** (both locales, key-synced): `bstatus_*`, `sstatus_*`, `evv_*` (banner variants + CTAs + GPS copy), `care_*` (+ the customer lock copy), `money_*`, `list_*`, dispute note. - 8 new registry icons (`check_in`/`check_out`/`gps`/`schedule`/`clinical`/`medication`/`emergency`/`lock`) and one new token `--bal-secondary-soft` (the نمای پرستار chip / EVV affordance), both schemes. ## What is now testable, and exactly how Run `npm run dev` (mock is primary — no backend needed). The b9 endpoints are also live if you flip the flag. 1. **Confirmed booking (customer):** open the رزروها tab → the list shows the two seeded bookings; open one → **status timeline** at `confirmed`, the **session schedule** (booking #5002 shows exactly **one** session; #5001 shows **3**), and the **money summary** in Toman. Toggle `/en`↔`/fa` → strings + `dir` flip; the timeline reads RTL. 2. **Care gate:** open a booking **as the nurse** (`/nurse/visits` → a session → view booking) → the **care-instructions card** is visible (conditions/meds/allergies/instructions/emergency). As the **customer**, the card is absent and the Network tab shows the care request **was never made** (proven by the `BookingDetailView` test too). 3. **Nurse check-in:** on `/nurse/visits` (or in the nurse booking detail), tap **«ثبت ورود (EVV)»** → "در حال دریافت موقعیت…" → the **«ورود ثبت شد … موقعیت تایید شد (EVV)»** banner; the session chip → `in_progress`; the timeline → `in_progress`. Set `NEXT_PUBLIC_EVV_MOCK_GPS=out_of_range` → the **advisory** «موقعیت خارج از محدوده (در حال بررسی)» banner and the check-in **still succeeds**. `=denied` → the nurse still checks in (no block; advisory toast + no-gps banner). 4. **Nurse check-out:** tap **«ثبت خروج (EVV)»** → the session chip → `completed` with elapsed duration; for the single-visit booking (#5002) the timeline advances to `completed` + the dispute-window note appears — all from the server response, no client-side step jump. 5. **Caching:** in React Query Devtools, an EVV mutation invalidates `bookingDetail`/`sessionEvv`/`today`/ `list` and the UI re-renders from the refetch; revisiting within `staleTime` does not refetch. 6. `npm run check` green · `npm run test:ci` green (195 tests, +22). ## What is mocked / waiting on a real service - **`services/bookings` — mock-primary** (`USE_BOOKINGS_MOCK=true`). A booking only exists after `bookings/convert` runs on a **paid** request, and both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet, so a real `bookings/list` returns nothing. The mock seeds confirmed bookings + sessions + care + the EVV state machine. Real `bookingsClientApi` maps the routes 1:1; the swap is one flag (see mocks-registry). `serverApi.getBookingDetail` is ready for the RSC prefetch on the real path. - **`ILocationProvider`** (`NEXT_PUBLIC_EVV_MOCK_GPS`) — GPS capture seam; the real path is `navigator.geolocation`. Server-side address-match math stays behind the backend geocoding seam. - Both are recorded in [mocks-registry.md](./mocks-registry.md). ## Contract consumed + gaps filed - **Consumed:** [bookings-evv.md](../../contracts/domains/bookings-evv.md) + the b9 swagger shapes — types derive 1:1. No shape was guessed. - **Filed:** **REQ-015** — confirm the booking/session/EVV enum **string codes** (bare `string` in swagger) match the client unions, and the **`checkInAddressMatch` tri-state** (`true`/`false`/`null`) so the banner can distinguish an advisory mismatch from "no GPS captured." Low-risk (mock-primary now); worth locking before f9/f13 reuse the shapes. ## Deliberate design decisions (non-obvious) - **Two-stage disclosure is a UI gate, not just a server check.** `useCareInstructions` is `enabled` only for the assigned-nurse view on a `confirmed`+ booking; the customer/unassigned viewer **never fires** the request (a 403/404 is treated as a defect path). The `BookingDetailView` test asserts the customer never calls `getCareInstructions` and the nurse does. - **EVV mismatch/denial is advisory, never a block.** Out-of-range check-in succeeds with a **warning**-tokened banner (never the error token); GPS denial still submits. Check-out is never gated on the match. - **Server-truth timeline + display-only money.** The timeline renders `BookingDetailDto.status` exactly (no client step advance); money is rendered as-sent (no sum/derive/re-split); `payoutEligibleAt` is never recomputed. Sessions are **embedded** in the detail (no standalone list endpoint) — `useBookingSessions` is a `select` over the one detail query, so invalidating `bookingDetail(id)` refreshes both. - **`ILocationProvider`** is the single new client seam; the `NEXT_PUBLIC_EVV_MOCK_GPS` default is `in_range` while mock-primary so the happy path is demoable without a device (real GPS would never fall near the seeded Tehran address). ## Follow-ups for later phases - **f9 (checkout/pay):** the money summary here shows the confirmed **split only**; the **tax (مالیات) line**, escrow notice, and invoice are the checkout surface — b9's `BookingDetailDto` has **no tax field** (flagged; f9 owns it). The C5 accept CTA still lands on `/bookings/checkout?request_id=…` (f7 stub). - **f13 (reviews & records):** the E3 **visit-note authoring** (bottom half) and the full **E2 patient-record viewer** are deferred here; the booking-detail/EVV/care pattern (timeline + sessions + gated care + EVV banner) is the template they extend. The customer-side **care-details authoring** (`submit_care_instructions`) write form is also f13 — f8 only reads the gated record. - **f15 (admin):** the EVV-review queue (mismatch / no-show worklist) is the admin console; f8 raises no alerts client-side (no-show detection is a server job). - **Swap to real:** flip `USE_BOOKINGS_MOCK=false` once `bookings/convert` is reachable client-side (b10 card capture) — `bookingsClientApi` + `bookingsServerApi` are wired; no hook/component change.