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 } from '../../../../api/hooks/inventory'; import { useValidateImportFile, useImportSalesData } from '../../../../api/hooks/sales'; import type { ImportValidationResponse } from '../../../../api/types/dataImport'; import type { ProductSuggestionResponse } 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 validateFileMutation = useValidateImportFile(); const createIngredient = useCreateIngredient(); const importMutation = useImportSalesData(); const classifyBatchMutation = useClassifyBatch(); 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)); }; // 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`); // Import sales data if available setProgressState({ stage: 'importing_sales', progress: 50, 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
)}
))}
) : (

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)]" />