ui phase 3
This commit is contained in:
+137
@@ -0,0 +1,137 @@
|
||||
'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, AppIcon, PatientForm, RelationSelect, StepperHeader } from '@/components';
|
||||
import BrandMark from '@/components/auth/BrandMark';
|
||||
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;
|
||||
|
||||
// Distinct per-option glyph (the prior defect: all four relations shared the generic 'account'
|
||||
// icon) — 'elderly' fits a parent, 'favorite' a spouse, 'infant' a child, 'account' one's self.
|
||||
const RELATION_ICONS: Record<string, string> = {
|
||||
parent: 'elderly',
|
||||
spouse: 'favorite',
|
||||
child: 'infant',
|
||||
self: 'account',
|
||||
};
|
||||
|
||||
type Phase = 'welcome' | 'relation' | 'patient';
|
||||
|
||||
/**
|
||||
* The chrome-free A3 → A4 first-run journey: a one-screen welcome moment, then pick who care is
|
||||
* for, then register the first patient. `FocusedLayout` (the route group above this) strips the
|
||||
* bottom nav/bell so there's nothing to tab away to mid-setup. The welcome screen doesn't count
|
||||
* as a stepper step; relation → patient does. The chosen relation pre-shapes the patient (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 OnboardingScreen() {
|
||||
const t = useTranslations('onboarding');
|
||||
const tc = useTranslations('common');
|
||||
const router = useRouter();
|
||||
const locale = useLocale();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const createPatient = useCreatePatient();
|
||||
|
||||
const [phase, setPhase] = useState<Phase>('welcome');
|
||||
const [relation, setRelation] = useState<Relation | null>(null);
|
||||
|
||||
const relationOptions = RELATION_CODES.map((code) => ({
|
||||
code,
|
||||
label: t(`relation_${code}`),
|
||||
icon: RELATION_ICONS[code] ?? 'account',
|
||||
}));
|
||||
|
||||
const handleCreate = (input: CreatePatientInput) => {
|
||||
createPatient.mutate(
|
||||
{ ...input, relation },
|
||||
{
|
||||
onSuccess: () => {
|
||||
enqueueSnackbar(t('saved'), { variant: 'success' });
|
||||
router.replace(`/${locale}${ROUTES.HOME}`);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (phase === 'welcome') {
|
||||
return (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center', minHeight: '70vh', gap: 3, textAlign: 'center' }}>
|
||||
<BrandMark />
|
||||
<Stack sx={{ gap: 1, maxWidth: ONBOARDING_MAX_WIDTH }}>
|
||||
<Typography variant="h5" component="h1">
|
||||
{t('welcome_title')}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{t('welcome_subtitle')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<AppButton color="primary" variant="contained" onClick={() => setPhase('relation')}>
|
||||
{t('welcome_cta')}
|
||||
</AppButton>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const activeStep = phase === 'relation' ? 0 : 1;
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: ONBOARDING_MAX_WIDTH, mx: 'auto', display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<StepperHeader steps={[t('step_relation'), t('step_patient')]} activeStep={activeStep} />
|
||||
|
||||
{phase === 'relation' ? (
|
||||
<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={() => setPhase('patient')}
|
||||
endIcon={<AppIcon icon="forward" size={18} aria-hidden="true" />}
|
||||
>
|
||||
{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={() => setPhase('relation')}
|
||||
cancelLabel={tc('back')}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user