Files
bakery-ia/frontend/src/components/domain/suppliers/SupplierWizard/SupplierDeliveryStep.tsx
Claude 877e0b6b47 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.
2025-11-06 18:01:11 +00:00

199 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};