diff --git a/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx index d13c3190..bc10d037 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx @@ -15,6 +15,7 @@ import { import { useTenant } from '../../../../stores/tenant.store'; import OrdersService from '../../../../api/services/orders'; import { showToast } from '../../../../utils/toast'; +import { isValidEmail, isValidSpanishPhone } from '../../../../utils/validation'; interface WizardDataProps extends WizardStepProps { data: Record; @@ -35,9 +36,39 @@ const CustomerDetailsStep: React.FC = ({ data, onDataChange, on country: data.country || 'España', }); + const [validationErrors, setValidationErrors] = useState>({}); + + const validatePhone = (phone: string) => { + if (phone && !isValidSpanishPhone(phone)) { + setValidationErrors(prev => ({ ...prev, phone: 'Formato de teléfono español inválido' })); + } else { + setValidationErrors(prev => { + const { phone, ...rest } = prev; + return rest; + }); + } + }; + + const validateEmail = (email: string) => { + if (email && !isValidEmail(email)) { + setValidationErrors(prev => ({ ...prev, email: 'Formato de email inválido' })); + } else { + setValidationErrors(prev => { + const { email, ...rest } = prev; + return rest; + }); + } + }; + const handleContinue = () => { - onDataChange({ ...data, ...customerData }); - onNext(); + // Validate before continuing + validatePhone(customerData.phone); + if (customerData.email) validateEmail(customerData.email); + + if (Object.keys(validationErrors).length === 0) { + onDataChange({ ...data, ...customerData }); + onNext(); + } }; return ( @@ -124,9 +155,20 @@ const CustomerDetailsStep: React.FC = ({ data, onDataChange, on type="tel" value={customerData.phone} onChange={(e) => setCustomerData({ ...customerData, phone: e.target.value })} + onBlur={(e) => validatePhone(e.target.value)} placeholder="+34 123 456 789" - className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" + className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 bg-[var(--bg-primary)] text-[var(--text-primary)] ${ + validationErrors.phone + ? 'border-red-500 focus:ring-red-500' + : 'border-[var(--border-secondary)] focus:ring-[var(--color-primary)]' + }`} /> + {validationErrors.phone && ( +

+ + {validationErrors.phone} +

+ )}
@@ -138,9 +180,20 @@ const CustomerDetailsStep: React.FC = ({ data, onDataChange, on type="email" value={customerData.email} onChange={(e) => setCustomerData({ ...customerData, email: e.target.value })} + onBlur={(e) => validateEmail(e.target.value)} placeholder="contacto@empresa.com" - className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" + className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 bg-[var(--bg-primary)] text-[var(--text-primary)] ${ + validationErrors.email + ? 'border-red-500 focus:ring-red-500' + : 'border-[var(--border-secondary)] focus:ring-[var(--color-primary)]' + }`} /> + {validationErrors.email && ( +

+ + {validationErrors.email} +

+ )}
diff --git a/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx index 1eed5f93..387d03b9 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/SupplierWizard.tsx @@ -5,6 +5,7 @@ import { useTenant } from '../../../../stores/tenant.store'; import { suppliersService } from '../../../../api/services/suppliers'; import { inventoryService } from '../../../../api/services/inventory'; import { showToast } from '../../../../utils/toast'; +import { isPositiveNumber, isInteger, isValidEmail, isValidSpanishPhone } from '../../../../utils/validation'; interface WizardDataProps extends WizardStepProps { data: Record; @@ -24,9 +25,53 @@ const SupplierInfoStep: React.FC = ({ data, onDataChange, onNex notes: data.notes || '', }); + const [validationErrors, setValidationErrors] = useState>({}); + + const validateLeadTimeDays = (value: string) => { + if (!value) { + setValidationErrors(prev => ({ ...prev, leadTimeDays: 'Campo requerido' })); + } else if (!isPositiveNumber(value) || !isInteger(value)) { + setValidationErrors(prev => ({ ...prev, leadTimeDays: 'Debe ser un número entero positivo' })); + } else { + setValidationErrors(prev => { + const { leadTimeDays, ...rest } = prev; + return rest; + }); + } + }; + + const validatePhone = (phone: string) => { + if (phone && !isValidSpanishPhone(phone)) { + setValidationErrors(prev => ({ ...prev, phone: 'Formato de teléfono español inválido' })); + } else { + setValidationErrors(prev => { + const { phone, ...rest } = prev; + return rest; + }); + } + }; + + const validateEmail = (email: string) => { + if (email && !isValidEmail(email)) { + setValidationErrors(prev => ({ ...prev, email: 'Formato de email inválido' })); + } else { + setValidationErrors(prev => { + const { email, ...rest } = prev; + return rest; + }); + } + }; + const handleContinue = () => { - onDataChange({ ...data, ...supplierData }); - onNext(); + // Validate before continuing + validateLeadTimeDays(supplierData.leadTimeDays); + validatePhone(supplierData.phone); + if (supplierData.email) validateEmail(supplierData.email); + + if (Object.keys(validationErrors).length === 0) { + onDataChange({ ...data, ...supplierData }); + onNext(); + } }; return ( @@ -116,10 +161,21 @@ const SupplierInfoStep: React.FC = ({ data, onDataChange, onNex type="number" value={supplierData.leadTimeDays} onChange={(e) => setSupplierData({ ...supplierData, leadTimeDays: e.target.value })} + onBlur={(e) => validateLeadTimeDays(e.target.value)} placeholder="Ej: 7" min="0" - className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--bg-primary)] text-[var(--text-primary)]" + className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 bg-[var(--bg-primary)] text-[var(--text-primary)] ${ + validationErrors.leadTimeDays + ? 'border-red-500 focus:ring-red-500' + : 'border-[var(--border-secondary)] focus:ring-[var(--color-primary)]' + }`} /> + {validationErrors.leadTimeDays && ( +

+ + {validationErrors.leadTimeDays} +

+ )}