import React, { useState } from 'react'; import { motion, AnimatePresence } from 'motion/react'; import { Eye, EyeOff, Save, RotateCcw, AlertTriangle } from 'lucide-react'; import { Input } from './Input'; import { Switch } from './Switch'; import { Button } from './Button'; import { TabsRoot, TabsList, TabsTrigger, TabsContent } from './Tabs'; export interface FormField { key: string; label: string; type: 'text' | 'password' | 'number' | 'switch' | 'select'; placeholder?: string; description?: string; options?: { value: string; label: string }[]; validation?: { required?: boolean; min?: number; max?: number; pattern?: string; }; } export interface FormSection { key: string; title: string; description?: string; fields: FormField[]; } export interface DynamicFormSchema { sections: FormSection[]; } export interface SettingContribution { key: string; label: string; type: 'text' | 'number' | 'password' | 'select' | 'switch'; default: any; scope: 'tenant' | 'workspace' | 'system'; description?: string; options?: Array<{ value: string; label: string }>; validation?: { required?: boolean; min?: number; max?: number; pattern?: string; }; } export interface SettingsSection { id: string; title: string; description?: string; contributions: SettingContribution[]; } export interface DynamicFormProps { schema?: DynamicFormSchema; sections?: SettingsSection[]; initialData: Record; onSubmit: (data: Record) => void | Promise; onReset?: () => void; isLoading?: boolean; className?: string; } export const DynamicForm: React.FC = ({ schema, sections, initialData, onSubmit, onReset, isLoading = false, className = '', }) => { // Normalizar sections y schema a un formato común de secciones para renderizado const normalizedSections: FormSection[] = React.useMemo(() => { if (sections) { return sections.map((sec) => ({ key: sec.id, title: sec.title, description: sec.description, fields: sec.contributions.map((c) => ({ key: c.key, label: c.label, type: c.type === 'switch' ? 'switch' : c.type, description: c.description, options: c.options, validation: c.validation, })), })); } return schema?.sections || []; }, [sections, schema]); const [formData, setFormData] = useState>(() => { const data: Record = {}; normalizedSections.forEach((sec) => { sec.fields.forEach((field) => { data[field.key] = initialData[field.key] !== undefined ? initialData[field.key] : ''; }); }); return data; }); const [errors, setErrors] = useState>({}); const [visiblePasswords, setVisiblePasswords] = useState>({}); const [activeTab, setActiveTab] = useState(() => normalizedSections[0]?.key || ''); // Sincronizar datos iniciales cuando cambien externamente (Reactividad y Carga) React.useEffect(() => { const data: Record = {}; normalizedSections.forEach((sec) => { sec.fields.forEach((field) => { data[field.key] = initialData[field.key] !== undefined ? initialData[field.key] : ''; }); }); setFormData(data); }, [initialData, normalizedSections]); // Si cambia la pestaña activa por defecto al cargar las secciones React.useEffect(() => { if (normalizedSections.length > 0 && !activeTab) { setActiveTab(normalizedSections[0].key); } }, [normalizedSections, activeTab]); const handleFieldChange = (key: string, value: any) => { setFormData((prev) => ({ ...prev, [key]: value })); // Limpiar error al modificar el campo if (errors[key]) { setErrors((prev) => { const next = { ...prev }; delete next[key]; return next; }); } }; const togglePasswordVisibility = (key: string) => { setVisiblePasswords((prev) => ({ ...prev, [key]: !prev[key] })); }; const validateForm = (): boolean => { const nextErrors: Record = {}; normalizedSections.forEach((sec) => { sec.fields.forEach((field) => { const val = formData[field.key]; const validation = field.validation; if (validation?.required) { if (val === undefined || val === null || val === '') { nextErrors[field.key] = `El campo ${field.label} es obligatorio`; } } if (val !== undefined && val !== null && val !== '') { if (field.type === 'number' && validation?.min !== undefined && Number(val) < validation.min) { nextErrors[field.key] = `El valor mínimo es ${validation.min}`; } if (field.type === 'number' && validation?.max !== undefined && Number(val) > validation.max) { nextErrors[field.key] = `El valor máximo es ${validation.max}`; } if (validation?.pattern) { const regex = new RegExp(validation.pattern); if (!regex.test(String(val))) { nextErrors[field.key] = `El formato del campo ${field.label} no es válido`; } } } }); }); setErrors(nextErrors); return Object.keys(nextErrors).length === 0; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (validateForm()) { await onSubmit(formData); } else { // Buscar primera sección con errores y enfocarla const firstErrorKey = Object.keys(errors)[0]; if (firstErrorKey) { const errorSection = normalizedSections.find((sec) => sec.fields.some((f) => f.key === firstErrorKey) ); if (errorSection && errorSection.key !== activeTab) { setActiveTab(errorSection.key); } } } }; const handleReset = () => { if (onReset) { onReset(); } else { const data: Record = {}; normalizedSections.forEach((sec) => { sec.fields.forEach((field) => { data[field.key] = initialData[field.key] !== undefined ? initialData[field.key] : ''; }); }); setFormData(data); setErrors({}); } }; return (
{normalizedSections.length > 1 && ( {normalizedSections.map((sec) => { const hasSectionError = sec.fields.some((f) => !!errors[f.key]); return ( {sec.title} {hasSectionError && ( )} ); })} {normalizedSections.map((sec) => { if (sec.key !== activeTab) return null; return ( {sec.description && (

{sec.title}

{sec.description}

)}
{sec.fields.map((field) => { const error = errors[field.key]; const val = formData[field.key]; if (field.type === 'switch') { return (
{field.description && ( {field.description} )}
handleFieldChange(field.key, checked)} />
); } if (field.type === 'select') { return (
{field.description && !error && ( {field.description} )} {error && ( {error} )}
); } // Text, Password, Number const isPassword = field.type === 'password'; const isVisible = !!visiblePasswords[field.key]; return (
handleFieldChange(field.key, e.target.value)} error={error} icon={ isPassword ? ( ) : undefined } /> {field.description && !error && ( {field.description} )}
); })}
); })}
)} {/* Si solo hay una sección y no es necesario usar Tabs */} {normalizedSections.length === 1 && (
{normalizedSections[0].description && (

{normalizedSections[0].title}

{normalizedSections[0].description}

)}
{normalizedSections[0].fields.map((field) => { const error = errors[field.key]; const val = formData[field.key]; if (field.type === 'switch') { return (
{field.description && ( {field.description} )}
handleFieldChange(field.key, checked)} />
); } if (field.type === 'select') { return (
{field.description && !error && ( {field.description} )} {error && ( {error} )}
); } const isPassword = field.type === 'password'; const isVisible = !!visiblePasswords[field.key]; return (
handleFieldChange(field.key, e.target.value)} error={error} icon={ isPassword ? ( ) : undefined } /> {field.description && !error && ( {field.description} )}
); })}
)} {/* Botones de Acción */}
); };