Implement Phase 2: Recipe & Supplier wizard modals (JTBD-driven UX)

Following Jobs-To-Be-Done analysis, break down complex forms into multi-step wizards to reduce cognitive load for non-technical bakery owners.

**Core Infrastructure:**
- Add reusable WizardModal component with progress tracking, validation, and navigation
  - Multi-step progress bar with clickable previous steps
  - Per-step validation with clear error messaging
  - Back/Next/Complete navigation with loading states
  - Optional step skipping support
  - Responsive modal design (sm/md/lg/xl/2xl sizes)

**Recipe Wizard (4 steps):**
- Step 1 (Product): Name, category, finished product, cuisine type, difficulty, description
- Step 2 (Ingredients): Dynamic ingredient list with add/remove, quantities, units, optional flags
- Step 3 (Production): Times (prep/cook/rest), yield, batch sizes, temperature, humidity, special flags
- Step 4 (Review): Instructions, storage, nutritional info, allergens, final summary

**Supplier Wizard (3 steps):**
- Step 1 (Basic): Name, type, status, contact person, email, phone, tax ID, registration
- Step 2 (Delivery): Payment terms, lead time, minimum order, delivery schedule, address
- Step 3 (Review): Certifications, sustainability practices, notes, summary

**Benefits:**
- Reduces form overwhelm from 8 sections to 4 sequential steps (recipes) and 3 steps (suppliers)
- Clear progress indication and next actions
- Validation feedback per step instead of at end
- Summary review before final submission
- Matches mental model of "configure then review" workflow

Files:
- WizardModal: Reusable wizard infrastructure
- RecipeWizard: 4-step recipe creation (Product → Ingredients → Production → Review)
- SupplierWizard: 3-step supplier creation (Basic → Delivery → Review)

