/** * UserFormDialog Component * Dialog for creating and editing users with form validation */ 'use client'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { Alert } from '@/components/ui/alert'; import { toast } from 'sonner'; import { useCreateUser, useUpdateUser, type User } from '@/lib/api/hooks/useAdmin'; // ============================================================================ // Validation Schema // ============================================================================ const userFormSchema = z.object({ email: z.string().min(1, 'Email is required').email('Please enter a valid email address'), first_name: z .string() .min(1, 'First name is required') .min(2, 'First name must be at least 2 characters') .max(50, 'First name must not exceed 50 characters'), last_name: z .string() .max(50, 'Last name must not exceed 50 characters') .optional() .or(z.literal('')), password: z.string(), is_active: z.boolean(), is_superuser: z.boolean(), }); type UserFormData = z.infer; // ============================================================================ // Component // ============================================================================ interface UserFormDialogProps { open: boolean; onOpenChange: (open: boolean) => void; user?: User | null; mode: 'create' | 'edit'; } export function UserFormDialog({ open, onOpenChange, user, mode }: UserFormDialogProps) { const isEdit = mode === 'edit' && user; const createUser = useCreateUser(); const updateUser = useUpdateUser(); const form = useForm({ resolver: zodResolver(userFormSchema), defaultValues: { email: '', first_name: '', last_name: '', password: '', is_active: true, is_superuser: false, }, }); // Reset form when dialog opens/closes or user changes // istanbul ignore next - Form reset logic tested in E2E (admin-users.spec.ts) useEffect(() => { if (open && isEdit) { form.reset({ email: user.email, first_name: user.first_name, last_name: user.last_name || '', password: '', is_active: user.is_active, is_superuser: user.is_superuser, }); } else if (open && !isEdit) { form.reset({ email: '', first_name: '', last_name: '', password: '', is_active: true, is_superuser: false, }); } }, [open, isEdit, user, form]); // istanbul ignore next - Form submission logic fully tested in E2E (admin-users.spec.ts) const onSubmit = async (data: UserFormData) => { try { // Validate password for create mode if (!isEdit) { if (!data.password || data.password.length === 0) { form.setError('password', { message: 'Password is required' }); return; } if (data.password.length < 8) { form.setError('password', { message: 'Password must be at least 8 characters' }); return; } if (!/[0-9]/.test(data.password)) { form.setError('password', { message: 'Password must contain at least one number' }); return; } if (!/[A-Z]/.test(data.password)) { form.setError('password', { message: 'Password must contain at least one uppercase letter', }); return; } } if (isEdit) { // Validate password if provided in edit mode if (data.password && data.password.length > 0) { if (data.password.length < 8) { form.setError('password', { message: 'Password must be at least 8 characters' }); return; } if (!/[0-9]/.test(data.password)) { form.setError('password', { message: 'Password must contain at least one number' }); return; } if (!/[A-Z]/.test(data.password)) { form.setError('password', { message: 'Password must contain at least one uppercase letter', }); return; } } // Prepare update data (exclude password if empty, email is not updatable) const updateData: Record = { first_name: data.first_name, last_name: data.last_name || null, is_active: data.is_active, is_superuser: data.is_superuser, }; // Only include password if provided if (data.password && data.password.length > 0) { updateData.password = data.password; } await updateUser.mutateAsync({ userId: user.id, userData: updateData, }); toast.success(`User ${data.first_name} ${data.last_name || ''} updated successfully`); onOpenChange(false); form.reset(); } else { // Create new user await createUser.mutateAsync({ email: data.email, first_name: data.first_name, last_name: data.last_name || undefined, password: data.password, is_superuser: data.is_superuser, }); toast.success(`User ${data.first_name} ${data.last_name || ''} created successfully`); onOpenChange(false); form.reset(); } } catch (error) { toast.error(error instanceof Error ? error.message : 'Operation failed'); } }; const { register, handleSubmit, formState: { errors, isSubmitting }, watch, setValue, } = form; const isActive = watch('is_active'); const isSuperuser = watch('is_superuser'); // istanbul ignore next - JSX rendering tested in E2E (admin-users.spec.ts) return ( {isEdit ? 'Edit User' : 'Create New User'} {isEdit ? 'Update user information and permissions' : 'Add a new user to the system with specified permissions'}
{/* Email */}
{errors.email && (

{errors.email.message}

)}
{/* First Name */}
{errors.first_name && (

{errors.first_name.message}

)}
{/* Last Name */}
{errors.last_name && (

{errors.last_name.message}

)}
{/* Password */}
{errors.password && (

{errors.password.message}

)} {!isEdit && (

Must be at least 8 characters with 1 number and 1 uppercase letter

)}
{/* Checkboxes */}
setValue('is_active', checked as boolean)} disabled={isSubmitting} />
setValue('is_superuser', checked as boolean)} disabled={isSubmitting} />
{/* Server Error Display */} {(createUser.isError || updateUser.isError) && ( {createUser.isError && createUser.error instanceof Error ? createUser.error.message : updateUser.error instanceof Error ? updateUser.error.message : 'An error occurred'} )}
); }