Files
bakery-ia/frontend/src/components/domain/unified-wizard/UnifiedAddWizard.tsx
2025-11-19 22:12:51 +01:00

272 lines
10 KiB
TypeScript

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 { toast } from 'react-hot-toast';
import type { ProductionBatchCreate } from '../../../api/types/production';
import { ProductionPriorityEnum } from '../../../api/types/production';
// Import specific wizards
import { InventoryWizardSteps, ProductTypeStep, BasicInfoStep, StockConfigStep } 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';
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;
}
export const UnifiedAddWizard: React.FC<UnifiedAddWizardProps> = ({
isOpen,
onClose,
onComplete,
initialItemType,
}) => {
const [selectedItemType, setSelectedItemType] = useState<ItemType | null>(
initialItemType || null
);
const [wizardData, setWizardData] = useState<AnyWizardData>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// Get current tenant
const { currentTenant } = useTenant();
// API hooks
const createPurchaseOrderMutation = useCreatePurchaseOrder();
const createProductionBatchMutation = useCreateProductionBatch();
// 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<AnyWizardData>({});
// Update ref whenever data changes
useEffect(() => {
dataRef.current = wizardData;
}, [wizardData]);
// Reset state when modal closes
const handleClose = useCallback(() => {
setSelectedItemType(initialItemType || null);
setWizardData({});
dataRef.current = {};
setIsSubmitting(false);
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
const handleWizardComplete = useCallback(
async () => {
if (!selectedItemType || !currentTenant?.id) {
return;
}
setIsSubmitting(true);
try {
const finalData = dataRef.current as any; // Cast to any for flexible data access
// Handle Purchase Order submission
if (selectedItemType === 'purchase-order') {
const subtotal = (finalData.items || []).reduce(
(sum: number, item: any) => sum + (item.subtotal || 0),
0
);
await createPurchaseOrderMutation.mutateAsync({
tenantId: currentTenant.id,
data: {
supplier_id: finalData.supplier_id,
required_delivery_date: finalData.required_delivery_date,
priority: finalData.priority || 'normal',
subtotal: String(subtotal),
tax_amount: String(finalData.tax_amount || 0),
shipping_cost: String(finalData.shipping_cost || 0),
discount_amount: String(finalData.discount_amount || 0),
notes: finalData.notes || undefined,
items: (finalData.items || []).map((item: any) => ({
inventory_product_id: item.inventory_product_id,
ordered_quantity: item.ordered_quantity,
unit_price: String(item.unit_price),
unit_of_measure: item.unit_of_measure,
})),
},
});
toast.success('Orden de compra creada exitosamente');
}
// Handle Production Batch submission
if (selectedItemType === 'production-batch') {
// 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)
: [];
const batchData: ProductionBatchCreate = {
product_id: finalData.product_id,
product_name: finalData.product_name,
recipe_id: finalData.recipe_id || undefined,
planned_start_time: finalData.planned_start_time,
planned_end_time: finalData.planned_end_time,
planned_quantity: Number(finalData.planned_quantity),
planned_duration_minutes: Number(finalData.planned_duration_minutes),
priority: (finalData.priority || ProductionPriorityEnum.MEDIUM) as ProductionPriorityEnum,
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');
}
// Call the parent's onComplete callback
onComplete?.(selectedItemType, finalData);
// Close the modal
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,
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',
title: 'Seleccionar tipo',
description: 'Elige qué deseas agregar',
component: () => (
<ItemTypeSelector onSelect={handleItemTypeSelect} />
),
},
];
}
// 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);
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.entryMethod]); // Add entryMethod for dynamic sales-entry steps
// Get wizard title based on selected item type
const getWizardTitle = (): string => {
if (!selectedItemType) {
return 'Agregar Contenido';
}
const titleMap: Record<ItemType, string> = {
'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 (
<WizardModal
isOpen={isOpen}
onClose={handleClose}
onComplete={handleWizardComplete}
title={getWizardTitle()}
steps={wizardSteps}
icon={<Sparkles className="w-6 h-6" />}
size="xl"
dataRef={dataRef}
onDataChange={handleDataChange}
/>
);
};
export default UnifiedAddWizard;