27 KiB
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 (discovery: search/results/nurse profile) · frontend-phase-3-b4 (addresses & map picker) · backend b8 contract (booking-requests.md) · Unlocks: frontend-phase-8-b9 (booking detail · sessions · EVV) Before you start, read
../_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): 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 insrc/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): 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. Patientcustomer_addressesalready carry coordinates from f3's geocode. - f2 onboarding (frontend-phase-2-b3): 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): a nurse's service variants
(
nurse_service_variants— name, price unitper_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): 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
bookingsrow in this phase. The "pay & confirm" step (C6 summary, escrow notice, card/BNPL) is (DEFERRED → f9). Booking detail, sessions, and EVV are (DEFERRED → f8). 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.mdand../_shared/frontend-conventions-checklist.md.client/CLAUDE.mdin full — the RSC/client boundary, layouts (never above[locale]), i18n, theme/tokens, cookies, theservices/{domain}fetch pattern, anti-patterns. Mirror theauthservice exactly when you createservices/bookingRequests.- Invoke the
frontend-designerskill — 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, theApp*library, the layout shells, and the hard RTL/dark-mode rules. Do not hand-pick colours insx. 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— 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(from backend-phase-8) — the exact request/response shapes, routes, status codes, and enums. Plus the shared conventionsapi-conventions.mdandmoney-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 keybookingRequestKeys.nurseInbox(filter).apis/clientApi.ts— wrapclientFetchfor 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,statusfilter.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, defaultstatus=pending_nurse_response.POST .../booking_requests/accept_booking_request→ nurse accept.POST .../booking_requests/reject_booking_request→ nurse reject (withnurse_rejection_reason).
hooks/— one hook per file:useCreateBookingRequest.ts(useMutation) — on success, navigate to C5 with the new id andsetQueryData/invalidate the customer list.useBookingRequest.ts(useQuery) — the C5 detail; polls while status is non-terminal (refetchInterval~ every 15–30s whilepending_nurse_response/accepted_awaiting_payment, and stops on a terminal/convertedstatus via aselect/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.mdand mock that field behind theservices/bookingRequestsclientApi 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)/<customer-segment>/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 selectedpatient_idis 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 theper_*code, not a hardcoded label). Sendsnurse_service_variant_id. - Address block — reuse the f3 address picker / map block (منزل/home), selecting a
customer_address_idfrom the address book (with the map preview). Do not rebuild the picker. - Date + time pickers — produce a single UTC
scheduled_start_aton the wire; display Shamsi via the f0 date util. (Recurring/multi-session scheduling UI is (DEFERRED → later) — one start time here;session_countis 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)). - CTA: ارسال درخواست — fires
useCreateBookingRequest; loading state while the server computes/freezes the deadline; on success navigate to C5. Surface domain400s (e.g. tenancy: patient/address not owned; same-gender mismatch; variant not bookable) as field/form errors — but do not toast401/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:
- درخواست ثبت شد (done as soon as the request exists),
- در انتظار تایید پرستار (active while
pending_nurse_response), - پرداخت و تایید نهایی (future; becomes active on
accepted_awaiting_payment).
- Countdown — a
CountdownTimershared composite (src/components/..., co-located test) ticking down tonurse_response_deadline_at(computed from the server-supplied UTC instant vsDate.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 topayment_deadline_at, and a CTA "ادامه پرداخت ←" that navigates to checkout (the checkout screen itself is f9 — wire the route, stub the destination if f9 isn't merged).rejected_by_nurse→ terminal state card with thenurse_rejection_reasonand 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; stub if not merged).
3.4 Nurse request inbox + detail (nurse)
Under the nurse shell (the wireframe's "نمای پرستار"), e.g. (private-routes)/<nurse-segment>/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'snurse_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_notesas the clinical context. It must never renderbooking_care_instructionsor 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 toaccepted_awaiting_payment, apayment_deadline_atis 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 capturingnurse_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/400for a stale accept/reject — surface it gracefully, then refetch).
- Accept (
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.mdshows b8 isn't merged, or booking-requests.md is missing a shape, build a mockclientApibehind theservices/bookingRequestsseam (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 inmocks-registry.mdand your report; swapping to the realclientApimust be a one-file change. File any contract gap indev/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/catalogor f4's name) — reuse, do not redefine.
5. Critical rules you must not get wrong
- Two-stage clinical disclosure. The nurse sees only
customer_notesbefore accepting — never any encryptedbooking_care_instructionsor 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. required_caregiver_genderis 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 mismatch400clearly.- No money, no booking row here. This is the request phase. Do not render a price-breakdown/escrow/pay
step (that's C6 / f9) 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) againstDate.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/
convertedstatus 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,
selectfor slices. - RTL + both locales + tokens + MUI primitives.
fadefault & RTL; every string inen.jsonandfa.jsonin sync; colours fromtokens.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, plus:
services/bookingRequestsexists (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
400s 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/
convertedstatus; no needless refetch; the ticking countdown doesn't re-render the whole page. booking/bookingRequestsi18n keys added to both locales in sync; colours from tokens; RTL verified.npm run checkgreen;npm run test:cigreen for the new shared composites (summary card, countdown timer) and any touched shared component.client/CLAUDE.mdProject Structure updated for the new route segments + theservices/bookingRequestsdomain 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:
- 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.
- 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. - 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).
- 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.
- 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.
- Quality: locale switch flips
dir+ strings on every screen; dark mode holds;npm run checkandnpm run test:cipass; 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.mdProject Structure (the new customer + nurse route segments, theservices/bookingRequestsdomain, the new shared composites —BookingRequestSummaryCard,CountdownTimer). Fix any doc drift you touch. If you discover/decide a request-flow rule theproduct/docs don't capture (e.g. the exact tracker wording, the re-request UX), note it inproduct/business/05-booking-and-scheduling.md(don't invent rules — record decisions) and regenerate the HTML view perproduct/CLAUDE.mdif you edited Markdown. - Contracts: this phase consumes
../../contracts/domains/booking-requests.md— deriveservices/bookingRequests/types.tsfrom it; produce no contract. Append any missing/ambiguous shape todev/shared-working-context/frontend/requests/for-backend.md. - Handoff & report: append your phase summary to
dev/shared-working-context/frontend/STATUS.md; writedev/shared-working-context/reports/frontend-phase-7-report.mdcovering 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 theservices/bookingRequestsseam + how it swaps to real), contracts consumed (booking-requests.md) and any gaps filed, and follow-ups for f8/f9 (theconverted→ booking detail handoff, the payment CTA → checkout handoff). Updatemocks-registry.mdonly if you added a client-side mock seam. - Memory: save a
projectmemory 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 inMEMORY.md.