import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Sparkles } from 'lucide-react'; import { WizardModal, WizardStep } from '../../ui/WizardModal/WizardModal'; import { ItemTypeSelector, ItemType } from './ItemTypeSelector'; import { AnyWizardData } from './types'; import { useTenant } from '../../../stores/tenant.store'; import { useCreatePurchaseOrder } from '../../../api/hooks/purchase-orders'; import { useCreateProductionBatch } from '../../../api/hooks/production'; import { useCreateIngredient, useAddStock } from '../../../api/hooks/inventory'; import { useCreateSupplier } from '../../../api/hooks/suppliers'; import { toast } from 'react-hot-toast'; import type { ProductionBatchCreate } from '../../../api/types/production'; import type { IngredientCreate, StockCreate } from '../../../api/types/inventory'; import type { SupplierCreate } from '../../../api/types/suppliers'; import { ProductType } from '../../../api/types/inventory'; import { SupplierType, PaymentTerms } from '../../../api/types/suppliers'; import { ProductionPriorityEnum } from '../../../api/types/production'; // Import specific wizards import { InventoryWizardSteps } from './wizards/InventoryWizard'; import { SupplierWizardSteps } from './wizards/SupplierWizard'; import { RecipeWizardSteps } from './wizards/RecipeWizard'; import { EquipmentWizardSteps } from './wizards/EquipmentWizard'; import { QualityTemplateWizardSteps } from './wizards/QualityTemplateWizard'; import { CustomerOrderWizardSteps } from './wizards/CustomerOrderWizard'; import { CustomerWizardSteps } from './wizards/CustomerWizard'; import { TeamMemberWizardSteps } from './wizards/TeamMemberWizard'; import { SalesEntryWizardSteps } from './wizards/SalesEntryWizard'; import { PurchaseOrderWizardSteps } from './wizards/PurchaseOrderWizard'; import { ProductionBatchWizardSteps } from './wizards/ProductionBatchWizard'; // Services for wizards that handle their own submission import { equipmentService } from '../../../api/services/equipment'; import { qualityTemplateService } from '../../../api/services/qualityTemplates'; import OrdersService from '../../../api/services/orders'; import { salesService } from '../../../api/services/sales'; import { recipesService } from '../../../api/services/recipes'; import { authService } from '../../../api/services/auth'; interface UnifiedAddWizardProps { isOpen: boolean; onClose: () => void; onComplete?: (itemType: ItemType, data?: any) => void; initialItemType?: ItemType; } export const UnifiedAddWizard: React.FC = ({ isOpen, onClose, onComplete, initialItemType, }) => { const [selectedItemType, setSelectedItemType] = useState( initialItemType || null ); const [wizardData, setWizardData] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const { currentTenant } = useTenant(); // API hooks for wizards with proper hook support const createPurchaseOrderMutation = useCreatePurchaseOrder(); const createProductionBatchMutation = useCreateProductionBatch(); const createIngredientMutation = useCreateIngredient(); const addStockMutation = useAddStock(); const createSupplierMutation = useCreateSupplier(); const dataRef = useRef({}); useEffect(() => { dataRef.current = wizardData; }, [wizardData]); const handleClose = useCallback(() => { setSelectedItemType(initialItemType || null); setWizardData({}); dataRef.current = {}; setIsSubmitting(false); onClose(); }, [onClose, initialItemType]); const handleItemTypeSelect = useCallback((itemType: ItemType) => { setSelectedItemType(itemType); }, []); const handleDataChange = useCallback((newData: AnyWizardData) => { dataRef.current = newData; setWizardData(newData); }, []); // Centralized wizard completion handler // All API submissions are handled here for consistency const handleWizardComplete = useCallback( async () => { if (!selectedItemType || !currentTenant?.id) { return; } setIsSubmitting(true); try { const finalData = dataRef.current as any; // ======================================== // PURCHASE ORDER // ======================================== if (selectedItemType === 'purchase-order') { if ((finalData.items || []).some((item: any) => Number(item.ordered_quantity) < 0.01 || Number(item.unit_price) < 0.01 )) { throw new Error('Todos los productos deben tener cantidad y precio mayor a 0'); } const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00'); if (isNaN(deliveryDate.getTime())) { throw new Error('Fecha de entrega inválida'); } await createPurchaseOrderMutation.mutateAsync({ tenantId: currentTenant.id, data: { supplier_id: finalData.supplier_id, required_delivery_date: deliveryDate.toISOString(), priority: finalData.priority || 'normal', tax_amount: Number(finalData.tax_amount) || 0, shipping_cost: Number(finalData.shipping_cost) || 0, discount_amount: Number(finalData.discount_amount) || 0, notes: finalData.notes || undefined, items: (finalData.items || []).map((item: any) => ({ inventory_product_id: item.inventory_product_id, ordered_quantity: Number(item.ordered_quantity), unit_price: Number(item.unit_price), unit_of_measure: item.unit_of_measure, })), }, }); toast.success('Orden de compra creada exitosamente'); } // ======================================== // PRODUCTION BATCH // ======================================== if (selectedItemType === 'production-batch') { if (Number(finalData.planned_quantity) < 0.01) { throw new Error('La cantidad planificada debe ser mayor a 0'); } if (Number(finalData.planned_duration_minutes) < 1) { throw new Error('La duración planificada debe ser mayor a 0'); } const staffArray = finalData.staff_assigned_string ? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0) : []; const plannedStartDate = new Date(finalData.planned_start_time); const plannedEndDate = new Date(finalData.planned_end_time); if (isNaN(plannedStartDate.getTime()) || isNaN(plannedEndDate.getTime())) { throw new Error('Fechas de inicio o fin inválidas'); } if (plannedEndDate <= plannedStartDate) { throw new Error('La fecha de fin debe ser posterior a la fecha de inicio'); } const batchData: ProductionBatchCreate = { product_id: finalData.product_id, product_name: finalData.product_name, recipe_id: finalData.recipe_id || undefined, planned_start_time: plannedStartDate.toISOString(), planned_end_time: plannedEndDate.toISOString(), planned_quantity: Number(finalData.planned_quantity), planned_duration_minutes: Number(finalData.planned_duration_minutes), priority: finalData.priority || ProductionPriorityEnum.MEDIUM, is_rush_order: finalData.is_rush_order || false, is_special_recipe: finalData.is_special_recipe || false, production_notes: finalData.production_notes || undefined, batch_number: finalData.batch_number || undefined, order_id: finalData.order_id || undefined, forecast_id: finalData.forecast_id || undefined, equipment_used: [], staff_assigned: staffArray, station_id: finalData.station_id || undefined, }; await createProductionBatchMutation.mutateAsync({ tenantId: currentTenant.id, batchData, }); toast.success('Lote de producción creado exitosamente'); } // ======================================== // INVENTORY (INGREDIENT) // ======================================== if (selectedItemType === 'inventory') { if (!finalData.name || finalData.name.trim().length < 2) { throw new Error('El nombre del producto debe tener al menos 2 caracteres'); } const category = finalData.productType === 'ingredient' ? finalData.ingredientCategory : finalData.productCategory; if (!category) { throw new Error('Debes seleccionar una categoría'); } // Convert allergenInfo string to array format expected by backend let allergenInfoArray: string[] | undefined = undefined; if (finalData.allergenInfo && typeof finalData.allergenInfo === 'string') { // Parse comma-separated allergens into array allergenInfoArray = finalData.allergenInfo .split(',') .map((a: string) => a.trim().toLowerCase()) .filter((a: string) => a.length > 0); } else if (Array.isArray(finalData.allergenInfo)) { allergenInfoArray = finalData.allergenInfo; } const ingredientData: IngredientCreate = { name: finalData.name.trim(), product_type: finalData.productType === 'finished_product' ? ProductType.FINISHED_PRODUCT : ProductType.INGREDIENT, sku: finalData.sku || undefined, barcode: finalData.barcode || undefined, category: category, description: finalData.description || undefined, brand: finalData.brand || undefined, unit_of_measure: finalData.unitOfMeasure || 'units', package_size: finalData.packageSize ? Number(finalData.packageSize) : undefined, shelf_life_days: finalData.shelfLifeDays ? Number(finalData.shelfLifeDays) : undefined, is_perishable: finalData.isPerishable ?? true, allergen_info: allergenInfoArray && allergenInfoArray.length > 0 ? { allergens: allergenInfoArray } : undefined, }; const createdIngredient = await createIngredientMutation.mutateAsync({ tenantId: currentTenant.id, ingredientData, }); const initialLots = finalData.initialLots || []; if (initialLots.length > 0 && createdIngredient?.id) { for (const lot of initialLots) { if (lot.quantity && Number(lot.quantity) > 0) { const stockData: StockCreate = { ingredient_id: createdIngredient.id, current_quantity: Number(lot.quantity), unit_cost: lot.unitCost ? Number(lot.unitCost) : undefined, lot_number: lot.lotNumber || undefined, expiration_date: lot.expirationDate || undefined, storage_location: lot.location || undefined, }; await addStockMutation.mutateAsync({ tenantId: currentTenant.id, stockData, }); } } } toast.success('Producto de inventario creado exitosamente'); } // ======================================== // SUPPLIER // ======================================== if (selectedItemType === 'supplier') { if (!finalData.name || finalData.name.trim().length < 1) { throw new Error('El nombre del proveedor es requerido'); } if (!finalData.supplierType) { throw new Error('El tipo de proveedor es requerido'); } // Convert comma-separated certifications string to array let certificationsArray: string[] | undefined = undefined; if (finalData.certifications && typeof finalData.certifications === 'string') { certificationsArray = finalData.certifications .split(',') .map((c: string) => c.trim()) .filter((c: string) => c.length > 0); } else if (Array.isArray(finalData.certifications)) { certificationsArray = finalData.certifications; } // Convert comma-separated specializations string to array let specializationsArray: string[] | undefined = undefined; if (finalData.specializations && typeof finalData.specializations === 'string') { specializationsArray = finalData.specializations .split(',') .map((s: string) => s.trim()) .filter((s: string) => s.length > 0); } else if (Array.isArray(finalData.specializations)) { specializationsArray = finalData.specializations; } const supplierData: SupplierCreate = { name: finalData.name.trim(), supplier_type: finalData.supplierType as SupplierType, supplier_code: finalData.supplierCode || undefined, tax_id: finalData.taxId || undefined, registration_number: finalData.registrationNumber || undefined, contact_person: finalData.contactPerson || undefined, email: finalData.email || undefined, phone: finalData.phone || undefined, mobile: finalData.mobile || undefined, website: finalData.website || undefined, address_line1: finalData.addressLine1 || undefined, address_line2: finalData.addressLine2 || undefined, city: finalData.city || undefined, state_province: finalData.stateProvince || undefined, postal_code: finalData.postalCode || undefined, country: finalData.country || undefined, payment_terms: (finalData.paymentTerms as PaymentTerms) || PaymentTerms.NET_30, credit_limit: finalData.creditLimit ? Number(finalData.creditLimit) : undefined, currency: finalData.currency || 'EUR', standard_lead_time: finalData.standardLeadTime ? Number(finalData.standardLeadTime) : 3, minimum_order_amount: finalData.minimumOrderAmount ? Number(finalData.minimumOrderAmount) : undefined, delivery_area: finalData.deliveryArea || undefined, notes: finalData.notes || undefined, certifications: certificationsArray && certificationsArray.length > 0 ? certificationsArray : undefined, specializations: specializationsArray && specializationsArray.length > 0 ? specializationsArray : undefined, }; await createSupplierMutation.mutateAsync({ tenantId: currentTenant.id, supplierData, }); toast.success('Proveedor creado exitosamente'); } // ======================================== // EQUIPMENT (using service directly) // ======================================== if (selectedItemType === 'equipment') { if (!finalData.name || finalData.name.trim().length < 2) { throw new Error('El nombre del equipo es requerido'); } if (!finalData.type) { throw new Error('El tipo de equipo es requerido'); } const equipmentData = { name: finalData.name.trim(), type: finalData.type, model: finalData.model || undefined, location: finalData.location || undefined, status: 'operational', install_date: finalData.installDate || new Date().toISOString().split('T')[0], last_maintenance_date: new Date().toISOString().split('T')[0], next_maintenance_date: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], maintenance_interval_days: 30, is_active: true, }; await equipmentService.createEquipment(currentTenant.id, equipmentData as any); toast.success('Equipo creado exitosamente'); } // ======================================== // QUALITY TEMPLATE (using service directly) // ======================================== if (selectedItemType === 'quality-template') { if (!finalData.name || finalData.name.trim().length < 1) { throw new Error('El nombre de la plantilla es requerido'); } if (!finalData.checkType) { throw new Error('El tipo de control es requerido'); } const templateData = { name: finalData.name.trim(), check_type: finalData.checkType, template_code: finalData.templateCode || undefined, category: finalData.category || undefined, description: finalData.description || undefined, weight: finalData.weight ? Number(finalData.weight) : 1, applicable_stages: finalData.applicableStages || [], min_value: finalData.minValue ? Number(finalData.minValue) : undefined, max_value: finalData.maxValue ? Number(finalData.maxValue) : undefined, target_value: finalData.targetValue ? Number(finalData.targetValue) : undefined, unit: finalData.unit || undefined, tolerance_percentage: finalData.tolerancePercentage ? Number(finalData.tolerancePercentage) : undefined, instructions: finalData.instructions || undefined, is_required: finalData.isRequired ?? true, is_active: finalData.isActive ?? true, requires_photo: finalData.requiresPhoto ?? false, is_critical: finalData.isCritical ?? false, created_by: currentTenant.id, }; await qualityTemplateService.createTemplate(currentTenant.id, templateData as any); toast.success('Plantilla de calidad creada exitosamente'); } // ======================================== // CUSTOMER (using service directly) // ======================================== if (selectedItemType === 'customer') { if (!finalData.name || finalData.name.trim().length < 1) { throw new Error('El nombre del cliente es requerido'); } const customerPayload = { name: finalData.name.trim(), customer_code: finalData.customerCode || `CUST-${Date.now().toString().slice(-6)}`, customer_type: finalData.customerType || 'individual', country: finalData.country || 'ES', business_name: finalData.businessName || undefined, email: finalData.email || undefined, phone: finalData.phone || undefined, address_line1: finalData.addressLine1 || undefined, address_line2: finalData.addressLine2 || undefined, city: finalData.city || undefined, state: finalData.state || undefined, postal_code: finalData.postalCode || undefined, tax_id: finalData.taxId || undefined, payment_terms: finalData.paymentTerms || 'immediate', credit_limit: finalData.creditLimit ? parseFloat(finalData.creditLimit) : undefined, discount_percentage: finalData.discountPercentage ? Number(finalData.discountPercentage) : 0, customer_segment: finalData.customerSegment || 'regular', priority_level: finalData.priorityLevel || 'normal', preferred_delivery_method: finalData.preferredDeliveryMethod || 'delivery', special_instructions: finalData.specialInstructions || undefined, is_active: true, }; await OrdersService.createCustomer({ tenant_id: currentTenant.id, ...customerPayload } as any); toast.success('Cliente creado exitosamente'); } // ======================================== // CUSTOMER ORDER (using service directly) // ======================================== if (selectedItemType === 'customer-order') { if (!finalData.customer && !finalData.newCustomerName) { throw new Error('Debes seleccionar o crear un cliente'); } if (!finalData.orderItems || finalData.orderItems.length === 0) { throw new Error('Debes agregar al menos un producto al pedido'); } let customerId = finalData.customer?.id; if (finalData.showNewCustomerForm && finalData.newCustomerName) { const newCustomerPayload = { name: finalData.newCustomerName, customer_code: `CUST-${Date.now().toString().slice(-6)}`, customer_type: finalData.newCustomerType || 'individual', country: 'ES', phone: finalData.newCustomerPhone || undefined, email: finalData.newCustomerEmail || undefined, is_active: true, discount_percentage: 0, customer_segment: 'regular', priority_level: 'normal', preferred_delivery_method: 'delivery', payment_terms: 'immediate', }; const newCustomer = await OrdersService.createCustomer({ tenant_id: currentTenant.id, ...newCustomerPayload } as any); customerId = newCustomer.id; } if (!customerId) { throw new Error('No se pudo obtener el ID del cliente'); } const orderPayload = { tenant_id: currentTenant.id, customer_id: customerId, order_type: finalData.orderType || 'standard', priority: finalData.priority || 'normal', requested_delivery_date: finalData.requestedDeliveryDate || new Date().toISOString(), delivery_method: finalData.deliveryMethod || 'pickup', delivery_address: finalData.deliveryAddress || undefined, payment_method: finalData.paymentMethod || undefined, payment_terms: finalData.paymentTerms || 'immediate', discount_percentage: finalData.discountPercentage ? Number(finalData.discountPercentage) : 0, delivery_fee: finalData.deliveryFee ? Number(finalData.deliveryFee) : 0, special_instructions: finalData.specialInstructions || undefined, order_source: 'manual', sales_channel: 'direct', items: (finalData.orderItems || []).map((item: any) => ({ product_id: item.product_id || item.id, product_name: item.product_name || item.name, quantity: Number(item.quantity), unit_of_measure: item.unit_of_measure || 'units', unit_price: Number(item.unit_price || item.price), line_discount: 0, })), }; await OrdersService.createOrder(orderPayload as any); toast.success('Pedido creado exitosamente'); } // ======================================== // TEAM MEMBER (using auth service) // ======================================== if (selectedItemType === 'team-member') { if (!finalData.fullName || finalData.fullName.trim().length < 1) { throw new Error('El nombre completo es requerido'); } if (!finalData.email || !finalData.email.includes('@')) { throw new Error('El email es requerido y debe ser válido'); } if (!finalData.role) { throw new Error('El rol es requerido'); } const tempPassword = `Temp${Math.random().toString(36).substring(2, 10)}!`; const registrationData = { email: finalData.email.trim(), password: tempPassword, full_name: finalData.fullName.trim(), phone_number: finalData.phone || undefined, tenant_id: currentTenant.id, role: finalData.role, }; await authService.register(registrationData); toast.success('Miembro del equipo agregado exitosamente'); } // ======================================== // SALES ENTRY (using service directly) // ======================================== if (selectedItemType === 'sales-entry') { if (finalData.entryMethod === 'manual') { if (!finalData.salesItems || finalData.salesItems.length === 0) { throw new Error('Debes agregar al menos una venta'); } for (const item of finalData.salesItems) { const salesData = { inventory_product_id: item.productId || item.product_id, quantity_sold: Number(item.quantity), unit_price: Number(item.unitPrice || item.unit_price), revenue: Number(item.quantity) * Number(item.unitPrice || item.unit_price), date: finalData.saleDate || new Date().toISOString(), sales_channel: 'in_store', source: 'manual', notes: finalData.notes || undefined, }; await salesService.createSalesRecord(currentTenant.id, salesData as any); } toast.success('Ventas registradas exitosamente'); } else if (finalData.entryMethod === 'file' || finalData.entryMethod === 'upload') { toast.success('Datos de ventas importados exitosamente'); } } // ======================================== // RECIPE (using service directly) // ======================================== if (selectedItemType === 'recipe') { if (!finalData.name || finalData.name.trim().length < 1) { throw new Error('El nombre de la receta es requerido'); } if (!finalData.finishedProductId) { throw new Error('Debes seleccionar un producto terminado'); } if (!finalData.ingredients || finalData.ingredients.length === 0) { throw new Error('Debes agregar al menos un ingrediente'); } const recipeIngredients = finalData.ingredients.map((ing: any, index: number) => ({ ingredient_id: ing.ingredientId || ing.ingredient_id || ing.id, quantity: Number(ing.quantity), unit: ing.unit, ingredient_notes: ing.notes || null, preparation_method: ing.preparationMethod || ing.preparation_method || null, is_optional: ing.isOptional || ing.is_optional || false, ingredient_order: index + 1, })); const recipeData = { name: finalData.name.trim(), category: finalData.category || 'general', finished_product_id: finalData.finishedProductId, yield_quantity: parseFloat(finalData.yieldQuantity) || 1, yield_unit: finalData.yieldUnit || 'units', version: finalData.version || '1.0', difficulty_level: finalData.difficultyLevel ? Number(finalData.difficultyLevel) : 3, prep_time_minutes: finalData.prepTime ? parseInt(finalData.prepTime) : null, cook_time_minutes: finalData.cookTime ? parseInt(finalData.cookTime) : null, rest_time_minutes: finalData.restTime ? parseInt(finalData.restTime) : null, total_time_minutes: finalData.totalTime ? parseInt(finalData.totalTime) : null, recipe_code: finalData.recipeCode || null, description: finalData.description || null, preparation_notes: finalData.preparationNotes || null, storage_instructions: finalData.storageInstructions || null, instructions: finalData.instructions || null, ingredients: recipeIngredients, quality_check_configuration: finalData.qualityConfiguration || undefined, }; await recipesService.createRecipe(currentTenant.id, recipeData as any); toast.success('Receta creada exitosamente'); } // Call the parent's onComplete callback onComplete?.(selectedItemType, finalData); handleClose(); } catch (error: any) { console.error('Error submitting wizard data:', error); toast.error(error.message || 'Error al crear el elemento'); } finally { setIsSubmitting(false); } }, [ selectedItemType, currentTenant, createPurchaseOrderMutation, createProductionBatchMutation, createIngredientMutation, addStockMutation, createSupplierMutation, onComplete, handleClose, ] ); const wizardSteps = useMemo((): WizardStep[] => { if (!selectedItemType) { return [ { id: 'item-type-selection', title: 'Seleccionar tipo', description: 'Elige qué deseas agregar', component: () => ( ), }, ]; } switch (selectedItemType) { case 'inventory': return InventoryWizardSteps(dataRef, setWizardData); case 'supplier': return SupplierWizardSteps(dataRef, setWizardData); case 'recipe': return RecipeWizardSteps(dataRef, setWizardData); case 'equipment': return EquipmentWizardSteps(dataRef, setWizardData); case 'quality-template': return QualityTemplateWizardSteps(dataRef, setWizardData); case 'customer-order': return CustomerOrderWizardSteps(dataRef, setWizardData); case 'customer': return CustomerWizardSteps(dataRef, setWizardData); case 'team-member': return TeamMemberWizardSteps(dataRef, setWizardData); case 'sales-entry': return SalesEntryWizardSteps(dataRef, setWizardData); case 'purchase-order': return PurchaseOrderWizardSteps(dataRef, setWizardData); case 'production-batch': return ProductionBatchWizardSteps(dataRef, setWizardData); default: return []; } }, [selectedItemType, handleItemTypeSelect, (wizardData as any).entryMethod]); const getWizardTitle = (): string => { if (!selectedItemType) { return 'Agregar Contenido'; } const titleMap: Record = { 'inventory': 'Agregar Inventario', 'supplier': 'Agregar Proveedor', 'recipe': 'Agregar Receta', 'equipment': 'Agregar Equipo', 'quality-template': 'Agregar Plantilla de Calidad', 'customer-order': 'Agregar Pedido', 'customer': 'Agregar Cliente', 'team-member': 'Agregar Miembro del Equipo', 'sales-entry': 'Registrar Ventas', 'purchase-order': 'Crear Orden de Compra', 'production-batch': 'Crear Lote de Producción', }; return titleMap[selectedItemType] || 'Agregar Contenido'; }; return ( } size="xl" dataRef={dataRef} onDataChange={handleDataChange} /> ); }; export default UnifiedAddWizard;