frontend phase 2: onboarding & profiles — customer/patient, nurse profile & bank

Turns a logged-in user into a usable account, consuming the b3 identity-profiles
contract behind the services/{domain} seam.

Services (mock default true; real HTTP clients wired for a one-line flip):
- services/patients: rewritten to b3 PatientDto + client-augmented relation/conditions;
  full CRUD, optimistic soft-archive, cache-splice on create, age<->birthDate helper.
- services/profiles: customer + nurse profile get/upsert + avatar (404->null mapping).
- services/nurse: payout bank accounts + IBAN(Sheba) util + pending-only polling.

Screens: A3->A4 onboarding wizard, E1 patients list/CRUD, A5 home (first-login gate +
nudge), customer profile (no national-ID), nurse profile bootstrap (unverified
placeholder), nurse bank settings (pending/verified/mismatch + make-primary).

Shared composites (each tested): GenderToggle, ConditionChips, RelationSelect,
PatientForm, PatientCard, BankStatusPanel; reuses f0 StepperHeader/StatusChip/PhoneField.
Adds onboarding/home/profile/nurseProfile/bank i18n namespaces (both locales, in sync),
the --bal-primary-soft token, and nurse sidebar Profile + Bank entries.

Contract gaps filed: REQ-005 (patient relation/conditions), REQ-006 (avatar route),
REQ-007 (customer name/language). Gate: check + 112 tests + build all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 22:04:38 +03:30
parent 82561c4cc6
commit 4b4243c451
70 changed files with 3111 additions and 190 deletions
@@ -0,0 +1,82 @@
# Frontend phase 2 (f2-b3) — Onboarding & profiles — report
**Track:** frontend · **Consumes:** [`dev/contracts/domains/identity-profiles.md`](../../contracts/domains/identity-profiles.md) (backend-phase-3) · **Date:** 2026-07-02
## What was built
### Domain services (f0 `services/{domain}` pattern: types ← contract, keys factory, apis[client+mock+seam], one-hook-per-file, hooks-only barrel)
- **`services/patients`** — rewritten from the f0 reference stub to the b3 `PatientDto`. Full CRUD seam
(`list`/`get`/`create`/`update`/`archive`) + real `patientsClientApi` (action routes `patients/{list,get,create,
update,archive}`) + `patientsMockApi`. Hooks: `usePatients` (staleTime), `useCreatePatient`, `useUpdatePatient`
(both invalidate lists), `useArchivePatient` (**optimistic** remove + rollback + settle-invalidate). Helpers
`age.ts` (age↔birthDate) and the client-augmented `relation`/`conditions` (REQ-005, mock-persisted).
- **`services/profiles`** — customer + nurse profile. Seam `getCustomerProfile`/`upsertCustomerProfile`/
`getNurseProfile`/`upsertNurseProfile`/`uploadAvatar`; real client maps 404→`null` (no profile yet). Hooks
`useCustomerProfile`/`useNurseProfile` (queries) and `useUpsertCustomerProfile`/`useUpsertNurseProfile`
(setQueryData + invalidate `/me` for profile-completion) and `useUploadAvatar`.
- **`services/nurse`** — payout bank accounts (kept separate). Seam `list`/`add`/`setPrimary`/`verifyOwnership`;
`iban.ts` (Sheba `IR`+24 validate/normalize + bank-name from the 3-digit code). Hooks `useNurseBankAccounts`
(poll via `refetchInterval` **only while any account is pending**), `useAddNurseBankAccount`,
`useSetPrimaryBankAccount`. `deriveBankStatus` maps `matchedNationalId` (null→pending, false→mismatch,
true→verified).
### Shared composites (`src/components/…`, each with a co-located `*.test.tsx`)
`GenderToggle` (required male/female, never defaulted, can't deselect), `ConditionChips` (multi-select codes),
`RelationSelect` (radio cards), `PatientForm` (the A4 form — name/age/gender/conditions/relation, reused
create+edit), `PatientCard` (E1 card), `BankStatusPanel` (the three ownership states, masked IBAN, non-accusatory
mismatch). Reused the f0 `StepperHeader`/`StatusChip`/`PhoneNumberField` (not re-implemented). Added `--bal-primary-soft`
token (both schemes) and 5 AppIcon registry names (edit/archive/bank/camera/warning).
### Screens
- **A3→A4 onboarding** (`(customer)/onboarding/page.tsx`) — 2-step stepper: relation → patient form; creates the
first patient and lands on Home.
- **A5 Home** (`(customer)/page.tsx`, now a client component) — first-login gate: a customer with 0 patients is
redirected into onboarding (waits for a settled list so a post-create refetch never bounces back); otherwise the
"complete patient record" nudge (+ a profile nudge until `hasCustomerProfile`).
- **E1 patients** (`(customer)/patients/page.tsx`) — cached list, skeleton + empty states, add/edit dialog (A4 form),
soft-archive with confirm.
- **Customer profile** (`(customer)/profile/page.tsx`) — name + preferred language + emergency contact (reused
phone field). **No national-ID field.**
- **Nurse profile** (`nurse/profile/page.tsx`) — avatar upload + bio + years; unverified/not-bookable placeholder
→ verification; services/availability correctly deferred (a caption, not a stub).
- **Nurse bank** (`nurse/bank/page.tsx`) — IBAN + holder form; renders each account via `BankStatusPanel` in its
pending/verified/mismatch state; mismatch offers re-enter.
- **NurseLayout** sidebar gains Profile + Bank.
## What is now testable and exactly how (`npm run dev`, mocks default on — no backend needed)
- **Onboarding:** log in as customer → land on **A3** (step 1) → pick a relation → **A4**; submit **without gender**
→ blocked + "gender required"; fill it + submit → lands on **Home (A5)** with the nudge; the flow doesn't
re-trigger (a patient now exists).
- **Patients (E1):** Patients tab shows the new patient card (relation/name/age·gender/conditions). "+ Add patient"
→ dialog (A4 form) → appears without a full reload (invalidate). Edit → persists. Archive (confirm) → card
disappears (soft, `isActive=false`), not hard-deleted. Fresh session → empty state with add CTA. React Query
Devtools shows the list cached + invalidated on mutation.
- **Customer profile:** edit name + emergency contact → save → Home profile nudge clears. No national-ID field.
- **Nurse profile + bank:** log in as nurse → bootstrap profile (avatar + bio) → saves, shows **unverified /
not-bookable**. Bank settings → enter an IBAN → **pending** "در حال استعلام" panel → (after ~1 poll) **verified**
green with **masked** IBAN (last-4). Enter `IR000000000000000000000000`**mismatch** with re-enter CTA.
- **i18n / RTL:** toggle locale → strings flip fa↔en, `dir` flips, gender toggle / chips / stepper mirror.
## What is mocked client-side + how to make it real
All three services default to `USE_*_MOCK = true` (real HTTP clients fully wired for a one-line flip). See the
[mock registry](./mocks-registry.md) rows for `PatientsApi`, `ProfilesApi`, `NurseBankAccountsApi`. The b3
endpoints are live; the mocks stay on because of the three filed gaps:
- **REQ-005** — `PatientDto.relation` + `conditions` (client-augmented meanwhile).
- **REQ-006** — avatar/object-storage upload route + `avatarUrl` (real `uploadAvatar` throws `501`).
- **REQ-007** — customer `firstName`/`lastName`/`preferredLanguage` home (name is `/me`-only, read-only today).
Once each lands, flip the corresponding flag — no hook/component/call-site change.
## Contracts consumed
`identity-profiles.md` (b3) as the type source: `NurseProfileDto`, `CustomerProfileDto`, `PatientDto`,
`NurseBankAccountDto`, the enums (`gender` load-bearing), IBAN masking (last-4), guarded `isVerified`, tenancy-404.
Gaps filed in `requests/for-backend.md` (REQ-005/006/007) — no shapes guessed; augmented fields are clearly marked.
## Follow-ups for later phases
- **f3 (addresses & geo):** reuse this profile shell + the `services/{domain}` pattern; the A4/E1 sibling address
book slots in.
- **f4 (catalog & service builder):** the nurse **services-and-prices** builder and **available-days** picker slot
onto the B7 profile (both deferred here; a caption marks them).
- **f5 (verification):** replaces the neutral unverified placeholder on the nurse profile with the real
"not bookable until verified" banner; flips `isVerified` inside the backend transaction.
- **f7 (booking):** consumes the patient (needs a known `gender`) created here.
- When REQ-005/006/007 land, flip the three mock flags.