'use client';
import { ChangeEvent, FunctionComponent, useEffect, useRef } from 'react';
import { useLocale, useTranslations } from 'next-intl';
import { useForm, FormProvider, useWatch } from 'react-hook-form';
import { useSnackbar } from 'notistack';
import { Avatar, Box, MenuItem, Stack, Typography } from '@mui/material';
import {
AccentCard,
AppButton,
AppIcon,
AppLoading,
FormSection,
PageHeader,
RhfChipSelect,
RhfTextField,
TrustBadge,
} from '@/components';
import { CONTENT_MAX_WIDTH } from '@/components/config';
import { ROUTES } from '@/constants';
import { digitsOnly } from '@/utils';
import { useNurseProfile, useUpsertNurseProfile, useUploadAvatar } from '@/services/profiles';
import type { NurseProfile } from '@/services/profiles/types';
import { useVerificationStatus } from '@/services/verification';
import { ownBadgeState, SPECIALTY_PRESETS } from '@/services/verification/types';
const MAX_YEARS = 80;
const MAX_YEARS_DIGITS = 2;
const OTHER_CODE = '__other';
const EDUCATION_LEVELS = ['diploma', 'associate', 'bachelor', 'master', 'doctorate'] as const;
const EDUCATION_FIELDS = ['nursing', 'midwifery', 'anesthesia', 'operating_room', 'public_health'] as const;
interface ProfileFormValues {
avatarUrl: string | null;
bio: string;
years: string;
educationLevel: string;
educationLevelOther: string;
educationField: string;
educationFieldOther: string;
specializations: string[];
}
function parseSpecializations(json: string): string[] {
try {
const parsed = JSON.parse(json);
return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === 'string') : [];
} catch {
return [];
}
}
/**
* Splits a stored free-text value into the (select code, "other" free-text) pair the form edits: a
* value the preset list knows becomes the code, anything else becomes «سایر» + the raw text.
*/
function splitPreset(stored: string, presets: readonly string[]): { code: string; other: string } {
if (presets.includes(stored)) return { code: stored, other: '' };
return stored ? { code: OTHER_CODE, other: stored } : { code: '', other: '' };
}
/** Nurse profile bootstrap (B7 header): avatar + bio + experience + qualifications. */
export default function NurseProfilePage() {
const { data: profile, isLoading } = useNurseProfile();
if (isLoading) return ;
return ;
}
/**
* The nurse's public-facing profile, rebuilt around three named questions instead of one undivided
* column of inputs.
*
* What it replaced: a flat stack of avatar → bio → years → two selects → two conditional "other"
* fields → a chip row → a save button, with no headings and no statement of what any of it was for.
* The nurse could not tell which fields families actually see, which were required, or how much was
* left — and every keystroke re-rendered the trust badge, the verification banner and the uploader
* along with the field being typed into, because each input owned a `useState` at page level.
*
* Now: `FormSection` groups the fields into معرفی / تجربه و تحصیلات / تخصصها, each saying who the
* answer is for; react-hook-form owns the values so a keystroke re-renders one field; and validation
* lives on the field it governs rather than in a hand-rolled block at the top of the submit handler.
*/
const NurseProfileForm: FunctionComponent<{ initial: NurseProfile | null }> = ({ initial }) => {
const t = useTranslations('nurseProfile');
const tv = useTranslations('verification');
const tc = useTranslations('common');
const locale = useLocale();
const { enqueueSnackbar } = useSnackbar();
const upsert = useUpsertNurseProfile();
const uploadAvatar = useUploadAvatar();
const { data: verificationStatus } = useVerificationStatus();
const badgeState = ownBadgeState(verificationStatus);
const fileInputRef = useRef(null);
const level = splitPreset(initial?.educationLevel ?? '', EDUCATION_LEVELS);
const field = splitPreset(initial?.educationField ?? '', EDUCATION_FIELDS);
const form = useForm({
mode: 'onTouched',
defaultValues: {
avatarUrl: initial?.avatarUrl ?? null,
bio: initial?.bio ?? '',
years: initial ? String(initial.yearsOfExperience) : '',
educationLevel: level.code,
educationLevelOther: level.other,
educationField: field.code,
educationFieldOther: field.other,
specializations: parseSpecializations(initial?.specializationsJson ?? '[]'),
},
});
const { control, handleSubmit, setValue, getValues } = form;
// Only these three drive conditional rendering, so they are the only fields worth subscribing the
// page to — the rest re-render nothing but themselves.
const avatarUrl = useWatch({ control, name: 'avatarUrl' });
const educationLevel = useWatch({ control, name: 'educationLevel' });
const educationField = useWatch({ control, name: 'educationField' });
// A staged-but-unsaved avatar must never be silently discarded — warn on reload/tab-close.
const avatarDirty = avatarUrl !== (initial?.avatarUrl ?? null);
useEffect(() => {
if (!avatarDirty) return;
const handler = (event: BeforeUnloadEvent) => {
event.preventDefault();
// Chrome (and most engines) only show the native confirm dialog when returnValue is set.
event.returnValue = '';
};
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [avatarDirty]);
const onFileSelected = (event: ChangeEvent) => {
const file = event.target.files?.[0];
event.target.value = '';
if (!file) return;
uploadAvatar.mutate(file, {
onSuccess: (result) => setValue('avatarUrl', result.url, { shouldDirty: true }),
onError: () => enqueueSnackbar(t('avatar_upload_error'), { variant: 'error' }),
});
};
const save = (values: ProfileFormValues) => {
const resolvedLevel =
values.educationLevel === OTHER_CODE ? values.educationLevelOther.trim() : values.educationLevel;
const resolvedField =
values.educationField === OTHER_CODE ? values.educationFieldOther.trim() : values.educationField;
upsert.mutate(
{
bio: values.bio.trim(),
yearsOfExperience: values.years.trim() === '' ? 0 : Number(values.years),
educationLevel: resolvedLevel,
educationField: resolvedField,
specializationsJson: JSON.stringify(values.specializations),
avatarUrl: values.avatarUrl,
},
{
onSuccess: () => {
enqueueSnackbar(t('saved'), { variant: 'success' });
// Re-baseline so the beforeunload guard and `isDirty` stop reporting saved work as unsaved.
form.reset(getValues());
},
onError: () => enqueueSnackbar(t('save_error'), { variant: 'error' }),
},
);
};
return (
} />
{/* Blocked-until-verified nudge — shown until the aggregate is approved (incl. the expired state). */}
{badgeState !== 'verified' ? (
{t('unverified_title')}
{t('unverified_body')}
{t('unverified_cta')}
) : null}
{avatarUrl ? null : }
{t('photo_hint')}
fileInputRef.current?.click()}
disabled={uploadAvatar.isPending}
sx={{ alignSelf: 'flex-start' }}
>
{uploadAvatar.isPending ? t('uploading') : t('upload')}
name="bio"
label={t('bio')}
helperText={t('bio_hint')}
multiline
minRows={3}
fullWidth
/>
name="years"
label={t('years')}
transform={(raw) => digitsOnly(raw).slice(0, MAX_YEARS_DIGITS)}
rules={{
validate: (value) => {
const trimmed = String(value ?? '').trim();
if (trimmed === '') return true;
const parsed = Number(trimmed);
return (Number.isInteger(parsed) && parsed >= 0 && parsed <= MAX_YEARS) || t('years_invalid');
},
}}
slotProps={{ htmlInput: { dir: 'ltr', inputMode: 'numeric', style: { textAlign: 'start' } } }}
sx={{ maxWidth: 200 }}
/>
name="educationLevel" select label={t('education_level_label')} fullWidth>
{EDUCATION_LEVELS.map((code) => (
))}
name="educationField" select label={t('education_field_label')} fullWidth>
{EDUCATION_FIELDS.map((code) => (
))}
{educationLevel === OTHER_CODE ? (
name="educationLevelOther"
label={t('education_level_other_label')}
rules={{ required: t('education_other_required') }}
fullWidth
/>
) : null}
{educationField === OTHER_CODE ? (
name="educationFieldOther"
label={t('education_field_other_label')}
rules={{ required: t('education_other_required') }}
fullWidth
/>
) : null}
name="specializations"
options={SPECIALTY_PRESETS.map((code) => ({
code,
label: tv.has(`specialty_${code}`) ? tv(`specialty_${code}`) : code,
}))}
allowCustomValues
/>
{upsert.isPending ? tc('saving') : t('save')}
{t('preview_cta')}
{t('deferred_services')}
);
};