# Frontend Phase 7 — Booking request flow (C4 + C5 + nurse inbox) — report

**Date:** 2026-07-09 · **Track:** frontend · **Depends on:** f6 (search/nurse profile → the CTA + the
nurse's variants), f3 (addresses + map preview), f2 (patients), f0 (money/date utils, services pattern,
stepper/status composites) · **Consumes:** b8 `booking-requests.md` · **Unlocks:** f8 (booking detail →
the `converted` handoff) + f9 (checkout → the payment CTA).

## What was built

The money-free **request phase** of the engagement lifecycle — a customer turns a nurse profile into a
sent request and both sides close the loop, with server-frozen deadlines and two-stage clinical disclosure
made visible.

### `services/bookingRequests/` (the domain, copies the f0/auth shape)
- **`types.ts`** — `RequiredCaregiverGender`, the full `BookingRequestStatus` union + `TERMINAL_*` set +
  `isTerminalBookingRequestStatus`, `BookingRequestDto` (customer/admin full-address vs nurse masked view),
  `BookingRequestListItem`, `CreateBookingRequestPayload`, `RejectBookingRequestPayload`, the paginated
  list params, `BookingRequestDisplayContext` (mock-only display aid), and the `BookingRequestsApi` seam.
  Derived from the b8 contract; `variantPrice` is documented **client-augmented** (REQ-013).
- **`keys.ts`** — `bookingRequestKeys.list(role,status,page)` / `.nurseInbox(...)` / `.detail(id)`.
- **`constants.ts`** — `USE_BOOKING_REQUESTS_MOCK` (true, primary), poll/stale/gc times, page size, the
  notes/reason limits, and the mock-only deadline windows.
- **`apis/`** — `mockApi.ts` (**primary**; a shared in-memory state machine, see below), `clientApi.ts`
  (real b8 1:1 mapping — action-style snake_case routes, camelCase bodies, ids from the route), `index.ts`
  (seam selection by the flag).
