frontend phase 5 & backend phase 12

This commit is contained in:
hamid
2026-07-09 03:05:14 +03:30
parent 465f75c29e
commit dc64472631
98 changed files with 11847 additions and 136 deletions
@@ -3,10 +3,12 @@ import { ChangeEvent, FunctionComponent, useRef, useState } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { useSnackbar } from 'notistack';
import { Avatar, Box, Paper, Stack, TextField, Typography } from '@mui/material';
import { AppButton, AppIcon, AppLoading } from '@/components';
import { AppButton, AppIcon, AppLoading, TrustBadge } from '@/components';
import { ROUTES } from '@/constants';
import { useNurseProfile, useUpsertNurseProfile, useUploadAvatar } from '@/services/profiles';
import type { NurseProfile } from '@/services/profiles/types';
import { useVerificationStatus } from '@/services/verification';
import { ownBadgeState } from '@/services/verification/types';
const MAX_YEARS = 80;
@@ -24,6 +26,8 @@ const NurseProfileForm: FunctionComponent<{ initial: NurseProfile | null }> = ({
const { enqueueSnackbar } = useSnackbar();
const upsert = useUpsertNurseProfile();
const uploadAvatar = useUploadAvatar();
const { data: verificationStatus } = useVerificationStatus();
const badgeState = ownBadgeState(verificationStatus);
const fileInputRef = useRef<HTMLInputElement>(null);
const [avatarUrl, setAvatarUrl] = useState<string | null>(initial?.avatarUrl ?? null);
@@ -63,41 +67,47 @@ const NurseProfileForm: FunctionComponent<{ initial: NurseProfile | null }> = ({
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 560 }}>
<Box>
<Typography variant="h5" component="h1">
{t('title')}
</Typography>
<Stack direction="row" sx={{ gap: 1.5, alignItems: 'center', flexWrap: 'wrap' }}>
<Typography variant="h5" component="h1">
{t('title')}
</Typography>
{/* The public trust signal on the nurse's own profile — the same badge f6 reuses in search. */}
<TrustBadge state={badgeState} />
</Stack>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('subtitle')}
</Typography>
</Box>
{/* Not bookable until verification (f5) — a neutral placeholder, not the real banner. */}
<Paper
elevation={0}
sx={{ p: 2, borderRadius: 2, border: '1px solid', borderColor: 'divider', borderInlineStartWidth: 4, borderInlineStartColor: 'var(--bal-warning)' }}
>
<Stack direction="row" sx={{ gap: 1.5, alignItems: 'flex-start' }}>
<AppIcon icon="warning" size={24} color="var(--bal-warning)" />
<Stack sx={{ gap: 1, flexGrow: 1 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
{t('unverified_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('unverified_body')}
</Typography>
</Box>
<AppButton
color="primary"
variant="outlined"
to={`/${locale}${ROUTES.NURSE_VERIFICATION}`}
sx={{ m: 0, alignSelf: 'flex-start' }}
>
{t('unverified_cta')}
</AppButton>
{/* Blocked-until-verified banner — shown until the aggregate is approved (incl. the expired state). */}
{badgeState !== 'verified' ? (
<Paper
elevation={0}
sx={{ p: 2, borderRadius: 2, border: '1px solid', borderColor: 'divider', borderInlineStartWidth: 4, borderInlineStartColor: 'var(--bal-warning)' }}
>
<Stack direction="row" sx={{ gap: 1.5, alignItems: 'flex-start' }}>
<AppIcon icon="warning" size={24} color="var(--bal-warning)" />
<Stack sx={{ gap: 1, flexGrow: 1 }}>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
{t('unverified_title')}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('unverified_body')}
</Typography>
</Box>
<AppButton
color="primary"
variant="outlined"
to={`/${locale}${ROUTES.NURSE_VERIFICATION}`}
sx={{ m: 0, alignSelf: 'flex-start' }}
>
{t('unverified_cta')}
</AppButton>
</Stack>
</Stack>
</Stack>
</Paper>
</Paper>
) : null}
<Stack direction="row" sx={{ gap: 2, alignItems: 'center' }}>
<Avatar src={avatarUrl ?? undefined} sx={{ width: 72, height: 72, bgcolor: 'var(--bal-primary-soft)' }}>