import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { LucideIcon, Plus, Save, X, Trash2 } from 'lucide-react'; import Modal, { ModalHeader, ModalBody, ModalFooter } from '../Modal/Modal'; import { Button } from '../Button'; import { Input } from '../Input'; import { Select } from '../Select'; import { StatusIndicatorConfig } from '../StatusCard'; import { statusColors } from '../../../styles/colors'; // Constants to prevent re-creation on every render const EMPTY_VALIDATION_ERRORS = {}; const EMPTY_INITIAL_DATA = {}; /** * ListFieldRenderer - Native component for managing lists of structured items */ interface ListFieldRendererProps { field: AddModalField; value: any[]; onChange: (newValue: any[]) => void; error?: string; } const ListFieldRenderer: React.FC = ({ field, value, onChange, error }) => { const { t } = useTranslation(['common']); const listConfig = field.listConfig!; const addItem = () => { const newItem: any = { id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` }; // Initialize with default values listConfig.itemFields.forEach(itemField => { newItem[itemField.name] = itemField.defaultValue ?? (itemField.type === 'number' || itemField.type === 'currency' ? 0 : ''); }); onChange([...value, newItem]); }; const removeItem = (index: number) => { onChange(value.filter((_, i) => i !== index)); }; const updateItem = (index: number, fieldName: string, newValue: any) => { const updated = value.map((item, i) => i === index ? { ...item, [fieldName]: newValue } : item ); onChange(updated); }; const calculateSubtotal = (item: any) => { if (!listConfig.showSubtotals || !listConfig.subtotalFields) return 0; const quantity = item[listConfig.subtotalFields.quantity] || 0; const price = item[listConfig.subtotalFields.price] || 0; return quantity * price; }; const calculateTotal = () => { if (!listConfig.showSubtotals) return 0; return value.reduce((total, item) => total + calculateSubtotal(item), 0); }; const renderItemField = (item: any, itemIndex: number, fieldConfig: any) => { const fieldValue = item[fieldConfig.name] ?? ''; switch (fieldConfig.type) { case 'select': return ( ); case 'number': case 'currency': return ( updateItem(itemIndex, fieldConfig.name, parseFloat(e.target.value) || 0)} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm" min="0" step={fieldConfig.type === 'currency' ? '0.01' : '0.1'} placeholder={fieldConfig.placeholder} required={fieldConfig.required} /> ); default: // text return ( updateItem(itemIndex, fieldConfig.name, e.target.value)} className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)] text-sm" placeholder={fieldConfig.placeholder} required={fieldConfig.required} /> ); } }; return (

{field.label}

