Files
2026-06-28 21:59:59 +03:30

336 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend Phase 2 — Onboarding & profiles (customer, patient, nurse, bank)
> **Mission:** with auth and roles in place, turn a freshly-logged-in user into a *usable account*.
> A family completes the "who is care for?" onboarding and registers their first **patient** (the
> care recipient, who is not the payer); a nurse bootstraps a public profile and adds the payout
> **bank account** that the verification pipeline and payouts later depend on. This phase implements
> the wireframe onboarding screens (A3, A4, E1) plus the customer profile and the nurse profile /
> bank settings, and stands up the `services/profiles`, `services/patients`, and `services/nurse`
> domains following the f0 data pattern. It is the gate that makes addresses (f3) and booking (f7)
> possible — a booking needs a patient with a known gender, and a payout needs a verified bank account.
>
> **Track:** frontend · **Depends on:** [`frontend-phase-1-b2.md`](./frontend-phase-1-b2.md) (auth/OTP/roles, `AuthContext`) + the **backend-phase-3** contract ([`identity-profiles.md`](../../contracts/domains/identity-profiles.md)) · **Unlocks:** [`frontend-phase-3-b4.md`](./frontend-phase-3-b4.md) (addresses & geo), [`frontend-phase-7-b8.md`](./frontend-phase-7-b8.md) (booking request — needs a patient)
> **Before you start, read [`../_shared/agent-operating-rules.md`](../_shared/agent-operating-rules.md).** It is not optional.
---
## 1. Context — where this sits
The user can now log in by phone-OTP and the app knows their role (f1-b2). What they *can't* yet do is
say **who they are caring for**. Balinyaar's whole model rests on the **customer ≠ patient** split: the
payer (an adult child, a spouse) is almost never the care recipient (an elderly parent, an infant, a
post-surgical adult). This phase captures that split for the customer side and, for the nurse side, the
two pieces of profile state that everything downstream hangs off — a public-facing profile and a payout
destination. No search, no booking, no money yet — just the identity/profile surface those later phases
read from.
**What already exists (do not rebuild):**
- **From [`frontend-phase-0.md`](./frontend-phase-0.md):** the three actor app shells + role-scoped
route groups under `[locale]`; the **customer 5-tab bottom nav** (خانه/Home · رزروها/Bookings ·
بیماران/Patients · کیف‌پول/Wallet · پروفایل/Profile); the `services/{domain}` reference pattern
(`types.ts` / `keys.ts` / `apis/clientApi.ts` / `hooks/use*.ts` / `index.ts`) modelled on
`src/services/auth/*`; the TanStack-Query caching rules (per-domain `keys` factory, deliberate
`staleTime`/`gcTime`, **invalidate on mutation**); the money/format util in `src/utils/`; the
contracts→types pattern; the i18n namespace plan; and the shared composite components — the
**stepper / progress header**, the **status chip** (verified/pending/…), and the **phone-number
field** — which you **reuse here, not re-create**.
- **From [`frontend-phase-1-b2.md`](./frontend-phase-1-b2.md):** phone login (A1), OTP (A2), the
customer↔nurse login switch, the role router, and roles surfaced in `AuthContext` (read the current
user + role from there; don't re-fetch `/me` ad-hoc). The OTP-input and phone-field composites are
already wired — reuse them.
- **From the backend ([`identity-profiles.md`](../../contracts/domains/identity-profiles.md), b3):** the
live endpoints for customer profile, patients CRUD, nurse profile bootstrap, and nurse bank accounts
(with the IBAN-ownership inquiry). **Consume the contract; do not guess shapes.** If a shape you need
is missing or unclear, append it to
[`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
and mock behind the `services/{domain}` seam meanwhile (operating-rules §6).
> **Scope fence:** this phase builds the **B7 *profile* part only** — avatar + short bio. The nurse
> **services-and-prices builder** on B7 (the "+ افزودن خدمت" list) is **(DEFERRED)** to
> [`frontend-phase-4-b5.md`](./frontend-phase-4-b5.md) (catalog & service builder). The nurse
> **available-days picker** on B7 is **(DEFERRED)** with catalog/availability. **Addresses** (the A4/E1
> sibling "address book") are **(DEFERRED)** to [`frontend-phase-3-b4.md`](./frontend-phase-3-b4.md).
> The whole **nurse verification pipeline** (B3B6, identity/Shahkar/license) is
> **(DEFERRED)** to [`frontend-phase-5-b6.md`](./frontend-phase-5-b6.md) — here the nurse only gets an
> *unverified* profile and a bank account; surfacing the "not bookable until verified" banner is part of
> f5, not this phase (a simple placeholder is acceptable).
## 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) — the RSC/client boundary, layouts, i18n, theme,
cookies, the `services/{domain}` fetch pattern, anti-patterns. Re-confirm the f0 *Project Structure*
additions so you place new folders correctly.
- **Invoke the `frontend-designer` skill** before any visual work — it is the design/brand contract
(teal `#1d4a40` / terracotta `#d98c6a` palette, `tokens.css`, typography, the `App*` library, the
layout shells, the hard UI rules). The onboarding stepper, gender toggle, condition chips, patient
cards, empty states, and the bank-status panel are all visual deliverables and **must** go through it.
- **Product — the business rules (the source of truth, not the code):**
- [`../../../product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md)
— the customer/patient split, role-staged KYC, and what is MVP vs DEFERRED (customer national-ID
KYC is deferred; do **not** add it to any form).
- [`../../../product/data-model/01-identity-and-access.md`](../../../product/data-model/01-identity-and-access.md)
— the exact tables and columns behind these screens (`customer_profiles`, `patients`,
`nurse_profiles`, `nurse_bank_accounts`) and their constraints (single-primary bank account;
`iban_hash` uniqueness; `matched_national_id`; the guarded `is_verified`).
- **Product — the visual baseline:**
[`../../../product/wireframes/index.html`](../../../product/wireframes/index.html) — read screens **A3**
(برای چه کسی؟ — 2-step progress bar, single-select), **A4** (ثبت بیمار — name, age, gender toggle مرد/زن,
condition chips), **A5** (Home — the "complete patient record" nudge you land on), **E1** (لیست بیماران —
patient cards + "+ افزودن بیمار", Patients tab active), and the **B7** profile header (photo + short
bio). Match the RTL Persian layout, the brand colours, and the status legend (green = verified, amber =
pending, grey = manual/later).
- **Contracts:** [`../../contracts/domains/identity-profiles.md`](../../contracts/domains/identity-profiles.md)
(the b3 contract you consume — endpoints, request/response shapes, enums, masking, failure cases),
plus the cross-cutting conventions you already follow:
[`api-conventions.md`](../../contracts/conventions/api-conventions.md) (envelope, snake_case routes,
pagination, status codes) and [`money-and-types.md`](../../contracts/conventions/money-and-types.md)
(enums-as-codes, **`gender` = `male`/`female` is load-bearing**, masked PII like last-4 of an IBAN).
- **Code to mirror:** `src/services/auth/*` (the exact service skeleton every new domain copies) and the
three shared composites from f0 (`stepper/progress header`, `status chip`, `phone-number field`) — read
their props before reusing them.
- **The handoff you're handed:**
[`../../shared-working-context/backend/handoff/after-backend-phase-3.md`](../../shared-working-context/backend/handoff/after-backend-phase-3.md)
(what b3 shipped, which endpoints are live, what's mocked behind a seam — e.g. the IBAN-ownership
inquiry).
## 3. Scope — build this
Three new domain services, the customer onboarding flow, patient CRUD, the customer profile, and the
nurse profile + bank settings. Every user-visible string is an i18n key in **both** `en.json` and
`fa.json` (RTL-first); every list is cached and invalidated per the f0 pattern; every screen is built
through the **frontend-designer** skill.
### 3.1 The domain services (copy the f0 pattern)
Three services under `src/services/`, each with `types.ts` (from the b3 contract — never guessed),
`keys.ts` (a query-key factory), `apis/clientApi.ts` (wrapping `clientFetch`; `serverApi.ts` only if an
RSC needs prefetch), `hooks/use*.ts` (one hook per file), and `index.ts`:
- **`services/profiles`** — the customer & nurse *profile* domain.
- `useCustomerProfile()` (`useQuery`) → `GET …/me/customer_profile` (or the b3 route).
- `useUpsertCustomerProfile()` (`useMutation``PUT …/me/customer_profile`) — name, contact,
`default_emergency_contact_name`/`default_emergency_contact_phone`; **invalidates** the customer-profile
query and patches `AuthContext`'s cached `/me` if profile-completion changes.
- `useNurseProfile()` (`useQuery`) → the nurse's own profile.
- `useCreateNurseProfile()` / `useUpdateNurseProfile()` (`useMutation``POST/PUT …/me/nurse_profile`)
— bootstrap (`is_verified` stays `false`, server-owned — never sent by the client), then edit
`bio`/`avatar_url`/`years_experience`. Invalidate the nurse-profile query on success.
- **`services/patients`** — the care-recipient domain (customer-scoped).
- `usePatients()` (`useQuery`, paginated per `api-conventions.md`) → `GET …/patients``{ items, total }`.
- `useCreatePatient()` (`POST …/patients`), `useUpdatePatient()` (`PUT …/patients/{id}`),
`useArchivePatient()` (the archive/soft-delete route — sets `is_active=false`, **not** a hard delete).
- All three mutations **invalidate `patientKeys.list()`** (or `setQueryData` to splice the row) so the
E1 list never refetches needlessly; archive optimistically removes/greys the card then reconciles.
- **`services/nurse`** — the nurse payout **bank account** sub-domain (kept separate from the profile
because verification/payouts read it independently).
- `useNurseBankAccounts()` (`useQuery`) → list (usually one primary).
- `useAddNurseBankAccount()` (`POST …/me/nurse_bank_accounts`) — submit IBAN (Sheba) + account-holder
name; the server kicks off the ownership inquiry (mocked behind `IBankAccountOwnershipVerifier` in
b3) and returns the account in a **pending** state.
- `useSetPrimaryBankAccount()` (where the contract exposes it) — single-primary enforcement is
server-side; reflect it in cache.
- All mutations invalidate the bank-accounts query so the pending→verified/mismatch transition shows
on the next read (poll/refetch — see §3.5).
> Where a b3 endpoint isn't live when you build, ship a **mock `clientApi`** behind the same seam (a
> fixed in-memory patient list; a bank account that flips pending→verified after one refetch; a
> mismatch account for a known test IBAN) and record it in your report + the mock registry, so it swaps
> cleanly once the real endpoint lands.
### 3.2 Customer onboarding — A3 → A4 (the "who is care for" flow)
A two-step wizard, mounted in the customer route group, run once after first login (and re-enterable
from the patient list). **Reuse the f0 stepper/progress header** for the 2-step bar.
- **A3 · برای چه کسی؟ (Who is care for?)** — a single-select radio list of relations: **پدر/مادر**
(parent), **همسر** (spouse), **فرزند** (child), **خودم** (self). Selecting a relation carries forward
to pre-shape A4 (e.g. "خودم" pre-fills the patient as the customer). Primary CTA **ادامه** advances the
stepper; back is allowed. The relation is a stable enum code (`parent`/`spouse`/`child`/`self`), an
i18n-labelled chip — never a hardcoded Persian string in logic.
- **A4 · ثبت بیمار (Add patient)** — the patient form: **full name**, **age** (or birth date per the
contract — map to `birth_date`), **gender toggle (مرد/زن → `male`/`female`)**, and **condition chips**
(multi-select: سالمند/elderly, پس از جراحی/post-surgery, دیابت/diabetes, + بیشتر). On submit it calls
`useCreatePatient()`; the chosen relation is stored with the patient. CTA **ذخیره و ادامه** creates the
patient, invalidates the list, and **routes to Home (A5)** where the "complete patient record" nudge is
already shown.
Validation: name required; **gender required** (it drives same-gender matching downstream — see §5);
age/birth-date validated; conditions optional. Surface 400 field errors from the envelope inline.
### 3.3 Patient list & CRUD — E1
The **Patients** bottom-nav tab. Reuse the f0 cards/empty-state primitives where they exist.
- **E1 · لیست بیماران (Patients list)** — patient cards showing relation + name, age/gender, and condition
chips, with a prominent **+ افزودن بیمار** CTA. States to build: **loading skeleton**, **empty state**
(no patients → the A4 add flow as the prominent CTA), and the populated list. Patients tab active in the
bottom nav.
- **Add / Edit** — the same A4 form, reused for create and edit (`useCreatePatient` / `useUpdatePatient`).
Edit pre-fills from the cached row.
- **Archive** — a confirm dialog ("آرشیو بیمار؟") then `useArchivePatient()`; the card is removed/greyed.
**Never a hard delete** — archive only (`is_active=false`); a patient referenced by a past booking must
survive.
> The full **patient record viewer** (E2 — medications/routine/history/tasks tabs) and **nurse visit
> notes** (E3) are **(DEFERRED)** to [`frontend-phase-13-b14.md`](./frontend-phase-13-b14.md). E1 here is
> the list/CRUD shell only.
### 3.4 Customer profile + emergency contact
A profile screen under the customer **پروفایل/Profile** tab: editable `first_name`/`last_name`,
`preferred_language`, optional `avatar_url`, and the **emergency contact** (`default_emergency_contact_name`
+ `_phone`, the phone via the **reused f0 phone-field**). Saves through `useUpsertCustomerProfile()`,
invalidates the profile query, and reflects profile-completion back into the Home nudge. Do **not** add a
national-ID field — customer KYC is **(DEFERRED)** and the column stays unused at launch.
### 3.5 Nurse profile bootstrap + bank settings (the B7 *profile* part)
Mounted in the nurse route group.
- **Nurse profile bootstrap (B7 header)** — **avatar/profile photo** (upload via the contract's
avatar/object-storage route; if not live, mock behind the seam) + **short bio** (+ `years_experience`
if the contract carries it). `useCreateNurseProfile()` on first entry, then `useUpdateNurseProfile()`.
The nurse profile is created **unverified** (`is_verified=false`, server-owned) and **not bookable**
show a neutral "تکمیل احراز هویت برای فعال‌سازی" placeholder pointing at verification (the real banner is
f5). The **services-and-prices builder** and **available-days picker** on B7 are **(DEFERRED)** to f4.
- **Nurse bank-account settings (payout IBAN)** — an **IBAN (شبا) entry** form (Sheba format validation
client-side: `IR` + 24 digits) + account-holder name, submitted via `useAddNurseBankAccount()`. Render
the three ownership-inquiry states off the contract's status field, each a distinct UI state built with
the **reused f0 status chip**:
- **pending** (`matched_national_id` null / inquiry in flight) — amber "در حال استعلام مالکیت حساب" panel;
poll/refetch (`refetchInterval` while pending, then stop) so the transition appears without a manual
reload.
- **verified** (`matched_national_id=true`, `is_verified=true`) — green "حساب تاییدشد" + the **masked**
IBAN (last 4) per the money-and-types masking rule.
- **mismatch** (`matched_national_id=false`) — a clear, **non-accusatory** error state: "حساب باید به نام
خودتان باشد" with a re-enter CTA. Ownership mismatch is gated server-side; surface it as a friendly
domain error, never a raw 4xx toast.
## 4. Mocks & seams in this phase
This is a **frontend** phase; it owns no backend seam. It **consumes** the b3 contract and, where an
endpoint isn't live yet, mocks **behind the `services/{domain}` seam** (a mock `clientApi`) per
operating-rules §6 — the same pattern f0 established. Specifically you may need to mock:
- **Patients CRUD** — an in-memory list seeded with one patient, supporting create/edit/archive.
- **The IBAN-ownership inquiry result** — the real check is the backend's
`IBankAccountOwnershipVerifier` seam (introduced in **b3**, recorded in the
[mock registry](../../shared-working-context/reports/mocks-registry.md)); on the client just drive the
**pending → verified** transition (and a **mismatch** for a known test IBAN) so the three UI states are
demonstrable end-to-end.
- **Avatar/photo upload** — if the contract's storage route isn't live, accept the file and echo a fake
URL behind the seam.
Record every client-side mock in your phase report and in the mock registry with **how f-later (or the
b3 merge) swaps it out** — the swap must be implementation-only, no call-site changes.
## 5. Critical rules you must not get wrong
- **Customer ≠ patient — never collapse them.** The payer and the care recipient are distinct rows; a
patient is created under the signed-in customer and is **tenancy-scoped server-side**. Don't assume the
logged-in user is the patient (except the explicit "خودم" relation, which still creates a patient row).
- **Patient `gender` is REQUIRED.** It is load-bearing for **same-gender caregiver matching** (a near-hard
requirement) used by search (f6) and booking (f7). The gender toggle (`male`/`female`) must be a required
field — never default it, never let the form submit without it.
- **Tenancy is enforced server-side — surface friendly errors.** A `403`/`404` from acting on someone
else's patient/profile is the fetch layer's concern (it already toasts auth errors); your hooks add only
the **domain-specific** message ("این بیمار در دسترس شما نیست"). Never try to enforce tenancy on the
client or expose another customer's data.
- **No customer national-ID KYC.** It is DEFERRED; the column is unused at launch. Do not add a national-ID
field to the customer profile or gate browsing/booking on it.
- **`is_verified` is server-owned and guarded.** The client **never** sends or sets it; a freshly
bootstrapped nurse profile is unverified and not bookable. Reflect that read-only state; the flip happens
only inside the backend verification transaction (f5).
- **Bank account: three states, money-safe.** Render **pending-verification**, **ownership-mismatch**, and
**verified** distinctly; the IBAN is **masked** (last 4) once stored; one primary account per nurse is
server-enforced. **First payout is gated on `matched_national_id=true`** — never present a mismatched or
pending account as ready to pay. The mismatch copy must be **non-accusatory**.
- **Archive, don't delete.** Patient removal is soft (`is_active=false`) so historical bookings stay intact.
- **Caching is a feature.** Patient/profile/bank queries use deliberate `queryKey`/`staleTime`, and every
create/edit/archive **invalidates** (or `setQueryData`) — never re-fetch data already in cache. Keep the
bank `refetchInterval` only while pending; stop it once resolved. Minimise re-renders (colocate form
state, stable callbacks).
- **RSC/client boundary, RTL, both locales, tokens.** Forms and lists are client components (no
`next-intl/server`/`next/headers` in them); `fa` is default and **RTL** — design RTL-first and verify the
gender toggle, chips, and stepper mirror correctly; every string in **both** `en.json`/`fa.json`; colours
from `tokens.css`; MUI v9 API + the pre-built themes only. **MUI primitives stay MUI**; the stepper /
status-chip / phone-field are the **f0 shared composites — reuse, don't re-implement.** Any genuinely new
shareable composite (e.g. a `PatientCard`, a `GenderToggle`, a `ConditionChips`, a `BankStatusPanel`)
lives at the shared `src/components/…` level with a co-located `*.test.tsx`.
## 6. Definition of Done
The shared [definition-of-done.md](../_shared/definition-of-done.md), plus phase-specific:
- [ ] **A3 → A4** runs end-to-end: a new customer picks a relation, fills the patient form (name, age,
**required** gender, optional conditions), and **lands on Home (A5)** with one patient created.
- [ ] **E1** patient list works: empty state with add CTA; create/edit reuse the A4 form; archive
(soft) with confirm; the list is cached and invalidated on every mutation (no needless refetch).
- [ ] **Customer profile + emergency contact** saves and reflects profile-completion; no national-ID field.
- [ ] **Nurse profile bootstrap** (avatar + bio) creates an unverified, not-bookable profile; the
services builder + availability picker are correctly **deferred** (not stubbed as working).
- [ ] **Nurse bank account** submits an IBAN and shows all three states — **pending → verified** (mock
transition) and **ownership-mismatch** — with a masked IBAN on verify and non-accusatory mismatch copy.
- [ ] `services/profiles`, `services/patients`, `services/nurse` follow the f0 pattern (keys factory,
one-hook-per-file, invalidation); types derive from the b3 contract (or a gap is filed in
`requests/for-backend.md` and mocked behind the seam).
- [ ] New shared composites each have a co-located test; the **f0 stepper/status-chip/phone-field are
reused** (not duplicated).
- [ ] `npm run check` green; `npm run test:ci` green for the shared components added; `en.json`/`fa.json`
in sync; `client/CLAUDE.md` *Project Structure* updated for the new services/route folders.
## 7. How to test (what a human can verify after this phase)
Run `npm run dev` (and the b3 server, or the seam mock).
- **Customer onboarding:** log in as a customer → land on **A3**, the 2-step bar shows step 1; pick a
relation → **A4**; try to submit without gender → blocked with a required-field error; fill it and
submit → you land on **Home (A5)** and the "complete patient record" nudge is present. *Expected:* one
patient exists and the flow doesn't re-trigger on next login.
- **Patient CRUD (E1):** open the **Patients** tab → see the patient as a card (relation, name,
age/gender, condition chips). Add a second patient → it appears without a full reload (cache spliced/
invalidated). Edit it → changes persist. Archive it (confirm) → the card disappears; it is **not**
hard-deleted. Open Patients on a fresh account → the **empty state** with the add CTA. Inspect React
Query Devtools: the list query is cached and mutations invalidate it.
- **Customer profile:** edit name + emergency contact → save → the Home nudge reflects completion. Confirm
there is **no** national-ID field.
- **Nurse profile + bank:** log in as a nurse → bootstrap the profile (set avatar + a short bio) → it
saves and shows an **unverified / not-bookable** state. Open bank settings → enter an IBAN → see the
**pending** "در حال استعلام" panel, then (after the mock resolves / a refetch) the **verified** green
state with a **masked** IBAN. Enter the known **mismatch** test IBAN → see the **ownership-mismatch**
error with re-enter CTA. *Expected:* the three states are visually distinct and the verified account
shows last-4 only.
- **i18n / RTL:** switch locale → strings flip `fa``en` and `dir` flips; the gender toggle, chips, and
stepper mirror correctly. `npm run check` and `npm run test:ci` pass.
## 8. Hand off & document (close the phase)
- **Docs:** update the **Project Structure** tree in [`client/CLAUDE.md`](../../../client/CLAUDE.md) for
the new `services/profiles`, `services/patients`, `services/nurse` domains, the new shared composites
(`PatientCard`, `GenderToggle`, `ConditionChips`, `BankStatusPanel`), and any new route segments under
the customer/nurse groups. If you discover/confirm a business rule the product docs don't capture
(e.g. a relation-enum decision), record it in
[`../../../product/business/01-actors-and-onboarding.md`](../../../product/business/01-actors-and-onboarding.md)
— don't invent rules. Note any reusable pattern in `client/CLAUDE.md`.
- **Contract:** **consume** [`../../contracts/domains/identity-profiles.md`](../../contracts/domains/identity-profiles.md)
(b3) as the type source — do not guess shapes. Any gap (a missing field, an unclear enum, the
bank-status field, the avatar route) goes to
[`../../shared-working-context/frontend/requests/for-backend.md`](../../shared-working-context/frontend/requests/for-backend.md)
(append only — never edit backend files); mock behind the `services/{domain}` seam until b3 delivers it.
- **Handoff & report:** append your phase summary to
[`../../shared-working-context/frontend/STATUS.md`](../../shared-working-context/frontend/STATUS.md);
write [`../../shared-working-context/reports/frontend-phase-2-report.md`](../../shared-working-context/reports/README.md)
covering what was built, **what is now testable and exactly how** (the A3→A4→Home flow, patient CRUD,
the bank state transitions), what is **mocked client-side** (patients list, IBAN-inquiry transition,
avatar upload) and exactly how each swaps to the real b3 endpoint, and follow-ups for f3 (addresses
reuse this profile shell), f4 (the nurse services builder slots onto the B7 profile), and f5
(verification banner replaces the placeholder). Add/extend rows in
[`../../shared-working-context/reports/mocks-registry.md`](../../shared-working-context/reports/mocks-registry.md)
for every client-side mock.
- **Memory:** save a `project` memory note for the non-obvious decisions this phase fixes (the
relation-enum + customer/patient split on the client, the three bank-account UI states and the
`matched_national_id` gating, the patients caching/invalidation strategy), with a one-line pointer in
`MEMORY.md`. Don't record what the code/contract already make obvious.