146 lines
5.5 KiB
TypeScript
146 lines
5.5 KiB
TypeScript
'use client';
|
|
import { FunctionComponent, useState } from 'react';
|
|
import { Box, CircularProgress, Paper, Stack, Typography } from '@mui/material';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useLocale, useTranslations } from 'next-intl';
|
|
|
|
import AppIcon from '@/components/common/AppIcon';
|
|
import { AppButton } from '@/components';
|
|
import { APP_ROLES, type AppRole } from '@/constants';
|
|
import { useSelectRole } from '@/services/auth';
|
|
import { resolveRoleDestination } from '@/services/auth/routing';
|
|
import type { PublicRole } from '@/services/auth/types';
|
|
import { AUTH_CARD_MAX_WIDTH } from './constants';
|
|
import BrandMark from './BrandMark';
|
|
|
|
interface SelectRoleProps {
|
|
/** Login intent carried from A1/B1 — pre-selects the matching option. Admin is never offered. */
|
|
initialRole?: AppRole;
|
|
}
|
|
|
|
// 'family' (a household under one roof) reads as "receiving care"; 'visits' (a clinical-visit
|
|
// glyph) reads as "nurse professional" — a house icon for the nurse option was the prior defect.
|
|
const ROLE_OPTIONS: ReadonlyArray<{ role: PublicRole; icon: string }> = [
|
|
{ role: 'customer', icon: 'family' },
|
|
{ role: 'nurse', icon: 'visits' },
|
|
];
|
|
|
|
/**
|
|
* First-use role picker for a brand-new user whose `/me` has no public role. Selecting one calls
|
|
* `me/select_role` (which rotates the token so the new claim lands), then routes into that app.
|
|
* Admin roles are internal-only and never selectable here.
|
|
* @component SelectRole
|
|
*/
|
|
const SelectRole: FunctionComponent<SelectRoleProps> = ({ initialRole }) => {
|
|
const t = useTranslations('auth');
|
|
const router = useRouter();
|
|
const locale = useLocale();
|
|
const selectRole = useSelectRole();
|
|
|
|
const preset: PublicRole = initialRole === APP_ROLES.NURSE ? 'nurse' : 'customer';
|
|
const [selected, setSelected] = useState<PublicRole>(preset);
|
|
|
|
const confirm = () => {
|
|
selectRole.mutate(selected, {
|
|
onSuccess: (me) => router.replace(`/${locale}${resolveRoleDestination(me, selected)}`),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Stack sx={{ alignItems: 'center', justifyContent: 'center', minHeight: '70vh', px: 2, py: 4 }}>
|
|
<Stack sx={{ width: '100%', maxWidth: AUTH_CARD_MAX_WIDTH, gap: 3 }}>
|
|
<BrandMark />
|
|
<Stack sx={{ gap: 0.5, textAlign: 'center' }}>
|
|
<Typography variant="h6" component="h1">
|
|
{t('select_role_title')}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
{t('select_role_subtitle')}
|
|
</Typography>
|
|
</Stack>
|
|
|
|
<Stack sx={{ gap: 2 }}>
|
|
{ROLE_OPTIONS.map(({ role, icon }) => {
|
|
const isSelected = selected === role;
|
|
return (
|
|
<Paper
|
|
key={role}
|
|
role="radio"
|
|
aria-checked={isSelected}
|
|
tabIndex={0}
|
|
onClick={() => setSelected(role)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
event.preventDefault();
|
|
setSelected(role);
|
|
}
|
|
}}
|
|
elevation={0}
|
|
sx={{
|
|
p: 2.5,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 2,
|
|
border: '2px solid',
|
|
borderColor: isSelected ? 'primary.main' : 'divider',
|
|
borderRadius: 'var(--bal-radius-md)',
|
|
// Selection is never color-only: the soft fill pairs with a check glyph below.
|
|
bgcolor: isSelected ? 'var(--bal-primary-soft)' : 'transparent',
|
|
transition:
|
|
'background-color var(--bal-motion-base) var(--bal-easing-standard), border-color var(--bal-motion-base) var(--bal-easing-standard)',
|
|
}}
|
|
>
|
|
<Box
|
|
aria-hidden="true"
|
|
sx={{
|
|
width: 56,
|
|
height: 56,
|
|
flexShrink: 0,
|
|
borderRadius: '50%',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
bgcolor: 'var(--bal-bg-paper)',
|
|
boxShadow: 'var(--bal-shadow-1)',
|
|
}}
|
|
>
|
|
<AppIcon icon={icon} size={30} color="var(--bal-primary)" />
|
|
</Box>
|
|
<Stack sx={{ gap: 0.25, flexGrow: 1 }}>
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
|
|
{t(`role_${role}`)}
|
|
</Typography>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
|
{t(`role_${role}_desc`)}
|
|
</Typography>
|
|
</Stack>
|
|
<Box sx={{ width: 24, height: 24, flexShrink: 0 }} aria-hidden="true">
|
|
{isSelected && <AppIcon icon="verified" size={24} color="var(--bal-primary)" />}
|
|
</Box>
|
|
</Paper>
|
|
);
|
|
})}
|
|
</Stack>
|
|
|
|
<Typography variant="caption" sx={{ color: 'text.secondary', textAlign: 'center' }}>
|
|
{t('role_add_later_note')}
|
|
</Typography>
|
|
|
|
<AppButton
|
|
color="primary"
|
|
variant="contained"
|
|
fullWidth
|
|
onClick={confirm}
|
|
disabled={selectRole.isPending}
|
|
startIcon={selectRole.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
|
|
>
|
|
{t('continue')}
|
|
</AppButton>
|
|
</Stack>
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
export default SelectRole;
|