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:
@@ -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)) {
|
|
||||||
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 = () => {
|
|
||||||
// Validate before continuing
|
|
||||||
validateLeadTimeDays(supplierData.leadTimeDays);
|
|
||||||
validatePhone(supplierData.phone);
|
|
||||||
if (supplierData.email) validateEmail(supplierData.email);
|
|
||||||
|
|
||||||
if (Object.keys(validationErrors).length === 0) {
|
|
||||||
onDataChange({ ...data, ...supplierData });
|
onDataChange({ ...data, ...supplierData });
|
||||||
onNext();
|
}, [supplierData]);
|
||||||
|
|
||||||
|
const handleCreateSupplier = async () => {
|
||||||
|
if (!currentTenant?.id) {
|
||||||
|
setError('Could not obtain tenant information');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
name: supplierData.name,
|
||||||
|
supplier_type: supplierData.supplierType,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
await suppliersService.createSupplier(currentTenant.id, payload);
|
||||||
|
showToast.success('Supplier created successfully');
|
||||||
|
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"
|
|
||||||
>
|
>
|
||||||
Continuar
|
<div className="space-y-4">
|
||||||
</button>
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
</div>
|
<div>
|
||||||
</div>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
);
|
Supplier Code
|
||||||
};
|
|
||||||
|
|
||||||
// Step 2: Products & Pricing
|
|
||||||
const ProductsPricingStep: React.FC<WizardDataProps> = ({ data, onDataChange, onComplete }) => {
|
|
||||||
const { currentTenant } = useTenant();
|
|
||||||
const [products, setProducts] = useState(data.products || []);
|
|
||||||
const [ingredients, setIngredients] = useState<any[]>([]);
|
|
||||||
const [loadingIngredients, setLoadingIngredients] = useState(true);
|
|
||||||
const [saving, setSaving] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchIngredients();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const fetchIngredients = async () => {
|
|
||||||
if (!currentTenant?.id) return;
|
|
||||||
|
|
||||||
setLoadingIngredients(true);
|
|
||||||
try {
|
|
||||||
const result = await inventoryService.getIngredients(currentTenant.id);
|
|
||||||
setIngredients(result);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error fetching ingredients:', err);
|
|
||||||
setError('Error al cargar los ingredientes');
|
|
||||||
} finally {
|
|
||||||
setLoadingIngredients(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddProduct = () => {
|
|
||||||
setProducts([
|
|
||||||
...products,
|
|
||||||
{ id: Date.now(), ingredientId: '', price: 0, minimumOrder: 1 },
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateProduct = (index: number, field: string, value: any) => {
|
|
||||||
const updated = products.map((item: any, i: number) => {
|
|
||||||
if (i === index) {
|
|
||||||
return { ...item, [field]: value };
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
setProducts(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveProduct = (index: number) => {
|
|
||||||
setProducts(products.filter((_: any, i: number) => i !== index));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
|
||||||
if (!currentTenant?.id) {
|
|
||||||
setError('No se pudo obtener información del tenant');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaving(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create the supplier
|
|
||||||
const supplierData = {
|
|
||||||
name: data.name,
|
|
||||||
supplier_type: 'ingredients',
|
|
||||||
contact_person: data.contactPerson || undefined,
|
|
||||||
email: data.email || undefined,
|
|
||||||
phone: data.phone,
|
|
||||||
address: data.address || undefined,
|
|
||||||
payment_terms: data.paymentTerms || undefined,
|
|
||||||
lead_time_days: data.leadTimeDays ? parseInt(data.leadTimeDays) : undefined,
|
|
||||||
tax_id: undefined,
|
|
||||||
notes: data.notes || undefined,
|
|
||||||
status: 'active',
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdSupplier = await suppliersService.createSupplier(currentTenant.id, supplierData);
|
|
||||||
|
|
||||||
// Create price list for the products if any
|
|
||||||
if (products.length > 0 && createdSupplier.id) {
|
|
||||||
const priceListItems = products.map((product: any) => ({
|
|
||||||
inventory_product_id: product.ingredientId,
|
|
||||||
unit_price: product.price,
|
|
||||||
minimum_order_quantity: product.minimumOrder,
|
|
||||||
is_active: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
await suppliersService.createSupplierPriceList(currentTenant.id, createdSupplier.id, {
|
|
||||||
name: `Lista de Precios - ${data.name}`,
|
|
||||||
effective_date: new Date().toISOString().split('T')[0],
|
|
||||||
currency: 'EUR',
|
|
||||||
is_active: true,
|
|
||||||
items: priceListItems,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
showToast.success('Proveedor creado exitosamente');
|
|
||||||
onDataChange({ ...data, products });
|
|
||||||
onComplete();
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('Error saving supplier:', err);
|
|
||||||
const errorMessage = err.response?.data?.detail || 'Error al guardar el proveedor';
|
|
||||||
setError(errorMessage);
|
|
||||||
showToast.error(errorMessage);
|
|
||||||
} finally {
|
|
||||||
setSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loadingIngredients) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center py-12">
|
|
||||||
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-primary)]" />
|
|
||||||
<span className="ml-3 text-[var(--text-secondary)]">Cargando ingredientes...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="text-center pb-4 border-b border-[var(--border-primary)]">
|
|
||||||
<Package 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">
|
|
||||||
Productos y Precios
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-[var(--text-secondary)]">
|
|
||||||
{data.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm flex items-start gap-2">
|
|
||||||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
|
||||||
<span>{error}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="block text-sm font-medium text-[var(--text-secondary)]">
|
|
||||||
Ingredientes que Suministra
|
|
||||||
</label>
|
</label>
|
||||||
<button
|
<input
|
||||||
onClick={handleAddProduct}
|
type="text"
|
||||||
disabled={ingredients.length === 0}
|
value={supplierData.supplierCode}
|
||||||
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"
|
onChange={(e) => setSupplierData({ ...supplierData, supplierCode: e.target.value })}
|
||||||
>
|
placeholder="SUP-001"
|
||||||
+ Agregar Producto
|
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)]"
|
||||||
</button>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ingredients.length === 0 ? (
|
<div>
|
||||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<p className="text-[var(--text-tertiary)]">
|
Mobile
|
||||||
No hay ingredientes disponibles. Crea ingredientes primero en la sección de Inventario.
|
</label>
|
||||||
</p>
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={supplierData.mobile}
|
||||||
|
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>
|
</div>
|
||||||
) : products.length === 0 ? (
|
|
||||||
<div className="text-center py-12 border-2 border-dashed border-[var(--border-secondary)] rounded-lg">
|
<div>
|
||||||
<p className="text-[var(--text-tertiary)]">No hay productos agregados</p>
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<p className="text-xs text-[var(--text-tertiary)] mt-1">Opcional - puedes agregar productos más tarde</p>
|
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>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
<div>
|
||||||
{products.map((product: any, index: number) => (
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
<div
|
Registration Number
|
||||||
key={product.id}
|
</label>
|
||||||
className="p-3 border border-[var(--border-secondary)] rounded-lg bg-[var(--bg-secondary)]/30"
|
<input
|
||||||
>
|
type="text"
|
||||||
<div className="grid grid-cols-12 gap-2 items-center">
|
value={supplierData.registrationNumber}
|
||||||
<div className="col-span-12 md:col-span-5">
|
onChange={(e) => setSupplierData({ ...supplierData, registrationNumber: e.target.value })}
|
||||||
<select
|
placeholder="Business registration number"
|
||||||
value={product.ingredientId}
|
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)]"
|
||||||
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>
|
||||||
<div className="col-span-5 md:col-span-3">
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Website
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={supplierData.website}
|
||||||
|
onChange={(e) => setSupplierData({ ...supplierData, website: e.target.value })}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Address Line 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>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Address Line 2
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={supplierData.addressLine2}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
State/Province
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Country
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={supplierData.country}
|
||||||
|
onChange={(e) => setSupplierData({ ...supplierData, country: e.target.value })}
|
||||||
|
placeholder="Country"
|
||||||
|
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">
|
||||||
|
Credit Limit
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={product.price}
|
value={supplierData.creditLimit}
|
||||||
onChange={(e) => handleUpdateProduct(index, 'price', parseFloat(e.target.value) || 0)}
|
onChange={(e) => setSupplierData({ ...supplierData, creditLimit: e.target.value })}
|
||||||
placeholder="Precio/unidad"
|
placeholder="10000.00"
|
||||||
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"
|
min="0"
|
||||||
step="0.01"
|
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>
|
</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)]">
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Minimum Order Amount
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Delivery Area
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="isPreferredSupplier"
|
||||||
|
checked={supplierData.isPreferredSupplier}
|
||||||
|
onChange={(e) => setSupplierData({ ...supplierData, isPreferredSupplier: 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="isPreferredSupplier" className="text-sm font-medium text-[var(--text-secondary)]">
|
||||||
|
Preferred Supplier
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="autoApproveEnabled"
|
||||||
|
checked={supplierData.autoApproveEnabled}
|
||||||
|
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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">
|
||||||
|
Certifications
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={supplierData.certifications}
|
||||||
|
onChange={(e) => setSupplierData({ ...supplierData, certifications: e.target.value })}
|
||||||
|
placeholder="e.g., ISO 9001, HACCP, Organic (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">
|
||||||
|
Specializations
|
||||||
|
</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>
|
||||||
|
</AdvancedOptionsSection>
|
||||||
|
|
||||||
|
<div className="flex justify-center 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 !!(
|
||||||
|
data.name &&
|
||||||
|
data.supplierType &&
|
||||||
|
data.status &&
|
||||||
|
data.paymentTerms &&
|
||||||
|
data.currency &&
|
||||||
|
data.standardLeadTime >= 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'products-pricing',
|
|
||||||
title: 'Productos y Precios',
|
|
||||||
description: 'Lista de precios',
|
|
||||||
component: (props) => <ProductsPricingStep {...props} data={data} onDataChange={setData} />,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user