admin/src/pages/users-admin/user-admin-form/UserAdminFormPage.tsx

325 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 & Partial<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 as any) : (createUserSchema as any)),
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;