manual improvement 2 & add telegram bot
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { FormProvider, useForm, useWatch } from 'react-hook-form';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { Box, Divider, MenuItem, Skeleton, Stack, TextField, Typography } from '@mui/material';
|
||||
import { Box, Divider, MenuItem, Skeleton, Stack, Typography } from '@mui/material';
|
||||
import {
|
||||
AppButton,
|
||||
AppIcon,
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
FormDialogShell,
|
||||
PhoneNumberField,
|
||||
ProfileSummary,
|
||||
RhfControlGroup,
|
||||
RhfTextField,
|
||||
} from '@/components';
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher';
|
||||
import { ThemeModeSetting } from '@/components/settings';
|
||||
@@ -44,6 +47,14 @@ export default function CustomerProfilePage() {
|
||||
);
|
||||
}
|
||||
|
||||
interface AccountFormValues {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
language: string;
|
||||
emergencyName: string;
|
||||
emergencyPhone: string;
|
||||
}
|
||||
|
||||
const AccountHub: FunctionComponent<{
|
||||
initial: CustomerProfile | null;
|
||||
nameFallback: { firstName: string | null; lastName: string | null };
|
||||
@@ -57,69 +68,63 @@ const AccountHub: FunctionComponent<{
|
||||
const upsert = useUpsertCustomerProfile();
|
||||
const logout = useLogout();
|
||||
|
||||
const initialFirstName = initial?.firstName ?? nameFallback.firstName ?? '';
|
||||
const initialLastName = initial?.lastName ?? nameFallback.lastName ?? '';
|
||||
const initialLanguage = initial?.preferredLanguage ?? 'fa';
|
||||
const initialEmergencyName = initial?.defaultEmergencyContactName ?? '';
|
||||
const initialEmergencyPhone = digitsOnly(initial?.defaultEmergencyContactPhone ?? '');
|
||||
|
||||
const [firstName, setFirstName] = useState(initialFirstName);
|
||||
const [lastName, setLastName] = useState(initialLastName);
|
||||
const [language, setLanguage] = useState(initialLanguage);
|
||||
const [emergencyName, setEmergencyName] = useState(initialEmergencyName);
|
||||
const [emergencyPhone, setEmergencyPhone] = useState(initialEmergencyPhone);
|
||||
|
||||
const [personalSheetOpen, setPersonalSheetOpen] = useState(false);
|
||||
const [languageSheetOpen, setLanguageSheetOpen] = useState(false);
|
||||
const [emergencySheetOpen, setEmergencySheetOpen] = useState(false);
|
||||
const [signOutOpen, setSignOutOpen] = useState(false);
|
||||
|
||||
const [nameError, setNameError] = useState(false);
|
||||
const [phoneError, setPhoneError] = useState(false);
|
||||
// ONE form behind all three sheets. Each sheet edits its own slice, but every save writes the whole
|
||||
// profile (the wire upsert has no PATCH semantics), so the untouched fields have to come from
|
||||
// somewhere — a single form is that somewhere, and `dirtyFields` then answers per-sheet "is there
|
||||
// unsaved work here?" without a hand-written comparison per section.
|
||||
const form = useForm<AccountFormValues>({
|
||||
mode: 'onTouched',
|
||||
defaultValues: {
|
||||
firstName: initial?.firstName ?? nameFallback.firstName ?? '',
|
||||
lastName: initial?.lastName ?? nameFallback.lastName ?? '',
|
||||
language: initial?.preferredLanguage ?? 'fa',
|
||||
emergencyName: initial?.defaultEmergencyContactName ?? '',
|
||||
emergencyPhone: digitsOnly(initial?.defaultEmergencyContactPhone ?? ''),
|
||||
},
|
||||
});
|
||||
const { control, formState, getValues, reset, trigger } = form;
|
||||
const { dirtyFields } = formState;
|
||||
const watched = useWatch({ control });
|
||||
|
||||
const displayName = [firstName, lastName].filter(Boolean).join(' ').trim() || phone || '';
|
||||
const emergencyComplete = Boolean(emergencyName.trim() && emergencyPhone);
|
||||
const displayName = [watched.firstName, watched.lastName].filter(Boolean).join(' ').trim() || phone || '';
|
||||
const emergencyComplete = Boolean(watched.emergencyName?.trim() && watched.emergencyPhone);
|
||||
|
||||
// Every sheet saves the FULL profile object (the wire upsert has no PATCH semantics) — `patch`
|
||||
// carries just the fields that sheet owns, the rest come from the shared draft state so editing
|
||||
// one section never blanks another (the bug the old flat form was one refactor away from).
|
||||
const save = (
|
||||
patch: Partial<Record<'firstName' | 'lastName' | 'preferredLanguage', string | null>>,
|
||||
onDone: () => void,
|
||||
) => {
|
||||
const save = (onDone: () => void) => {
|
||||
const values = getValues();
|
||||
upsert.mutate(
|
||||
{
|
||||
defaultEmergencyContactName: emergencyName.trim(),
|
||||
defaultEmergencyContactPhone: emergencyPhone,
|
||||
firstName: firstName.trim() || null,
|
||||
lastName: lastName.trim() || null,
|
||||
preferredLanguage: language,
|
||||
...patch,
|
||||
defaultEmergencyContactName: values.emergencyName.trim(),
|
||||
defaultEmergencyContactPhone: values.emergencyPhone,
|
||||
firstName: values.firstName.trim() || null,
|
||||
lastName: values.lastName.trim() || null,
|
||||
preferredLanguage: values.language,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
enqueueSnackbar(t('saved'), { variant: 'success' });
|
||||
// Re-baseline so the saved slice stops counting as unsaved work in its sheet's discard guard.
|
||||
reset(getValues());
|
||||
onDone();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const savePersonal = () =>
|
||||
save({ firstName: firstName.trim() || null, lastName: lastName.trim() || null }, () => setPersonalSheetOpen(false));
|
||||
const saveLanguage = () => save({ preferredLanguage: language }, () => setLanguageSheetOpen(false));
|
||||
const saveEmergency = () => {
|
||||
const nameInvalid = emergencyName.trim().length === 0;
|
||||
const phoneInvalid = !isIranianMobile(emergencyPhone);
|
||||
setNameError(nameInvalid);
|
||||
setPhoneError(phoneInvalid);
|
||||
if (nameInvalid || phoneInvalid) return;
|
||||
save({}, () => setEmergencySheetOpen(false));
|
||||
const savePersonal = () => save(() => setPersonalSheetOpen(false));
|
||||
const saveLanguage = () => save(() => setLanguageSheetOpen(false));
|
||||
const saveEmergency = async () => {
|
||||
if (!(await trigger(['emergencyName', 'emergencyPhone']))) return;
|
||||
save(() => setEmergencySheetOpen(false));
|
||||
};
|
||||
|
||||
const personalDirty = firstName !== initialFirstName || lastName !== initialLastName;
|
||||
const languageDirty = language !== initialLanguage;
|
||||
const emergencyDirty = emergencyName !== initialEmergencyName || emergencyPhone !== initialEmergencyPhone;
|
||||
const personalDirty = Boolean(dirtyFields.firstName || dirtyFields.lastName);
|
||||
const languageDirty = Boolean(dirtyFields.language);
|
||||
const emergencyDirty = Boolean(dirtyFields.emergencyName || dirtyFields.emergencyPhone);
|
||||
|
||||
const goTo = (path: string) => router.push(`/${locale}${path}`);
|
||||
|
||||
@@ -131,6 +136,7 @@ const AccountHub: FunctionComponent<{
|
||||
const cancelLabel = tc('cancel');
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, maxWidth: 520 }}>
|
||||
<ProfileSummary displayName={displayName} phone={phone} initialsFallback={displayName || undefined} />
|
||||
|
||||
@@ -138,8 +144,8 @@ const AccountHub: FunctionComponent<{
|
||||
<AccountRow icon="account" label={t('row_personal')} onClick={() => setPersonalSheetOpen(true)} />
|
||||
<EmergencyContactCard
|
||||
complete={emergencyComplete}
|
||||
name={emergencyName}
|
||||
phone={emergencyPhone}
|
||||
name={watched.emergencyName ?? ''}
|
||||
phone={watched.emergencyPhone ?? ''}
|
||||
onEdit={() => setEmergencySheetOpen(true)}
|
||||
/>
|
||||
<AccountRow icon="location" label={t('row_addresses')} onClick={() => goTo(ROUTES.ADDRESSES)} />
|
||||
@@ -172,8 +178,8 @@ const AccountHub: FunctionComponent<{
|
||||
discardCancelLabel={cancelLabel}
|
||||
>
|
||||
<Stack sx={{ gap: 2.5 }}>
|
||||
<TextField label={t('first_name')} value={firstName} onChange={(e) => setFirstName(e.target.value)} fullWidth />
|
||||
<TextField label={t('last_name')} value={lastName} onChange={(e) => setLastName(e.target.value)} fullWidth />
|
||||
<RhfTextField<AccountFormValues> name="firstName" label={t('first_name')} fullWidth />
|
||||
<RhfTextField<AccountFormValues> name="lastName" label={t('last_name')} fullWidth />
|
||||
<SheetActions onCancel={() => setPersonalSheetOpen(false)} onSave={savePersonal} saving={upsert.isPending} saveLabel={tc('save')} cancelLabel={cancelLabel} />
|
||||
</Stack>
|
||||
</FormDialogShell>
|
||||
@@ -201,16 +207,10 @@ const AccountHub: FunctionComponent<{
|
||||
<LocaleSwitcher />
|
||||
</Stack>
|
||||
<Divider />
|
||||
<TextField
|
||||
select
|
||||
label={t('language')}
|
||||
value={language}
|
||||
onChange={(e) => setLanguage(e.target.value)}
|
||||
helperText={t('language_hint')}
|
||||
>
|
||||
<RhfTextField<AccountFormValues> name="language" select label={t('language')} helperText={t('language_hint')}>
|
||||
<MenuItem value="fa">{t('language_fa')}</MenuItem>
|
||||
<MenuItem value="en">{t('language_en')}</MenuItem>
|
||||
</TextField>
|
||||
</RhfTextField>
|
||||
<SheetActions onCancel={() => setLanguageSheetOpen(false)} onSave={saveLanguage} saving={upsert.isPending} saveLabel={tc('save')} cancelLabel={cancelLabel} />
|
||||
</Stack>
|
||||
</FormDialogShell>
|
||||
@@ -231,27 +231,27 @@ const AccountHub: FunctionComponent<{
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{t('emergency_hint')}
|
||||
</Typography>
|
||||
<TextField
|
||||
<RhfTextField<AccountFormValues>
|
||||
name="emergencyName"
|
||||
label={t('emergency_name')}
|
||||
value={emergencyName}
|
||||
onChange={(e) => {
|
||||
setEmergencyName(e.target.value);
|
||||
if (nameError) setNameError(false);
|
||||
}}
|
||||
error={nameError}
|
||||
fullWidth
|
||||
/>
|
||||
<PhoneNumberField
|
||||
label={t('emergency_phone')}
|
||||
value={emergencyPhone}
|
||||
onChange={(v) => {
|
||||
setEmergencyPhone(v);
|
||||
if (phoneError) setPhoneError(false);
|
||||
}}
|
||||
error={phoneError}
|
||||
helperText={phoneError ? t('emergency_phone_invalid') : undefined}
|
||||
rules={{ validate: (value) => String(value ?? '').trim().length > 0 }}
|
||||
fullWidth
|
||||
/>
|
||||
<RhfControlGroup<AccountFormValues>
|
||||
name="emergencyPhone"
|
||||
rules={{ validate: (value) => isIranianMobile(String(value ?? '')) }}
|
||||
>
|
||||
{({ field, hasError }) => (
|
||||
<PhoneNumberField
|
||||
label={t('emergency_phone')}
|
||||
value={(field.value as string) ?? ''}
|
||||
onChange={field.onChange}
|
||||
error={hasError}
|
||||
helperText={hasError ? t('emergency_phone_invalid') : undefined}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
</RhfControlGroup>
|
||||
<SheetActions onCancel={() => setEmergencySheetOpen(false)} onSave={saveEmergency} saving={upsert.isPending} saveLabel={tc('save')} cancelLabel={cancelLabel} />
|
||||
</Stack>
|
||||
</FormDialogShell>
|
||||
@@ -270,6 +270,7 @@ const AccountHub: FunctionComponent<{
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user