# 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.
