# Frontend Phase 7 — Booking request flow (customer request + nurse inbox) > **Mission:** turn a nurse profile into a *sent request* and close the request loop on both sides. The > customer fills the **request form** (C4) — patient, service variant, address, date/time, and the > first-class **caregiver-gender preference** — and lands on the **awaiting-acceptance** screen (C5) with > a live countdown to the nurse's response deadline and a 3-step status tracker. The nurse opens an > **incoming-requests inbox**, sees a request showing *only* the customer's notes (two-stage clinical > disclosure), and accepts or rejects it. On accept, the customer flips to a 30-minute **payment-deadline** > countdown that hands off to checkout (f9). This is the money-free request phase — no payment, no booking > row yet — and it is where the platform's trust contract (same-gender match, deadlines, terminal states) > becomes visible to both actors. > > **Track:** frontend · **Depends on:** [frontend-phase-6-b7](./frontend-phase-6-b7.md) (discovery: search/results/nurse profile) · [frontend-phase-3-b4](./frontend-phase-3-b4.md) (addresses & map picker) · backend **b8** contract ([booking-requests.md](../../contracts/domains/booking-requests.md)) · **Unlocks:** [frontend-phase-8-b9](./frontend-phase-8-b9.md) (booking detail · sessions · EVV) > **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional. --- ## 1. Context — where this sits We are at the hinge of the customer journey: discovery is done (f6), the customer is looking at a nurse profile (C3) and taps **درخواست رزرو**. This phase builds the *request phase* of the booking lifecycle — the deliberately money-free intent that lives in `booking_requests` and **only** becomes a `bookings` row later, after the nurse accepts *and* payment captures (f9 / b9). Nothing here touches money or creates a booking. The product framing: a family requests a specific nurse for a specific patient at a specific address and time, the nurse retains accept/reject autonomy (a deliberate worker-classification stance), and both sides see frozen deadlines so the engagement can't hang forever. **What already exists (do not rebuild):** - **f0 foundations** ([frontend-phase-0](./frontend-phase-0.md)): the three actor shells (customer mobile + 5-tab bottom nav, nurse shell, admin), the `services/{domain}` + TanStack Query caching pattern (template = `src/services/auth/*`: `types.ts` / `keys.ts` / `apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`), `clientFetch`/`serverFetch` + `ApiError` (`@/lib/api`), the contracts→types convention, the money/Shamsi format utils in `src/utils/`, and the shared composites (stepper/progress header, status chip, OTP/phone inputs). **Reuse these — do not re-create the pattern.** - **f3 addresses** ([frontend-phase-3-b4](./frontend-phase-3-b4.md)): the customer **address book**, the **map picker**, and the cascading province/city/district selectors, all behind `services/addresses` (or the f3 domain name). The request form **reuses** the address picker/list — it does not build a new one. Patient `customer_addresses` already carry coordinates from f3's geocode. - **f2 onboarding** ([frontend-phase-2-b3](./frontend-phase-2-b3.md)): the **patient** list/CRUD behind `services/patients`. The request form's patient selector **reads** that list; it does not add a new patient-creation path (link out to the f2 "add patient" flow for the empty case). - **f4 catalog** ([frontend-phase-4-b5](./frontend-phase-4-b5.md)): a nurse's **service variants** (`nurse_service_variants` — name, price unit `per_hour`/`per_session`/`per_half_day`/`per_day`/`per_24h`, price). The service-type selector **reads** the chosen nurse's published variants. - **f6 discovery** ([frontend-phase-6-b7](./frontend-phase-6-b7.md)): search (C1), results (C2), and the **nurse profile (C3)** with its **درخواست رزرو** CTA. This phase is the destination of that CTA — wire the navigation from C3 into the request form, passing the `nurse_id` (and optionally a pre-selected variant). > **Money/booking note:** there is **no payment and no `bookings` row** in this phase. The "pay & confirm" > step (C6 summary, escrow notice, card/BNPL) is **(DEFERRED → [f9](./frontend-phase-9-b10.md))**. Booking > detail, sessions, and EVV are **(DEFERRED → [f8](./frontend-phase-8-b9.md))**. Build only up to the two > countdowns (response deadline, then payment deadline) and the hand-off CTA into checkout. ## 2. Required reading (do this first) - [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md) and [`../_shared/frontend-conventions-checklist.md`](../_shared/frontend-conventions-checklist.md). - [`client/CLAUDE.md`](../../../client/CLAUDE.md) in full — the RSC/client boundary, layouts (never above `[locale]`), i18n, theme/tokens, cookies, the `services/{domain}` fetch pattern, anti-patterns. Mirror the **`auth`** service exactly when you create `services/bookingRequests`. - **Invoke the `frontend-designer` skill** — mandatory for all visual work in this phase (C4 form, C5 awaiting screen + tracker, the nurse inbox list + detail). It is the brand/design contract: palette (teal `#1d4a40`, terracotta `#d98c6a`, cream), tokens, typography, the `App*` library, the layout shells, and the hard RTL/dark-mode rules. Do not hand-pick colours in `sx`. - [`product/wireframes/index.html`](../../../product/wireframes/index.html) — the visual baseline. Study **C3 → C4 → C5** and the nurse "نمای پرستار" framing. The screens this phase implements: - **C4 · فرم درخواست** — patient selector (dropdown), service-type selector, address (map block, منزل), date + time pickers, nurse-gender preference (**خانم / آقا / فرقی ندارد**). CTA: **ارسال درخواست**. - **C5 · در انتظار تایید پرستار** — ⏳ status, "درخواست برای پرستار ارسال شد"; a summary card (nurse + time); the 3-step tracker **درخواست ثبت شد → در انتظار تایید پرستار → پرداخت و تایید نهایی**; a countdown to `nurse_response_deadline_at`. No CTA in the waiting state; on accept it shows the payment-deadline countdown + a "continue to payment" CTA. - **Nurse request inbox** — there is no dedicated wireframe panel, so design it consistently with the nurse shell: a list of pending requests each with a per-request countdown, and a request-detail showing **only** `customer_notes`, with accept / reject (reason) actions. - [`product/business/05-booking-and-scheduling.md`](../../../product/business/05-booking-and-scheduling.md) — the request→accept→pay→confirm lifecycle, the frozen deadlines, the two-table split, same-gender matching, and the two-stage clinical-disclosure rule. **These are decisions, not guesses — read them.** - **The contract you consume:** [`../../contracts/domains/booking-requests.md`](../../contracts/domains/booking-requests.md) (from **backend-phase-8**) — the exact request/response shapes, routes, status codes, and enums. Plus the shared conventions [`api-conventions.md`](../../contracts/conventions/api-conventions.md) and [`money-and-types.md`](../../contracts/conventions/money-and-types.md) (envelope, snake_case routes, pagination, enums-as-codes, UTC timestamps + Shamsi display, IRR-as-string, gender as load-bearing). - The latest backend handoff `dev/shared-working-context/backend/handoff/after-backend-phase-8.md` — what b8 shipped, which endpoints are live, and what (if anything) is still mocked server-side. - The f6/f3 frontend reports in `dev/shared-working-context/reports/` — to reuse the patient/address/ variant query keys and the discovery navigation rather than re-fetching or re-deriving them. ## 3. Scope — build this ### 3.1 The `services/bookingRequests` domain (consume b8) Create `src/services/bookingRequests/` by copying the `auth` template structure exactly: - **`types.ts`** — string-literal union types mirroring the **booking-requests.md** contract (do **not** guess shapes). At minimum: - `RequiredCaregiverGender = 'male' | 'female' | 'any'` (the wire codes behind خانم/آقا/فرقی ندارد). - `BookingRequestStatus = 'pending_nurse_response' | 'accepted_awaiting_payment' | 'rejected_by_nurse' | 'expired_no_response' | 'payment_deadline_expired' | 'converted'`. - `BookingRequestDto` (id, `nurse_id`, `patient_id`, `nurse_service_variant_id`, `customer_address_id`, `scheduled_start_at` (UTC), `required_caregiver_gender`, `customer_notes`, `status`, `nurse_response_deadline_at` (UTC), `payment_deadline_at` (UTC, nullable until accept), `nurse_rejection_reason` (nullable), plus the display fields the contract returns — nurse name/avatar, variant name + price-unit, patient name, address label). Money values (variant price) are **IRR digit strings**, parsed via the f0 money util — never floats. - `CreateBookingRequestPayload`, `RejectBookingRequestPayload` (reason). - **`keys.ts`** — a query-key factory: `bookingRequestKeys.lists(role, statusFilter)`, `bookingRequestKeys.detail(id)`, and the **nurse inbox** list key `bookingRequestKeys.nurseInbox(filter)`. - **`apis/clientApi.ts`** — wrap `clientFetch` for each endpoint the contract defines (names from **booking-requests.md** — expected, snake_cased per api-conventions): - `POST .../booking_requests/create_booking_request` → create (customer). - `GET .../booking_requests/list_booking_requests` → customer's requests, paginated, `status` filter. - `GET .../booking_requests/get_booking_request` → one request (polled on C5). - `GET .../booking_requests/list_nurse_requests` (or the contract's nurse-inbox route) → nurse's incoming requests, paginated, default `status=pending_nurse_response`. - `POST .../booking_requests/accept_booking_request` → nurse accept. - `POST .../booking_requests/reject_booking_request` → nurse reject (with `nurse_rejection_reason`). - **`hooks/` — one hook per file:** - `useCreateBookingRequest.ts` (`useMutation`) — on success, navigate to C5 with the new id and `setQueryData`/invalidate the customer list. - `useBookingRequest.ts` (`useQuery`) — the C5 detail; **polls** while status is non-terminal (`refetchInterval` ~ every 15–30s while `pending_nurse_response` / `accepted_awaiting_payment`, and **stops** on a terminal/`converted` status via a `select`/enabled guard) so the customer sees the accept/reject/expire transition without a refresh. - `useNurseRequestInbox.ts` (`useQuery`) — the nurse list, with light polling for new requests. - `useAcceptBookingRequest.ts` / `useRejectBookingRequest.ts` (`useMutation`) — **invalidate the nurse inbox list and the request detail** on success so the request leaves the pending list immediately. - **`index.ts`** — barrel. > If any shape the screens need is **missing** from booking-requests.md (e.g. the contract doesn't return > the nurse's display name on the request DTO, or omits the price-unit), **append the gap** to > `dev/shared-working-context/frontend/requests/for-backend.md` and **mock that field behind the > `services/bookingRequests` clientApi seam** meanwhile (operating-rules §6). Record the mock in your > report so it swaps out cleanly when b8 fills the gap. Never edit backend files. ### 3.2 C4 — the request form (customer) A page under the customer shell (e.g. `(private-routes)//booking-requests/new`, reachable from the C3 **درخواست رزرو** CTA with the `nurse_id`). RTL-first, mobile. Fields: - **Patient selector** — a dropdown reading `services/patients` (f2). Empty state → a CTA linking to the f2 "add patient" flow (don't inline patient creation here). The selected `patient_id` is sent. - **Service-type selector** — reads the chosen nurse's `nurse_service_variants` (f4). Each option shows the variant name + formatted price + price-unit label (i18n key off the `per_*` code, **not** a hardcoded label). Sends `nurse_service_variant_id`. - **Address block** — **reuse the f3 address picker / map block** (منزل/home), selecting a `customer_address_id` from the address book (with the map preview). Do not rebuild the picker. - **Date + time pickers** — produce a single UTC `scheduled_start_at` on the wire; **display** Shamsi via the f0 date util. (Recurring/multi-session scheduling UI is **(DEFERRED → later)** — one start time here; `session_count` is a server/booking concern.) - **Nurse-gender preference** — a 3-option segmented control: **خانم (female) / آقا (male) / فرقی ندارد (any)** → `required_caregiver_gender`. This is a **first-class field**, never a hidden default; if the nurse's profile already fixes a gender, still send the explicit code the customer chose. - **Request-stage notes** — a free-text field mapped to `customer_notes`. Copy must make clear this is the *only* thing the nurse sees before accepting (it is **not** the clinical care record, which is post-confirmation and **(DEFERRED → [f8](./frontend-phase-8-b9.md))**). - **CTA: ارسال درخواست** — fires `useCreateBookingRequest`; loading state while the server computes/freezes the deadline; on success navigate to C5. Surface domain `400`s (e.g. tenancy: patient/address not owned; same-gender mismatch; variant not bookable) as field/form errors — but do **not** toast `401/403/5xx` (the fetch layer already does). Validate client-side at the boundary (all required fields chosen, future date) before enabling the CTA; the server re-validates and is authoritative. ### 3.3 C5 — awaiting nurse acceptance + status tracker (customer) A page keyed by the request id (e.g. `.../booking-requests/[id]`). Uses `useBookingRequest` (polling). - **Header:** ⏳ "درخواست برای پرستار ارسال شد". - **Summary card** — nurse (avatar + name), patient, service variant + price, address label, requested time (Shamsi). Compose this as a **shared composite** (`src/components/...`) so the booking-detail screen in f8 can reuse it (a "BookingRequestSummaryCard"); co-locate a `*.test.tsx`. - **3-step status tracker** — reuse the **f0 stepper/progress header** composite: 1. **درخواست ثبت شد** (done as soon as the request exists), 2. **در انتظار تایید پرستار** (active while `pending_nurse_response`), 3. **پرداخت و تایید نهایی** (future; becomes active on `accepted_awaiting_payment`). - **Countdown** — a `CountdownTimer` shared composite (`src/components/...`, co-located test) ticking down to `nurse_response_deadline_at` (computed from the **server-supplied UTC instant** vs `Date.now()` — the client never computes the deadline, only renders it). It is a pure presentational countdown; when it hits zero, the UI shows "in expectation of server confirmation" and the poll resolves the real terminal status. Use a single interval, cleaned up on unmount; do **not** re-render the whole page each tick (isolate the ticking state in the timer component). - **State transitions (driven by polled `status`):** - `accepted_awaiting_payment` → swap step 2 to done, step 3 active; show **"✓ پرستار تایید کرد"**, a prominent **30-minute payment countdown** to `payment_deadline_at`, and a CTA **"ادامه پرداخت ←"** that navigates to checkout (**the checkout screen itself is [f9](./frontend-phase-9-b10.md) — wire the route, stub the destination if f9 isn't merged**). - `rejected_by_nurse` → terminal state card with the `nurse_rejection_reason` and a "request another nurse" CTA back into discovery (f6). - `expired_no_response` → terminal "no response in time" card + re-request CTA. - `payment_deadline_expired` → terminal "payment window lapsed" card + re-request CTA. - `converted` → the request became a booking → route to booking detail (**[f8](./frontend-phase-8-b9.md)**; stub if not merged). ### 3.4 Nurse request inbox + detail (nurse) Under the **nurse shell** (the wireframe's "نمای پرستار"), e.g. `(private-routes)//requests`. - **Inbox list** (`useNurseRequestInbox`) — pending requests, each row a card: patient first name/age, service variant, requested time (Shamsi), the **required-caregiver-gender** chip, and a **per-request countdown** to that request's `nurse_response_deadline_at`. Empty state: "درخواست جدیدی ندارید". Paginated (page/page_size per api-conventions). Light polling so new requests appear. - **Request detail** — shows the request summary **and only `customer_notes`** as the clinical context. **It must never render `booking_care_instructions` or any encrypted clinical field** — those don't exist pre-accept and are out of this contract; rendering them would break two-stage disclosure. Actions: - **Accept** (`useAcceptBookingRequest`) — on success the request moves to `accepted_awaiting_payment`, a `payment_deadline_at` is set server-side, and the customer's C5 (via its poll) starts the 30-min payment countdown. Invalidate the inbox list + this detail. - **Reject** (`useRejectBookingRequest`) — a small reason dialog capturing `nurse_rejection_reason`; on success the request leaves the inbox. Invalidate the inbox list + detail. - Both actions are disabled / show a terminal banner if the request already expired (the server returns `409`/`400` for a stale accept/reject — surface it gracefully, then refetch). ### 3.5 i18n + tokens Add a **`booking`** (and/or `bookingRequests`) namespace to **both** `messages/en.json` and `messages/fa.json`, in sync, RTL-first. Every visible string is a key — the gender labels (خانم/آقا/فرقی ندارد), the three tracker steps, the price-unit labels (off the `per_*` codes), all terminal-state copy, countdown labels, and the empty states. Colours from `tokens.css` only; financial/terracotta accent (e.g. the payment-deadline countdown) uses the brand terracotta token, not a literal. ## 4. Mocks & seams in this phase This phase **introduces no new external seam** — booking requests carry **no money** and call no third party. It only consumes the b8 HTTP contract. - **Backend-not-ready / contract-gap fallback:** if `after-backend-phase-8.md` shows b8 isn't merged, or booking-requests.md is missing a shape, build a **mock `clientApi`** behind the `services/bookingRequests` seam (same function signatures the real one will have), driving a small in-memory state machine so the whole flow is demoable: create → (timer or manual) accept/reject/expire. Record it in [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) and your report; swapping to the real `clientApi` must be a one-file change. File any contract gap in `dev/shared-working-context/frontend/requests/for-backend.md` (never edit backend files). - **Reused seams:** the patient list (`services/patients`, f2), the address picker (`services/addresses`, f3), and the nurse variants (`services/catalog` or f4's name) — **reuse**, do not redefine. ## 5. Critical rules you must not get wrong - **Two-stage clinical disclosure.** The nurse sees **only** `customer_notes` before accepting — never any encrypted `booking_care_instructions` or other clinical detail. That data isn't in this contract and must not appear anywhere in the inbox/detail UI. Full care instructions are post-confirmation and belong to [f8](./frontend-phase-8-b9.md). - **`required_caregiver_gender` is a first-class field.** Always sent explicitly (`male`/`female`/`any`), never defaulted or dropped — it drives same-gender bodily-care matching. The server re-validates; surface a mismatch `400` clearly. - **No money, no booking row here.** This is the request phase. Do not render a price-breakdown/escrow/pay step (that's C6 / [f9](./frontend-phase-9-b10.md)) and do not assume a booking exists until `converted`. - **Deadlines come from the server, frozen.** Render countdowns from the server-supplied UTC instants (`nurse_response_deadline_at`, `payment_deadline_at`) against `Date.now()`; the client **never computes or recomputes** a deadline. Show the **response** countdown pre-accept and the **30-minute payment** countdown post-accept; show the correct **terminal** state (`rejected_by_nurse` / `expired_no_response` / `payment_deadline_expired`) when the poll resolves it. - **Invalidate on accept/reject.** A nurse action must invalidate the inbox list + the request detail so the request leaves the pending list immediately and the customer's polled C5 reflects it — never leave stale cache. Equally, don't over-poll: stop polling once a terminal/`converted` status is reached. - **Minimal re-renders.** The ticking countdown state is isolated in the timer component (not lifted to a page that would re-render the form/summary every second). Stable query keys, `select` for slices. - **RTL + both locales + tokens + MUI primitives.** `fa` default & RTL; every string in `en.json` and `fa.json` in sync; colours from `tokens.css`; MUI v9 primitives/`App*` reused, shared composites (summary card, countdown) at the shared level with co-located tests — never re-implement a root primitive and never bury a reusable composite in a page. ## 6. Definition of Done The shared [definition-of-done.md](../_shared/definition-of-done.md), plus: - [ ] `services/bookingRequests` exists (types/keys/apis/hooks/index), typed **from** booking-requests.md (gaps filed + mocked behind the seam, not guessed). - [ ] **C4 request form** submits a valid request (patient + variant + address + date/time + gender + notes) and navigates to C5; client-side validation gates the CTA; domain `400`s surface as form/field errors. - [ ] **C5 awaiting screen** shows the summary card, the 3-step tracker, and a live countdown to `nurse_response_deadline_at`; it transitions (via poll) through accept (→ 30-min payment countdown + checkout CTA), reject, and both expiry terminal states. - [ ] **Nurse inbox** lists pending requests (with per-request countdown + gender chip), the detail shows **only `customer_notes`**, and accept/reject work and **invalidate the inbox + detail**. - [ ] Polling stops on terminal/`converted` status; no needless refetch; the ticking countdown doesn't re-render the whole page. - [ ] `booking`/`bookingRequests` i18n keys added to **both** locales in sync; colours from tokens; RTL verified. - [ ] `npm run check` green; `npm run test:ci` green for the new shared composites (summary card, countdown timer) and any touched shared component. - [ ] `client/CLAUDE.md` *Project Structure* updated for the new route segments + the `services/bookingRequests` domain and any new shared components. ## 7. How to test (what a human can verify after this phase) Run `npm run dev` (point `NEXT_PUBLIC_API_URL` at a b8 server, or use the mock `clientApi` seam if b8 isn't merged). Then: 1. **Submit a request.** From a nurse profile (C3) tap **درخواست رزرو** → on C4 pick a patient, a service variant, an address (map block), a future date/time, and a gender preference (خانم/آقا/فرقی ندارد), add a note → **ارسال درخواست**. *Expected:* land on **C5** showing the summary card, the 3-step tracker (step 2 active), and a countdown ticking down to the nurse's response deadline. 2. **Nurse sees it.** In the nurse shell open **requests** (inbox). *Expected:* the new request appears with the patient/variant/time, the **required-gender chip**, and a per-request countdown; opening the detail shows **only the `customer_notes`** — no clinical/care fields anywhere. 3. **Nurse accepts.** Tap accept. *Expected:* the request leaves the pending inbox immediately (cache invalidated); on the customer's **C5** (without a manual refresh, via the poll) step 2 flips to done, step 3 activates, a **✓ پرستار تایید کرد** badge appears, the **30-minute payment countdown** starts, and the **ادامه پرداخت ←** CTA appears (routing toward checkout/f9). 4. **Reject path.** On a different request, nurse rejects with a reason. *Expected:* customer's C5 shows the terminal **rejected** card with the reason + a re-request CTA back into discovery. 5. **Expiry paths.** Let (or simulate via the mock) the response deadline lapse → C5 shows **expired_no_response**; let the payment window lapse after accept → C5 shows **payment_deadline_expired**. Both are terminal with a re-request CTA. 6. **Quality:** locale switch flips `dir` + strings on every screen; dark mode holds; `npm run check` and `npm run test:ci` pass; React Query Devtools shows the inbox/detail invalidating on accept/reject and the poll stopping at a terminal status. ## 8. Hand off & document (close the phase) - **Docs:** update `client/CLAUDE.md` *Project Structure* (the new customer + nurse route segments, the `services/bookingRequests` domain, the new shared composites — `BookingRequestSummaryCard`, `CountdownTimer`). Fix any doc drift you touch. If you discover/decide a request-flow rule the `product/` docs don't capture (e.g. the exact tracker wording, the re-request UX), note it in [`product/business/05-booking-and-scheduling.md`](../../../product/business/05-booking-and-scheduling.md) (don't invent rules — record decisions) and regenerate the HTML view per `product/CLAUDE.md` if you edited Markdown. - **Contracts:** this phase **consumes** [`../../contracts/domains/booking-requests.md`](../../contracts/domains/booking-requests.md) — derive `services/bookingRequests/types.ts` from it; produce no contract. Append any missing/ambiguous shape to `dev/shared-working-context/frontend/requests/for-backend.md`. - **Handoff & report:** append your phase summary to `dev/shared-working-context/frontend/STATUS.md`; write `dev/shared-working-context/reports/frontend-phase-7-report.md` covering *what was built* (C4/C5/nurse inbox + the domain service), *what is now testable and exactly how* (the steps in §7), *what is mocked* (any contract-gap field behind the `services/bookingRequests` seam + how it swaps to real), *contracts consumed* (booking-requests.md) and any gaps filed, and *follow-ups* for f8/f9 (the `converted` → booking detail handoff, the payment CTA → checkout handoff). Update [`mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md) only if you added a client-side mock seam. - **Memory:** save a `project` memory note for the non-obvious decisions here — the dual-countdown design (server-frozen deadlines, client renders only), the polling-until-terminal pattern for request status, and the two-stage-disclosure boundary in the nurse inbox — with a one-line pointer in `MEMORY.md`.