Improve unified wizard
This commit is contained in:
@@ -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<UnifiedAddWizardProps> = ({
|
||||
const [wizardData, setWizardData] = useState<AnyWizardData>({});
|
||||
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<AnyWizardData>({});
|
||||
|
||||
// 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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
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<UnifiedAddWizardProps> = ({
|
||||
];
|
||||
}
|
||||
|
||||
// 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<UnifiedAddWizardProps> = ({
|
||||
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';
|
||||
|
||||
Reference in New Issue
Block a user