- **`hooks/`** — `useCreateBookingRequest` (mutation → seeds the detail cache + invalidates the customer
  list), `useBookingRequest(id, role)` (query; **polls while non-terminal, stops at terminal/`converted`**
  via a `refetchInterval` guard; `role` drives the mock's nurse-view masking), `useNurseRequestInbox`
  (query; light polling), `useCustomerRequests` (query; for f8 reuse + create/cancel invalidation),
  `useAccept`/`useReject`/`useCancel` (mutations → invalidate inbox list + detail). Barrel re-exports hooks.

### Screens (all RTL/i18n/dark-mode)
- **C4** `/bookings/request` (customer) — the request form. Patient dropdown (f2; empty → link to
  `/patients`), service-variant dropdown (the chosen nurse's `services` from the f6 profile) + a
  `PriceDisplay` of the picked rate, address dropdown (f3) + a read-only **map-pin preview** of the saved
  address's coordinates, native date + time-window fields, a **first-class 3-way caregiver-gender toggle**
  (خانم/آقا/فرقی ندارد) with a why-line, and a stage-1 notes field with a counter + honesty copy. Defaults
  are **derived during render** (no setState-in-effect): the variant to the carried/first, the address to
  the primary/first. The CTA is gated on all required fields + a client-side same-gender-mismatch block;
  domain 400 codes map to field/form errors. On success → C5.
- **C5** `/bookings/request/[id]` (customer) — the awaiting screen. `BookingRequestSummaryCard` + the
  3-step `StepperHeader` tracker + a status-driven body: **pending** shows a `CountdownTimer` to
  `nurseResponseDeadlineAt`; **accepted** swaps the tracker, shows a ✓ badge + a terracotta 30-min
  `CountdownTimer` to `paymentDeadlineAt` + the **ادامه پرداخت** CTA → `/bookings/checkout`; **rejected /
  expired / payment-expired / cancelled** are terminal cards with a re-request CTA into search;
  **converted** routes to booking (f8). Cancel-with-confirm while pending/accepted. Polls until terminal.
- **Nurse inbox** `/nurse/requests` — pending requests, each a card with patient name, Shamsi time, the
  **required-gender chip**, a `customerNotes` preview, and a per-request `CountdownTimer`. Empty state +
  light polling. New nurse-shell nav item.
- **Nurse detail** `/nurse/requests/[id]` — the summary + **only `customerNotes`** (masked coarse
  city/district, a disclosure note) + accept / reject-with-reason (a dialog capturing the reason). A stale
  `409` surfaces a warning + refetch. Both actions invalidate the inbox + detail.
- **`/bookings/checkout`** — an f9 DEFERRED stub (PlaceholderScreen) so the accept CTA doesn't dead-end.

### Shared composites (tested)
- **`CountdownTimer`** — a pure countdown to a **server-frozen** UTC instant; owns its own 1-second tick so
  only it re-renders (never the page), stops at zero and shows an elapsed label + fires `onElapsed` once,
  renders locale digits forced LTR. 4 tests (MM:SS / HH:MM:SS / tick / elapsed+onElapsed via fake timers).
- **`BookingRequestSummaryCard`** — nurse identity + rating, patient, priced service (`PriceDisplay` when a
  price is present), address label, Shamsi date/time. Reused by C5 + nurse detail; f8 reuses it too. 4 tests.

### Other
- Routes: `BOOKING_REQUEST` (now the C4 form), `BOOKING_REQUEST_STATUS`, `CHECKOUT`, `NURSE_REQUESTS`.
  Icons: `requests`, `payment`. i18n: `booking` namespace fully fleshed + `nav.requests`, both locales in
  sync. The prior `/bookings/request` f6-handoff **stub was replaced** by the real C4 form.

## Now testable, and exactly how (§7 of the phase)
Run `npm run dev` (mock is primary — no backend needed). Within one browser tab (the mock store is a shared
module singleton):
1. **Submit:** from a nurse profile (C3) tap **درخواست رزرو** → C4 pick patient/variant/address/date/time +
   a gender → **ارسال درخواست** → land on **C5** (tracker step 2 active, response countdown ticking).
2. **Nurse sees it:** open `/nurse/requests` → the new request appears with the gender chip + notes +
   countdown; open the detail → **only the notes** (no address/clinical fields).
3. **Accept:** tap accept → it leaves the pending inbox (invalidated); navigate back to **C5** → step 2
   done, step 3 active, **✓ پرستار تایید کرد**, the **30-min payment countdown**, and **ادامه پرداخت →**
   (routes to the checkout stub with `request_id`).
4. **Reject:** on another request, reject with a reason → C5 shows the **rejected** terminal card + reason.
5. **Expiry:** the mock shortens the response window (see below) — once it lapses, a C5 poll (or reopening)
   shows **expired_no_response**; after accept, the 30-min window lapsing shows **payment_deadline_expired**.
6. **Quality:** flip fa↔en (dir + strings), dark mode holds; Devtools shows the inbox/detail invalidating on
   accept/reject and the C5 poll stopping at a terminal status.

## What is mocked (and how it swaps to real)
`services/bookingRequests` is **mock-primary** (`USE_BOOKING_REQUESTS_MOCK = true`). b8 is live server-side,
but this is a **client-side** mock for two reasons: (a) every *input* id — the nurse (f6 search), the
patient (f2), the address (f3) — comes from a **mock-primary** upstream domain, so a real
`booking_requests/create` would reference ids that don't exist in a real DB; (b) the contract DTO omits the
variant price the summary renders (REQ-013). The mock is a **shared in-memory state machine**: one
module-level store drives create → the nurse inbox → accept/reject/cancel → the customer C5 poll, with a
**lazy expiry sweep** on every read (the client stand-in for the server's background job), the nurse-view
**address masking**, and the forward-only status/deadline rules. Mock-only deadline windows: the payment
window is contract-accurate (30 min); the **response** window is shortened to 30 min (a stand-in for the
server's 24h) so a session can observe the expiry path. The real `bookingRequestsClientApi` maps the b8
contract 1:1 (the `context`/`role` args are mock-only and ignored). **Swapping to real is a one-line flag
flip** once the upstream domains are live and REQ-013 lands — no hook/component change. (Client-side mock;
recorded here, not in `mocks-registry.md`, which tracks backend DI seams.)

## Contracts consumed / requests filed
- **Consumed (not edited):** `dev/contracts/domains/booking-requests.md` (b8) — types derive from it. Note:
  the actual routes are `booking_requests/{create,accept/{id},reject/{id},cancel/{id},list,get/{id}}` (the
  phase file's illustrative `create_booking_request`-style names were superseded by the contract), and the
  wire uses `requestedDate` + `requestedTimeStart/End` (not a single `scheduled_start_at`).
- **Filed:** `frontend/requests/for-backend.md` — **REQ-013** (`variantPrice` + `nurseAvatarUrl` on
  `BookingRequestDto`) and **REQ-014** (`variantLabel` + `patientAge` on `BookingRequestListItemDto`).

## Follow-ups for later phases
- **f8 (booking detail):** the C5 `converted` state routes to `/bookings` today — wire it to the real
  booking-detail route when f8 lands. f8 should reuse `BookingRequestSummaryCard`.
- **f9 (checkout):** the C5 accept CTA → `/bookings/checkout?request_id=…` (a stub). f9 builds the C6
  summary + escrow + card/BNPL and consumes the accepted request id.
- **Swap to real b8:** flip `USE_BOOKING_REQUESTS_MOCK=false` once search/patients/addresses are live and
  REQ-013/014 land; verify the nurse-view masking + deadline freezing against the server.

## Gate
`npm run check` green · `npm run test:ci` green (173 tests, +8) · `npm run build` green with
`NEXT_PUBLIC_API_URL` set (all five new routes compile + prerender). Without the env var the build fails on
the **pre-existing** "Missing .env variable!" from `@/config` — it hits the existing `/fa/search` route
identically, so it is environmental, not an f7 defect.
