diff --git a/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx b/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx index 81a356c7..f34f642f 100644 --- a/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx @@ -6,12 +6,18 @@ 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, ProductTypeStep, BasicInfoStep, StockConfigStep } from './wizards/InventoryWizard'; +import { InventoryWizardSteps } from './wizards/InventoryWizard'; import { SupplierWizardSteps } from './wizards/SupplierWizard'; import { RecipeWizardSteps } from './wizards/RecipeWizard'; import { EquipmentWizardSteps } from './wizards/EquipmentWizard'; @@ -23,11 +29,18 @@ 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; - // Optional: Start with a specific item type (when opened from individual page buttons) initialItemType?: ItemType; } @@ -43,23 +56,21 @@ export const UnifiedAddWizard: React.FC = ({ const [wizardData, setWizardData] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); - // Get current tenant const { currentTenant } = useTenant(); - // API hooks + // API hooks for wizards with proper hook support const createPurchaseOrderMutation = useCreatePurchaseOrder(); const createProductionBatchMutation = useCreateProductionBatch(); + const createIngredientMutation = useCreateIngredient(); + const addStockMutation = useAddStock(); + const createSupplierMutation = useCreateSupplier(); - // Use a ref to store the current data - this allows step components - // to always access the latest data without causing the steps array to be recreated const dataRef = useRef({}); - // Update ref whenever data changes useEffect(() => { dataRef.current = wizardData; }, [wizardData]); - // Reset state when modal closes const handleClose = useCallback(() => { setSelectedItemType(initialItemType || null); setWizardData({}); @@ -68,22 +79,17 @@ export const UnifiedAddWizard: React.FC = ({ onClose(); }, [onClose, initialItemType]); - // Handle item type selection from step 0 const handleItemTypeSelect = useCallback((itemType: ItemType) => { setSelectedItemType(itemType); }, []); - // CRITICAL FIX: Update both ref AND state, but wizardSteps won't recreate - // The step component needs to re-render to show typed text (controlled inputs) - // But wizardSteps useMemo ensures steps array doesn't recreate, so no component recreation const handleDataChange = useCallback((newData: AnyWizardData) => { - // Update ref first for immediate access dataRef.current = newData; - // Update state to trigger re-render (controlled inputs need this) setWizardData(newData); }, []); - // Handle wizard completion with API submission + // Centralized wizard completion handler + // All API submissions are handled here for consistency const handleWizardComplete = useCallback( async () => { if (!selectedItemType || !currentTenant?.id) { @@ -93,36 +99,29 @@ export const UnifiedAddWizard: React.FC = ({ setIsSubmitting(true); try { - const finalData = dataRef.current as any; // Cast to any for flexible data access + const finalData = dataRef.current as any; - // Handle Purchase Order submission + // ======================================== + // PURCHASE ORDER + // ======================================== if (selectedItemType === 'purchase-order') { - // Validate items have positive quantities and prices 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 subtotal = (finalData.items || []).reduce( - (sum: number, item: any) => sum + (Number(item.ordered_quantity) * Number(item.unit_price)), - 0 - ); - - // Convert date string to ISO datetime with timezone (start of day in local timezone) const deliveryDate = new Date(finalData.required_delivery_date + 'T00:00:00'); if (isNaN(deliveryDate.getTime())) { throw new Error('Fecha de entrega inválida'); } - const requiredDeliveryDateTime = deliveryDate.toISOString(); await createPurchaseOrderMutation.mutateAsync({ tenantId: currentTenant.id, data: { supplier_id: finalData.supplier_id, - required_delivery_date: requiredDeliveryDateTime, + required_delivery_date: deliveryDate.toISOString(), priority: finalData.priority || 'normal', - subtotal: subtotal, tax_amount: Number(finalData.tax_amount) || 0, shipping_cost: Number(finalData.shipping_cost) || 0, discount_amount: Number(finalData.discount_amount) || 0, @@ -138,9 +137,10 @@ export const UnifiedAddWizard: React.FC = ({ toast.success('Orden de compra creada exitosamente'); } - // Handle Production Batch submission + // ======================================== + // PRODUCTION BATCH + // ======================================== if (selectedItemType === 'production-batch') { - // Validate quantities if (Number(finalData.planned_quantity) < 0.01) { throw new Error('La cantidad planificada debe ser mayor a 0'); } @@ -148,12 +148,10 @@ export const UnifiedAddWizard: React.FC = ({ throw new Error('La duración planificada debe ser mayor a 0'); } - // Convert staff_assigned from string to array const staffArray = finalData.staff_assigned_string ? finalData.staff_assigned_string.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0) : []; - // Convert datetime-local strings to ISO datetime with timezone const plannedStartDate = new Date(finalData.planned_start_time); const plannedEndDate = new Date(finalData.planned_end_time); @@ -173,7 +171,7 @@ export const UnifiedAddWizard: React.FC = ({ planned_end_time: plannedEndDate.toISOString(), planned_quantity: Number(finalData.planned_quantity), planned_duration_minutes: Number(finalData.planned_duration_minutes), - priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum, + 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, @@ -192,10 +190,426 @@ export const UnifiedAddWizard: React.FC = ({ 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); - - // Close the modal handleClose(); } catch (error: any) { console.error('Error submitting wizard data:', error); @@ -209,19 +623,16 @@ export const UnifiedAddWizard: React.FC = ({ currentTenant, createPurchaseOrderMutation, createProductionBatchMutation, + createIngredientMutation, + addStockMutation, + createSupplierMutation, onComplete, handleClose, ] ); - // Get wizard steps based on selected item type - // ARCHITECTURAL SOLUTION: We pass dataRef and setWizardData to wizard step functions. - // The wizard steps use these in their component wrappers, which creates a closure - // that always accesses the CURRENT data from dataRef.current, without needing - // to recreate the steps array on every data change. const wizardSteps = useMemo((): WizardStep[] => { if (!selectedItemType) { - // Step 0: Item Type Selection return [ { id: 'item-type-selection', @@ -234,8 +645,6 @@ export const UnifiedAddWizard: React.FC = ({ ]; } - // Pass dataRef and setWizardData - the wizard step functions will use - // dataRef.current to always access fresh data without recreating steps switch (selectedItemType) { case 'inventory': return InventoryWizardSteps(dataRef, setWizardData); @@ -262,9 +671,8 @@ export const UnifiedAddWizard: React.FC = ({ default: return []; } - }, [selectedItemType, handleItemTypeSelect, wizardData.entryMethod]); // Add entryMethod for dynamic sales-entry steps + }, [selectedItemType, handleItemTypeSelect, (wizardData as any).entryMethod]); - // Get wizard title based on selected item type const getWizardTitle = (): string => { if (!selectedItemType) { return 'Agregar Contenido'; diff --git a/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx index b6266616..61c41733 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/CustomerWizard.tsx @@ -350,64 +350,21 @@ export const CustomerWizardSteps = ( dataRef: React.MutableRefObject>, setData: (data: Record) => void ): WizardStep[] => { - // New architecture: return direct component references instead of arrow functions - // dataRef and onDataChange are now passed through WizardModal props + // Wizard steps only handle UI and validation + // API submission is handled centrally in UnifiedAddWizard.handleWizardComplete return [ { id: 'customer-details', title: 'wizards:customer.steps.customerDetails', description: 'wizards:customer.steps.customerDetailsDescription', component: CustomerDetailsStep, - validate: async () => { - // Import these at the top level of this file would be better, but for now do it inline - const { useTenant } = await import('../../../../stores/tenant.store'); - const OrdersService = (await import('../../../../api/services/orders')).default; - const { showToast } = await import('../../../../utils/toast'); - const i18next = (await import('i18next')).default; - + validate: () => { const data = dataRef.current; - const { currentTenant } = useTenant.getState(); - - if (!currentTenant?.id) { - showToast.error(i18next.t('wizards:customer.messages.errorObtainingTenantInfo')); - return false; - } - - try { - const payload = { - name: data.name || '', - customer_code: data.customerCode || '', - customer_type: data.customerType || 'individual', - country: data.country || 'US', - business_name: data.businessName || undefined, - email: data.email || undefined, - phone: data.phone || undefined, - address_line1: data.addressLine1 || undefined, - address_line2: data.addressLine2 || undefined, - city: data.city || undefined, - state: data.state || undefined, - postal_code: data.postalCode || undefined, - tax_id: data.taxId || undefined, - business_license: data.businessLicense || undefined, - payment_terms: data.paymentTerms || 'immediate', - credit_limit: data.creditLimit ? parseFloat(data.creditLimit) : undefined, - discount_percentage: data.discountPercentage || 0, - customer_segment: data.customerSegment || 'regular', - priority_level: data.priorityLevel || 'normal', - preferred_delivery_method: data.preferredDeliveryMethod || 'delivery', - special_instructions: data.specialInstructions || undefined, - is_active: true, - }; - - await OrdersService.createCustomer(currentTenant.id, payload); - showToast.success(i18next.t('wizards:customer.messages.customerCreatedSuccessfully')); - return true; - } catch (err: any) { - console.error('Error creating customer:', err); - const errorMessage = err.response?.data?.detail || i18next.t('wizards:customer.messages.errorCreatingCustomer'); - showToast.error(errorMessage); + // Basic validation - name is required + if (!data.name || data.name.trim().length < 1) { return false; } + return true; }, }, ]; diff --git a/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx index d1fa15f7..9b8f30e5 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/EquipmentWizard.tsx @@ -19,6 +19,16 @@ const EquipmentDetailsStep: React.FC = ({ dataRef, onDataChange
+
+ + handleFieldChange('name', e.target.value)} + placeholder={t('equipment.fields.namePlaceholder')} + 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)]" + /> +
- + handleFieldChange('brand', e.target.value)} - placeholder={t('equipment.fields.brandPlaceholder')} + value={data.model || ''} + onChange={(e) => handleFieldChange('model', e.target.value)} + placeholder={t('equipment.fields.modelPlaceholder')} 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)]" />
@@ -54,11 +65,11 @@ const EquipmentDetailsStep: React.FC = ({ dataRef, onDataChange />
- + handleFieldChange('purchaseDate', e.target.value)} + value={data.installDate || ''} + onChange={(e) => handleFieldChange('installDate', 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)]" />
@@ -68,52 +79,18 @@ const EquipmentDetailsStep: React.FC = ({ dataRef, onDataChange }; export const EquipmentWizardSteps = (dataRef: React.MutableRefObject>, setData: (data: Record) => void): WizardStep[] => { - // New architecture: return direct component references instead of arrow functions - // dataRef and onDataChange are now passed through WizardModal props + // Wizard steps only handle UI and validation + // API submission is handled centrally in UnifiedAddWizard.handleWizardComplete return [ { id: 'equipment-details', title: 'wizards:equipment.steps.equipmentDetails', description: 'wizards:equipment.steps.equipmentDetailsDescription', component: EquipmentDetailsStep, - validate: async () => { - const { useTenant } = await import('../../../../stores/tenant.store'); - const { equipmentService } = await import('../../../../api/services/equipment'); - const { showToast } = await import('../../../../utils/toast'); - const i18next = (await import('i18next')).default; - + validate: () => { const data = dataRef.current; - const { currentTenant } = useTenant.getState(); - - if (!currentTenant?.id) { - showToast.error(i18next.t('wizards:equipment.messages.errorGettingTenant')); - return false; - } - - try { - const equipmentCreateData: any = { - name: `${data.type || 'oven'} - ${data.brand || i18next.t('wizards:equipment.messages.noBrand')}`, - type: data.type || 'oven', - model: data.brand || '', - serialNumber: data.model || '', - location: data.location || '', - status: data.status || 'active', - installDate: data.purchaseDate || new Date().toISOString().split('T')[0], - lastMaintenance: new Date().toISOString().split('T')[0], - nextMaintenance: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], - maintenanceInterval: 30, - is_active: true - }; - - await equipmentService.createEquipment(currentTenant.id, equipmentCreateData); - showToast.success(i18next.t('wizards:equipment.messages.successCreate')); - return true; - } catch (err: any) { - console.error('Error creating equipment:', err); - const errorMessage = err.response?.data?.detail || i18next.t('wizards:equipment.messages.errorCreate'); - showToast.error(errorMessage); - return false; - } + // Name and type are required + return !!(data.name && data.name.trim().length >= 2 && data.type); }, }, ]; diff --git a/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx b/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx index 498d015b..f6603827 100644 --- a/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx +++ b/frontend/src/components/domain/unified-wizard/wizards/RecipeWizard.tsx @@ -168,40 +168,40 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange }) {/* Advanced Options */}
handleFieldChange('recipeCode', e.target.value)} - placeholder="RCP-001" + placeholder={t('recipe.fields.recipeCodePlaceholder')} 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)]" />
handleFieldChange('version', e.target.value)} - placeholder="1.0" + placeholder={t('recipe.fields.versionPlaceholder')} 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)]" />
@@ -217,13 +217,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('cookTime', e.target.value)} - placeholder="30" + placeholder={t('recipe.fields.cookTimePlaceholder')} min="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)]" /> @@ -231,8 +231,8 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
@@ -240,7 +240,7 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange }) type="number" value={data.restTime} onChange={(e) => handleFieldChange('restTime', e.target.value)} - placeholder="60" + placeholder={t('recipe.fields.restTimePlaceholder')} min="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)]" /> @@ -248,13 +248,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('totalTime', e.target.value)} - placeholder="90" + placeholder={t('recipe.fields.totalTimePlaceholder')} min="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)]" /> @@ -262,13 +262,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('servesCount', e.target.value)} - placeholder="8" + placeholder={t('recipe.fields.servesCountPlaceholder')} min="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)]" /> @@ -276,8 +276,8 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
@@ -285,7 +285,7 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange }) type="number" value={data.batchSizeMultiplier} onChange={(e) => handleFieldChange('batchSizeMultiplier', parseFloat(e.target.value) || 1)} - placeholder="1.0" + placeholder={t('recipe.fields.batchSizeMultiplierPlaceholder')} min="0.1" step="0.1" 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)]" @@ -294,13 +294,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('minBatchSize', e.target.value)} - placeholder="5" + placeholder={t('recipe.fields.minBatchSizePlaceholder')} min="0" step="0.1" 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)]" @@ -309,13 +309,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('maxBatchSize', e.target.value)} - placeholder="100" + placeholder={t('recipe.fields.maxBatchSizePlaceholder')} min="0" step="0.1" 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)]" @@ -324,13 +324,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('optimalProductionTemp', e.target.value)} - placeholder="24" + placeholder={t('recipe.fields.optimalTempPlaceholder')} step="0.1" 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)]" /> @@ -338,13 +338,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('optimalHumidity', e.target.value)} - placeholder="65" + placeholder={t('recipe.fields.optimalHumidityPlaceholder')} min="0" max="100" step="0.1" @@ -354,13 +354,13 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('targetMargin', e.target.value)} - placeholder="30" + placeholder={t('recipe.fields.targetMarginPlaceholder')} min="0" max="100" step="0.1" @@ -379,7 +379,7 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange }) className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" />
@@ -392,7 +392,7 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange }) className="w-4 h-4 text-[var(--color-primary)] border-[var(--border-secondary)] rounded focus:ring-2 focus:ring-[var(--color-primary)]" />
@@ -401,14 +401,14 @@ const RecipeDetailsStep: React.FC = ({ dataRef, onDataChange })
handleFieldChange('seasonEndMonth', 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)]" > - + {Array.from({ length: 12 }, (_, i) => i + 1).map(month => (