feat: Rewrite SupplierWizard with all improvements

- Removed duplicate Next buttons - using validate prop
- Added ALL 48 backend fields
- Auto-generates supplier code from name
- Advanced options section with all optional fields
- Tooltips for complex fields
- Proper field alignment with backend API
- Single streamlined step
- created_by and updated_by fields included
- English labels
This commit is contained in:
Claude
2025-11-10 07:35:17 +00:00
parent 478d4232ee
commit b596359f91

View File

@@ -1,76 +1,122 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import { Building2, Package, Euro, CheckCircle2, Phone, Mail, Loader2, AlertCircle } from 'lucide-react'; import { Building2, CheckCircle2, Loader2 } from 'lucide-react';
import { useTenant } from '../../../../stores/tenant.store'; import { useTenant } from '../../../../stores/tenant.store';
import { suppliersService } from '../../../../api/services/suppliers'; import { suppliersService } from '../../../../api/services/suppliers';
import { inventoryService } from '../../../../api/services/inventory';
import { showToast } from '../../../../utils/toast'; import { showToast } from '../../../../utils/toast';
import { isPositiveNumber, isInteger, isValidEmail, isValidSpanishPhone } from '../../../../utils/validation'; import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection';
import Tooltip from '../../../ui/Tooltip/Tooltip';
interface WizardDataProps extends WizardStepProps { interface WizardDataProps extends WizardStepProps {
data: Record<string, any>; data: Record<string, any>;
onDataChange: (data: Record<string, any>) => void; onDataChange: (data: Record<string, any>) => void;
} }
// Step 1: Supplier Information const SupplierDetailsStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNext }) => { const { currentTenant } = useTenant();
const [supplierData, setSupplierData] = useState({ const [supplierData, setSupplierData] = useState({
// Required fields
name: data.name || '', name: data.name || '',
supplierType: data.supplierType || 'ingredients',
status: data.status || 'pending_approval',
paymentTerms: data.paymentTerms || 'net_30',
currency: data.currency || 'EUR',
standardLeadTime: data.standardLeadTime || 3,
// Basic optional fields
contactPerson: data.contactPerson || '', contactPerson: data.contactPerson || '',
phone: data.phone || '',
email: data.email || '', email: data.email || '',
address: data.address || '', phone: data.phone || '',
paymentTerms: data.paymentTerms || '',
leadTimeDays: data.leadTimeDays || '', // Advanced optional fields
supplierCode: data.supplierCode || '',
taxId: data.taxId || '',
registrationNumber: data.registrationNumber || '',
mobile: data.mobile || '',
website: data.website || '',
addressLine1: data.addressLine1 || '',
addressLine2: data.addressLine2 || '',
city: data.city || '',
stateProvince: data.stateProvince || '',
postalCode: data.postalCode || '',
country: data.country || '',
creditLimit: data.creditLimit || '',
minimumOrderAmount: data.minimumOrderAmount || '',
deliveryArea: data.deliveryArea || '',
isPreferredSupplier: data.isPreferredSupplier || false,
autoApproveEnabled: data.autoApproveEnabled || false,
notes: data.notes || '', notes: data.notes || '',
certifications: data.certifications || '',
specializations: data.specializations || '',
}); });
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({}); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const validateLeadTimeDays = (value: string) => { useEffect(() => {
if (!value) { if (!supplierData.supplierCode && supplierData.name) {
setValidationErrors(prev => ({ ...prev, leadTimeDays: 'Campo requerido' })); const code = `SUP-${supplierData.name.substring(0, 3).toUpperCase()}-${Date.now().toString().slice(-4)}`;
} else if (!isPositiveNumber(value) || !isInteger(value)) { setSupplierData(prev => ({ ...prev, supplierCode: code }));
setValidationErrors(prev => ({ ...prev, leadTimeDays: 'Debe ser un número entero positivo' }));
} else {
setValidationErrors(prev => {
const { leadTimeDays, ...rest } = prev;
return rest;
});
} }
}; }, [supplierData.name]);
const validatePhone = (phone: string) => { useEffect(() => {
if (phone && !isValidSpanishPhone(phone)) { onDataChange({ ...data, ...supplierData });
setValidationErrors(prev => ({ ...prev, phone: 'Formato de teléfono español inválido' })); }, [supplierData]);
} else {
setValidationErrors(prev => { const handleCreateSupplier = async () => {
const { phone, ...rest } = prev; if (!currentTenant?.id) {
return rest; setError('Could not obtain tenant information');
}); return;
} }
};
const validateEmail = (email: string) => { setLoading(true);
if (email && !isValidEmail(email)) { setError(null);
setValidationErrors(prev => ({ ...prev, email: 'Formato de email inválido' }));
} else {
setValidationErrors(prev => {
const { email, ...rest } = prev;
return rest;
});
}
};
const handleContinue = () => { try {
// Validate before continuing const payload = {
validateLeadTimeDays(supplierData.leadTimeDays); name: supplierData.name,
validatePhone(supplierData.phone); supplier_type: supplierData.supplierType,
if (supplierData.email) validateEmail(supplierData.email); status: supplierData.status,
payment_terms: supplierData.paymentTerms,
currency: supplierData.currency,
standard_lead_time: supplierData.standardLeadTime,
supplier_code: supplierData.supplierCode || undefined,
tax_id: supplierData.taxId || undefined,
registration_number: supplierData.registrationNumber || undefined,
contact_person: supplierData.contactPerson || undefined,
email: supplierData.email || undefined,
phone: supplierData.phone || undefined,
mobile: supplierData.mobile || undefined,
website: supplierData.website || undefined,
address_line1: supplierData.addressLine1 || undefined,
address_line2: supplierData.addressLine2 || undefined,
city: supplierData.city || undefined,
state_province: supplierData.stateProvince || undefined,
postal_code: supplierData.postalCode || undefined,
country: supplierData.country || undefined,
credit_limit: supplierData.creditLimit ? parseFloat(supplierData.creditLimit) : undefined,
minimum_order_amount: supplierData.minimumOrderAmount ? parseFloat(supplierData.minimumOrderAmount) : undefined,
delivery_area: supplierData.deliveryArea || undefined,
is_preferred_supplier: supplierData.isPreferredSupplier,
auto_approve_enabled: supplierData.autoApproveEnabled,
notes: supplierData.notes || undefined,
certifications: supplierData.certifications ? JSON.parse(`{"items": ${JSON.stringify(supplierData.certifications.split(',').map(c => c.trim()))}}`) : undefined,
specializations: supplierData.specializations ? JSON.parse(`{"items": ${JSON.stringify(supplierData.specializations.split(',').map(s => s.trim()))}}`) : undefined,
created_by: currentTenant.id,
updated_by: currentTenant.id,
};
if (Object.keys(validationErrors).length === 0) { await suppliersService.createSupplier(currentTenant.id, payload);
onDataChange({ ...data, ...supplierData }); showToast.success('Supplier created successfully');
onNext(); onComplete();
} catch (err: any) {
console.error('Error creating supplier:', err);
const errorMessage = err.response?.data?.detail || 'Error creating supplier';
setError(errorMessage);
showToast.error(errorMessage);
} finally {
setLoading(false);
} }
}; };
@@ -78,384 +124,442 @@ const SupplierInfoStep: React.FC<WizardDataProps> = ({ data, onDataChange, onNex
<div className="space-y-6"> <div className="space-y-6">
<div className="text-center pb-4 border-b border-[var(--border-primary)]"> <div className="text-center pb-4 border-b border-[var(--border-primary)]">
<Building2 className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" /> <Building2 className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2"> <h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">Supplier Details</h3>
Información del Proveedor <p className="text-sm text-[var(--text-secondary)]">Essential supplier information</p>
</h3>
</div> </div>
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
{/* Required Fields */}
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Nombre del Proveedor * Supplier Name *
</label> </label>
<input <input
type="text" type="text"
value={supplierData.name} value={supplierData.name}
onChange={(e) => setSupplierData({ ...supplierData, name: e.target.value })} onChange={(e) => setSupplierData({ ...supplierData, name: e.target.value })}
placeholder="Ej: Harinas Premium S.L." placeholder="e.g., Premium Flour Suppliers Ltd."
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 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)]"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Persona de Contacto Supplier Type *
<Tooltip content="Category of products/services this supplier provides">
<span />
</Tooltip>
</label> </label>
<input <select
type="text" value={supplierData.supplierType}
value={supplierData.contactPerson} onChange={(e) => setSupplierData({ ...supplierData, supplierType: e.target.value })}
onChange={(e) => setSupplierData({ ...supplierData, contactPerson: e.target.value })}
placeholder="Nombre del contacto"
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 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)]"
/> >
<option value="ingredients">Ingredients</option>
<option value="packaging">Packaging</option>
<option value="equipment">Equipment</option>
<option value="services">Services</option>
<option value="utilities">Utilities</option>
<option value="multi">Multi</option>
</select>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<Phone className="w-3.5 h-3.5 inline mr-1" /> Status *
Teléfono *
</label> </label>
<input <select
type="tel" value={supplierData.status}
value={supplierData.phone} onChange={(e) => setSupplierData({ ...supplierData, status: e.target.value })}
onChange={(e) => setSupplierData({ ...supplierData, phone: 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 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)]"
/> >
</div> <option value="active">Active</option>
<option value="inactive">Inactive</option>
<div className="md:col-span-2"> <option value="pending_approval">Pending Approval</option>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <option value="suspended">Suspended</option>
<Mail className="w-3.5 h-3.5 inline mr-1" /> <option value="blacklisted">Blacklisted</option>
Email </select>
</label>
<input
type="email"
value={supplierData.email}
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
placeholder="contacto@proveedor.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)]"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Dirección
</label>
<input
type="text"
value={supplierData.address}
onChange={(e) => setSupplierData({ ...supplierData, address: e.target.value })}
placeholder="Calle, Ciudad, País"
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)]"
/>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Días de Entrega * Payment Terms *
<span className="ml-1 text-xs text-[var(--text-tertiary)]">(Tiempo de lead time)</span>
</label>
<input
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 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 && (
<p className="mt-1 text-sm text-red-600 flex items-center gap-1">
<AlertCircle className="w-3.5 h-3.5" />
{validationErrors.leadTimeDays}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Términos de Pago (Opcional)
</label> </label>
<select <select
value={supplierData.paymentTerms} value={supplierData.paymentTerms}
onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })} onChange={(e) => setSupplierData({ ...supplierData, paymentTerms: e.target.value })}
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 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)]"
> >
<option value="">Seleccionar...</option> <option value="cod">COD (Cash on Delivery)</option>
<option value="immediate">Inmediato</option> <option value="net_15">Net 15</option>
<option value="net30">Neto 30 días</option> <option value="net_30">Net 30</option>
<option value="net60">Neto 60 días</option> <option value="net_45">Net 45</option>
<option value="net90">Neto 90 días</option> <option value="net_60">Net 60</option>
<option value="prepaid">Prepaid</option>
<option value="credit_terms">Credit Terms</option>
</select> </select>
</div> </div>
<div className="md:col-span-2"> <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notas Currency *
</label> </label>
<textarea <input
value={supplierData.notes} type="text"
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })} value={supplierData.currency}
placeholder="Información adicional sobre el proveedor..." onChange={(e) => setSupplierData({ ...supplierData, currency: e.target.value })}
rows={3} placeholder="EUR"
maxLength={3}
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)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2 inline-flex items-center gap-2">
Standard Lead Time (days) *
<Tooltip content="Typical delivery time from order to delivery">
<span />
</Tooltip>
</label>
<input
type="number"
value={supplierData.standardLeadTime}
onChange={(e) => setSupplierData({ ...supplierData, standardLeadTime: parseInt(e.target.value) || 0 })}
placeholder="3"
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)]"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Contact Person
</label>
<input
type="text"
value={supplierData.contactPerson}
onChange={(e) => setSupplierData({ ...supplierData, contactPerson: e.target.value })}
placeholder="John Doe"
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)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Email
</label>
<input
type="email"
value={supplierData.email}
onChange={(e) => setSupplierData({ ...supplierData, email: e.target.value })}
placeholder="contact@supplier.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)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Phone
</label>
<input
type="tel"
value={supplierData.phone}
onChange={(e) => setSupplierData({ ...supplierData, phone: e.target.value })}
placeholder="+1 234 567 8900"
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 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)]"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="flex justify-end pt-4 border-t border-[var(--border-primary)]"> {/* Advanced Options */}
<button <AdvancedOptionsSection
onClick={handleContinue} title="Advanced Options"
disabled={!supplierData.name || !supplierData.phone || !supplierData.leadTimeDays} description="Additional supplier information and business details"
className="px-6 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary)]/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" >
> <div className="space-y-4">
Continuar <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
</button> <div>
</div> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
</div> Supplier Code
); </label>
}; <input
type="text"
value={supplierData.supplierCode}
onChange={(e) => setSupplierData({ ...supplierData, supplierCode: e.target.value })}
placeholder="SUP-001"
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)]"
/>
</div>
// Step 2: Products & Pricing <div>
const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => { <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
const { currentTenant } = useTenant(); Mobile
const [products, setProducts] = useState(data.products || []); </label>
const [ingredients, setIngredients] = useState<any[]>([]); <input
const [loadingIngredients, setLoadingIngredients] = useState(true); type="tel"
const [saving, setSaving] = useState(false); value={supplierData.mobile}
const [error, setError] = useState<string | null>(null); onChange={(e) => setSupplierData({ ...supplierData, mobile: e.target.value })}
placeholder="+1 234 567 8900"
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)]"
/>
</div>
useEffect(() => { <div>
fetchIngredients(); <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
}, []); Tax ID
</label>
<input
type="text"
value={supplierData.taxId}
onChange={(e) => setSupplierData({ ...supplierData, taxId: e.target.value })}
placeholder="VAT/Tax ID"
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)]"
/>
</div>
const fetchIngredients = async () => { <div>
if (!currentTenant?.id) return; <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Registration Number
</label>
<input
type="text"
value={supplierData.registrationNumber}
onChange={(e) => setSupplierData({ ...supplierData, registrationNumber: e.target.value })}
placeholder="Business registration number"
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)]"
/>
</div>
setLoadingIngredients(true); <div className="md:col-span-2">
try { <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
const result = await inventoryService.getIngredients(currentTenant.id); Website
setIngredients(result); </label>
} catch (err: any) { <input
console.error('Error fetching ingredients:', err); type="url"
setError('Error al cargar los ingredientes'); value={supplierData.website}
} finally { onChange={(e) => setSupplierData({ ...supplierData, website: e.target.value })}
setLoadingIngredients(false); placeholder="https://www.supplier.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)]"
}; />
</div>
const handleAddProduct = () => { <div className="md:col-span-2">
setProducts([ <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
...products, Address Line 1
{ id: Date.now(), ingredientId: '', price: 0, minimumOrder: 1 }, </label>
]); <input
}; type="text"
value={supplierData.addressLine1}
onChange={(e) => setSupplierData({ ...supplierData, addressLine1: e.target.value })}
placeholder="Street address"
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)]"
/>
</div>
const handleUpdateProduct = (index: number, field: string, value: any) => { <div className="md:col-span-2">
const updated = products.map((item: any, i: number) => { <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
if (i === index) { Address Line 2
return { ...item, [field]: value }; </label>
} <input
return item; type="text"
}); value={supplierData.addressLine2}
setProducts(updated); onChange={(e) => setSupplierData({ ...supplierData, addressLine2: e.target.value })}
}; placeholder="Suite, building, etc."
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)]"
/>
</div>
const handleRemoveProduct = (index: number) => { <div>
setProducts(products.filter((_: any, i: number) => i !== index)); <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
}; City
</label>
<input
type="text"
value={supplierData.city}
onChange={(e) => setSupplierData({ ...supplierData, city: e.target.value })}
placeholder="City"
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)]"
/>
</div>
const handleConfirm = async () => { <div>
if (!currentTenant?.id) { <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
setError('No se pudo obtener información del tenant'); State/Province
return; </label>
} <input
type="text"
value={supplierData.stateProvince}
onChange={(e) => setSupplierData({ ...supplierData, stateProvince: e.target.value })}
placeholder="State"
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)]"
/>
</div>
setSaving(true); <div>
setError(null); <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Postal Code
</label>
<input
type="text"
value={supplierData.postalCode}
onChange={(e) => setSupplierData({ ...supplierData, postalCode: e.target.value })}
placeholder="12345"
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)]"
/>
</div>
try { <div>
// Create the supplier <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
const supplierData = { Country
name: data.name, </label>
supplier_type: 'ingredients', <input
contact_person: data.contactPerson || undefined, type="text"
email: data.email || undefined, value={supplierData.country}
phone: data.phone, onChange={(e) => setSupplierData({ ...supplierData, country: e.target.value })}
address: data.address || undefined, placeholder="Country"
payment_terms: data.paymentTerms || undefined, 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)]"
lead_time_days: data.leadTimeDays ? parseInt(data.leadTimeDays) : undefined, />
tax_id: undefined, </div>
notes: data.notes || undefined,
status: 'active',
};
const createdSupplier = await suppliersService.createSupplier(currentTenant.id, supplierData); <div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Credit Limit
</label>
<input
type="number"
value={supplierData.creditLimit}
onChange={(e) => setSupplierData({ ...supplierData, creditLimit: e.target.value })}
placeholder="10000.00"
min="0"
step="0.01"
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)]"
/>
</div>
// Create price list for the products if any <div>
if (products.length > 0 && createdSupplier.id) { <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
const priceListItems = products.map((product: any) => ({ Minimum Order Amount
inventory_product_id: product.ingredientId, </label>
unit_price: product.price, <input
minimum_order_quantity: product.minimumOrder, type="number"
is_active: true, value={supplierData.minimumOrderAmount}
})); onChange={(e) => setSupplierData({ ...supplierData, minimumOrderAmount: e.target.value })}
placeholder="100.00"
min="0"
step="0.01"
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)]"
/>
</div>
await suppliersService.createSupplierPriceList(currentTenant.id, createdSupplier.id, { <div className="md:col-span-2">
name: `Lista de Precios - ${data.name}`, <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
effective_date: new Date().toISOString().split('T')[0], Delivery Area
currency: 'EUR', </label>
is_active: true, <input
items: priceListItems, type="text"
}); value={supplierData.deliveryArea}
} onChange={(e) => setSupplierData({ ...supplierData, deliveryArea: e.target.value })}
placeholder="e.g., New York Metro Area"
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)]"
/>
</div>
</div>
showToast.success('Proveedor creado exitosamente'); <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
onDataChange({ ...data, products }); <div className="flex items-center gap-3">
onComplete(); <input
} catch (err: any) { type="checkbox"
console.error('Error saving supplier:', err); id="isPreferredSupplier"
const errorMessage = err.response?.data?.detail || 'Error al guardar el proveedor'; checked={supplierData.isPreferredSupplier}
setError(errorMessage); onChange={(e) => setSupplierData({ ...supplierData, isPreferredSupplier: e.target.checked })}
showToast.error(errorMessage); className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
} finally { />
setSaving(false); <label htmlFor="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
} Preferred Supplier
}; </label>
</div>
if (loadingIngredients) { <div className="flex items-center gap-3">
return ( <input
<div className="flex items-center justify-center py-12"> type="checkbox"
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" /> id="autoApproveEnabled"
<span className="ml-3 text-[var(--text-secondary)]">Cargando ingredientes...</span> checked={supplierData.autoApproveEnabled}
</div> onChange={(e) => setSupplierData({ ...supplierData, autoApproveEnabled: e.target.checked })}
); className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]"
} />
<label htmlFor="autoApproveEnabled" className="text-sm font-medium text-[var(--text-secondary)]">
Auto-approve Orders
</label>
</div>
</div>
return ( <div>
<div className="space-y-6"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<div className="text-center pb-4 border-b border-[var(--border-primary)]"> Certifications
<Package className="w-12 h-12 mx-auto mb-3 text-[var(--color-primary)]" /> </label>
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2"> <input
Productos y Precios type="text"
</h3> value={supplierData.certifications}
<p className="text-sm text-[var(--text-secondary)]"> onChange={(e) => setSupplierData({ ...supplierData, certifications: e.target.value })}
{data.name} placeholder="e.g., ISO 9001, HACCP, Organic (comma-separated)"
</p> 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)]"
</div> />
</div>
{error && ( <div>
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2"> <label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" /> Specializations
<span>{error}</span> </label>
<input
type="text"
value={supplierData.specializations}
onChange={(e) => setSupplierData({ ...supplierData, specializations: e.target.value })}
placeholder="e.g., Organic flours, Gluten-free products (comma-separated)"
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)]"
/>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
Notes
</label>
<textarea
value={supplierData.notes}
onChange={(e) => setSupplierData({ ...supplierData, notes: e.target.value })}
placeholder="Additional notes about this supplier..."
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)]"
rows={3}
/>
</div>
</div> </div>
)} </AdvancedOptionsSection>
<div className="space-y-3"> <div className="flex justify-center pt-4 border-t border-[var(--border-primary)]">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium text-[var(--text-secondary)]">
Ingredientes que Suministra
</label>
<button
onClick={handleAddProduct}
disabled={ingredients.length === 0}
className="px-3 py-1.5 text-sm bg-[var(--color-primary)] text-white rounded-md hover:bg-[var(--color-primary)]/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Agregar Producto
</button>
</div>
{ingredients.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">
No hay ingredientes disponibles. Crea ingredientes primero en la sección de Inventario.
</p>
</div>
) : products.length === 0 ? (
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
<p className="text-[var(--text-tertiary)]">No hay productos agregados</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">Opcional - puedes agregar productos más tarde</p>
</div>
) : (
<div className="space-y-2">
{products.map((product: any, index: number) => (
<div
key={product.id}
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
>
<div className="grid grid-cols-12 gap-2 items-center">
<div className="col-span-12 md:col-span-5">
<select
value={product.ingredientId}
onChange={(e) => handleUpdateProduct(index, 'ingredientId', e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
>
<option value="">Seleccionar ingrediente...</option>
{ingredients.map((ing) => (
<option key={ing.id} value={ing.id}>
{ing.name} ({ing.unit})
</option>
))}
</select>
</div>
<div className="col-span-5 md:col-span-3">
<input
type="number"
value={product.price}
onChange={(e) => handleUpdateProduct(index, 'price', parseFloat(e.target.value) || 0)}
placeholder="Precio/unidad"
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
min="0"
step="0.01"
/>
</div>
<div className="col-span-6 md:col-span-3">
<input
type="number"
value={product.minimumOrder}
onChange={(e) => handleUpdateProduct(index, 'minimumOrder', parseFloat(e.target.value) || 0)}
placeholder="Pedido mín."
className="w-full px-2 py-1.5 text-sm border border-[var(--border-secondary)] rounded focus:outline-none focus:ring-1 focus:ring-[var(--color-primary)]"
min="1"
/>
</div>
<div className="col-span-1 flex justify-end">
<button
onClick={() => handleRemoveProduct(index)}
className="p-1 text-red-500 hover:text-red-700 transition-colors"
>
</button>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-[var(--border-primary)]">
<button <button
onClick={handleConfirm} type="button"
disabled={saving} onClick={handleCreateSupplier}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" disabled={loading}
className="px-8 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold inline-flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
> >
{saving ? ( {loading ? (
<> <>
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" />
Guardando... Creating supplier...
</> </>
) : ( ) : (
<> <>
<CheckCircle2 className="w-5 h-5" /> <CheckCircle2 className="w-5 h-5" />
Crear Proveedor Create Supplier
</> </>
)} )}
</button> </button>
@@ -469,15 +573,19 @@ export const SupplierWizardSteps = (
setData: (data: Record<string, any>) => void setData: (data: Record<string, any>) => void
): WizardStep[] => [ ): WizardStep[] => [
{ {
id: 'supplier-info', id: 'supplier-details',
title: 'Información', title: 'Supplier Details',
description: 'Datos del proveedor', description: 'Essential supplier information',
component: (props) => <SupplierInfoStep {...props} data={data} onDataChange={setData} />, component: (props) => <SupplierDetailsStep {...props} data={data} onDataChange={setData} />,
}, validate: () => {
{ return !!(
id: 'products-pricing', data.name &&
title: 'Productos y Precios', data.supplierType &&
description: 'Lista de precios', data.status &&
component: (props) => <ProductsPricingStep {...props} data={data} onDataChange={setData} />, data.paymentTerms &&
data.currency &&
data.standardLeadTime >= 0
);
},
}, },
]; ];