325 lines
15 KiB
TypeScript
325 lines
15 KiB
TypeScript
import React, { useEffect } from 'react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import { useForm, Controller } from 'react-hook-form';
|
||
import { yupResolver } from '@hookform/resolvers/yup';
|
||
import * as yup from 'yup';
|
||
import { User, ArrowLeft, Save, UserPlus } from 'lucide-react';
|
||
import { useUser, useCreateUser, useUpdateUser } from '../core/_hooks';
|
||
import { CreateUserRequest, UpdateUserRequest } from '../core/_models';
|
||
import { PageContainer } from '../../../components/ui/Typography';
|
||
import { Button } from '../../../components/ui/Button';
|
||
import { Input } from '../../../components/ui/Input';
|
||
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
|
||
|
||
// Validation schema
|
||
const createUserSchema = yup.object({
|
||
phone_number: yup
|
||
.string()
|
||
.required('شماره تلفن الزامی است')
|
||
.matches(/^(\+98|0)?9\d{9}$/, 'شماره تلفن معتبر نیست'),
|
||
first_name: yup
|
||
.string()
|
||
.required('نام الزامی است')
|
||
.min(1, 'نام باید حداقل ۱ کاراکتر باشد')
|
||
.max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد'),
|
||
last_name: yup
|
||
.string()
|
||
.required('نام خانوادگی الزامی است')
|
||
.min(1, 'نام خانوادگی باید حداقل ۱ کاراکتر باشد')
|
||
.max(100, 'نام خانوادگی نباید بیشتر از ۱۰۰ کاراکتر باشد'),
|
||
email: yup
|
||
.string()
|
||
.email('ایمیل معتبر نیست')
|
||
.max(255, 'ایمیل نباید بیشتر از ۲۵۵ کاراکتر باشد')
|
||
.nullable(),
|
||
national_code: yup
|
||
.string()
|
||
.max(20, 'کد ملی نباید بیشتر از ۲۰ کاراکتر باشد')
|
||
.nullable(),
|
||
verified: yup.boolean().required('وضعیت تأیید الزامی است'),
|
||
password: yup
|
||
.string()
|
||
.min(6, 'رمز عبور باید حداقل ۶ کاراکتر باشد')
|
||
.max(100, 'رمز عبور نباید بیشتر از ۱۰۰ کاراکتر باشد')
|
||
.nullable(),
|
||
});
|
||
|
||
const updateUserSchema = yup.object({
|
||
first_name: yup
|
||
.string()
|
||
.required('نام الزامی است')
|
||
.min(1, 'نام باید حداقل ۱ کاراکتر باشد')
|
||
.max(100, 'نام نباید بیشتر از ۱۰۰ کاراکتر باشد'),
|
||
last_name: yup
|
||
.string()
|
||
.required('نام خانوادگی الزامی است')
|
||
.min(1, 'نام خانوادگی باید حداقل ۱ کاراکتر باشد')
|
||
.max(100, 'نام خانوادگی نباید بیشتر از ۱۰۰ کاراکتر باشد'),
|
||
email: yup
|
||
.string()
|
||
.email('ایمیل معتبر نیست')
|
||
.max(255, 'ایمیل نباید بیشتر از ۲۵۵ کاراکتر باشد')
|
||
.nullable(),
|
||
national_code: yup
|
||
.string()
|
||
.max(20, 'کد ملی نباید بیشتر از ۲۰ کاراکتر باشد')
|
||
.nullable(),
|
||
verified: yup.boolean().required('وضعیت تأیید الزامی است'),
|
||
});
|
||
|
||
type FormData = CreateUserRequest | UpdateUserRequest;
|
||
|
||
const UserAdminFormPage: React.FC = () => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
const isEdit = !!id;
|
||
|
||
// Hooks
|
||
const { data: user, isLoading: userLoading } = useUser(id || '');
|
||
const createUserMutation = useCreateUser();
|
||
const updateUserMutation = useUpdateUser();
|
||
|
||
const {
|
||
register,
|
||
handleSubmit,
|
||
formState: { errors, isValid },
|
||
reset,
|
||
setValue,
|
||
} = useForm<FormData>({
|
||
resolver: yupResolver(isEdit ? updateUserSchema : createUserSchema),
|
||
mode: 'onChange',
|
||
defaultValues: {
|
||
verified: false,
|
||
},
|
||
});
|
||
|
||
// Populate form in edit mode
|
||
useEffect(() => {
|
||
if (isEdit && user) {
|
||
reset({
|
||
first_name: user.first_name,
|
||
last_name: user.last_name,
|
||
email: user.email || '',
|
||
national_code: user.national_code || '',
|
||
verified: user.verified,
|
||
} as any);
|
||
}
|
||
}, [isEdit, user, reset]);
|
||
|
||
// Handlers
|
||
const onSubmit = (data: FormData) => {
|
||
if (isEdit && id) {
|
||
updateUserMutation.mutate(
|
||
{ id, userData: data as UpdateUserRequest },
|
||
{
|
||
onSuccess: () => {
|
||
navigate(`/users-admin/${id}`);
|
||
},
|
||
}
|
||
);
|
||
} else {
|
||
createUserMutation.mutate(data as CreateUserRequest, {
|
||
onSuccess: (newUser) => {
|
||
navigate(`/users-admin/${newUser.id}`);
|
||
},
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleBack = () => {
|
||
if (isEdit) {
|
||
navigate(`/users-admin/${id}`);
|
||
} else {
|
||
navigate('/users-admin');
|
||
}
|
||
};
|
||
|
||
const isLoading = createUserMutation.isPending || updateUserMutation.isPending;
|
||
|
||
if (isEdit && userLoading) {
|
||
return (
|
||
<PageContainer>
|
||
<div className="flex justify-center items-center py-12">
|
||
<LoadingSpinner />
|
||
</div>
|
||
</PageContainer>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<PageContainer>
|
||
<div className="max-w-4xl mx-auto space-y-6">
|
||
{/* Header */}
|
||
<div className="flex items-center gap-4">
|
||
<Button
|
||
variant="secondary"
|
||
onClick={handleBack}
|
||
className="flex items-center gap-2"
|
||
data-testid="back-button"
|
||
>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
بازگشت
|
||
</Button>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
|
||
{isEdit ? <User className="h-6 w-6" /> : <UserPlus className="h-6 w-6" />}
|
||
{isEdit ? 'ویرایش کاربر' : 'ایجاد کاربر جدید'}
|
||
</h1>
|
||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||
{isEdit ? 'ویرایش اطلاعات کاربر' : 'افزودن کاربر جدید به سیستم'}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||
{/* اطلاعات اصلی */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||
<div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-4 rounded-t-xl">
|
||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||
<User className="h-5 w-5" />
|
||
اطلاعات اصلی کاربر
|
||
</h2>
|
||
</div>
|
||
<div className="p-6">
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<Input
|
||
label="نام"
|
||
type="text"
|
||
placeholder="مثال: محمد"
|
||
error={errors.first_name?.message}
|
||
{...register('first_name')}
|
||
data-testid="first-name-input"
|
||
required
|
||
aria-required="true"
|
||
/>
|
||
<Input
|
||
label="نام خانوادگی"
|
||
type="text"
|
||
placeholder="مثال: احمدی"
|
||
error={errors.last_name?.message}
|
||
{...register('last_name')}
|
||
data-testid="last-name-input"
|
||
required
|
||
aria-required="true"
|
||
/>
|
||
{!isEdit && (
|
||
<Input
|
||
label="شماره تلفن"
|
||
type="tel"
|
||
placeholder="مثال: 09123456789"
|
||
error={errors.phone_number?.message}
|
||
{...register('phone_number')}
|
||
data-testid="phone-number-input"
|
||
required
|
||
aria-required="true"
|
||
/>
|
||
)}
|
||
<Input
|
||
label="ایمیل"
|
||
type="email"
|
||
placeholder="مثال: user@example.com"
|
||
error={errors.email?.message}
|
||
{...register('email')}
|
||
data-testid="email-input"
|
||
/>
|
||
<Input
|
||
label="کد ملی"
|
||
type="text"
|
||
placeholder="مثال: 1234567890"
|
||
error={errors.national_code?.message}
|
||
{...register('national_code')}
|
||
data-testid="national-code-input"
|
||
/>
|
||
{!isEdit && (
|
||
<Input
|
||
label="رمز عبور"
|
||
type="password"
|
||
placeholder="حداقل ۶ کاراکتر"
|
||
error={errors.password?.message}
|
||
{...register('password')}
|
||
data-testid="password-input"
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* تنظیمات حساب */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
||
<div className="bg-gradient-to-r from-green-600 to-green-700 px-6 py-4 rounded-t-xl">
|
||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
||
<User className="h-5 w-5" />
|
||
تنظیمات حساب
|
||
</h2>
|
||
</div>
|
||
<div className="p-6">
|
||
<div className="space-y-4">
|
||
<div className="space-y-2">
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||
وضعیت تأیید
|
||
</label>
|
||
<div className="flex items-center gap-4">
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
value="true"
|
||
{...register('verified')}
|
||
data-testid="verified-true-radio"
|
||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600"
|
||
/>
|
||
<span className="mr-2 text-sm text-gray-900 dark:text-gray-100">تأیید شده</span>
|
||
</label>
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
value="false"
|
||
{...register('verified')}
|
||
data-testid="verified-false-radio"
|
||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600"
|
||
/>
|
||
<span className="mr-2 text-sm text-gray-900 dark:text-gray-100">تأیید نشده</span>
|
||
</label>
|
||
</div>
|
||
{errors.verified && (
|
||
<p className="text-sm text-red-600 dark:text-red-400" role="alert">
|
||
{errors.verified.message}
|
||
</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* دکمههای اکشن */}
|
||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||
<div className="flex flex-col sm:flex-row gap-3 justify-end">
|
||
<Button
|
||
type="button"
|
||
variant="secondary"
|
||
onClick={handleBack}
|
||
className="sm:order-1"
|
||
data-testid="cancel-button"
|
||
>
|
||
انصراف
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
variant="primary"
|
||
loading={isLoading}
|
||
disabled={!isValid}
|
||
className="sm:order-2 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800"
|
||
data-testid="submit-button"
|
||
>
|
||
<Save className="h-4 w-4 ml-2" />
|
||
{isEdit ? 'بهروزرسانی کاربر' : 'ایجاد کاربر'}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</PageContainer>
|
||
);
|
||
};
|
||
|
||
export default UserAdminFormPage;
|