{value.length === 0 ? (

{listConfig.emptyStateText || 'No hay elementos agregados'}

Haz clic en "{listConfig.addButtonLabel || 'Agregar'}" para comenzar

) : (
{value.map((item, itemIndex) => (
Elemento #{itemIndex + 1}
{listConfig.itemFields.map((fieldConfig) => (
{renderItemField(item, itemIndex, fieldConfig)}
))}
{listConfig.showSubtotals && (
Subtotal: €{calculateSubtotal(item).toFixed(2)}
)}
))}
)} {listConfig.showSubtotals && value.length > 0 && (
Total: €{calculateTotal().toFixed(2)}
)}
); }; export interface AddModalField { label: string; name: string; type?: 'text' | 'email' | 'tel' | 'number' | 'currency' | 'date' | 'select' | 'textarea' | 'component' | 'list'; required?: boolean; placeholder?: string; options?: Array<{label: string; value: string | number}>; validation?: (value: string | number | any) => string | null; helpText?: string; span?: 1 | 2; // For grid layout defaultValue?: string | number | any; component?: React.ComponentType; // For custom components componentProps?: Record; // Props for custom components // List field configuration listConfig?: { itemFields: Array<{ name: string; label: string; type: 'text' | 'number' | 'select' | 'currency'; required?: boolean; placeholder?: string; options?: Array<{label: string; value: string | number}>; defaultValue?: any; validation?: (value: any) => string | null; }>; addButtonLabel?: string; removeButtonLabel?: string; emptyStateText?: string; showSubtotals?: boolean; // For calculating item totals subtotalFields?: { quantity: string; price: string }; // Field names for calculation }; } export interface AddModalSection { title: string; icon?: LucideIcon; fields: AddModalField[]; columns?: 1 | 2; // Grid columns for this section } export interface AddModalProps { isOpen: boolean; onClose: () => void; // Content title: string; subtitle?: string; statusIndicator?: StatusIndicatorConfig; sections: AddModalSection[]; // Actions onSave: (formData: Record) => Promise; onCancel?: () => void; // Layout size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; loading?: boolean; // Initial form data initialData?: Record; // Validation validationErrors?: Record; onValidationError?: (errors: Record) => void; } /** * AddModal - Specialized modal component for creating new items * Provides a simplified interface compared to EditViewModal, focused on creation workflows * * Features: * - Form-based interface optimized for adding new items * - Built-in form state management * - Validation support with error display * - Responsive grid layout for fields * - Loading states and action buttons * - Support for various field types */ export const AddModal: React.FC = ({ isOpen, onClose, title, subtitle, statusIndicator, sections, onSave, onCancel, size = 'lg', loading = false, initialData = EMPTY_INITIAL_DATA, validationErrors = EMPTY_VALIDATION_ERRORS, onValidationError, }) => { const [formData, setFormData] = useState>({}); const [fieldErrors, setFieldErrors] = useState>({}); const { t } = useTranslation(['common']); // Track if we've initialized the form data for this modal session const initializedRef = useRef(false); // Initialize form data when modal opens useEffect(() => { if (isOpen && !initializedRef.current) { const defaultFormData: Record = {}; // Populate with default values from sections sections.forEach(section => { section.fields.forEach(field => { if (field.defaultValue !== undefined) { defaultFormData[field.name] = field.defaultValue; } else { defaultFormData[field.name] = field.type === 'number' || field.type === 'currency' ? 0 : ''; } }); }); // Merge with initialData setFormData({ ...defaultFormData, ...initialData }); setFieldErrors({}); initializedRef.current = true; } else if (!isOpen) { // Reset initialization flag when modal closes initializedRef.current = false; } }, [isOpen, initialData]); // Update field errors when validation errors change useEffect(() => { setFieldErrors(validationErrors); }, [validationErrors]); const defaultStatusIndicator: StatusIndicatorConfig = statusIndicator || { color: statusColors.inProgress.primary, text: 'Nuevo', icon: Plus, isCritical: false, isHighlight: true }; const handleFieldChange = (fieldName: string, value: string | number) => { setFormData(prev => ({ ...prev, [fieldName]: value })); // Clear field error if it exists if (fieldErrors[fieldName]) { const newErrors = { ...fieldErrors }; delete newErrors[fieldName]; setFieldErrors(newErrors); onValidationError?.(newErrors); } // Run field validation if provided const field = findFieldByName(fieldName); if (field?.validation) { const error = field.validation(value); if (error) { const newErrors = { ...fieldErrors, [fieldName]: error }; setFieldErrors(newErrors); onValidationError?.(newErrors); } } }; const findFieldByName = (fieldName: string): AddModalField | undefined => { for (const section of sections) { const field = section.fields.find(f => f.name === fieldName); if (field) return field; } return undefined; }; const validateForm = (): boolean => { const errors: Record = {}; sections.forEach(section => { section.fields.forEach(field => { const value = formData[field.name]; // Check required fields if (field.required && (!value || String(value).trim() === '')) { errors[field.name] = `${field.label} ${t('common:modals.validation.required_field', 'es requerido')}`; } // Run custom validation if (value && field.validation) { const error = field.validation(value); if (error) { errors[field.name] = error; } } }); }); setFieldErrors(errors); onValidationError?.(errors); return Object.keys(errors).length === 0; }; const handleSave = async () => { if (!validateForm()) { return; } try { await onSave(formData); onClose(); } catch (error) { console.error('Error saving form:', error); // Don't close modal on error - let the parent handle error display } }; const handleCancel = () => { if (onCancel) { onCancel(); } onClose(); }; const renderField = (field: AddModalField): React.ReactNode => { const value = formData[field.name] ?? ''; const error = fieldErrors[field.name]; const handleChange = (e: React.ChangeEvent) => { const newValue = field.type === 'number' || field.type === 'currency' ? Number(e.target.value) : e.target.value; handleFieldChange(field.name, newValue); }; const inputValue = field.type === 'currency' ? Number(String(value).replace(/[^0-9.-]+/g, '')) : value; switch (field.type) { case 'select': return (