diff --git a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx index 14b80dad..3a226ba0 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect } from 'react'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; +import { AdvancedOptionsSection } from '../../../ui/AdvancedOptionsSection'; +import Tooltip from '../../../ui/Tooltip/Tooltip'; import { Users, Plus, @@ -7,29 +9,15 @@ import { Truck, CreditCard, Search, - CheckCircle2, Calendar, MapPin, + Info, Loader2, + CheckCircle2, } from 'lucide-react'; import { useTenant } from '../../../../stores/tenant.store'; import OrdersService from '../../../../api/services/orders'; import { inventoryService } from '../../../../api/services/inventory'; -import { showToast } from '../../../../utils/toast'; -import { - CustomerCreate, - CustomerType, - DeliveryMethod, - PaymentTerms, - CustomerSegment, - PriorityLevel, - OrderCreate, - OrderItemCreate, - OrderType, - OrderSource, - SalesChannel, - PaymentMethod, -} from '../../../../api/types/orders'; import { ProductType } from '../../../../api/types/inventory'; interface WizardDataProps extends WizardStepProps { @@ -38,7 +26,7 @@ interface WizardDataProps extends WizardStepProps { } // Step 1: Customer Selection -const CustomerSelectionStep: React.FC = ({ data, onDataChange, onNext }) => { +const CustomerSelectionStep: React.FC = ({ data, onDataChange }) => { const { currentTenant } = useTenant(); const [searchQuery, setSearchQuery] = useState(''); const [showNewCustomerForm, setShowNewCustomerForm] = useState(false); @@ -46,19 +34,31 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, const [customers, setCustomers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [creatingCustomer, setCreatingCustomer] = useState(false); const [newCustomer, setNewCustomer] = useState({ - name: '', - type: CustomerType.BUSINESS, - phone: '', - email: '', + name: data.newCustomerName || '', + type: data.newCustomerType || 'retail', + phone: data.newCustomerPhone || '', + email: data.newCustomerEmail || '', }); useEffect(() => { fetchCustomers(); }, []); + // Sync with parent wizard state in real-time + useEffect(() => { + onDataChange({ + ...data, + customer: selectedCustomer, + showNewCustomerForm, + newCustomerName: newCustomer.name, + newCustomerType: newCustomer.type, + newCustomerPhone: newCustomer.phone, + newCustomerEmail: newCustomer.email, + }); + }, [selectedCustomer, showNewCustomerForm, newCustomer]); + const fetchCustomers = async () => { if (!currentTenant?.id) return; @@ -73,7 +73,7 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, setCustomers(result); } catch (err: any) { console.error('Error loading customers:', err); - setError('Error al cargar clientes'); + setError('Error loading customers'); } finally { setLoading(false); } @@ -88,61 +88,15 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, setShowNewCustomerForm(false); }; - const handleContinue = async () => { - if (!currentTenant?.id) { - setError('No se pudo obtener información del tenant'); - return; - } - - if (showNewCustomerForm) { - // Create new customer via API - setCreatingCustomer(true); - setError(null); - - try { - const customerData: CustomerCreate = { - tenant_id: currentTenant.id, - customer_code: `CUST-${Date.now()}`, - name: newCustomer.name, - customer_type: newCustomer.type, - phone: newCustomer.phone, - email: newCustomer.email, - country: 'ES', - is_active: true, - preferred_delivery_method: DeliveryMethod.DELIVERY, - payment_terms: PaymentTerms.IMMEDIATE, - discount_percentage: 0, - customer_segment: CustomerSegment.REGULAR, - priority_level: PriorityLevel.NORMAL, - }; - - const createdCustomer = await OrdersService.createCustomer(customerData); - showToast.success('Cliente creado exitosamente'); - onDataChange({ ...data, customer: createdCustomer, isNewCustomer: true }); - onNext(); - } catch (err: any) { - console.error('Error creating customer:', err); - const errorMessage = err.response?.data?.detail || 'Error al crear el cliente'; - setError(errorMessage); - showToast.error(errorMessage); - } finally { - setCreatingCustomer(false); - } - } else { - onDataChange({ ...data, customer: selectedCustomer, isNewCustomer: false }); - onNext(); - } - }; - return (

- ¿Para quién es este pedido? + Select Customer

- Busca un cliente existente o crea uno nuevo + Search for an existing customer or create a new one

@@ -155,7 +109,7 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, {loading ? (
- Cargando clientes... + Loading customers...
) : !showNewCustomerForm ? ( <> @@ -166,7 +120,7 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, type="text" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} - placeholder="Buscar cliente por nombre..." + placeholder="Search customer by name..." className="w-full pl-10 pr-4 py-3 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)]" />
@@ -176,8 +130,8 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, {filteredCustomers.length === 0 ? (
-

No se encontraron clientes

-

Intenta con otro término de búsqueda

+

No customers found

+

Try a different search term

) : ( filteredCustomers.map((customer) => ( @@ -191,7 +145,6 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, }`} >
- {/* Customer Avatar */}
= ({ data, onDataChange,
- {/* Customer Info */}

= ({ data, onDataChange,

- {/* Customer Type Badge */} - {customer.customer_type === 'wholesale' ? 'Mayorista' : - customer.customer_type === 'restaurant' ? 'Restaurante' : - customer.customer_type === 'event' ? 'Eventos' : 'Minorista'} + {customer.customer_type} - - {/* Phone */} - {customer.phone && ( - - 📱 {customer.phone} - - )} - - {/* Email */} - {customer.email && ( - - ✉️ {customer.email} - - )} + {customer.phone && 📱 {customer.phone}}
- - {/* Additional Info */} - {(customer.city || customer.payment_terms) && ( -
- {customer.city && ( - - - {customer.city} - - )} - {customer.payment_terms && ( - - - {customer.payment_terms === 'immediate' ? 'Pago inmediato' : - customer.payment_terms === 'net_15' ? 'Net 15' : - customer.payment_terms === 'net_30' ? 'Net 30' : customer.payment_terms} - - )} -
- )}
@@ -278,7 +192,7 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, className="w-full p-4 border-2 border-dashed border-[var(--border-secondary)] rounded-lg hover:border-[var(--color-primary)] transition-colors text-[var(--text-secondary)] hover:text-[var(--color-primary)] flex items-center justify-center gap-2" > - Crear nuevo cliente + Create new customer ) : ( @@ -287,42 +201,42 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange,

- Nuevo Cliente + New Customer

setNewCustomer({ ...newCustomer, name: e.target.value })} - placeholder="Ej: Restaurante El Molino" + placeholder="E.g., The Mill Restaurant" 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)]" />
= ({ data, onDataChange,
setNewCustomer({ ...newCustomer, email: e.target.value })} - placeholder="contacto@restaurante.com" + placeholder="contact@restaurant.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)]" />
@@ -351,33 +265,17 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, onClick={() => setShowNewCustomerForm(false)} className="mt-4 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)]" > - ← Volver a la lista de clientes + ← Back to customer list
)} - - {/* Continue Button */} -
- -
); }; // Step 2: Order Items -const OrderItemsStep: React.FC = ({ data, onDataChange, onNext }) => { +const OrderItemsStep: React.FC = ({ data, onDataChange }) => { const { currentTenant } = useTenant(); const [orderItems, setOrderItems] = useState(data.orderItems || []); const [products, setProducts] = useState([]); @@ -388,6 +286,12 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext fetchProducts(); }, []); + // Sync with parent wizard state in real-time + useEffect(() => { + const totalAmount = orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0); + onDataChange({ ...data, orderItems, totalAmount }); + }, [orderItems]); + const fetchProducts = async () => { if (!currentTenant?.id) return; @@ -396,14 +300,13 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext try { const allIngredients = await inventoryService.getIngredients(currentTenant.id); - // Filter for finished products only const finishedProducts = allIngredients.filter( (ingredient) => ingredient.product_type === ProductType.FINISHED_PRODUCT ); setProducts(finishedProducts); } catch (err: any) { console.error('Error loading products:', err); - setError('Error al cargar productos'); + setError('Error loading products'); } finally { setLoading(false); } @@ -412,7 +315,16 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext const handleAddItem = () => { setOrderItems([ ...orderItems, - { id: Date.now(), productId: '', productName: '', quantity: 1, unitPrice: 0, customRequirements: '', subtotal: 0 }, + { + id: Date.now(), + productId: '', + productName: '', + quantity: 1, + unitPrice: 0, + customRequirements: '', + subtotal: 0, + unitOfMeasure: 'units', + }, ]); }; @@ -421,7 +333,6 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext if (i === index) { const newItem = { ...item, [field]: value }; - // If product selected, update price and name if (field === 'productId') { const product = products.find((p) => p.id === value); if (product) { @@ -431,7 +342,6 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext } } - // Auto-calculate subtotal if (field === 'quantity' || field === 'unitPrice' || field === 'productId') { newItem.subtotal = (newItem.quantity || 0) * (newItem.unitPrice || 0); } @@ -450,20 +360,15 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext return orderItems.reduce((sum: number, item: any) => sum + (item.subtotal || 0), 0); }; - const handleContinue = () => { - onDataChange({ ...data, orderItems, totalAmount: calculateTotal() }); - onNext(); - }; - return (

- ¿Qué productos incluye el pedido? + Order Items

- Cliente: {data.customer?.name} + Customer: {data.customer?.name || 'New Customer'}

@@ -476,458 +381,999 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext {loading ? (
- Cargando productos... + Loading products...
) : ( <> - {/* Order Items */}
-
- - -
- - {orderItems.length === 0 ? ( -
- -

No hay productos en el pedido

-

Haz clic en "Agregar Producto" para comenzar

-
- ) : ( -
- {orderItems.map((item: any, index: number) => ( -
+ + +
+ + {orderItems.length === 0 ? ( +
+ +

No products in order

+

Click "Add Product" to start

+
+ ) : ( +
+ {orderItems.map((item: any, index: number) => ( +
- ✕ - -
+
+ + Product #{index + 1} + + +
-
-
- - +
+
+ + +
+ +
+ + handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)} + className="w-full px-3 py-2 text-sm 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)]" + min="0" + step="1" + /> +
+ +
+ + handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)} + className="w-full px-3 py-2 text-sm 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)]" + min="0" + step="0.01" + /> +
+ +
+ + handleUpdateItem(index, 'customRequirements', e.target.value)} + placeholder="E.g., No nuts, extra chocolate..." + className="w-full px-3 py-2 text-sm 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)]" + /> +
+
+ +
+ + Subtotal: €{item.subtotal.toFixed(2)} + +
+ ))} +
+ )} -
- - handleUpdateItem(index, 'quantity', parseFloat(e.target.value) || 0)} - className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" - min="0" - step="1" - /> -
- -
- - handleUpdateItem(index, 'unitPrice', parseFloat(e.target.value) || 0)} - className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" - min="0" - step="0.01" - /> -
- -
- - handleUpdateItem(index, 'customRequirements', e.target.value)} - placeholder="Ej: Sin nueces, extra chocolate..." - className="w-full px-3 py-2 text-sm border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]" - /> -
-
- -
- - Subtotal: €{item.subtotal.toFixed(2)} + {orderItems.length > 0 && ( +
+
+ Order Total: + + €{calculateTotal().toFixed(2)}
- ))} + )}
- )} - - {/* Total */} - {orderItems.length > 0 && ( -
-
- Total del Pedido: - - €{calculateTotal().toFixed(2)} - -
-
- )} -
)} - - {/* Continue Button */} -
- -
); }; -// Step 3: Delivery & Payment -const DeliveryPaymentStep: React.FC = ({ data, onDataChange, onComplete }) => { - const { currentTenant } = useTenant(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [deliveryData, setDeliveryData] = useState({ - deliveryDate: data.deliveryDate || '', - deliveryTime: data.deliveryTime || '', +// Step 3: Delivery & Payment with ALL fields +const DeliveryPaymentStep: React.FC = ({ data, onDataChange }) => { + const [orderData, setOrderData] = useState({ + // Required fields + requestedDeliveryDate: data.requestedDeliveryDate || '', + orderNumber: data.orderNumber || '', + + // Basic order info + orderType: data.orderType || 'standard', + priority: data.priority || 'normal', + status: data.status || 'pending', + + // Delivery fields deliveryMethod: data.deliveryMethod || 'pickup', deliveryAddress: data.deliveryAddress || '', + deliveryInstructions: data.deliveryInstructions || '', + deliveryContactName: data.deliveryContactName || '', + deliveryContactPhone: data.deliveryContactPhone || '', + deliveryTimeWindow: data.deliveryTimeWindow || '', + deliveryFee: data.deliveryFee || '', + + // Payment fields paymentMethod: data.paymentMethod || 'invoice', + paymentTerms: data.paymentTerms || 'net_30', + paymentStatus: data.paymentStatus || 'pending', + paymentDueDate: data.paymentDueDate || '', + + // Pricing fields + subtotalAmount: data.subtotalAmount || '', + taxAmount: data.taxAmount || '', + discountPercentage: data.discountPercentage || '', + discountAmount: data.discountAmount || '', + shippingCost: data.shippingCost || '', + + // Production & scheduling + productionStartDate: data.productionStartDate || '', + productionDueDate: data.productionDueDate || '', + productionBatchNumber: data.productionBatchNumber || '', + productionNotes: data.productionNotes || '', + + // Fulfillment + actualDeliveryDate: data.actualDeliveryDate || '', + pickupLocation: data.pickupLocation || '', + shippingTrackingNumber: data.shippingTrackingNumber || '', + shippingCarrier: data.shippingCarrier || '', + + // Source & channel + orderSource: data.orderSource || 'manual', + salesChannel: data.salesChannel || 'direct', + salesRepId: data.salesRepId || '', + + // Communication + customerPurchaseOrder: data.customerPurchaseOrder || '', + internalNotes: data.internalNotes || '', + customerNotes: data.customerNotes || '', specialInstructions: data.specialInstructions || '', - orderStatus: data.orderStatus || 'pending', + + // Notifications + notifyCustomerOnStatusChange: data.notifyCustomerOnStatusChange ?? true, + notifyCustomerOnDelivery: data.notifyCustomerOnDelivery ?? true, + customerNotificationEmail: data.customerNotificationEmail || '', + customerNotificationPhone: data.customerNotificationPhone || '', + + // Quality & requirements + qualityCheckRequired: data.qualityCheckRequired ?? false, + qualityCheckStatus: data.qualityCheckStatus || '', + packagingInstructions: data.packagingInstructions || '', + labelingRequirements: data.labelingRequirements || '', + + // Advanced options + isRecurring: data.isRecurring ?? false, + recurringSchedule: data.recurringSchedule || '', + parentOrderId: data.parentOrderId || '', + relatedOrderIds: data.relatedOrderIds || '', + tags: data.tags || '', + metadata: data.metadata || '', }); - const handleConfirm = async () => { - if (!currentTenant?.id) { - setError('No se pudo obtener información del tenant'); - return; + // Auto-generate order number if not provided + useEffect(() => { + if (!orderData.orderNumber) { + const orderNum = `ORD-${Date.now().toString().slice(-8)}`; + setOrderData(prev => ({ ...prev, orderNumber: orderNum })); } + }, []); - setLoading(true); - setError(null); - - try { - // Map UI delivery method to API enum - const deliveryMethodMap: Record = { - pickup: DeliveryMethod.PICKUP, - delivery: DeliveryMethod.DELIVERY, - shipping: DeliveryMethod.DELIVERY, - }; - - // Map UI payment method to API enum - const paymentMethodMap: Record = { - cash: PaymentMethod.CASH, - card: PaymentMethod.CARD, - transfer: PaymentMethod.BANK_TRANSFER, - invoice: PaymentMethod.ACCOUNT, - 'invoice-30': PaymentMethod.ACCOUNT, - paid: PaymentMethod.CARD, - }; - - // Map UI payment method to payment terms - const paymentTermsMap: Record = { - cash: PaymentTerms.IMMEDIATE, - card: PaymentTerms.IMMEDIATE, - transfer: PaymentTerms.IMMEDIATE, - invoice: PaymentTerms.NET_30, - 'invoice-30': PaymentTerms.NET_30, - paid: PaymentTerms.IMMEDIATE, - }; - - // Prepare order items - const orderItems: OrderItemCreate[] = data.orderItems.map((item: any) => ({ - product_id: item.productId, - product_name: item.productName, - quantity: item.quantity, - unit_of_measure: item.unitOfMeasure || 'unit', - unit_price: item.unitPrice, - line_discount: 0, - customization_details: item.customRequirements || undefined, - special_instructions: item.customRequirements || undefined, - })); - - // Prepare delivery address - const deliveryAddress = (deliveryData.deliveryMethod === 'delivery' || deliveryData.deliveryMethod === 'shipping') - ? { address: deliveryData.deliveryAddress } - : undefined; - - // Create order data - const orderData: OrderCreate = { - tenant_id: currentTenant.id, - customer_id: data.customer.id, - order_type: OrderType.STANDARD, - priority: PriorityLevel.NORMAL, - requested_delivery_date: deliveryData.deliveryDate, - delivery_method: deliveryMethodMap[deliveryData.deliveryMethod], - delivery_address: deliveryAddress, - delivery_instructions: deliveryData.specialInstructions || undefined, - discount_percentage: 0, - delivery_fee: 0, - payment_method: paymentMethodMap[deliveryData.paymentMethod], - payment_terms: paymentTermsMap[deliveryData.paymentMethod], - special_instructions: deliveryData.specialInstructions || undefined, - order_source: OrderSource.MANUAL, - sales_channel: SalesChannel.DIRECT, - items: orderItems, - }; - - await OrdersService.createOrder(orderData); - - showToast.success('Pedido creado exitosamente'); - onDataChange({ ...data, ...deliveryData }); - onComplete(); - } catch (err: any) { - console.error('Error creating order:', err); - const errorMessage = err.response?.data?.detail || 'Error al crear el pedido'; - setError(errorMessage); - showToast.error(errorMessage); - } finally { - setLoading(false); - } - }; + // Sync with parent wizard state in real-time + useEffect(() => { + onDataChange({ ...data, ...orderData }); + }, [orderData]); return (

- Entrega y Pago + Delivery & Payment Details

- Configura los detalles de entrega y forma de pago + Configure delivery, payment, and order details

- {error && ( -
- {error} -
- )} - -
- {/* Delivery Date & Time */} -
-
- - setDeliveryData({ ...deliveryData, deliveryDate: e.target.value })} - min={new Date().toISOString().split('T')[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)]" - /> -
- -
- - setDeliveryData({ ...deliveryData, deliveryTime: 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)]" - /> -
-
- - {/* Delivery Method */} + {/* Required Fields */} +
-
- - - -
-
- - {/* Delivery Address (conditional) */} - {(deliveryData.deliveryMethod === 'delivery' || deliveryData.deliveryMethod === 'shipping') && ( -
- -