import React, { useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '../../../ui/Button'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { useCreateIngredient, useClassifyBatch, useAddStock } from '../../../../api/hooks/inventory'; import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales'; import { useSuppliers } from '../../../../api/hooks/suppliers'; import type { ImportValidationResponse } from '../../../../api/types/dataImport'; import type { ProductSuggestionResponse, StockCreate, StockResponse } from '../../../../api/types/inventory'; import { ProductionStage } from '../../../../api/types/inventory'; import { useAuth } from '../../../../contexts/AuthContext'; import { BatchAddIngredientsModal } from '../../inventory/BatchAddIngredientsModal'; interface UploadSalesDataStepProps { onNext: () => void; onPrevious: () => void; onComplete: (data?: any) => void; isFirstStep: boolean; isLastStep: boolean; } interface ProgressState { stage: string; progress: number; message: string; } interface InventoryItemForm { id: string; // Unique ID for UI tracking name: string; product_type: string; category: string; unit_of_measure: string; stock_quantity: number; cost_per_unit: number; estimated_shelf_life_days: number; requires_refrigeration: boolean; requires_freezing: boolean; is_seasonal: boolean; low_stock_threshold: number; reorder_point: number; notes: string; // AI suggestion metadata (if from AI) isSuggested: boolean; confidence_score?: number; sales_data?: { total_quantity: number; average_daily_sales: number; }; } export const UploadSalesDataStep: React.FC = ({ onComplete, isFirstStep }) => { const { t } = useTranslation(); const [selectedFile, setSelectedFile] = useState(null); const [isValidating, setIsValidating] = useState(false); const [validationResult, setValidationResult] = useState(null); const [inventoryItems, setInventoryItems] = useState([]); const [showInventoryStep, setShowInventoryStep] = useState(false); const [error, setError] = useState(''); const [progressState, setProgressState] = useState(null); const [showGuide, setShowGuide] = useState(false); const fileInputRef = useRef(null); // Form state for adding/editing const [isAdding, setIsAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [showBatchModal, setShowBatchModal] = useState(false); const [formData, setFormData] = useState({ id: '', name: '', product_type: 'ingredient', category: '', unit_of_measure: 'kg', stock_quantity: 0, cost_per_unit: 0, estimated_shelf_life_days: 30, requires_refrigeration: false, requires_freezing: false, is_seasonal: false, low_stock_threshold: 0, reorder_point: 0, notes: '', isSuggested: false, }); const [formErrors, setFormErrors] = useState>({}); const currentTenant = useCurrentTenant(); const { user } = useAuth(); const tenantId = currentTenant?.id || ''; // API hooks const validateFileMutation = useValidateImportFile(); const createIngredient = useCreateIngredient(); const importMutation = useImportSalesData(); const classifyBatchMutation = useClassifyBatch(); const addStockMutation = useAddStock(); // Fetch suppliers for stock entry const { data: suppliersData } = useSuppliers(tenantId, { limit: 100 }, { enabled: !!tenantId }); const suppliers = (suppliersData || []).filter(s => s.status === 'active'); // Stock lots state const [addingStockForId, setAddingStockForId] = useState(null); const [stockFormData, setStockFormData] = useState({ current_quantity: '', expiration_date: '', supplier_id: '', batch_number: '', }); const [stockErrors, setStockErrors] = useState>({}); const [ingredientStocks, setIngredientStocks] = useState>({}); const handleFileSelect = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { setSelectedFile(file); setValidationResult(null); setError(''); await handleAutoValidateAndClassify(file); } }; const handleDrop = async (event: React.DragEvent) => { event.preventDefault(); const file = event.dataTransfer.files[0]; if (file) { setSelectedFile(file); setValidationResult(null); setError(''); await handleAutoValidateAndClassify(file); } }; const handleDragOver = (event: React.DragEvent) => { event.preventDefault(); }; const handleAutoValidateAndClassify = async (file: File) => { if (!currentTenant?.id) return; setIsValidating(true); setError(''); setProgressState({ stage: 'preparing', progress: 0, message: 'Preparando validación automática del archivo...' }); try { // Step 1: Validate the file const validationResult = await validateFileMutation.mutateAsync({ tenantId: currentTenant.id, file }); if (validationResult && validationResult.is_valid !== undefined) { setValidationResult(validationResult); setProgressState({ stage: 'analyzing', progress: 60, message: 'Validación exitosa. Generando sugerencias automáticamente...' }); await generateInventorySuggestionsAuto(validationResult); } else { setError('Respuesta de validación inválida del servidor'); setProgressState(null); setIsValidating(false); } } catch (error) { setError('Error validando archivo: ' + (error instanceof Error ? error.message : 'Error desconocido')); setProgressState(null); setIsValidating(false); } }; const generateInventorySuggestionsAuto = async (validationData: ImportValidationResponse) => { if (!currentTenant?.id) { setError('No hay datos de validación disponibles para generar sugerencias'); setIsValidating(false); setProgressState(null); return; } try { setProgressState({ stage: 'analyzing', progress: 65, message: 'Analizando productos de ventas...' }); const products = validationData.product_list?.map((productName: string) => ({ product_name: productName })) || []; if (products.length === 0) { setError('No se encontraron productos en los datos de ventas'); setProgressState(null); setIsValidating(false); return; } setProgressState({ stage: 'classifying', progress: 75, message: 'Clasificando productos con IA...' }); const classificationResponse = await classifyBatchMutation.mutateAsync({ tenantId: currentTenant.id, products }); setProgressState({ stage: 'preparing', progress: 90, message: 'Preparando sugerencias de inventario...' }); // Convert AI suggestions to inventory items (NOT created yet, just added to list) const items: InventoryItemForm[] = classificationResponse.suggestions.map((suggestion: ProductSuggestionResponse, index: number) => { const defaultStock = Math.max( Math.ceil((suggestion.sales_data?.average_daily_sales || 1) * 7), 1 ); const estimatedCost = suggestion.category === 'Dairy' ? 5.0 : suggestion.category === 'Baking Ingredients' ? 2.0 : 3.0; const minimumStock = Math.max(1, Math.ceil(defaultStock * 0.2)); const reorderPoint = Math.max(minimumStock + 2, Math.ceil(defaultStock * 0.3), minimumStock + 1); return { id: `ai-${index}-${Date.now()}`, name: suggestion.suggested_name, product_type: suggestion.product_type, category: suggestion.category, unit_of_measure: suggestion.unit_of_measure, stock_quantity: defaultStock, cost_per_unit: estimatedCost, estimated_shelf_life_days: suggestion.estimated_shelf_life_days || 30, requires_refrigeration: suggestion.requires_refrigeration, requires_freezing: suggestion.requires_freezing, is_seasonal: suggestion.is_seasonal, low_stock_threshold: minimumStock, reorder_point: reorderPoint, notes: `AI generado - Confianza: ${Math.round(suggestion.confidence_score * 100)}%`, isSuggested: true, confidence_score: suggestion.confidence_score, sales_data: suggestion.sales_data ? { total_quantity: suggestion.sales_data.total_quantity, average_daily_sales: suggestion.sales_data.average_daily_sales, } : undefined, }; }); setInventoryItems(items); setShowInventoryStep(true); setProgressState(null); setIsValidating(false); } catch (err) { console.error('Error generating inventory suggestions:', err); setError('Error al generar sugerencias de inventario. Por favor, inténtalo de nuevo.'); setProgressState(null); setIsValidating(false); } }; // Form validation const validateForm = (): boolean => { const newErrors: Record = {}; if (!formData.name.trim()) { newErrors.name = 'El nombre es requerido'; } if (!formData.category.trim()) { newErrors.category = 'La categoría es requerida'; } if (formData.stock_quantity < 0) { newErrors.stock_quantity = 'El stock debe ser 0 o mayor'; } if (formData.cost_per_unit < 0) { newErrors.cost_per_unit = 'El costo debe ser 0 o mayor'; } if (formData.estimated_shelf_life_days <= 0) { newErrors.estimated_shelf_life_days = 'Los días de caducidad deben ser mayores a 0'; } setFormErrors(newErrors); return Object.keys(newErrors).length === 0; }; // Add or update item in list const handleSubmitForm = (e: React.FormEvent) => { e.preventDefault(); if (!validateForm()) return; if (editingId) { // Update existing item setInventoryItems(items => items.map(item => item.id === editingId ? { ...formData, id: editingId } : item ) ); setEditingId(null); } else { // Add new item const newItem: InventoryItemForm = { ...formData, id: `manual-${Date.now()}`, isSuggested: false, }; setInventoryItems(items => [...items, newItem]); } resetForm(); }; const resetForm = () => { setFormData({ id: '', name: '', product_type: 'ingredient', category: '', unit_of_measure: 'kg', stock_quantity: 0, cost_per_unit: 0, estimated_shelf_life_days: 30, requires_refrigeration: false, requires_freezing: false, is_seasonal: false, low_stock_threshold: 0, reorder_point: 0, notes: '', isSuggested: false, }); setFormErrors({}); setIsAdding(false); setEditingId(null); }; const handleEdit = (item: InventoryItemForm) => { setFormData(item); setEditingId(item.id); setIsAdding(true); }; const handleDelete = (itemId: string) => { if (!window.confirm('¿Estás seguro de que quieres eliminar este ingrediente de la lista?')) { return; } setInventoryItems(items => items.filter(item => item.id !== itemId)); }; // Stock lot handlers const handleAddStockClick = (ingredientId: string) => { setAddingStockForId(ingredientId); setStockFormData({ current_quantity: '', expiration_date: '', supplier_id: suppliers.length === 1 ? suppliers[0].id : '', batch_number: '', }); setStockErrors({}); }; const handleCancelStock = () => { setAddingStockForId(null); setStockFormData({ current_quantity: '', expiration_date: '', supplier_id: '', batch_number: '', }); setStockErrors({}); }; const validateStockForm = (): boolean => { const newErrors: Record = {}; if (!stockFormData.current_quantity || Number(stockFormData.current_quantity) <= 0) { newErrors.current_quantity = 'La cantidad debe ser mayor que cero'; } if (stockFormData.expiration_date) { const expDate = new Date(stockFormData.expiration_date); const today = new Date(); today.setHours(0, 0, 0, 0); if (expDate < today) { newErrors.expiration_date = 'La fecha de caducidad está en el pasado'; } const threeDaysFromNow = new Date(today); threeDaysFromNow.setDate(threeDaysFromNow.getDate() + 3); if (expDate < threeDaysFromNow) { newErrors.expiration_warning = '⚠️ Este ingrediente caduca muy pronto!'; } } setStockErrors(newErrors); return Object.keys(newErrors).filter(k => k !== 'expiration_warning').length === 0; }; const handleSaveStockLot = (addAnother: boolean = false) => { if (!addingStockForId || !validateStockForm()) return; // Create a temporary stock lot entry (will be saved when ingredients are created) const newLot: StockResponse = { id: `temp-${Date.now()}`, tenant_id: tenantId, ingredient_id: addingStockForId, current_quantity: Number(stockFormData.current_quantity), expiration_date: stockFormData.expiration_date || undefined, supplier_id: stockFormData.supplier_id || undefined, batch_number: stockFormData.batch_number || undefined, production_stage: ProductionStage.RAW_INGREDIENT, quality_status: 'good', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } as StockResponse; // Add to local state setIngredientStocks(prev => ({ ...prev, [addingStockForId]: [...(prev[addingStockForId] || []), newLot], })); if (addAnother) { // Reset form for adding another lot setStockFormData({ current_quantity: '', expiration_date: '', supplier_id: stockFormData.supplier_id, // Keep supplier selected batch_number: '', }); setStockErrors({}); } else { handleCancelStock(); } }; const handleDeleteStockLot = (ingredientId: string, stockId: string) => { setIngredientStocks(prev => ({ ...prev, [ingredientId]: (prev[ingredientId] || []).filter(s => s.id !== stockId), })); }; // Create all inventory items when Next is clicked const handleNext = async () => { if (inventoryItems.length === 0) { setError('Por favor agrega al menos un ingrediente antes de continuar'); return; } if (!currentTenant?.id) { setError('No se encontró información del tenant'); return; } setProgressState({ stage: 'creating_inventory', progress: 10, message: `Creando ${inventoryItems.length} ingredientes...` }); try { // Create all ingredients in parallel const creationPromises = inventoryItems.map(item => { const ingredientData = { name: item.name, product_type: item.product_type, category: item.category, unit_of_measure: item.unit_of_measure, low_stock_threshold: item.low_stock_threshold, max_stock_level: item.stock_quantity * 2, reorder_point: item.reorder_point, shelf_life_days: item.estimated_shelf_life_days, requires_refrigeration: item.requires_refrigeration, requires_freezing: item.requires_freezing, is_seasonal: item.is_seasonal, average_cost: item.cost_per_unit, notes: item.notes || undefined, }; return createIngredient.mutateAsync({ tenantId: currentTenant.id, ingredientData }).then(created => ({ ...created, initialStock: item.stock_quantity })); }); const results = await Promise.allSettled(creationPromises); const createdIngredients = results .filter(r => r.status === 'fulfilled') .map(r => (r as PromiseFulfilledResult).value); const failedCount = results.filter(r => r.status === 'rejected').length; if (failedCount > 0) { console.warn(`${failedCount} ingredientes fallaron al crear de ${inventoryItems.length}`); } console.log(`Creados exitosamente ${createdIngredients.length} ingredientes`); // Create stock lots for ingredients setProgressState({ stage: 'creating_stock', progress: 40, message: 'Creando lotes de stock...' }); const stockCreationPromises: Promise[] = []; createdIngredients.forEach((ingredient) => { // Find the original UI item to get its temporary ID const originalItem = inventoryItems.find(item => item.name === ingredient.name); if (originalItem) { const lots = ingredientStocks[originalItem.id] || []; // Create stock lots for this ingredient lots.forEach((lot) => { const stockData: StockCreate = { ingredient_id: ingredient.id, current_quantity: lot.current_quantity, production_stage: ProductionStage.RAW_INGREDIENT, quality_status: 'good', expiration_date: lot.expiration_date, supplier_id: lot.supplier_id, batch_number: lot.batch_number, }; stockCreationPromises.push( addStockMutation.mutateAsync({ tenantId: currentTenant.id, stockData }).catch(err => { console.error(`Error creando lote de stock para ${ingredient.name}:`, err); return null; }) ); }); } }); if (stockCreationPromises.length > 0) { await Promise.allSettled(stockCreationPromises); console.log(`Creados exitosamente ${stockCreationPromises.length} lotes de stock`); } // Import sales data if available setProgressState({ stage: 'importing_sales', progress: 60, message: 'Importando datos de ventas...' }); let salesImportResult = null; try { if (selectedFile) { const result = await importMutation.mutateAsync({ tenantId: currentTenant.id, file: selectedFile }); salesImportResult = result; if (result.success) { console.log('Datos de ventas importados exitosamente'); } } } catch (importError) { console.error('Error importando datos de ventas:', importError); } setProgressState(null); // Complete step onComplete({ createdIngredients, totalItems: createdIngredients.length, validationResult, file: selectedFile, salesImportResult, inventoryConfigured: true, shouldAutoCompleteSuppliers: true, userId: user?.id }); } catch (err) { console.error('Error creando ingredientes:', err); setError('Error al crear ingredientes. Por favor, inténtalo de nuevo.'); setProgressState(null); } }; const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const categoryOptions = [ 'Baking Ingredients', 'Dairy', 'Fruits', 'Vegetables', 'Meat', 'Seafood', 'Spices', 'Other' ]; const unitOptions = ['kg', 'g', 'L', 'ml', 'units', 'dozen']; // INVENTORY LIST VIEW (after AI suggestions loaded) if (showInventoryStep) { const canContinue = inventoryItems.length >= 1; return (
{/* Why This Matters */}

{t('onboarding:ai_suggestions.why_title', 'Configurar Inventario')}

Revisa y edita los ingredientes sugeridos por IA. Puedes agregar más ingredientes manualmente. Cuando hagas clic en "Siguiente", se crearán todos los ingredientes.

{/* Inventory Items List */}

Ingredientes ({inventoryItems.length})

{inventoryItems.length > 0 ? (
{inventoryItems.map((item) => (
{item.name}
{item.isSuggested && item.confidence_score && ( IA {Math.round(item.confidence_score * 100)}% )}

{item.category} • {item.unit_of_measure}

Stock: {item.stock_quantity} {item.unit_of_measure} Costo: €{item.cost_per_unit.toFixed(2)}/{item.unit_of_measure} Caducidad: {item.estimated_shelf_life_days} días
{item.sales_data && (
📊 Ventas: {item.sales_data.average_daily_sales.toFixed(1)}/día
)}
{/* Stock Lots Section */} {(() => { const lots = ingredientStocks[item.id] || []; const isAddingStock = addingStockForId === item.id; return (
{lots.length > 0 && (
Lotes agregados ({lots.length})
{lots.map((lot) => (
{lot.current_quantity} {item.unit_of_measure} {lot.expiration_date && ( 📅 Caduca: {new Date(lot.expiration_date).toLocaleDateString('es-ES')} )} {lot.batch_number && ( Lote: {lot.batch_number} )}
))}
)} {isAddingStock ? (
setStockFormData(prev => ({ ...prev, current_quantity: e.target.value }))} className="w-full px-2 py-1 text-sm border rounded" placeholder="0" min="0" step="0.01" /> {stockErrors.current_quantity && (

{stockErrors.current_quantity}

)}
setStockFormData(prev => ({ ...prev, expiration_date: e.target.value }))} className="w-full px-2 py-1 text-sm border rounded" /> {stockErrors.expiration_date && (

{stockErrors.expiration_date}

)} {stockErrors.expiration_warning && (

{stockErrors.expiration_warning}

)}
setStockFormData(prev => ({ ...prev, batch_number: e.target.value }))} className="w-full px-2 py-1 text-sm border rounded" placeholder="Opcional" />
💡 Los lotes con fecha de caducidad se gestionarán automáticamente con FIFO
) : ( )}
); })()}
))}
) : (

No hay ingredientes en la lista todavía

)}
{/* Add/Edit Form */} {isAdding ? (

{editingId ? 'Editar Ingrediente' : 'Agregar Ingrediente Manualmente'}

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" placeholder="Ej: Harina de trigo" /> {formErrors.name && (

{formErrors.name}

)}
{formErrors.category && (

{formErrors.category}

)}
setFormData({ ...formData, stock_quantity: parseFloat(e.target.value) || 0 })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" /> {formErrors.stock_quantity && (

{formErrors.stock_quantity}

)}
setFormData({ ...formData, cost_per_unit: parseFloat(e.target.value) || 0 })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" /> {formErrors.cost_per_unit && (

{formErrors.cost_per_unit}

)}
setFormData({ ...formData, estimated_shelf_life_days: parseInt(e.target.value) || 30 })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" /> {formErrors.estimated_shelf_life_days && (

{formErrors.estimated_shelf_life_days}

)}
setFormData({ ...formData, low_stock_threshold: parseInt(e.target.value) || 0 })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" />
setFormData({ ...formData, reorder_point: parseInt(e.target.value) || 0 })} className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-secondary)] rounded-lg focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] text-[var(--text-primary)]" />