# 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** (B3–B6, 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.