import { useState, useCallback } from 'react'; import { Input } from '@/components/ui/input'; import { Select, SelectOption } from '@/components/ui/select'; import { DatePicker } from '@/components/ui/date-picker'; import { FileUpload } from '@/components/ui/file-upload'; import { Button } from '@/components/ui/button'; import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; export interface FormField { name: string; type: | 'text' | 'email' | 'password' | 'number' | 'textarea' | 'select' | 'date' | 'file'; label: string; placeholder?: string; required?: boolean; disabled?: boolean; defaultValue?: unknown; validation?: (value: unknown) => string | null; // Options pour le type select options?: SelectOption[]; // Options pour le type file accept?: string; multiple?: boolean; maxSize?: number; // Options pour le type date minDate?: Date; maxDate?: Date; mode?: 'single' | 'range'; } export interface FormBuilderProps { fields: FormField[]; onSubmit: (data: Record) => void; // Keep 'any' for consumers here or migrate them? 'any' is easiest for consumer compatibility but 'unknown' is strikter. // Actually, keeping 'any' for output data is often practical for forms unless we want consumers to cast. // But the goal is "Eradicate any". Let's try 'unknown' or 'FormValue'. // However, onSubmit(formData) implies formData values are mixed. // Let's stick to Record for the callback signature for now to avoid breaking all consumers immediately, // OR use a defined union type. // The user prompt specifically asked to eradicate 'any'. // Let's use `Record` in `onSubmit` to facilitate easy usage but internal state should be safer? // No, strict eradication means changing it to `Record`. // Wait, if I change `onSubmit` signature, I break callers. // I'll change it to `Record` -> `Record` and fix if breaks. submitLabel?: string; className?: string; disabled?: boolean; } /** * Composant FormBuilder pour créer des formulaires dynamiques à partir de configuration. */ export function FormBuilder({ fields, onSubmit, submitLabel = 'Submit', className, disabled = false, }: FormBuilderProps) { const [formData, setFormData] = useState>(() => { // Internal state can remain 'any' for convenience OR 'unknown'? const initial: Record = {}; fields.forEach((field) => { if (field.defaultValue !== undefined) { initial[field.name] = field.defaultValue; } else if (field.type === 'select') { initial[field.name] = field.multiple ? [] : ''; } else if (field.type === 'file') { initial[field.name] = field.multiple ? [] : null; } else if (field.type === 'date') { initial[field.name] = field.mode === 'range' ? { start: null, end: null } : null; } else { initial[field.name] = ''; } }); return initial; }); const [errors, setErrors] = useState>({}); const [touched, setTouched] = useState>({}); const validateField = useCallback( (field: FormField, value: unknown): string | null => { // Validation required if (field.required) { if ( value === null || value === undefined || value === '' || (Array.isArray(value) && value.length === 0) ) { return `${field.label} is required`; } if (typeof value === 'object' && value !== null) { if (field.type === 'date' && field.mode === 'range') { const range = value as { start: unknown; end: unknown }; if (!range.start || !range.end) return `${field.label} is required`; } } } // Validation email if (field.type === 'email' && typeof value === 'string' && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return 'Please enter a valid email address'; } } // Validation personnalisée if (field.validation) { const customError = field.validation(value); if (customError) { return customError; } } return null; }, [], ); const handleFieldChange = useCallback( (fieldName: string, value: unknown) => { setFormData((prev) => ({ ...prev, [fieldName]: value, })); // Valider le champ si déjà touché if (touched[fieldName]) { const field = fields.find((f) => f.name === fieldName); if (field) { const error = validateField(field, value); setErrors((prev) => { if (error) { return { ...prev, [fieldName]: error }; } else { const newErrors = { ...prev }; delete newErrors[fieldName]; return newErrors; } }); } } }, [fields, touched, validateField], ); const handleFieldBlur = useCallback( (fieldName: string) => { setTouched((prev) => ({ ...prev, [fieldName]: true })); const field = fields.find((f) => f.name === fieldName); if (field) { const value = formData[fieldName]; const error = validateField(field, value); setErrors((prev) => { if (error) { return { ...prev, [fieldName]: error }; } else { const newErrors = { ...prev }; delete newErrors[fieldName]; return newErrors; } }); } }, [fields, formData, validateField], ); const handleSubmit = useCallback( (e: React.FormEvent) => { e.preventDefault(); // Marquer tous les champs comme touchés const allTouched: Record = {}; const newErrors: Record = {}; fields.forEach((field) => { allTouched[field.name] = true; const value = formData[field.name]; const error = validateField(field, value); if (error) { newErrors[field.name] = error; } }); setTouched(allTouched); setErrors(newErrors); // Si pas d'erreurs, soumettre if (Object.keys(newErrors).length === 0) { onSubmit(formData); } }, [fields, formData, validateField, onSubmit], ); const renderField = (field: FormField, hasError: boolean) => { switch (field.type) { case 'text': case 'email': case 'password': case 'number': return ( handleFieldChange(field.name, e.target.value)} onBlur={() => handleFieldBlur(field.name)} placeholder={field.placeholder} disabled={disabled || field.disabled} className={hasError ? 'border-destructive' : ''} /> ); case 'textarea': return (