ui phase 3
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
import type { ReactNode } from 'react';
|
||||
import { FocusedLayout } from '@/layout';
|
||||
import { RoleGuard } from '@/components/auth';
|
||||
import { APP_ROLES } from '@/constants';
|
||||
|
||||
/*
|
||||
* Customer-focused route group — a chrome-free counterpart to `(customer)` for flows the user
|
||||
* should not tab away from mid-task (today only first-run onboarding, ui-phase-3 §3.5). A route
|
||||
* group adds chrome without adding a URL segment, so `/onboarding` is unchanged. RoleGuard still
|
||||
* gates it on a resolved customer role, identically to the full `(customer)` shell.
|
||||
*/
|
||||
export default function CustomerFocusedRouteLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<RoleGuard expected={APP_ROLES.CUSTOMER}>
|
||||
<FocusedLayout>{children}</FocusedLayout>
|
||||
</RoleGuard>
|
||||
);
|
||||
}
|
||||
+52
-11
@@ -4,7 +4,8 @@ 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 { 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';
|
||||
@@ -12,12 +13,26 @@ 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';
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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 OnboardingPage() {
|
||||
export default function OnboardingScreen() {
|
||||
const t = useTranslations('onboarding');
|
||||
const tc = useTranslations('common');
|
||||
const router = useRouter();
|
||||
@@ -25,10 +40,14 @@ export default function OnboardingPage() {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const createPatient = useCreatePatient();
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
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: 'account' }));
|
||||
const relationOptions = RELATION_CODES.map((code) => ({
|
||||
code,
|
||||
label: t(`relation_${code}`),
|
||||
icon: RELATION_ICONS[code] ?? 'account',
|
||||
}));
|
||||
|
||||
const handleCreate = (input: CreatePatientInput) => {
|
||||
createPatient.mutate(
|
||||
@@ -42,11 +61,32 @@ export default function OnboardingPage() {
|
||||
);
|
||||
};
|
||||
|
||||
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={step} />
|
||||
<StepperHeader steps={[t('step_relation'), t('step_patient')]} activeStep={activeStep} />
|
||||
|
||||
{step === 0 ? (
|
||||
{phase === 'relation' ? (
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Typography variant="h6" component="h1">
|
||||
@@ -66,7 +106,8 @@ export default function OnboardingPage() {
|
||||
variant="contained"
|
||||
fullWidth
|
||||
disabled={!relation}
|
||||
onClick={() => setStep(1)}
|
||||
onClick={() => setPhase('patient')}
|
||||
endIcon={<AppIcon icon="forward" size={18} aria-hidden="true" />}
|
||||
>
|
||||
{t('continue')}
|
||||
</AppButton>
|
||||
@@ -86,7 +127,7 @@ export default function OnboardingPage() {
|
||||
submitLabel={t('save_continue')}
|
||||
submitting={createPatient.isPending}
|
||||
onSubmit={handleCreate}
|
||||
onCancel={() => setStep(0)}
|
||||
onCancel={() => setPhase('relation')}
|
||||
cancelLabel={tc('back')}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import OnboardingScreen from './OnboardingScreen';
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'onboarding' });
|
||||
return { title: t('welcome_title') };
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return <OnboardingScreen />;
|
||||
}
|
||||
Reference in New Issue
Block a user