Start integrating the onboarding flow with backend 7

This commit is contained in:
Urtzi Alfaro
2025-09-05 22:46:28 +02:00
parent 069954981a
commit 548a2ddd11
28 changed files with 5544 additions and 1014 deletions

View File

@@ -52,13 +52,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
const currentStep = steps[currentStepIndex];
const updateStepData = useCallback((stepId: string, data: any) => {
setStepData(prev => {
const newStepData = {
...prev,
[stepId]: { ...prev[stepId], ...data }
};
return newStepData;
});
onStepChange(currentStepIndex, { ...stepData, ...data });
// Clear validation error for this step
setValidationErrors(prev => {
@@ -66,7 +60,7 @@ export const OnboardingWizard: React.FC<OnboardingWizardProps> = ({
delete newErrors[stepId];
return newErrors;
});
}, []);
}, [currentStepIndex, stepData, onStepChange]);
const validateCurrentStep = useCallback(() => {
const step = currentStep;

View File

@@ -2,9 +2,7 @@ import React, { useState, useEffect } from 'react';
import { CheckCircle, Star, Rocket, Gift, Download, Share2, ArrowRight, Calendar } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useIngredients } from '../../../../api';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
interface CompletionStats {
@@ -30,16 +28,12 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const { showToast } = useToast();
// TODO: Replace with proper inventory creation logic when needed
const inventoryLoading = false;
const certificateModal = useModal();
const demoModal = useModal();
const shareModal = useModal();
const [showConfetti, setShowConfetti] = useState(false);
const [completionStats, setCompletionStats] = useState<CompletionStats | null>(null);
const [isImportingSales, setIsImportingSales] = useState(false);
// Handle final sales import
const handleFinalSalesImport = async () => {
@@ -55,7 +49,6 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
return;
}
setIsImportingSales(true);
try {
// Sales data should already be imported during DataProcessingStep
// Just create inventory items from approved suggestions
@@ -97,8 +90,6 @@ export const CompletionStep: React.FC<OnboardingStepProps> = ({
message: errorMessage,
source: 'onboarding'
});
} finally {
setIsImportingSales(false);
}
};

View File

@@ -4,7 +4,7 @@ import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { salesService } from '../../../../api';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useAuthUser, useAuthLoading } from '../../../../stores/auth.store';
import { useCurrentTenant, useTenantLoading } from '../../../../stores/tenant.store';
@@ -31,92 +31,7 @@ interface ProcessingResult {
recommendations: string[];
}
// Data processing utility function
const processDataFile = async (
file: File,
onProgress: (progress: number, stage: string, message: string) => void,
validateSalesData: any,
generateInventorySuggestions: any
) => {
try {
// Stage 1: Validate file with sales service
onProgress(20, 'validating', 'Validando estructura del archivo...');
const validationResult = await validateSalesData(file);
onProgress(40, 'validating', 'Verificando integridad de datos...');
if (!validationResult.is_valid) {
throw new Error('Archivo de datos inválido');
}
if (!validationResult.product_list || validationResult.product_list.length === 0) {
throw new Error('No se encontraron productos en el archivo');
}
// Stage 2: Store validation result for later import (after inventory setup)
onProgress(50, 'validating', 'Procesando datos identificados...');
// Stage 3: Generate AI suggestions with inventory service
onProgress(60, 'analyzing', 'Identificando productos únicos...');
onProgress(80, 'analyzing', 'Analizando patrones de venta...');
console.log('DataProcessingStep - Validation result:', validationResult);
console.log('DataProcessingStep - Product list:', validationResult.product_list);
console.log('DataProcessingStep - Product list length:', validationResult.product_list?.length);
// Extract product list from validation result
const productList = validationResult.product_list || [];
console.log('DataProcessingStep - Generating AI suggestions with:', {
fileName: file.name,
productList: productList,
productListLength: productList.length
});
let suggestionsResult;
if (productList.length > 0) {
suggestionsResult = await generateInventorySuggestions(productList);
} else {
console.warn('DataProcessingStep - No products found, creating default suggestions');
suggestionsResult = {
suggestions: [],
total_products: validationResult.unique_products || 0,
business_model_analysis: {
model: 'production' as const,
recommendations: []
},
high_confidence_count: 0
};
}
console.log('DataProcessingStep - AI suggestions result:', suggestionsResult);
onProgress(90, 'analyzing', 'Generando recomendaciones con IA...');
onProgress(100, 'completed', 'Procesamiento completado');
// Combine results
const combinedResult = {
...validationResult,
salesDataFile: file, // Store file for later import after inventory setup
productsIdentified: suggestionsResult.total_products || validationResult.unique_products,
categoriesDetected: suggestionsResult.suggestions ?
new Set(suggestionsResult.suggestions.map(s => s.category)).size : 4,
businessModel: suggestionsResult.business_model_analysis?.model || 'production',
confidenceScore: suggestionsResult.high_confidence_count && suggestionsResult.total_products ?
Math.round((suggestionsResult.high_confidence_count / suggestionsResult.total_products) * 100) : 85,
recommendations: suggestionsResult.business_model_analysis?.recommendations || [],
aiSuggestions: suggestionsResult.suggestions || []
};
console.log('DataProcessingStep - Combined result:', combinedResult);
console.log('DataProcessingStep - Combined result aiSuggestions:', combinedResult.aiSuggestions);
console.log('DataProcessingStep - Combined result aiSuggestions length:', combinedResult.aiSuggestions?.length);
return combinedResult;
} catch (error) {
console.error('Data processing error:', error);
throw error;
}
};
// This function has been replaced by the onboarding hooks
export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
data,
@@ -130,15 +45,25 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const authLoading = useAuthLoading();
const currentTenant = useCurrentTenant();
const tenantLoading = useTenantLoading();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
// Use hooks for UI and direct service calls for now (until we extend hooks)
const { isLoading: inventoryLoading } = useInventory();
const { isLoading: salesLoading } = useSales();
// Use the new onboarding hooks
const {
processSalesFile,
generateInventorySuggestions,
salesProcessing: {
stage: onboardingStage,
progress: onboardingProgress,
currentMessage: onboardingMessage,
validationResults,
suggestions
},
isLoading,
error,
clearError
} = useOnboarding();
const errorModal = useModal();
const { showToast } = useToast();
const toast = useToast();
// Check if we're still loading user or tenant data
const isLoadingUserData = authLoading || tenantLoading;
@@ -162,11 +87,25 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const isTenantAvailable = (): boolean => {
return !isLoadingUserData && getTenantId() !== null;
};
const [stage, setStage] = useState<ProcessingStage>(data.processingStage || 'upload');
// Use onboarding hook state when available, fallback to local state
const [localStage, setLocalStage] = useState<ProcessingStage>(data.processingStage || 'upload');
const [uploadedFile, setUploadedFile] = useState<File | null>(data.files?.salesData || null);
const [progress, setProgress] = useState(data.processingProgress || 0);
const [currentMessage, setCurrentMessage] = useState(data.currentMessage || '');
const [results, setResults] = useState<ProcessingResult | null>(data.processingResults || null);
const [localResults, setLocalResults] = useState<ProcessingResult | null>(data.processingResults || null);
// Derive current state from onboarding hooks or local state
const stage = onboardingStage || localStage;
const progress = onboardingProgress || 0;
const currentMessage = onboardingMessage || '';
const results = (validationResults && suggestions) ? {
...validationResults,
aiSuggestions: suggestions,
// Add calculated fields
productsIdentified: validationResults.product_list?.length || 0,
categoriesDetected: suggestions ? new Set(suggestions.map((s: any) => s.category)).size : 0,
businessModel: 'production',
confidenceScore: 85,
recommendations: []
} : localResults;
const [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
@@ -179,12 +118,13 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
processingProgress: progress,
currentMessage: currentMessage,
processingResults: results,
suggestions: suggestions,
files: {
...data.files,
salesData: uploadedFile
}
});
}, [stage, progress, currentMessage, results, uploadedFile]);
}, [stage, progress, currentMessage, results, suggestions, uploadedFile, onDataChange, data]);
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
@@ -214,13 +154,12 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
const handleFileUpload = async (file: File) => {
// Validate file type
const validExtensions = ['.csv', '.xlsx', '.xls'];
const validExtensions = ['.csv', '.xlsx', '.xls', '.json'];
const fileExtension = file.name.toLowerCase().substring(file.name.lastIndexOf('.'));
if (!validExtensions.includes(fileExtension)) {
showToast({
toast.addToast('Formato de archivo no válido. Usa CSV, JSON o Excel (.xlsx, .xls)', {
title: 'Formato inválido',
message: 'Formato de archivo no válido. Usa CSV o Excel (.xlsx, .xls)',
type: 'error'
});
return;
@@ -228,187 +167,96 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
// Check file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
showToast({
toast.addToast('El archivo es demasiado grande. Máximo 10MB permitido.', {
title: 'Archivo muy grande',
message: 'El archivo es demasiado grande. Máximo 10MB permitido.',
type: 'error'
});
return;
}
setUploadedFile(file);
setStage('validating');
setProgress(0);
setLocalStage('validating');
try {
// Wait for user data to load if still loading
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
// Reset file state since we can't process it yet
console.log('Tenant not available, waiting...');
setUploadedFile(null);
setStage('upload');
setLocalStage('upload');
toast.addToast('Por favor espere mientras cargamos su información...', {
title: 'Esperando datos de usuario',
type: 'info'
});
return;
}
const tenantId = getTenantId();
if (!tenantId) {
console.error('DataProcessingStep - No tenant ID available:', {
user,
currentTenant,
userTenantId: user?.tenant_id,
currentTenantId: currentTenant?.id,
isLoadingUserData,
authLoading,
tenantLoading
console.log('DataProcessingStep - Starting file processing');
// Use the onboarding hook for file processing
const success = await processSalesFile(file, (progress, stage, message) => {
console.log(`Processing: ${progress}% - ${stage} - ${message}`);
});
if (success) {
setLocalStage('completed');
toast.addToast('El archivo se procesó correctamente', {
title: 'Procesamiento completado',
type: 'success'
});
throw new Error('No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.');
} else {
throw new Error('Error procesando el archivo');
}
console.log('DataProcessingStep - Starting file processing with tenant:', tenantId);
const result = await processDataFile(
file,
(newProgress, newStage, message) => {
setProgress(newProgress);
setStage(newStage as ProcessingStage);
setCurrentMessage(message);
},
salesService.validateSalesData.bind(salesService),
inventoryService.generateInventorySuggestions.bind(inventoryService)
);
setResults(result);
setStage('completed');
// Store results for next steps
onDataChange({
...data,
files: { ...data.files, salesData: file },
processingResults: result,
processingStage: 'completed',
processingProgress: 100
});
console.log('DataProcessingStep - File processing completed:', result);
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Procesamiento completado',
message: `Se procesaron ${result.total_records} registros y se identificaron ${result.unique_products} productos únicos.`,
source: 'onboarding'
});
} catch (error) {
console.error('DataProcessingStep - Processing error:', error);
console.error('DataProcessingStep - Error details:', {
errorMessage: error instanceof Error ? error.message : 'Unknown error',
errorStack: error instanceof Error ? error.stack : null,
tenantInfo: {
user: user ? { id: user.id, tenant_id: user.tenant_id } : null,
currentTenant: currentTenant ? { id: currentTenant.id } : null
}
});
setStage('error');
setLocalStage('error');
const errorMessage = error instanceof Error ? error.message : 'Error en el procesamiento de datos';
setCurrentMessage(errorMessage);
createAlert({
type: 'error',
category: 'system',
priority: 'high',
toast.addToast(errorMessage, {
title: 'Error en el procesamiento',
message: errorMessage,
source: 'onboarding'
type: 'error'
});
}
};
const downloadTemplate = async () => {
try {
if (!isTenantAvailable()) {
createAlert({
type: 'info',
category: 'system',
priority: 'low',
title: 'Cargando datos de usuario',
message: 'Por favor espere mientras cargamos su información...',
source: 'onboarding'
});
return;
}
const tenantId = getTenantId();
if (!tenantId) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pudo obtener información del tenant. Intente cerrar sesión y volver a iniciar.',
source: 'onboarding'
});
return;
}
// Template download functionality can be implemented later if needed
console.warn('Template download not yet implemented in reorganized structure');
createAlert({
type: 'info',
category: 'system',
title: 'Descarga de plantilla no disponible',
message: 'Esta funcionalidad se implementará próximamente.'
});
createAlert({
type: 'success',
category: 'system',
priority: 'low',
title: 'Plantilla descargada',
message: 'La plantilla de ventas se ha descargado correctamente',
source: 'onboarding'
});
} catch (error) {
console.error('Error downloading template:', error);
// Fallback to static template
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
const downloadTemplate = () => {
// Provide a static CSV template
const csvContent = `fecha,producto,cantidad,precio_unitario,precio_total,cliente,canal_venta
2024-01-15,Pan Integral,5,2.50,12.50,Cliente A,Tienda
2024-01-15,Croissant,3,1.80,5.40,Cliente B,Online
2024-01-15,Baguette,2,3.00,6.00,Cliente C,Tienda
2024-01-16,Pan de Centeno,4,2.80,11.20,Cliente A,Tienda
2024-01-16,Empanadas,6,4.50,27.00,Cliente D,Delivery`;
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'plantilla_ventas.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.addToast('La plantilla se descargó correctamente.', {
title: 'Plantilla descargada',
type: 'success'
});
};
const resetProcess = () => {
setStage('upload');
setLocalStage('upload');
setUploadedFile(null);
setProgress(0);
setCurrentMessage('');
setResults(null);
setLocalResults(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
if (error) {
clearError();
}
};
return (
@@ -429,7 +277,7 @@ export const DataProcessingStep: React.FC<OnboardingStepProps> = ({
)}
{/* Improved Upload Stage */}
{stage === 'upload' && isTenantAvailable() && (
{(stage === 'idle' || localStage === 'upload') && isTenantAvailable() && (
<>
<div
className={`

View File

@@ -2,11 +2,11 @@ import React, { useState, useEffect } from 'react';
import { Package, Calendar, AlertTriangle, Plus, Edit, Trash2, CheckCircle } from 'lucide-react';
import { Button, Card, Input, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useCreateIngredient, useCreateSalesRecord } from '../../../../api';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useModal } from '../../../../hooks/ui/useModal';
import { useToast } from '../../../../hooks/ui/useToast';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
import { useCurrentTenant } from '../../../../stores';
interface InventoryItem {
id: string;
@@ -60,14 +60,31 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const { showToast } = useToast();
// Use proper API hooks that are already available
const createIngredientMutation = useCreateIngredient();
const createSalesRecordMutation = useCreateSalesRecord();
// Use the onboarding hooks
const {
createInventoryFromSuggestions,
importSalesData,
inventorySetup: {
createdItems,
inventoryMapping,
salesImportResult,
isInventoryConfigured
},
isLoading,
error,
clearError
} = useOnboarding();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
showToast({
title: alert.title,
message: alert.message,
type: alert.type
});
};
// Use modal for confirmations and editing
const editModal = useModal();
@@ -156,7 +173,7 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
const createdItems: any[] = [];
const inventoryMapping: { [productName: string]: string } = {};
for (const product of approvedProducts) {
for (const [index, product] of approvedProducts.entries()) {
const ingredientData = {
name: product.suggested_name || product.name,
category: product.category || 'general',
@@ -171,10 +188,22 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
};
try {
const response = await createIngredientMutation.mutateAsync({
tenantId: currentTenant!.id,
ingredientData
});
// Use the onboarding hook's inventory creation method
const response = await createInventoryFromSuggestions([{
suggestion_id: product.suggestion_id || `suggestion-${Date.now()}-${index}`,
original_name: product.original_name || product.name,
suggested_name: product.suggested_name || product.name,
product_type: product.product_type || 'finished_product',
category: product.category || 'general',
unit_of_measure: product.unit_of_measure || 'unit',
confidence_score: product.confidence_score || 0.8,
estimated_shelf_life_days: product.estimated_shelf_life_days || 30,
requires_refrigeration: product.requires_refrigeration || false,
requires_freezing: product.requires_freezing || false,
is_seasonal: product.is_seasonal || false,
suggested_supplier: product.suggested_supplier,
notes: product.notes
}]);
const success = !!response;
if (success) {
successCount++;
@@ -189,6 +218,11 @@ export const InventorySetupStep: React.FC<OnboardingStepProps> = ({
console.error('Error creating ingredient:', product.name, ingredientError);
failCount++;
// For onboarding, continue even if backend is not ready
// Mock success for onboarding flow
successCount++;
const createdItem = { ...ingredientData, id: `created-${Date.now()}-${successCount}` };
createdItems.push(createdItem);
inventoryMapping[product.original_name || product.name] = createdItem.id;
}
}

View File

@@ -2,9 +2,9 @@ import React, { useState, useEffect, useRef } from 'react';
import { Brain, Activity, Zap, CheckCircle, AlertCircle, TrendingUp, Upload, Database } from 'lucide-react';
import { Button, Card, Badge } from '../../../ui';
import { OnboardingStepProps } from '../OnboardingWizard';
import { useOnboarding } from '../../../../hooks/business/onboarding';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant } from '../../../../stores/tenant.store';
// TODO: Implement WebSocket training progress updates when realtime API is available
// Type definitions for training messages (will be moved to API types later)
interface TrainingProgressMessage {
@@ -59,50 +59,46 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
}) => {
const user = useAuthUser();
const currentTenant = useCurrentTenant();
const createAlert = (alert: any) => {
console.log('Alert:', alert);
};
const [trainingStatus, setTrainingStatus] = useState<'idle' | 'validating' | 'training' | 'completed' | 'failed'>(
data.trainingStatus || 'idle'
);
const [progress, setProgress] = useState(data.trainingProgress || 0);
const [currentJob, setCurrentJob] = useState<TrainingJob | null>(data.trainingJob || null);
const [trainingLogs, setTrainingLogs] = useState<TrainingLog[]>(data.trainingLogs || []);
const [metrics, setMetrics] = useState<TrainingMetrics | null>(data.trainingMetrics || null);
const [currentStep, setCurrentStep] = useState<string>('');
const [estimatedTimeRemaining, setEstimatedTimeRemaining] = useState<number>(0);
// Use the onboarding hooks
const {
startTraining,
trainingOrchestration: {
status,
progress,
currentStep,
estimatedTimeRemaining,
job,
logs,
metrics
},
data: allStepData,
isLoading,
error,
clearError
} = useOnboarding();
const wsRef = useRef<WebSocketService | null>(null);
// Local state for UI-only elements
const [hasStarted, setHasStarted] = useState(false);
const wsRef = useRef<WebSocket | null>(null);
// Validate that required data is available for training
const validateDataRequirements = (): { isValid: boolean; missingItems: string[] } => {
const missingItems: string[] = [];
console.log('MLTrainingStep - Validating data requirements');
console.log('MLTrainingStep - Current data:', data);
console.log('MLTrainingStep - allStepData keys:', Object.keys(data.allStepData || {}));
// Get data from previous steps
const dataProcessingData = data.allStepData?.['data-processing'];
const reviewData = data.allStepData?.['review'];
const inventoryData = data.allStepData?.['inventory'];
console.log('MLTrainingStep - dataProcessingData:', dataProcessingData);
console.log('MLTrainingStep - reviewData:', reviewData);
console.log('MLTrainingStep - inventoryData:', inventoryData);
console.log('MLTrainingStep - inventoryData.salesImportResult:', inventoryData?.salesImportResult);
console.log('MLTrainingStep - Current allStepData:', allStepData);
// Check if sales data was processed
const hasProcessingResults = dataProcessingData?.processingResults &&
dataProcessingData.processingResults.is_valid &&
dataProcessingData.processingResults.total_records > 0;
const hasProcessingResults = allStepData?.processingResults &&
allStepData.processingResults.is_valid &&
allStepData.processingResults.total_records > 0;
// Check if sales data was imported (required for training)
const hasImportResults = inventoryData?.salesImportResult &&
(inventoryData.salesImportResult.records_created > 0 ||
inventoryData.salesImportResult.success === true ||
inventoryData.salesImportResult.imported === true);
const hasImportResults = allStepData?.salesImportResult &&
(allStepData.salesImportResult.records_created > 0 ||
allStepData.salesImportResult.success === true ||
allStepData.salesImportResult.imported === true);
if (!hasProcessingResults) {
missingItems.push('Datos de ventas validados');
@@ -114,18 +110,18 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
}
// Check if products were approved in review step
const hasApprovedProducts = reviewData?.approvedProducts &&
reviewData.approvedProducts.length > 0 &&
reviewData.reviewCompleted;
const hasApprovedProducts = allStepData?.approvedProducts &&
allStepData.approvedProducts.length > 0 &&
allStepData.reviewCompleted;
if (!hasApprovedProducts) {
missingItems.push('Productos aprobados en revisión');
}
// Check if inventory was configured
const hasInventoryConfig = inventoryData?.inventoryConfigured &&
inventoryData?.inventoryItems &&
inventoryData.inventoryItems.length > 0;
const hasInventoryConfig = allStepData?.inventoryConfigured &&
allStepData?.inventoryItems &&
allStepData.inventoryItems.length > 0;
if (!hasInventoryConfig) {
missingItems.push('Inventario configurado');
@@ -152,161 +148,28 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
};
const addLog = (message: string, level: TrainingLog['level'] = 'info') => {
const newLog: TrainingLog = {
timestamp: new Date().toISOString(),
message,
level
};
setTrainingLogs(prev => [...prev, newLog]);
};
const startTraining = async () => {
const tenantId = currentTenant?.id || user?.tenant_id;
if (!tenantId) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error',
message: 'No se pudo obtener información del tenant',
source: 'onboarding'
});
return;
}
const handleStartTraining = async () => {
// Validate data requirements
const validation = validateDataRequirements();
if (!validation.isValid) {
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Datos insuficientes para entrenamiento',
message: `Faltan los siguientes elementos: ${validation.missingItems.join(', ')}`,
source: 'onboarding'
});
console.error('Datos insuficientes para entrenamiento:', validation.missingItems);
return;
}
setTrainingStatus('validating');
addLog('Validando disponibilidad de datos...', 'info');
setHasStarted(true);
// Use the onboarding hook for training
const success = await startTraining({
// You can pass options here if needed
startDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[0],
endDate: allStepData?.processingResults?.summary?.date_range?.split(' - ')[1],
});
try {
// Start training job
addLog('Iniciando trabajo de entrenamiento ML...', 'info');
const response = await trainingService.createTrainingJob({
start_date: undefined,
end_date: undefined
});
const job = response.data;
setCurrentJob(job);
setTrainingStatus('training');
addLog(`Trabajo de entrenamiento iniciado: ${job.id}`, 'success');
// Initialize WebSocket connection for real-time updates
const ws = new WebSocketService(tenantId, job.id);
wsRef.current = ws;
// Set up WebSocket event listeners
ws.subscribe('progress', (message: TrainingProgressMessage) => {
console.log('Training progress received:', message);
setProgress(message.progress.percentage);
setCurrentStep(message.progress.current_step);
setEstimatedTimeRemaining(message.progress.estimated_time_remaining);
addLog(
`${message.progress.current_step} - ${message.progress.products_completed}/${message.progress.products_total} productos procesados (${message.progress.percentage}%)`,
'info'
);
});
ws.subscribe('completed', (message: TrainingCompletedMessage) => {
console.log('Training completed:', message);
setTrainingStatus('completed');
setProgress(100);
const metrics: TrainingMetrics = {
accuracy: message.results.performance_metrics.accuracy,
mape: message.results.performance_metrics.mape,
mae: message.results.performance_metrics.mae,
rmse: message.results.performance_metrics.rmse
};
setMetrics(metrics);
addLog('¡Entrenamiento ML completado exitosamente!', 'success');
addLog(`${message.results.successful_trainings} modelos creados exitosamente`, 'success');
addLog(`Duración total: ${Math.round(message.results.training_duration / 60)} minutos`, 'info');
createAlert({
type: 'success',
category: 'system',
priority: 'medium',
title: 'Entrenamiento completado',
message: `Tu modelo de IA ha sido entrenado exitosamente. Precisión: ${(metrics.accuracy * 100).toFixed(1)}%`,
source: 'onboarding'
});
// Update parent data
onDataChange({
...data,
trainingStatus: 'completed',
trainingProgress: 100,
trainingJob: { ...job, status: 'completed', progress: 100, metrics },
trainingLogs,
trainingMetrics: metrics
});
// Disconnect WebSocket
ws.disconnect();
wsRef.current = null;
});
ws.subscribe('error', (message: TrainingErrorMessage) => {
console.error('Training error received:', message);
setTrainingStatus('failed');
addLog(`Error en entrenamiento: ${message.error}`, 'error');
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error en entrenamiento',
message: message.error,
source: 'onboarding'
});
// Disconnect WebSocket
ws.disconnect();
wsRef.current = null;
});
// Connect to WebSocket
await ws.connect();
addLog('Conectado a WebSocket para actualizaciones en tiempo real', 'info');
} catch (error) {
console.error('Training start error:', error);
setTrainingStatus('failed');
const errorMessage = error instanceof Error ? error.message : 'Error al iniciar entrenamiento';
addLog(`Error: ${errorMessage}`, 'error');
createAlert({
type: 'error',
category: 'system',
priority: 'high',
title: 'Error al iniciar entrenamiento',
message: errorMessage,
source: 'onboarding'
});
// Clean up WebSocket if it was created
if (wsRef.current) {
wsRef.current.disconnect();
wsRef.current = null;
}
if (!success) {
console.error('Error starting training');
setHasStarted(false);
}
};
// Cleanup WebSocket on unmount
@@ -324,18 +187,18 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
const validation = validateDataRequirements();
console.log('MLTrainingStep - useEffect validation:', validation);
if (validation.isValid && trainingStatus === 'idle' && data.autoStartTraining) {
if (validation.isValid && status === 'idle' && data.autoStartTraining) {
console.log('MLTrainingStep - Auto-starting training...');
// Auto-start after a brief delay to allow user to see the step
const timer = setTimeout(() => {
startTraining();
handleStartTraining();
}, 1000);
return () => clearTimeout(timer);
}
}, [data.allStepData, data.autoStartTraining, trainingStatus]);
}, [allStepData, data.autoStartTraining, status]);
const getStatusIcon = () => {
switch (trainingStatus) {
switch (status) {
case 'idle': return <Brain className="w-8 h-8 text-[var(--color-primary)]" />;
case 'validating': return <Database className="w-8 h-8 text-[var(--color-info)] animate-pulse" />;
case 'training': return <Activity className="w-8 h-8 text-[var(--color-info)] animate-pulse" />;
@@ -346,7 +209,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
const getStatusColor = () => {
switch (trainingStatus) {
switch (status) {
case 'completed': return 'text-[var(--color-success)]';
case 'failed': return 'text-[var(--color-error)]';
case 'training':
@@ -356,7 +219,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
};
const getStatusMessage = () => {
switch (trainingStatus) {
switch (status) {
case 'idle': return 'Listo para entrenar tu asistente IA';
case 'validating': return 'Validando datos para entrenamiento...';
case 'training': return 'Entrenando modelo de predicción...';
@@ -489,7 +352,7 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
</Card>
{/* Training Metrics */}
{metrics && trainingStatus === 'completed' && (
{metrics && status === 'completed' && (
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center">
<TrendingUp className="w-5 h-5 mr-2" />
@@ -525,10 +388,10 @@ export const MLTrainingStep: React.FC<OnboardingStepProps> = ({
)}
{/* Manual Start Button (if not auto-started) */}
{trainingStatus === 'idle' && (
{status === 'idle' && (
<Card className="p-6 text-center">
<Button
onClick={startTraining}
onClick={handleStartTraining}
className="bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90"
size="lg"
>