From f929d88272a9f3f4668b27cd12b9eac90fe1f896 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 09:48:17 +0000 Subject: [PATCH] feat: Complete final 2 wizards - Customer Order and Recipe with full API integration --- .../wizards/CustomerOrderWizard.tsx | 309 +++++++++++++-- .../unified-wizard/wizards/RecipeWizard.tsx | 372 +++++++++++++++++- 2 files changed, 628 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx index 6d4a4300..7374dec0 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/CustomerOrderWizard.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; import { Users, @@ -10,7 +10,26 @@ import { CheckCircle2, Calendar, MapPin, + Loader2, } from 'lucide-react'; +import { useTenant } from '../../../../stores/tenant.store'; +import OrdersService from '../../../../api/services/orders'; +import { inventoryService } from '../../../../api/services/inventory'; +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 { data: Record; @@ -19,25 +38,47 @@ interface WizardDataProps extends WizardStepProps { // Step 1: Customer Selection const CustomerSelectionStep: React.FC = ({ data, onDataChange, onNext }) => { + const { currentTenant } = useTenant(); const [searchQuery, setSearchQuery] = useState(''); const [showNewCustomerForm, setShowNewCustomerForm] = useState(false); const [selectedCustomer, setSelectedCustomer] = useState(data.customer || null); - - // Mock customer data - replace with actual API call - const mockCustomers = [ - { id: 1, name: 'Restaurante El Molino', type: 'Mayorista', phone: '+34 123 456 789' }, - { id: 2, name: 'Cafetería Central', type: 'Minorista', phone: '+34 987 654 321' }, - { id: 3, name: 'Hotel Vista Mar', type: 'Eventos', phone: '+34 555 123 456' }, - ]; + 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: 'retail', + type: CustomerType.BUSINESS, phone: '', email: '', }); - const filteredCustomers = mockCustomers.filter((customer) => + useEffect(() => { + fetchCustomers(); + }, []); + + const fetchCustomers = async () => { + if (!currentTenant?.id) return; + + setLoading(true); + setError(null); + + try { + const result = await OrdersService.getCustomers({ + tenant_id: currentTenant.id, + active_only: true, + }); + setCustomers(result); + } catch (err: any) { + console.error('Error loading customers:', err); + setError('Error al cargar clientes'); + } finally { + setLoading(false); + } + }; + + const filteredCustomers = customers.filter((customer) => customer.name.toLowerCase().includes(searchQuery.toLowerCase()) ); @@ -46,13 +87,47 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, setShowNewCustomerForm(false); }; - const handleContinue = () => { + const handleContinue = async () => { + if (!currentTenant?.id) { + setError('No se pudo obtener información del tenant'); + return; + } + if (showNewCustomerForm) { - onDataChange({ ...data, customer: newCustomer, isNewCustomer: true }); + // 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); + onDataChange({ ...data, customer: createdCustomer, isNewCustomer: true }); + onNext(); + } catch (err: any) { + console.error('Error creating customer:', err); + setError(err.response?.data?.detail || 'Error al crear el cliente'); + } finally { + setCreatingCustomer(false); + } } else { onDataChange({ ...data, customer: selectedCustomer, isNewCustomer: false }); + onNext(); } - onNext(); }; return ( @@ -67,7 +142,18 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange,

- {!showNewCustomerForm ? ( + {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+ + Cargando clientes... +
+ ) : !showNewCustomerForm ? ( <> {/* Search Bar */}
@@ -97,7 +183,7 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange,

{customer.name}

- {customer.type} • {customer.phone} + {customer.customer_type} • {customer.phone || 'Sin teléfono'}

{selectedCustomer?.id === customer.id && ( @@ -197,10 +283,15 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange,
@@ -209,15 +300,36 @@ const CustomerSelectionStep: React.FC = ({ data, onDataChange, // Step 2: Order Items const OrderItemsStep: React.FC = ({ data, onDataChange, onNext }) => { + const { currentTenant } = useTenant(); const [orderItems, setOrderItems] = useState(data.orderItems || []); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); - // Mock product data - replace with actual API call - const mockProducts = [ - { id: 1, name: 'Baguette Tradicional', price: 1.5, unit: 'unidad' }, - { id: 2, name: 'Croissant de Mantequilla', price: 2.0, unit: 'unidad' }, - { id: 3, name: 'Pan Integral', price: 2.5, unit: 'kg' }, - { id: 4, name: 'Tarta de Manzana', price: 18.0, unit: 'unidad' }, - ]; + useEffect(() => { + fetchProducts(); + }, []); + + const fetchProducts = async () => { + if (!currentTenant?.id) return; + + setLoading(true); + setError(null); + + 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'); + } finally { + setLoading(false); + } + }; const handleAddItem = () => { setOrderItems([ @@ -233,10 +345,11 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext // If product selected, update price and name if (field === 'productId') { - const product = mockProducts.find((p) => p.id === parseInt(value)); + const product = products.find((p) => p.id === value); if (product) { newItem.productName = product.name; - newItem.unitPrice = product.price; + newItem.unitPrice = product.average_cost || product.last_purchase_price || 0; + newItem.unitOfMeasure = product.unit_of_measure; } } @@ -276,8 +389,21 @@ const OrderItemsStep: React.FC = ({ data, onDataChange, onNext

- {/* Order Items */} -
+ {error && ( +
+ {error} +
+ )} + + {loading ? ( +
+ + Cargando productos... +
+ ) : ( + <> + {/* Order Items */} +
)}
+ + )} {/* Continue Button */}
+ {error && ( +
+ {error} +
+ )} +
{/* Delivery Date & Time */}
@@ -611,11 +827,24 @@ const DeliveryPaymentStep: React.FC = ({ data, onDataChange, on
diff --git a/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx index 7c12b7b3..1cc68908 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx @@ -1,6 +1,11 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { WizardStep, WizardStepProps } from '../../../ui/WizardModal/WizardModal'; -import { ChefHat, Package, ClipboardCheck, CheckCircle2 } from 'lucide-react'; +import { ChefHat, Package, ClipboardCheck, CheckCircle2, Loader2, Plus, X, Search } from 'lucide-react'; +import { useTenant } from '../../../../stores/tenant.store'; +import { recipesService } from '../../../../api/services/recipes'; +import { inventoryService } from '../../../../api/services/inventory'; +import { IngredientResponse } from '../../../../api/types/inventory'; +import { RecipeCreate, RecipeIngredientCreate, MeasurementUnit } from '../../../../api/types/recipes'; interface WizardDataProps extends WizardStepProps { data: Record; @@ -8,12 +13,37 @@ interface WizardDataProps extends WizardStepProps { } const RecipeDetailsStep: React.FC = ({ data, onDataChange, onNext }) => { + const { currentTenant } = useTenant(); const [recipeData, setRecipeData] = useState({ name: data.name || '', category: data.category || 'bread', - yield: data.yield || '', + yieldQuantity: data.yieldQuantity || '', + yieldUnit: data.yieldUnit || 'units', + prepTime: data.prepTime || '', + finishedProductId: data.finishedProductId || '', instructions: data.instructions || '', }); + const [finishedProducts, setFinishedProducts] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + fetchFinishedProducts(); + }, []); + + const fetchFinishedProducts = async () => { + if (!currentTenant?.id) return; + setLoading(true); + try { + const result = await inventoryService.getIngredients(currentTenant.id, { + category: 'finished_product' + }); + setFinishedProducts(result); + } catch (err) { + console.error('Error loading finished products:', err); + } finally { + setLoading(false); + } + }; return (
@@ -24,29 +54,225 @@ const RecipeDetailsStep: React.FC = ({ data, onDataChange, onNe
- setRecipeData({ ...recipeData, name: e.target.value })} placeholder="Ej: Baguette Tradicional" 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)]" /> + setRecipeData({ ...recipeData, name: e.target.value })} + placeholder="Ej: Baguette Tradicional" + 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)]" + />
- setRecipeData({ ...recipeData, category: 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)]" + > + + + +
+
+ +
- setRecipeData({ ...recipeData, yield: e.target.value })} placeholder="12 unidades" 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)]" min="1" /> + setRecipeData({ ...recipeData, yieldQuantity: e.target.value })} + placeholder="12" + 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)]" + min="1" + /> +
+
+ + +
+
+ + setRecipeData({ ...recipeData, prepTime: e.target.value })} + placeholder="60" + 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)]" + min="0" + /> +
+
+ +