import React, { useState, useEffect, useCallback } from 'react'; import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react'; import toast from 'react-hot-toast'; import EnhancedTrainingProgress from '../../components/EnhancedTrainingProgress'; import { useTenant, useTraining, useData, useTrainingWebSocket, TenantCreate, TrainingJobRequest } from '../../api'; interface OnboardingPageProps { user: any; onComplete: () => void; } interface BakeryData { name: string; address: string; businessType: 'individual' | 'central_workshop'; coordinates?: { lat: number; lng: number }; products: string[]; hasHistoricalData: boolean; csvFile?: File; } interface TrainingProgress { progress: number; status: string; currentStep: string; productsCompleted: number; productsTotal: number; estimatedTimeRemaining: number; error?: string; } const MADRID_PRODUCTS = [ 'Croissants', 'Pan de molde', 'Baguettes', 'Panecillos', 'Ensaimadas', 'Napolitanas', 'Magdalenas', 'Donuts', 'Palmeras', 'Café', 'Chocolate caliente', 'Zumos', 'Bocadillos', 'Empanadas', 'Tartas' ]; const OnboardingPage: React.FC = ({ user, onComplete }) => { const [currentStep, setCurrentStep] = useState(1); const [isLoading, setIsLoading] = useState(false); const [bakeryData, setBakeryData] = useState({ name: '', address: '', businessType: 'individual', products: MADRID_PRODUCTS, // Automatically assign all products hasHistoricalData: false }); // Training progress state const [tenantId, setTenantId] = useState(''); const [trainingJobId, setTrainingJobId] = useState(''); const { createTenant, isLoading: tenantLoading } = useTenant(); const { startTrainingJob } = useTraining({ disablePolling: true }); const { uploadSalesHistory, validateSalesData } = useData(); const steps = [ { id: 1, title: 'Datos de Panadería', icon: Store }, { id: 2, title: 'Datos Históricos', icon: Upload }, { id: 3, title: 'Entrenamiento IA', icon: Brain }, { id: 4, title: 'Configuración Final', icon: Check } ]; const [trainingProgress, setTrainingProgress] = useState({ progress: 0, status: 'pending', currentStep: 'Iniciando...', productsCompleted: 0, productsTotal: 0, estimatedTimeRemaining: 0 }); // WebSocket connection for real-time training updates const { status, jobUpdates, connect, disconnect, isConnected, lastMessage, tenantId: resolvedTenantId, wsUrl } = useTrainingWebSocket(trainingJobId || 'pending', tenantId); // Handle WebSocket job updates const processWebSocketMessage = useCallback((message: any) => { const messageType = message.type; const data = message.data || message; // Fallback if data is at root level if (messageType === 'progress' || messageType === 'training_progress') { setTrainingProgress(prev => ({ ...prev, progress: typeof data.progress === 'number' ? data.progress : prev.progress, currentStep: data.current_step || data.currentStep || 'Procesando...', productsCompleted: data.products_completed || data.productsCompleted || prev.productsCompleted, productsTotal: data.products_total || data.productsTotal || prev.productsTotal, estimatedTimeRemaining: data.estimated_time_remaining_minutes || data.estimated_time_remaining || data.estimatedTimeRemaining || prev.estimatedTimeRemaining, status: 'running' })); } else if (messageType === 'completed' || messageType === 'training_completed') { setTrainingProgress(prev => ({ ...prev, progress: 100, status: 'completed', currentStep: 'Entrenamiento completado', estimatedTimeRemaining: 0 })); // Auto-advance to final step after 2 seconds setTimeout(() => { setCurrentStep(4); }, 2000); } else if (messageType === 'failed' || messageType === 'training_failed' || messageType === 'training_error') { setTrainingProgress(prev => ({ ...prev, status: 'failed', error: data.error || data.message || 'Error en el entrenamiento', currentStep: 'Error en el entrenamiento' })); } else if (messageType === 'initial_status') { setTrainingProgress(prev => ({ ...prev, progress: typeof data.progress === 'number' ? data.progress : prev.progress, status: data.status || prev.status, currentStep: data.current_step || data.currentStep || prev.currentStep })); } }, []); // Process WebSocket messages useEffect(() => { if (lastMessage) { processWebSocketMessage(lastMessage); } }, [lastMessage, processWebSocketMessage]); // Backup jobUpdates processing useEffect(() => { if (jobUpdates.length > 0) { const latestUpdate = jobUpdates[0]; processWebSocketMessage(latestUpdate); } }, [jobUpdates, processWebSocketMessage]); // Connect to WebSocket when training starts useEffect(() => { if (tenantId && trainingJobId && currentStep === 3) { connect(); } return () => { if (isConnected) { disconnect(); } }; }, [tenantId, trainingJobId, currentStep, connect, disconnect, isConnected]); const storeTenantId = (tenantId: string) => { try { // Method 1: Store tenant ID directly localStorage.setItem('current_tenant_id', tenantId); // Method 2: Update user_data to include tenant_id const existingUserData = localStorage.getItem('user_data'); if (existingUserData) { const userData = JSON.parse(existingUserData); userData.current_tenant_id = tenantId; userData.tenant_id = tenantId; // Backup key localStorage.setItem('user_data', JSON.stringify(userData)); } else { // Create user_data with tenant info if it doesn't exist localStorage.setItem('user_data', JSON.stringify({ current_tenant_id: tenantId, tenant_id: tenantId })); } // Method 3: Store in a dedicated tenant context localStorage.setItem('tenant_context', JSON.stringify({ current_tenant_id: tenantId, last_updated: new Date().toISOString() })); console.log('✅ Tenant ID stored successfully:', tenantId); } catch (error) { console.error('❌ Failed to store tenant ID:', error); } }; const handleNext = () => { if (validateCurrentStep()) { if (currentStep === 2) { // Always proceed to training step after CSV upload startTraining(); } else { setCurrentStep(prev => Math.min(prev + 1, steps.length)); } } }; const handlePrevious = () => { setCurrentStep(prev => Math.max(prev - 1, 1)); }; const validateCurrentStep = (): boolean => { switch (currentStep) { case 1: if (!bakeryData.name.trim()) { toast.error('El nombre de la panadería es obligatorio'); return false; } if (!bakeryData.address.trim()) { toast.error('La dirección es obligatoria'); return false; } return true; case 2: if (!bakeryData.csvFile) { toast.error('Por favor, selecciona un archivo con tus datos históricos'); return false; } // Validate file format const fileName = bakeryData.csvFile.name.toLowerCase(); const supportedFormats = ['.csv', '.xlsx', '.xls', '.json']; const isValidFormat = supportedFormats.some(format => fileName.endsWith(format)); if (!isValidFormat) { toast.error('Formato de archivo no soportado. Usa CSV, Excel (.xlsx, .xls) o JSON'); return false; } // Validate file size (10MB limit as per backend) const maxSize = 10 * 1024 * 1024; if (bakeryData.csvFile.size > maxSize) { toast.error('El archivo es demasiado grande. Máximo 10MB'); return false; } return true; default: return true; } }; const startTraining = async () => { setCurrentStep(3); setIsLoading(true); try { const token = localStorage.getItem('auth_token'); if (!token) { toast.error('Sesión expirada. Por favor, inicia sesión nuevamente.'); return; } // Create tenant first const tenantData: TenantCreate = { name: bakeryData.name, address: bakeryData.address, business_type: "bakery", postal_code: "28010", phone: "+34655334455", coordinates: bakeryData.coordinates, products: bakeryData.products, has_historical_data: bakeryData.hasHistoricalData, }; const tenant = await createTenant(tenantData); setTenantId(tenant.id); storeTenantId(tenant.id); // Step 2: Validate and Upload CSV file if provided if (bakeryData.csvFile) { try { const validationResult = await validateSalesData(tenant.id, bakeryData.csvFile); if (!validationResult.is_valid) { toast.error(`Error en los datos: ${validationResult.message}`); setTrainingProgress(prev => ({ ...prev, status: 'failed', error: 'Error en la validación de datos históricos' })); return; } await uploadSalesHistory(tenant.id, bakeryData.csvFile); toast.success('Datos históricos validados y subidos correctamente'); } catch (error) { console.error('CSV validation/upload error:', error); toast.error('Error al procesar los datos históricos'); setTrainingProgress(prev => ({ ...prev, status: 'failed', error: 'Error al procesar los datos históricos' })); return; } } // Prepare training job request - always use uploaded data since CSV is required const trainingRequest: TrainingJobRequest = { include_weather: true, include_traffic: false, min_data_points: 30, use_default_data: false // Always false since CSV upload is mandatory }; // Start training job using the proper API const trainingJob = await startTrainingJob(tenant.id, trainingRequest); setTrainingJobId(trainingJob.job_id); setTrainingProgress({ progress: 0, status: 'running', currentStep: 'Iniciando entrenamiento...', productsCompleted: 0, productsTotal: bakeryData.products.length, estimatedTimeRemaining: 600 // 10 minutes }); toast.success('Entrenamiento iniciado correctamente'); } catch (error) { console.error('Training start error:', error); toast.error('Error al iniciar el entrenamiento'); setTrainingProgress(prev => ({ ...prev, status: 'failed', error: 'Error al iniciar el entrenamiento' })); } finally { setIsLoading(false); } }; const handleComplete = async () => { if (!validateCurrentStep()) return; if (currentStep < 3) { // Start training process await startTraining(); } else { // Complete onboarding toast.success('¡Configuración completada exitosamente!'); onComplete(); } }; const handleRetryTraining = async () => { setTrainingProgress({ progress: 0, status: 'pending', currentStep: 'Preparando reintento...', productsCompleted: 0, productsTotal: bakeryData.products.length, estimatedTimeRemaining: 600 }); await startTraining(); }; const handleSkipTraining = () => { toast('Continuando sin entrenamiento. Podrás entrenar los modelos más tarde desde el dashboard.', { icon: 'ℹ️', duration: 4000 }); setCurrentStep(4); }; const handleTrainingTimeout = () => { // Option 1: Navigate to dashboard with limited functionality onComplete(); // This calls your existing completion handler // Option 2: Show a custom modal or message // setShowLimitedAccessMessage(true); // Option 3: Set a flag to enable partial dashboard access // setLimitedAccess(true); }; // Then update the EnhancedTrainingProgress call: const renderStep = () => { switch (currentStep) { case 1: return (

