Merge branch 'master' into feature/cases
This commit is contained in:
@@ -340,7 +340,7 @@ export default function CasesPage() {
|
||||
{formatCaseDateTime(item.sentAt, locale)}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-1 truncate">
|
||||
{item.treatmentTypes.map(treatmentLabel).join(', ')}
|
||||
{item.treatmentType ? treatmentLabel(item.treatmentType) : '—'}
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<CaseTaskProgressBar
|
||||
|
||||
@@ -1,28 +1,180 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import * as z from 'zod';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Link } from '@/i18n/navigation';
|
||||
import { Link, useRouter } from '@/i18n/navigation';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Lock } from 'lucide-react';
|
||||
import { useAuth } from '@/lib/hooks/useAuth';
|
||||
import { authApi } from '@/lib/api/auth';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Input } from '@/components/ui/shared/Input';
|
||||
import { Toast } from '@/components/ui/shared/Toast';
|
||||
|
||||
type PasswordForm = {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
|
||||
export default function AccountSettingsPage() {
|
||||
const t = useTranslations('settings');
|
||||
const tAuth = useTranslations('auth');
|
||||
const tCommon = useTranslations('common');
|
||||
const tValidation = useTranslations('validation');
|
||||
const { user, isAuthReady } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const isResetFlow = searchParams.get('reset') === '1';
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const passwordSchema = useMemo(
|
||||
() =>
|
||||
z
|
||||
.object({
|
||||
currentPassword: z.string(),
|
||||
newPassword: z
|
||||
.string()
|
||||
.min(8, tValidation('passwordMinLength'))
|
||||
.regex(/[A-Z]/, tValidation('passwordUppercase'))
|
||||
.regex(/[0-9]/, tValidation('passwordNumber')),
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
||||
message: tValidation('passwordsDoNotMatch'),
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!isResetFlow && !data.currentPassword.trim()) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: tValidation('passwordRequired'),
|
||||
path: ['currentPassword'],
|
||||
});
|
||||
}
|
||||
}),
|
||||
[isResetFlow, tValidation],
|
||||
);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<PasswordForm>({
|
||||
resolver: zodResolver(passwordSchema),
|
||||
defaultValues: {
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthReady && !user) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [isAuthReady, user, router]);
|
||||
|
||||
const onSubmit = async (data: PasswordForm) => {
|
||||
try {
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
await authApi.changePassword({
|
||||
...(isResetFlow ? {} : { currentPassword: data.currentPassword }),
|
||||
newPassword: data.newPassword,
|
||||
});
|
||||
|
||||
reset();
|
||||
setSuccessMessage(t('passwordChanged'));
|
||||
router.replace('/login');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : t('passwordChangeFailed');
|
||||
setError(message || t('passwordChangeFailed'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isAuthReady || !user) {
|
||||
return (
|
||||
<p className="text-text-secondary text-sm">{tCommon('loadingEllipsis')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Link
|
||||
href="/today"
|
||||
className="text-sm text-primary hover:opacity-90"
|
||||
>
|
||||
<Link href="/today" className="text-sm text-primary hover:opacity-90">
|
||||
{tCommon('backToApp')}
|
||||
</Link>
|
||||
<h1 className="text-2xl font-semibold text-text-primary mt-4">{t('accountTitle')}</h1>
|
||||
<p className="text-text-secondary text-sm mt-2">{t('accountSubtitle')}</p>
|
||||
<p className="text-text-secondary text-sm mt-2">
|
||||
{isResetFlow ? t('resetPasswordSubtitle') : t('accountSubtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="surface-card p-6 space-y-3">
|
||||
<p className="text-sm text-text-secondary">{t('accountPlaceholder')}</p>
|
||||
<div className="surface-card p-6 sm:p-8 max-w-lg">
|
||||
<h2 className="text-lg font-medium text-text-primary mb-1">
|
||||
{isResetFlow ? t('resetPasswordTitle') : t('changePasswordTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary mb-6">
|
||||
{user.email}
|
||||
{user.mobile ? ` · ${user.mobile}` : ''}
|
||||
</p>
|
||||
|
||||
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||
{!isResetFlow && (
|
||||
<Input
|
||||
label={t('currentPassword')}
|
||||
{...register('currentPassword')}
|
||||
type="password"
|
||||
placeholder={tAuth('passwordPlaceholder')}
|
||||
error={errors.currentPassword?.message}
|
||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label={t('newPassword')}
|
||||
{...register('newPassword')}
|
||||
type="password"
|
||||
placeholder={tAuth('passwordPlaceholder')}
|
||||
error={errors.newPassword?.message}
|
||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label={t('confirmNewPassword')}
|
||||
{...register('confirmPassword')}
|
||||
type="password"
|
||||
placeholder={tAuth('passwordPlaceholder')}
|
||||
error={errors.confirmPassword?.message}
|
||||
icon={<Lock className="h-5 w-5 icon-flat" />}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-950/30 border border-red-600/40 rounded-[var(--radius-md)]">
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" variant="primary" isLoading={isSubmitting}>
|
||||
{isResetFlow ? t('setNewPassword') : t('updatePassword')}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{successMessage && (
|
||||
<Toast variant="success">{successMessage}</Toast>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user