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:
@@ -12,6 +12,33 @@ for awareness.
|
||||
- **Requests filed:** frontend/requests/for-backend.md (yes/no)
|
||||
-->
|
||||
|
||||
## frontend-phase-2-b3 — Onboarding & profiles (customer, patient, nurse, bank) — 2026-07-02
|
||||
- **Shipped:** three domain services — `services/patients` (rewritten to the b3 `PatientDto` + client-augmented
|
||||
`relation`/`conditions`; full CRUD seam + mock + real client; `usePatients`/`useCreatePatient`/
|
||||
`useUpdatePatient`/`useArchivePatient` with optimistic soft-archive), `services/profiles` (customer + nurse
|
||||
profile get/upsert + avatar; `useCustomerProfile`/`useUpsertCustomerProfile`/`useNurseProfile`/
|
||||
`useUpsertNurseProfile`/`useUploadAvatar`), `services/nurse` (bank accounts; `useNurseBankAccounts` with
|
||||
pending-only `refetchInterval`, `useAddNurseBankAccount`, `useSetPrimaryBankAccount`; `iban.ts` Sheba
|
||||
validate/normalize/bank-name). Screens: **A3→A4 onboarding wizard** (`(customer)/onboarding`), **E1 patients
|
||||
list/CRUD** (add/edit dialog reusing the A4 form, soft-archive confirm, empty + skeleton states), **A5 Home**
|
||||
(first-login redirect into onboarding when 0 patients + "complete patient record" nudge), **customer profile**
|
||||
(name + preferred language + emergency contact, no national-ID), **nurse profile bootstrap** (avatar + bio +
|
||||
years, unverified "not bookable" placeholder → verification), **nurse bank settings** (IBAN form + the three
|
||||
ownership states pending/verified/mismatch). Shared composites `GenderToggle`/`ConditionChips`/`RelationSelect`/
|
||||
`PatientForm`/`PatientCard`/`BankStatusPanel` (each tested); reused the f0 `StepperHeader`/`StatusChip`/
|
||||
`PhoneNumberField`. Added `onboarding`/`home`/`profile`/`nurseProfile`/`bank` i18n namespaces + `patients`
|
||||
extensions (both locales); `--bal-primary-soft` token (both schemes); nurse sidebar gains Profile + Bank.
|
||||
- **Consumes:** dev/contracts/domains/identity-profiles.md (backend-phase-3). Routes `api/v1/{customer_profiles,
|
||||
nurse_profiles}/{me,upsert}`, `api/v1/patients/{list,get,create,update,archive}`, `api/v1/nurse_bank_accounts/
|
||||
{list,add,set_primary,verify_ownership}`.
|
||||
- **Mocked client-side:** `services/patients` (`USE_PATIENTS_MOCK`), `services/profiles` (`USE_PROFILES_MOCK`),
|
||||
`services/nurse` (`USE_NURSE_BANK_MOCK`) — all default `true`; real clients wired for a one-line flip. See
|
||||
mocks-registry + the report for exactly what/why (relation/conditions, avatar, customer name/language gaps).
|
||||
- **Gate:** npm run check green · npm run test:ci green (112 tests, +17) · npm run build green with
|
||||
NEXT_PUBLIC_API_URL set (routes /onboarding, /nurse/profile, /nurse/bank generated).
|
||||
- **Requests filed:** frontend/requests/for-backend.md — yes (REQ-005 patient relation/conditions, REQ-006 avatar
|
||||
upload route, REQ-007 customer name/preferred-language).
|
||||
|
||||
## frontend-phase-1-b2 — Auth: phone-OTP login & role routing — 2026-07-02
|
||||
- **Shipped:** `services/auth` rewritten for phone-OTP (types/keys/apis[client+mock+seam]/hooks:
|
||||
`useRequestOtp`/`useVerifyOtp`/`useMe`/`useRefresh`/`useLogout`/`useSelectRole`/`useSessionRoleSync`) —
|
||||
|
||||
@@ -52,6 +52,39 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
- **Proposed shape:** `{ isSuccess: false, statusCode: 400, message: "…", code: "otp_locked", data: { retryAfterSeconds: 60 } }`
|
||||
- **Status:** open
|
||||
|
||||
## REQ-005 — Patient `relation` + `conditions` fields — filed by frontend-phase-2-b3 — 2026-07-02
|
||||
- **Need:** Add two fields to `PatientDto` and the `patients/create` + `patients/update` bodies:
|
||||
1. `relation` (`parent`|`spouse`|`child`|`self`, nullable) — the care-recipient's relation to the payer.
|
||||
2. `conditions` (string[] of stable codes, e.g. `elderly`/`post_surgery`/`diabetes`/`mobility`/`dementia`).
|
||||
- **Why:** The A3 onboarding step captures the relation, and the A4 form + E1 patient cards show condition
|
||||
chips. Neither field exists on the wire `PatientDto` (only `initialMedicalNotes` free-text). The client
|
||||
currently augments them behind the `services/patients` seam (the mock persists them; `USE_PATIENTS_MOCK=true`)
|
||||
and drops them on the real path. Adding the columns lets the client flip the flag to the live endpoints.
|
||||
- **Proposed shape:** `PatientDto { …, relation: string|null, conditions: string[] }`; same fields accepted on
|
||||
create/update. Enum for `relation`; `conditions` a stable code list (could also be a normalized child table).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-006 — Avatar / object-storage upload route (nurse & customer) — filed by frontend-phase-2-b3 — 2026-07-02
|
||||
- **Need:** A multipart image-upload endpoint backed by `IObjectStorage` that returns a stored URL, plus an
|
||||
`avatarUrl` field on `NurseProfileDto` (and later `CustomerProfileDto`). e.g. `POST api/v1/nurse_profiles/avatar`
|
||||
(multipart/form-data) → `{ url }`, and persist `avatar_url` on the profile.
|
||||
- **Why:** The B7 nurse profile bootstrap and the customer profile both take a profile photo. The b3 contract
|
||||
has no avatar field or upload route, and the client fetch layer is JSON-only (can't send multipart). The
|
||||
client mocks this behind the `services/profiles` seam (`uploadAvatar` returns an object URL). The real
|
||||
`profilesClientApi.uploadAvatar` throws `501` until this lands.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-007 — Customer name + preferred-language update — filed by frontend-phase-2-b3 — 2026-07-02
|
||||
- **Need:** Either add `firstName`/`lastName`/`preferredLanguage` to the `customer_profiles/upsert` body +
|
||||
`CustomerProfileDto`, or confirm the customer name is only ever set elsewhere (and how). `MeResult` exposes
|
||||
`firstName`/`lastName` read-only with no update endpoint; `CustomerProfileDto` carries only the emergency
|
||||
contact.
|
||||
- **Why:** The customer profile screen edits first/last name + preferred language alongside the emergency
|
||||
contact. Absent a wire field/endpoint, the client augments name/language behind the `services/profiles` seam
|
||||
(mock-persisted; the real upsert sends only the emergency contact). Confirm the intended home for these so the
|
||||
client stops augmenting.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-004 — Confirm multi-role disambiguation (activeRole?) — filed by frontend-phase-1-b2 — 2026-07-02
|
||||
- **Need:** Confirm whether `MeResult` will gain an `activeRole` (the user's currently-selected actor) for a
|
||||
user who holds **both** `customer` and `nurse`, or whether the client should keep owning that choice.
|
||||
|
||||
@@ -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.
|
||||
@@ -44,5 +44,7 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
|
||||
| Seam (interface) | File | What it fakes | Config flag | Make it real → | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient list/create | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`) | Publish `/patients` endpoints, set flag `false` | 🟡 |
|
||||
| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient CRUD (list/get/create/update/soft-archive), **seeded empty** so onboarding + the empty state both demo; persists the client-augmented `relation`/`conditions` the wire `PatientDto` lacks (REQ-005) | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`, default `true`) | Deliver REQ-005 (relation/conditions on `PatientDto` + create/update), then set flag `false` — `patientsClientApi` is already wired to the b3 `patients/*` routes | 🟡 |
|
||||
| `ProfilesApi` | `client/src/services/profiles/apis/mockApi.ts` | Customer + nurse profile get/upsert and **avatar upload** (echoes an object-URL). Keeps guarded read-only fields (`isVerified=false`, zero aggregates). Augments customer name/language (REQ-007) + nurse `avatarUrl` (REQ-006) the wire DTOs lack | `USE_PROFILES_MOCK` (`services/profiles/constants.ts`, default `true`) | b3 `customer_profiles/*` + `nurse_profiles/*` are live; deliver REQ-006 (avatar route/field) + REQ-007 (customer name/language) then set flag `false` — `profilesClientApi` is wired (its `uploadAvatar` throws `501` until REQ-006) | 🟡 |
|
||||
| `NurseBankAccountsApi` | `client/src/services/nurse/apis/mockApi.ts` | Bank-account list/add/set-primary/verify-ownership. Drives the استعلام شبا **pending→verified/mismatch** transition over 2 list reads (so the poll shows it), single-primary enforcement, masked-IBAN (last-4); the configured mismatch IBAN (`IR000000000000000000000000`, matches backend default) resolves to `matchedNationalId=false` | `USE_NURSE_BANK_MOCK` (`services/nurse/constants.ts`, default `true`) | b3 `nurse_bank_accounts/*` are live (the real `add` resolves the inquiry synchronously — no client poll needed); set flag `false` — `nurseBankClientApi` is wired | 🟡 |
|
||||
| `AuthApi` | `client/src/services/auth/apis/mockApi.ts` (`authMockApi`) | Phone-OTP login offline: `requestOtp`→`{otpSent,resendAvailableInSeconds:120}`; `verifyOtp` accepts dev code **`123456`** and locks after 3 wrong tries (`otp_locked`); `getMe`/`selectRole`/`refresh` from a `MOCK_SCENARIO` toggle (`customer`/`nurse_unverified`/`no_role`) to exercise all router branches | `USE_AUTH_MOCK` (`services/auth/constants.ts`, default **false** — b2 is live) + `MOCK_SCENARIO` in `mockApi.ts` | The real `authClientApi` is already wired to the live b2 routes; set `USE_AUTH_MOCK = false` (already the default) — no hook/screen change | 🟢 real by default, 🟡 mock available |
|
||||
|
||||
Reference in New Issue
Block a user