Información de tu Panadería

setBakeryData(prev => ({ ...prev, name: e.target.value }))} className="w-full px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Ej: Panadería San Miguel" />
setBakeryData(prev => ({ ...prev, address: e.target.value }))} className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500" placeholder="Calle Mayor, 123, Madrid" />
); case 2: return (

Datos Históricos

Para obtener predicciones precisas, necesitamos tus datos históricos de ventas. Puedes subir archivos en varios formatos.

!

Formatos soportados y estructura de datos

Formatos aceptados:

📊 Hojas de cálculo:

  • .xlsx (Excel moderno)
  • .xls (Excel clásico)

📄 Datos estructurados:

  • .csv (Valores separados por comas)
  • .json (Formato JSON)

Columnas requeridas (en cualquier idioma):

  • Fecha: fecha, date, datum (formato: YYYY-MM-DD, DD/MM/YYYY, etc.)
  • Producto: producto, product, item, articulo, nombre
  • Cantidad: cantidad, quantity, cantidad_vendida, qty
{ const file = e.target.files?.[0]; if (file) { // Validate file size (10MB limit) const maxSize = 10 * 1024 * 1024; if (file.size > maxSize) { toast.error('El archivo es demasiado grande. Máximo 10MB.'); return; } setBakeryData(prev => ({ ...prev, csvFile: file, hasHistoricalData: true })); toast.success(`Archivo ${file.name} seleccionado correctamente`); } }} className="hidden" />
{bakeryData.csvFile ? (

{bakeryData.csvFile.name}

{(bakeryData.csvFile.size / 1024).toFixed(1)} KB • {bakeryData.csvFile.type || 'Archivo de datos'}

) : (

Archivo requerido: Selecciona un archivo con tus datos históricos de ventas

)}
{/* Sample formats examples */}
Ejemplos de formato:
{/* CSV Example */}
📄 CSV
fecha,producto,cantidad
2024-01-15,Croissants,45
2024-01-15,Pan de molde,32
2024-01-16,Baguettes,28
{/* Excel Example */}
📊 Excel
Fecha Producto Cantidad
15/01/2024Croissants45
15/01/2024Pan molde32
{/* JSON Example */}
🔧 JSON
[
{"{"}"fecha": "2024-01-15", "producto": "Croissants", "cantidad": 45{"}"},
{"{"}"fecha": "2024-01-15", "producto": "Pan de molde", "cantidad": 32{"}"}
]
); case 3: return ( { // Handle timeout - either navigate to dashboard or show limited access console.log('Training timeout - user wants to continue to dashboard'); // You can add your custom timeout logic here }} /> ); case 4: return (

¡Configuración Completada! 🎉

Tu panadería está lista para usar PanIA. Comenzarás a recibir predicciones precisas de demanda.

Resumen de configuración:

Panadería: {bakeryData.name}
Productos: {bakeryData.products.length} seleccionados
Datos históricos: ✅ {bakeryData.csvFile?.name.split('.').pop()?.toUpperCase()} subido
Modelo IA: ✅ Entrenado

💡 Próximo paso: Explora tu dashboard para ver las primeras predicciones y configurar alertas personalizadas.

); default: return null; } }; return (
{/* Header */}

Configuración de PanIA

Configuremos tu panadería para obtener predicciones precisas de demanda

{/* Progress Indicator */}
{steps.map((step, index) => (
step.id ? 'bg-green-500 border-green-500 text-white' : currentStep === step.id ? 'bg-primary-500 border-primary-500 text-white' : 'border-gray-300 text-gray-500' }`} > {currentStep > step.id ? ( ) : ( )}
{step.title}
))}
{/* Step Content */}
{renderStep()}
{/* Navigation */}
{/* Dynamic Next/Complete Button */} {currentStep < 3 ? ( ) : currentStep === 3 ? ( // Training step - show different buttons based on status
{trainingProgress.status === 'failed' ? ( <> ) : trainingProgress.status === 'completed' ? ( ) : ( // Training in progress - show status )}
) : ( // Final step - Complete button )}
{/* Help Section */}

¿Necesitas ayuda?

📧 soporte@pania.es
📞 +34 900 123 456
💬 Chat en vivo
); }; export default OnboardingPage;