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,50 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from '../../theme';
jest.mock('next-intl', () => ({ useTranslations: () => (key: string) => key }));
import PatientForm from './PatientForm';
function renderForm() {
const onSubmit = jest.fn();
render(
<ThemeProvider>
<PatientForm submitLabel="Save" onSubmit={onSubmit} />
</ThemeProvider>,
);
return { onSubmit };
}
describe('<PatientForm/> component', () => {
it('blocks submit and flags gender when it is missing', async () => {
const user = userEvent.setup();
const { onSubmit } = renderForm();
await user.type(screen.getByLabelText('name_label'), 'Ali Rezaei');
await user.type(screen.getByLabelText('age_label'), '40');
await user.click(screen.getByText('Save'));
expect(onSubmit).not.toHaveBeenCalled();
expect(screen.getByText('gender_required')).toBeInTheDocument();
});
it('submits the mapped patient input once name, age and gender are set', async () => {
const user = userEvent.setup();
const { onSubmit } = renderForm();
await user.type(screen.getByLabelText('name_label'), 'Ali Rezaei');
await user.type(screen.getByLabelText('age_label'), '40');
await user.click(screen.getByText('gender_male'));
await user.click(screen.getByText('Save'));
expect(onSubmit).toHaveBeenCalledTimes(1);
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
displayName: 'Ali Rezaei',
firstName: 'Ali',
lastName: 'Rezaei',
gender: 'male',
relation: null,
conditions: [],
birthDate: expect.stringMatching(/^\d{4}-01-01$/),
}),
);
});
});