Files
baya-monorepo/dev/phases/frontend/frontend-phase-7-b8.md
T
2026-06-28 21:59:59 +03:30

27 KiB
Raw Blame History

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 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): 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): 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 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): 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). 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.md and ../_shared/frontend-conventions-checklist.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 — 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 conventions api-conventions.md and 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 1530s 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)/<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 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 blockreuse 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)).
  • CTA: ارسال درخواست — fires useCreateBookingRequest; loading state while the server computes/freezes the deadline; on success navigate to C5. Surface domain 400s (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 — 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; 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'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 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.
  • 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) 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, 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 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/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 (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 — 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 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.