Files
baya-monorepo/client/src/components/auth/SelectRole.tsx
T
2026-07-27 22:27:04 +03:30

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;