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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export { SupplierWizardModal } from './SupplierWizardModal';
|
||||
export { SupplierBasicStep } from './SupplierBasicStep';
|
||||
export { SupplierDeliveryStep } from './SupplierDeliveryStep';
|
||||
export { SupplierReviewStep } from './SupplierReviewStep';
|
||||
Reference in New Issue
Block a user