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,97 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useLocale, useTranslations } from 'next-intl';
import { useSnackbar } from 'notistack';
import { Box, Stack, Typography } from '@mui/material';
import { AppButton, PatientForm, RelationSelect, StepperHeader } from '@/components';
import { ROUTES } from '@/constants';
import { useCreatePatient } from '@/services/patients';
import { RELATION_CODES } from '@/services/patients/constants';
import type { CreatePatientInput, Relation } from '@/services/patients/types';
const ONBOARDING_MAX_WIDTH = 520;
/**
* A3 → A4 onboarding wizard: pick who care is for, then register the first patient. The
* chosen relation pre-shapes the patient (it is hidden on the A4 form since it's already
* chosen here). On save it creates the patient and lands on Home (A5).
*/
export default function OnboardingPage() {
const t = useTranslations('onboarding');
const tc = useTranslations('common');
const router = useRouter();
const locale = useLocale();
const { enqueueSnackbar } = useSnackbar();
const createPatient = useCreatePatient();
const [step, setStep] = useState(0);
const [relation, setRelation] = useState<Relation | null>(null);
const relationOptions = RELATION_CODES.map((code) => ({ code, label: t(`relation_${code}`), icon: 'account' }));
const handleCreate = (input: CreatePatientInput) => {
createPatient.mutate(
{ ...input, relation },
{
onSuccess: () => {
enqueueSnackbar(t('saved'), { variant: 'success' });
router.replace(`/${locale}${ROUTES.HOME}`);
},
},
);
};
return (
<Box sx={{ maxWidth: ONBOARDING_MAX_WIDTH, mx: 'auto', display: 'flex', flexDirection: 'column', gap: 3 }}>
<StepperHeader steps={[t('step_relation'), t('step_patient')]} activeStep={step} />
{step === 0 ? (
<Stack sx={{ gap: 2 }}>
<Stack sx={{ gap: 0.5 }}>
<Typography variant="h6" component="h1">
{t('relation_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('relation_subtitle')}
</Typography>
</Stack>
<RelationSelect
options={relationOptions}
value={relation}
onChange={(code) => setRelation(code as Relation)}
/>
<AppButton
color="primary"
variant="contained"
fullWidth
disabled={!relation}
onClick={() => setStep(1)}
sx={{ m: 0 }}
>
{t('continue')}
</AppButton>
</Stack>
) : (
<Stack sx={{ gap: 2 }}>
<Stack sx={{ gap: 0.5 }}>
<Typography variant="h6" component="h1">
{t('patient_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('patient_subtitle')}
</Typography>
</Stack>
<PatientForm
initial={{ relation: relation ?? undefined }}
submitLabel={t('save_continue')}
submitting={createPatient.isPending}
onSubmit={handleCreate}
onCancel={() => setStep(0)}
cancelLabel={tc('back')}
/>
</Stack>
)}
</Box>
);
}