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:
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
import { FunctionComponent } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { AppIconButton } from '@/components/common';
|
||||
import type { Patient } from '@/services/patients/types';
|
||||
|
||||
export interface PatientCardProps {
|
||||
patient: Patient;
|
||||
/** Translated relation label; omitted when the patient has no relation set. */
|
||||
relationLabel?: string;
|
||||
/** Translated gender label. */
|
||||
genderLabel: string;
|
||||
/** Translated age label (e.g. "70 yrs"); omitted when the birth date is unknown. */
|
||||
ageLabel?: string;
|
||||
/** Translated condition labels; empty renders the "no conditions" line. */
|
||||
conditionLabels: string[];
|
||||
noConditionsLabel: string;
|
||||
onEdit: () => void;
|
||||
onArchive: () => void;
|
||||
editLabel: string;
|
||||
archiveLabel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patient summary card for the E1 list — relation + name, age/gender, and condition chips,
|
||||
* with edit and archive actions. All display text is translated by the caller.
|
||||
* @component PatientCard
|
||||
*/
|
||||
const PatientCard: FunctionComponent<PatientCardProps> = ({
|
||||
patient,
|
||||
relationLabel,
|
||||
genderLabel,
|
||||
ageLabel,
|
||||
conditionLabels,
|
||||
noConditionsLabel,
|
||||
onEdit,
|
||||
onArchive,
|
||||
editLabel,
|
||||
archiveLabel,
|
||||
}) => {
|
||||
const meta = [ageLabel, genderLabel].filter(Boolean).join(' · ');
|
||||
|
||||
return (
|
||||
<Paper elevation={0} sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'flex-start', gap: 1 }}>
|
||||
<Stack sx={{ flexGrow: 1, gap: 0.75, minWidth: 0 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
||||
{patient.displayName}
|
||||
</Typography>
|
||||
{relationLabel ? (
|
||||
<Chip size="small" label={relationLabel} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 600 }} />
|
||||
) : null}
|
||||
</Stack>
|
||||
|
||||
{meta ? (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{meta}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
{conditionLabels.length > 0 ? (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, mt: 0.25 }}>
|
||||
{conditionLabels.map((label) => (
|
||||
<Chip key={label} size="small" variant="outlined" label={label} />
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
||||
{noConditionsLabel}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack direction="row" sx={{ flexShrink: 0 }}>
|
||||
<AppIconButton icon="edit" title={editLabel} aria-label={editLabel} size="small" onClick={onEdit} />
|
||||
<AppIconButton icon="archive" title={archiveLabel} aria-label={archiveLabel} size="small" onClick={onArchive} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default PatientCard;
|
||||
Reference in New Issue
Block a user