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