Related to Phase 1 (ConfigurationProgressWidget) for post-onboarding guidance.
This commit is contained in:
Claude
2025-11-06 18:01:11 +00:00
parent 170caa9a0e
commit 877e0b6b47
13 changed files with 2280 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
import React from 'react';
import { Users } from 'lucide-react';
import type { WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import type { SupplierCreate, SupplierType, SupplierStatus, PaymentTerms } from '../../../../api/types/suppliers';
interface SupplierBasicStepProps extends WizardStepProps {
supplierData: Partial<SupplierCreate>;
onUpdate: (data: Partial<SupplierCreate>) => void;
}
export const SupplierBasicStep: React.FC<SupplierBasicStepProps> = ({
supplierData,
onUpdate,
onNext
}) => {
const handleFieldChange = (field: keyof SupplierCreate, value: any) => {
onUpdate({ ...supplierData, [field]: value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onNext();
};
const isValid = supplierData.name && supplierData.name.trim().length >= 1 && supplierData.supplier_type;
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header */}
<div className="flex items-start gap-4 pb-4 border-b border-[var(--border-secondary)]">
<div className="w-12 h-12 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center flex-shrink-0">
<Users className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">
Información Básica
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Configura los datos esenciales del proveedor
</p>
</div>
</div>
{/* Form Fields */}
<div className="space-y-5">
{/* Supplier Name */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Nombre del Proveedor <span className="text-red-500">*</span>
</label>
<input
type="text"
value={supplierData.name || ''}
onChange={(e) => handleFieldChange('name', e.target.value)}
placeholder="Ej: Molinos La Victoria"
className="w-full px-4 py-2.5 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)]"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Supplier Type */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Tipo de Proveedor <span className="text-red-500">*</span>
</label>
<select
value={supplierData.supplier_type || ''}
onChange={(e) => handleFieldChange('supplier_type', e.target.value as SupplierType)}
className="w-full px-4 py-2.5 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)]"
required
>
<option value="">Seleccionar...</option>
<option value="ingredients">Ingredientes</option>
<option value="packaging">Empaque</option>
<option value="equipment">Equipo</option>
<option value="utilities">Servicios Públicos</option>
<option value="services">Servicios</option>
<option value="logistics">Logística</option>
<option value="other">Otro</option>
</select>
</div>
{/* Status */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Estado <span className="text-red-500">*</span>
</label>
<select
value={supplierData.status || 'pending_approval'}
onChange={(e) => handleFieldChange('status', e.target.value as SupplierStatus)}
className="w-full px-4 py-2.5 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="pending_approval">Pendiente de Aprobación</option>
<option value="active">Activo</option>
<option value="inactive">Inactivo</option>
<option value="suspended">Suspendido</option>
<option value="terminated">Terminado</option>
<option value="evaluation">En Evaluación</option>
</select>
</div>
</div>
{/* Contact Information */}
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Persona de Contacto
</label>
<input
type="text"
value={supplierData.contact_person || ''}
onChange={(e) => handleFieldChange('contact_person', e.target.value)}
placeholder="Nombre del representante"
className="w-full px-4 py-2.5 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="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Email
</label>
<input
type="email"
value={supplierData.email || ''}
onChange={(e) => handleFieldChange('email', e.target.value)}
placeholder="correo@ejemplo.com"
className="w-full px-4 py-2.5 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-primary)] mb-1.5">
Teléfono
</label>
<input
type="tel"
value={supplierData.phone || ''}
onChange={(e) => handleFieldChange('phone', e.target.value)}
placeholder="+34 600 000 000"
className="w-full px-4 py-2.5 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>
{/* Tax & Registration */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
NIF/CIF (Opcional)
</label>
<input
type="text"
value={supplierData.tax_id || ''}
onChange={(e) => handleFieldChange('tax_id', e.target.value)}
placeholder="Ej: B12345678"
className="w-full px-4 py-2.5 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-primary)] mb-1.5">
Número de Registro (Opcional)
</label>
<input
type="text"
value={supplierData.registration_number || ''}
onChange={(e) => handleFieldChange('registration_number', e.target.value)}
placeholder="Número de registro oficial"
className="w-full px-4 py-2.5 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>
{/* Validation Message */}
{!isValid && (
<div className="p-3 bg-[var(--color-warning)]/10 border border-[var(--color-warning)]/30 rounded-lg">
<p className="text-sm text-[var(--text-primary)]">
<span className="font-medium"> Campos requeridos:</span> Asegúrate de completar el nombre y tipo de proveedor.
</p>
</div>
)}
{/* Hidden submit button for form handling */}
<button type="submit" className="hidden" disabled={!isValid} />
</form>
);
};

View File

@@ -0,0 +1,198 @@
import React from 'react';
import { Truck } from 'lucide-react';
import type { WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import type { SupplierCreate, PaymentTerms, DeliverySchedule } from '../../../../api/types/suppliers';
interface SupplierDeliveryStepProps extends WizardStepProps {
supplierData: Partial<SupplierCreate>;
onUpdate: (data: Partial<SupplierCreate>) => void;
}
export const SupplierDeliveryStep: React.FC<SupplierDeliveryStepProps> = ({
supplierData,
onUpdate,
onNext,
onBack
}) => {
const handleFieldChange = (field: keyof SupplierCreate, value: any) => {
onUpdate({ ...supplierData, [field]: value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onNext();
};
const isValid = supplierData.payment_terms;
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header */}
<div className="flex items-start gap-4 pb-4 border-b border-[var(--border-secondary)]">
<div className="w-12 h-12 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center flex-shrink-0">
<Truck className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">
Entrega y Términos
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Define términos de pago, entrega y ubicación
</p>
</div>
</div>
{/* Form Fields */}
<div className="space-y-5">
{/* Payment Terms & Lead Time */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Términos de Pago <span className="text-red-500">*</span>
</label>
<select
value={supplierData.payment_terms || ''}
onChange={(e) => handleFieldChange('payment_terms', e.target.value as PaymentTerms)}
className="w-full px-4 py-2.5 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)]"
required
>
<option value="">Seleccionar...</option>
<option value="immediate">Inmediato</option>
<option value="net_7">Neto 7 días</option>
<option value="net_15">Neto 15 días</option>
<option value="net_30">Neto 30 días</option>
<option value="net_60">Neto 60 días</option>
<option value="net_90">Neto 90 días</option>
<option value="cod">Contra reembolso</option>
<option value="cia">Efectivo por adelantado</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Tiempo de Entrega (días)
</label>
<input
type="number"
value={supplierData.lead_time_days || ''}
onChange={(e) => handleFieldChange('lead_time_days', e.target.value ? parseInt(e.target.value) : undefined)}
placeholder="Ej: 3"
min="0"
className="w-full px-4 py-2.5 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>
{/* Minimum Order & Delivery Schedule */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Pedido Mínimo ()
</label>
<input
type="number"
value={supplierData.minimum_order_value || ''}
onChange={(e) => handleFieldChange('minimum_order_value', e.target.value ? parseFloat(e.target.value) : undefined)}
placeholder="Ej: 100"
min="0"
step="0.01"
className="w-full px-4 py-2.5 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-primary)] mb-1.5">
Frecuencia de Entrega
</label>
<select
value={supplierData.delivery_schedule || ''}
onChange={(e) => handleFieldChange('delivery_schedule', e.target.value as DeliverySchedule)}
className="w-full px-4 py-2.5 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="daily">Diario</option>
<option value="weekly">Semanal</option>
<option value="biweekly">Quincenal</option>
<option value="monthly">Mensual</option>
<option value="on_demand">Bajo demanda</option>
<option value="custom">Personalizado</option>
</select>
</div>
</div>
{/* Address Section */}
<div className="space-y-4 pt-4 border-t border-[var(--border-secondary)]">
<h4 className="text-sm font-semibold text-[var(--text-primary)]">
Dirección del Proveedor (Opcional)
</h4>
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Calle y Número
</label>
<input
type="text"
value={supplierData.address_street || ''}
onChange={(e) => handleFieldChange('address_street', e.target.value)}
placeholder="Ej: Calle Mayor, 123"
className="w-full px-4 py-2.5 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="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Ciudad
</label>
<input
type="text"
value={supplierData.address_city || ''}
onChange={(e) => handleFieldChange('address_city', e.target.value)}
placeholder="Ej: Madrid"
className="w-full px-4 py-2.5 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-primary)] mb-1.5">
Código Postal
</label>
<input
type="text"
value={supplierData.address_postal_code || ''}
onChange={(e) => handleFieldChange('address_postal_code', e.target.value)}
placeholder="Ej: 28001"
className="w-full px-4 py-2.5 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-primary)] mb-1.5">
País
</label>
<input
type="text"
value={supplierData.address_country || ''}
onChange={(e) => handleFieldChange('address_country', e.target.value)}
placeholder="Ej: España"
className="w-full px-4 py-2.5 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>
{/* Validation Message */}
{!isValid && (
<div className="p-3 bg-[var(--color-warning)]/10 border border-[var(--color-warning)]/30 rounded-lg">
<p className="text-sm text-[var(--text-primary)]">
<span className="font-medium"> Campo requerido:</span> Selecciona los términos de pago.
</p>
</div>
)}
{/* Hidden submit button for form handling */}
<button type="submit" className="hidden" disabled={!isValid} />
</form>
);
};

View File

@@ -0,0 +1,225 @@
import React from 'react';
import { FileText, CheckCircle2, Users, Truck, Award } from 'lucide-react';
import type { WizardStepProps } from '../../../ui/WizardModal/WizardModal';
import type { SupplierCreate } from '../../../../api/types/suppliers';
interface SupplierReviewStepProps extends WizardStepProps {
supplierData: Partial<SupplierCreate>;
onUpdate: (data: Partial<SupplierCreate>) => void;
}
export const SupplierReviewStep: React.FC<SupplierReviewStepProps> = ({
supplierData,
onUpdate,
onNext,
onBack
}) => {
const handleFieldChange = (field: keyof SupplierCreate, value: any) => {
onUpdate({ ...supplierData, [field]: value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onNext();
};
const getSupplierTypeLabel = (type?: string) => {
const labels: Record<string, string> = {
'ingredients': 'Ingredientes',
'packaging': 'Empaque',
'equipment': 'Equipo',
'utilities': 'Servicios Públicos',
'services': 'Servicios',
'logistics': 'Logística',
'other': 'Otro'
};
return labels[type || ''] || type || 'No especificado';
};
const getPaymentTermsLabel = (terms?: string) => {
const labels: Record<string, string> = {
'immediate': 'Inmediato',
'net_7': 'Neto 7 días',
'net_15': 'Neto 15 días',
'net_30': 'Neto 30 días',
'net_60': 'Neto 60 días',
'net_90': 'Neto 90 días',
'cod': 'Contra reembolso',
'cia': 'Efectivo por adelantado'
};
return labels[terms || ''] || terms || 'No especificado';
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Header */}
<div className="flex items-start gap-4 pb-4 border-b border-[var(--border-secondary)]">
<div className="w-12 h-12 rounded-full bg-[var(--color-primary)]/10 flex items-center justify-center flex-shrink-0">
<FileText className="w-6 h-6 text-[var(--color-primary)]" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-1">
Detalles Adicionales y Revisión
</h3>
<p className="text-sm text-[var(--text-secondary)]">
Completa información adicional y revisa el proveedor
</p>
</div>
</div>
{/* Supplier Summary */}
<div className="p-4 bg-gradient-to-r from-[var(--color-primary)]/5 to-[var(--color-primary)]/10 border border-[var(--color-primary)]/20 rounded-lg">
<h4 className="text-sm font-semibold text-[var(--text-primary)] mb-3 flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)]" />
Resumen del Proveedor
</h4>
<div className="grid grid-cols-2 gap-4">
{/* Basic Info */}
<div className="space-y-2">
<div className="flex items-start gap-2">
<Users className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Proveedor</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{supplierData.name || 'Sin nombre'}</p>
</div>
</div>
<div className="flex items-start gap-2">
<Users className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Tipo</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{getSupplierTypeLabel(supplierData.supplier_type)}
</p>
</div>
</div>
{supplierData.contact_person && (
<div className="flex items-start gap-2">
<Users className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Contacto</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{supplierData.contact_person}</p>
</div>
</div>
)}
</div>
{/* Delivery Info */}
<div className="space-y-2">
<div className="flex items-start gap-2">
<Truck className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Términos de Pago</p>
<p className="text-sm font-medium text-[var(--text-primary)]">
{getPaymentTermsLabel(supplierData.payment_terms)}
</p>
</div>
</div>
{supplierData.lead_time_days && (
<div className="flex items-start gap-2">
<Truck className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Tiempo de Entrega</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{supplierData.lead_time_days} días</p>
</div>
</div>
)}
{supplierData.minimum_order_value && (
<div className="flex items-start gap-2">
<Truck className="w-4 h-4 text-[var(--text-secondary)] mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-[var(--text-tertiary)]">Pedido Mínimo</p>
<p className="text-sm font-medium text-[var(--text-primary)]">{supplierData.minimum_order_value}</p>
</div>
</div>
)}
</div>
</div>
</div>
{/* Additional Details */}
<div className="space-y-4">
<h4 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
<Award className="w-4 h-4" />
Información Adicional (Opcional)
</h4>
{/* Certifications */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Certificaciones
</label>
<input
type="text"
value={
supplierData.certifications
? typeof supplierData.certifications === 'string'
? supplierData.certifications
: JSON.stringify(supplierData.certifications)
: ''
}
onChange={(e) => {
const value = e.target.value;
handleFieldChange('certifications', value ? { certs: value.split(',').map(c => c.trim()) } : null);
}}
placeholder="Ej: ISO 9001, HACCP, Orgánico"
className="w-full px-4 py-2.5 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)]"
/>
<p className="mt-1 text-xs text-[var(--text-tertiary)]">
Separar con comas
</p>
</div>
{/* Sustainability Practices */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Prácticas de Sostenibilidad
</label>
<textarea
value={
supplierData.sustainability_practices
? typeof supplierData.sustainability_practices === 'string'
? supplierData.sustainability_practices
: JSON.stringify(supplierData.sustainability_practices)
: ''
}
onChange={(e) => {
const value = e.target.value;
handleFieldChange('sustainability_practices', value ? { practices: value } : null);
}}
placeholder="Describe las prácticas sostenibles del proveedor..."
rows={2}
className="w-full px-4 py-2.5 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)] resize-none"
/>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-1.5">
Notas
</label>
<textarea
value={supplierData.notes || ''}
onChange={(e) => handleFieldChange('notes', e.target.value)}
placeholder="Notas adicionales sobre el proveedor..."
rows={3}
className="w-full px-4 py-2.5 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)] resize-none"
/>
</div>
</div>
{/* Ready to Save Message */}
<div className="p-4 bg-[var(--color-success)]/10 border border-[var(--color-success)]/30 rounded-lg">
<p className="text-sm text-[var(--text-primary)] flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-[var(--color-success)]" />
<span>
<span className="font-medium">¡Listo para guardar!</span> Revisa la información y haz clic en "Completar" para crear el proveedor.
</span>
</p>
</div>
{/* Hidden submit button for form handling */}
<button type="submit" className="hidden" />
</form>
);
};

View File

@@ -0,0 +1,155 @@
import React, { useState } from 'react';
import { Users } from 'lucide-react';
import { WizardModal, WizardStep } from '../../../ui/WizardModal/WizardModal';
import { SupplierBasicStep } from './SupplierBasicStep';
import { SupplierDeliveryStep } from './SupplierDeliveryStep';
import { SupplierReviewStep } from './SupplierReviewStep';
import type { SupplierCreate, SupplierType, SupplierStatus, PaymentTerms } from '../../../../api/types/suppliers';
interface SupplierWizardModalProps {
isOpen: boolean;
onClose: () => void;
onCreateSupplier: (supplierData: SupplierCreate) => Promise<void>;
}
export const SupplierWizardModal: React.FC<SupplierWizardModalProps> = ({
isOpen,
onClose,
onCreateSupplier
}) => {
// Supplier state
const [supplierData, setSupplierData] = useState<Partial<SupplierCreate>>({
status: 'pending_approval' as SupplierStatus,
payment_terms: 'net_30' as PaymentTerms,
quality_rating: 0,
delivery_rating: 0,
pricing_rating: 0,
overall_rating: 0
});
const handleUpdate = (data: Partial<SupplierCreate>) => {
setSupplierData(data);
};
const handleComplete = async () => {
try {
// Generate supplier code if not provided
const supplierCode = supplierData.supplier_code ||
(supplierData.name?.substring(0, 3).toUpperCase() || 'SUP') +
String(Date.now()).slice(-3);
// Build final supplier data
const finalSupplierData: SupplierCreate = {
name: supplierData.name!,
supplier_code: supplierCode,
tax_id: supplierData.tax_id || null,
registration_number: supplierData.registration_number || null,
supplier_type: supplierData.supplier_type!,
status: supplierData.status || ('pending_approval' as SupplierStatus),
contact_person: supplierData.contact_person || null,
email: supplierData.email || null,
phone: supplierData.phone || null,
website: supplierData.website || null,
address_street: supplierData.address_street || null,
address_city: supplierData.address_city || null,
address_state: supplierData.address_state || null,
address_postal_code: supplierData.address_postal_code || null,
address_country: supplierData.address_country || null,
payment_terms: supplierData.payment_terms!,
credit_limit: supplierData.credit_limit || null,
currency: supplierData.currency || 'EUR',
tax_rate: supplierData.tax_rate || null,
lead_time_days: supplierData.lead_time_days || null,
minimum_order_value: supplierData.minimum_order_value || null,
delivery_schedule: supplierData.delivery_schedule || null,
preferred_delivery_method: supplierData.preferred_delivery_method || null,
bank_account: supplierData.bank_account || null,
bank_name: supplierData.bank_name || null,
swift_code: supplierData.swift_code || null,
certifications: supplierData.certifications || null,
quality_rating: supplierData.quality_rating || 0,
delivery_rating: supplierData.delivery_rating || 0,
pricing_rating: supplierData.pricing_rating || 0,
overall_rating: supplierData.overall_rating || 0,
sustainability_practices: supplierData.sustainability_practices || null,
insurance_info: supplierData.insurance_info || null,
notes: supplierData.notes || null,
business_hours: supplierData.business_hours || null,
specializations: supplierData.specializations || null
};
await onCreateSupplier(finalSupplierData);
// Reset state
setSupplierData({
status: 'pending_approval' as SupplierStatus,
payment_terms: 'net_30' as PaymentTerms,
quality_rating: 0,
delivery_rating: 0,
pricing_rating: 0,
overall_rating: 0
});
} catch (error) {
console.error('Error creating supplier:', error);
throw error;
}
};
// Define wizard steps
const steps: WizardStep[] = [
{
id: 'basic',
title: 'Información Básica',
description: 'Datos esenciales del proveedor',
component: (props) => (
<SupplierBasicStep
{...props}
supplierData={supplierData}
onUpdate={handleUpdate}
/>
),
validate: () => {
return !!(supplierData.name && supplierData.name.trim().length >= 1 && supplierData.supplier_type);
}
},
{
id: 'delivery',
title: 'Entrega y Términos',
description: 'Términos de pago y entrega',
component: (props) => (
<SupplierDeliveryStep
{...props}
supplierData={supplierData}
onUpdate={handleUpdate}
/>
),
validate: () => {
return !!supplierData.payment_terms;
}
},
{
id: 'review',
title: 'Revisión',
description: 'Detalles adicionales y revisión',
component: (props) => (
<SupplierReviewStep
{...props}
supplierData={supplierData}
onUpdate={handleUpdate}
/>
)
}
];
return (
<WizardModal
isOpen={isOpen}
onClose={onClose}
onComplete={handleComplete}
title="Nuevo Proveedor"
steps={steps}
icon={<Users className="w-6 h-6" />}
size="xl"
/>
);
};

View File

@@ -0,0 +1,4 @@
export { SupplierWizardModal } from './SupplierWizardModal';
export { SupplierBasicStep } from './SupplierBasicStep';
export { SupplierDeliveryStep } from './SupplierDeliveryStep';
export { SupplierReviewStep } from './SupplierReviewStep';