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
-
+
);
};
+interface SelectedIngredient {
+ id: string;
+ ingredientId: string;
+ quantity: number;
+ unit: MeasurementUnit;
+ notes: string;
+ order: number;
+}
+
const IngredientsStep: React.FC = ({ data, onDataChange, onComplete }) => {
+ const { currentTenant } = useTenant();
+ const [ingredients, setIngredients] = useState([]);
+ const [selectedIngredients, setSelectedIngredients] = useState(data.ingredients || []);
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [error, setError] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+
+ useEffect(() => {
+ fetchIngredients();
+ }, []);
+
+ const fetchIngredients = async () => {
+ if (!currentTenant?.id) return;
+ setLoading(true);
+ try {
+ const result = await inventoryService.getIngredients(currentTenant.id);
+ // Filter out finished products - we only want raw ingredients
+ const rawIngredients = result.filter(ing => ing.category !== 'finished_product');
+ setIngredients(rawIngredients);
+ } catch (err) {
+ setError('Error al cargar ingredientes');
+ console.error('Error loading ingredients:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleAddIngredient = () => {
+ const newIngredient: SelectedIngredient = {
+ id: Date.now().toString(),
+ ingredientId: '',
+ quantity: 0,
+ unit: MeasurementUnit.GRAMS,
+ notes: '',
+ order: selectedIngredients.length + 1,
+ };
+ setSelectedIngredients([...selectedIngredients, newIngredient]);
+ };
+
+ const handleUpdateIngredient = (id: string, field: keyof SelectedIngredient, value: any) => {
+ setSelectedIngredients(
+ selectedIngredients.map(ing =>
+ ing.id === id ? { ...ing, [field]: value } : ing
+ )
+ );
+ };
+
+ const handleRemoveIngredient = (id: string) => {
+ setSelectedIngredients(selectedIngredients.filter(ing => ing.id !== id));
+ };
+
+ const handleSaveRecipe = async () => {
+ if (!currentTenant?.id) {
+ setError('No se pudo obtener información del tenant');
+ return;
+ }
+
+ if (selectedIngredients.length === 0) {
+ setError('Debes agregar al menos un ingrediente');
+ return;
+ }
+
+ // Validate all ingredients are filled
+ const invalidIngredients = selectedIngredients.filter(
+ ing => !ing.ingredientId || ing.quantity <= 0
+ );
+ if (invalidIngredients.length > 0) {
+ setError('Todos los ingredientes deben tener un ingrediente seleccionado y cantidad mayor a 0');
+ return;
+ }
+
+ setSaving(true);
+ setError(null);
+
+ try {
+ // Prepare recipe data according to RecipeCreate interface
+ const recipeIngredients: RecipeIngredientCreate[] = selectedIngredients.map((ing, index) => ({
+ ingredient_id: ing.ingredientId,
+ quantity: ing.quantity,
+ unit: ing.unit,
+ ingredient_notes: ing.notes || null,
+ is_optional: false,
+ ingredient_order: index + 1,
+ }));
+
+ const recipeData: RecipeCreate = {
+ name: data.name,
+ category: data.category,
+ finished_product_id: data.finishedProductId,
+ yield_quantity: parseFloat(data.yieldQuantity),
+ yield_unit: data.yieldUnit as MeasurementUnit,
+ prep_time_minutes: data.prepTime ? parseInt(data.prepTime) : null,
+ instructions: data.instructions ? { steps: data.instructions } : null,
+ ingredients: recipeIngredients,
+ };
+
+ await recipesService.createRecipe(currentTenant.id, recipeData);
+ onDataChange({ ...data, ingredients: selectedIngredients });
+ onComplete();
+ } catch (err: any) {
+ console.error('Error creating recipe:', err);
+ setError(err.response?.data?.detail || 'Error al crear la receta');
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const filteredIngredients = ingredients.filter(ing =>
+ ing.name.toLowerCase().includes(searchTerm.toLowerCase())
+ );
+
return (
@@ -54,12 +280,132 @@ const IngredientsStep: React.FC
= ({ data, onDataChange, onComp
Ingredientes
{data.name}
-
-
La selección de ingredientes será agregada en una mejora futura
-
Por ahora, puedes crear la receta y agregar ingredientes después
-
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {loading ? (
+
+
+ Cargando ingredientes...
+
+ ) : (
+ <>
+
+ {selectedIngredients.length === 0 ? (
+
+
+
No hay ingredientes agregados
+
Haz clic en "Agregar Ingrediente" para comenzar
+
+ ) : (
+ selectedIngredients.map((selectedIng) => (
+
+
+
+
+
+
+
+
+
+
+
+ handleUpdateIngredient(selectedIng.id, 'quantity', parseFloat(e.target.value) || 0)}
+ placeholder="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)] text-sm"
+ min="0"
+ step="0.01"
+ />
+
+
+
+
+
+
+
+ handleUpdateIngredient(selectedIng.id, 'notes', e.target.value)}
+ placeholder="Opcional"
+ 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)] text-sm"
+ />
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ >
+ )}
+
-
+
);
@@ -67,5 +413,5 @@ const IngredientsStep: React.FC = ({ data, onDataChange, onComp
export const RecipeWizardSteps = (data: Record, setData: (data: Record) => void): WizardStep[] => [
{ id: 'recipe-details', title: 'Detalles de la Receta', description: 'Nombre, categoría, rendimiento', component: (props) => },
- { id: 'recipe-ingredients', title: 'Ingredientes', description: 'Configuración futura', component: (props) => , isOptional: true },
+ { id: 'recipe-ingredients', title: 'Ingredientes', description: 'Selección y cantidades', component: (props) => },
];