Improve the UX and UI of the onboarding steps

This commit is contained in:
Urtzi Alfaro
2025-08-11 07:50:24 +02:00
parent c4d4aeb449
commit 652a850d0f
4 changed files with 703 additions and 408 deletions

View File

@@ -1,8 +1,8 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react';
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader, TrendingUp } from 'lucide-react';
import toast from 'react-hot-toast';
import EnhancedTrainingProgress from '../../components/EnhancedTrainingProgress';
import SimplifiedTrainingProgress from '../../components/SimplifiedTrainingProgress';
import {
useTenant,
@@ -109,10 +109,9 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
// Only initialize from progress if we haven't manually navigated
try {
const { step } = await OnboardingRouter.getOnboardingStepFromProgress();
console.log('🎯 Initializing step from progress:', step);
setCurrentStep(step);
} catch (error) {
console.warn('Failed to get step from progress, using default:', error);
// Silently use default step on error
}
}
};
@@ -124,22 +123,16 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
useEffect(() => {
const fetchTenantIdFromBackend = async () => {
if (!tenantId && user) {
console.log('🏢 No tenant ID in localStorage, fetching from backend...');
try {
const userTenants = await getUserTenants();
console.log('📋 User tenants:', userTenants);
if (userTenants.length > 0) {
// Use the first (most recent) tenant for onboarding
const primaryTenant = userTenants[0];
console.log('✅ Found tenant from backend:', primaryTenant.id);
setTenantId(primaryTenant.id);
storeTenantId(primaryTenant.id);
} else {
console.log(' No tenants found for user - will be created in step 1');
}
} catch (error) {
console.warn('⚠️ Failed to fetch user tenants:', error);
// This is not a critical error - tenant will be created in step 1 if needed
}
}
@@ -194,14 +187,20 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
user_id: user?.id,
tenant_id: tenantId
}).catch(error => {
console.warn('Failed to mark training as completed in API:', error);
// Failed to mark training as completed in API
});
// Show celebration and auto-advance to final step after 3 seconds
// Users can still manually navigate to dashboard immediately if they want
toast.success('🎉 Training completed! Your AI model is ready to use.', {
duration: 5000,
icon: '🤖'
});
// Auto-advance to final step after 2 seconds
setTimeout(() => {
manualNavigation.current = true;
setCurrentStep(4);
}, 2000);
}, 3000);
} else if (messageType === 'failed' || messageType === 'training_failed' || messageType === 'training_error') {
setTrainingProgress(prev => ({
@@ -276,17 +275,13 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
last_updated: new Date().toISOString()
}));
console.log('✅ Tenant ID stored successfully:', tenantId);
} catch (error) {
console.error('❌ Failed to store tenant ID:', error);
// Handle storage error silently
}
};
const handleNext = async () => {
console.log(`🚀 HandleNext called for step ${currentStep}`);
if (!validateCurrentStep()) {
console.log('❌ Validation failed for current step');
return;
}
@@ -294,20 +289,14 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
try {
if (currentStep === 1) {
console.log('📝 Processing step 1: Register bakery');
await createBakeryAndTenant();
console.log('✅ Step 1 completed successfully');
} else if (currentStep === 2) {
console.log('📂 Processing step 2: Upload sales data');
await uploadAndValidateSalesData();
console.log('✅ Step 2 completed successfully');
}
} catch (error) {
console.error('❌ Error in step:', error);
toast.error('Error al procesar el paso. Por favor, inténtalo de nuevo.');
} finally {
setIsLoading(false);
console.log(`🔄 Loading state set to false, current step: ${currentStep}`);
}
};
@@ -319,16 +308,10 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
// Create bakery and tenant for step 1
const createBakeryAndTenant = async () => {
try {
console.log('🏪 Creating bakery tenant...');
let currentTenantId = tenantId;
// Check if user already has a tenant
if (currentTenantId) {
console.log(' User already has tenant ID:', currentTenantId);
console.log('✅ Using existing tenant, skipping creation');
} else {
console.log('📝 Creating new tenant');
if (!currentTenantId) {
const tenantData: TenantCreate = {
name: bakeryData.name,
address: bakeryData.address,
@@ -341,7 +324,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
};
const newTenant = await createTenant(tenantData);
console.log('✅ Tenant created:', newTenant.id);
currentTenantId = newTenant.id;
setTenantId(newTenant.id);
@@ -357,45 +339,32 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
tenant_id: currentTenantId, // Use the correct tenant ID
user_id: user?.id
});
console.log('✅ Step marked as completed');
} catch (stepError) {
console.warn('⚠️ Failed to mark step as completed, but continuing:', stepError);
// Don't throw here - step completion is not critical for UI flow
}
toast.success('Panadería registrada correctamente');
console.log('🎯 Advancing to step 2');
manualNavigation.current = true;
setCurrentStep(2);
} catch (error) {
console.error('❌ Error creating tenant:', error);
throw new Error('Error al registrar la panadería');
}
};
// Validate sales data without importing
const validateSalesFile = async () => {
console.log('🔍 validateSalesFile called');
console.log('tenantId:', tenantId);
console.log('bakeryData.csvFile:', bakeryData.csvFile);
if (!tenantId || !bakeryData.csvFile) {
console.log('❌ Missing tenantId or csvFile');
toast.error('Falta información del tenant o archivo');
return;
}
console.log('⏳ Setting validation status to validating');
setValidationStatus({ status: 'validating' });
try {
console.log('📤 Calling validateSalesData API');
const validationResult = await validateSalesData(tenantId, bakeryData.csvFile);
console.log('📥 Validation result:', validationResult);
if (validationResult.is_valid) {
console.log('✅ Validation successful');
setValidationStatus({
status: 'valid',
message: validationResult.message,
@@ -403,7 +372,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
});
toast.success('¡Archivo validado correctamente!');
} else {
console.log('❌ Validation failed');
setValidationStatus({
status: 'invalid',
message: validationResult.message
@@ -411,7 +379,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
console.error('❌ Error validating sales data:', error);
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
@@ -455,7 +422,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
// Automatically start training after successful upload
await startTraining();
} catch (error: any) {
console.error('Error uploading sales data:', error);
// Error uploading sales data
const message = error?.response?.data?.detail || error.message || 'Error al subir datos de ventas';
throw new Error(message);
}
@@ -498,22 +465,16 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
};
const validateCurrentStep = (): boolean => {
console.log(`🔍 Validating step ${currentStep}`);
console.log('Bakery data:', { name: bakeryData.name, address: bakeryData.address });
switch (currentStep) {
case 1:
if (!bakeryData.name.trim()) {
console.log('❌ Bakery name is empty');
toast.error('El nombre de la panadería es obligatorio');
return false;
}
if (!bakeryData.address.trim()) {
console.log('❌ Bakery address is empty');
toast.error('La dirección es obligatoria');
return false;
}
console.log('✅ Step 1 validation passed');
return true;
case 2:
if (!bakeryData.csvFile) {
@@ -591,7 +552,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
toast.success('Entrenamiento iniciado correctamente');
} catch (error) {
console.error('Training start error:', error);
// Training start error
toast.error('Error al iniciar el entrenamiento');
setTrainingProgress(prev => ({
...prev,
@@ -623,7 +584,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
toast.success('¡Configuración completada exitosamente!');
onComplete();
} catch (error) {
console.warn('Failed to mark final step as completed:', error);
// Failed to mark final step as completed
// Continue anyway for better UX
toast.success('¡Configuración completada exitosamente!');
onComplete();
@@ -663,19 +624,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
// setLimitedAccess(true);
};
// Then update the EnhancedTrainingProgress call:
<EnhancedTrainingProgress
progress={{
progress: trainingProgress.progress,
status: trainingProgress.status,
currentStep: trainingProgress.currentStep,
productsCompleted: trainingProgress.productsCompleted,
productsTotal: trainingProgress.productsTotal,
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
error: trainingProgress.error
}}
onTimeout={handleTrainingTimeout}
/>
const renderStep = () => {
switch (currentStep) {
@@ -827,7 +775,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
type="file"
accept=".csv,.xlsx,.xls,.json"
required
onChange={(e) => {
onChange={async (e) => {
const file = e.target.files?.[0];
if (file) {
// Validate file size (10MB limit)
@@ -837,14 +785,47 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
return;
}
// Update bakery data with the selected file
setBakeryData(prev => ({
...prev,
csvFile: file,
hasHistoricalData: true
}));
// Reset validation status when new file is selected
setValidationStatus({ status: 'idle' });
toast.success(`Archivo ${file.name} seleccionado correctamente`);
// Auto-validate the file after upload if tenantId exists
if (tenantId) {
setValidationStatus({ status: 'validating' });
try {
const validationResult = await validateSalesData(tenantId, file);
if (validationResult.is_valid) {
setValidationStatus({
status: 'valid',
message: validationResult.message,
records: validationResult.details?.total_records || 0
});
toast.success('¡Archivo validado correctamente!');
} else {
setValidationStatus({
status: 'invalid',
message: validationResult.message
});
toast.error(`Error en validación: ${validationResult.message}`);
}
} catch (error: any) {
setValidationStatus({
status: 'invalid',
message: 'Error al validar el archivo'
});
toast.error('Error al validar el archivo');
}
} else {
// If no tenantId yet, set to idle and wait for manual validation
setValidationStatus({ status: 'idle' });
}
}
}}
className="hidden"
@@ -911,9 +892,6 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
</div>
) : (
<div className="space-y-2">
<p className="text-sm text-gray-500">
Debug: validationStatus = "{validationStatus.status}", tenantId = "{tenantId || 'not set'}"
</p>
{!tenantId ? (
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
@@ -1052,68 +1030,156 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
);
case 3:
return (
<EnhancedTrainingProgress
progress={{
progress: trainingProgress.progress,
status: trainingProgress.status,
currentStep: trainingProgress.currentStep,
productsCompleted: trainingProgress.productsCompleted,
productsTotal: trainingProgress.productsTotal,
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
error: trainingProgress.error
}}
onTimeout={() => {
// 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
}}
/>
);
return (
<SimplifiedTrainingProgress
progress={{
progress: trainingProgress.progress,
status: trainingProgress.status,
currentStep: trainingProgress.currentStep,
productsCompleted: trainingProgress.productsCompleted,
productsTotal: trainingProgress.productsTotal,
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
error: trainingProgress.error
}}
onTimeout={() => {
toast.success('El entrenamiento continuará en segundo plano. ¡Puedes empezar a explorar!');
onComplete(); // Navigate to dashboard
}}
onBackgroundMode={() => {
toast.success('El entrenamiento continúa en segundo plano. ¡Bienvenido a tu panel de control!');
onComplete(); // Navigate to dashboard with limited features
}}
onEmailNotification={(email: string) => {
toast.success(`Te enviaremos un correo a ${email} cuando tu modelo esté listo!`);
// Here you would typically call an API to register the email notification
}}
/>
);
case 4:
return (
<div className="text-center space-y-6">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
<Check className="h-8 w-8 text-green-600" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
¡Configuración Completada! 🎉
</h3>
<p className="text-gray-600">
Tu panadería está lista para usar PanIA. Comenzarás a recibir predicciones precisas de demanda.
<div className="max-w-3xl mx-auto">
{/* Celebration Header */}
<div className="text-center mb-8">
<div className="relative">
<div className="w-24 h-24 bg-gradient-to-r from-green-400 to-green-600 rounded-full flex items-center justify-center mx-auto mb-6 shadow-lg animate-bounce">
<Check className="h-12 w-12 text-white" />
</div>
{/* Confetti effect */}
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 text-2xl animate-pulse">
🎉
</div>
</div>
<h2 className="text-3xl font-bold text-gray-900 mb-4">
¡Todo listo! 🎉
</h2>
<p className="text-xl text-gray-600 mb-8">
Tu asistente de IA para panadería está listo para ayudarte a predecir la demanda y hacer crecer tu negocio
</p>
</div>
<div className="bg-gradient-to-r from-primary-50 to-blue-50 p-6 rounded-xl">
<h4 className="font-medium text-gray-900 mb-3">Resumen de configuración:</h4>
<div className="text-left space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">Panadería:</span>
<span className="font-medium">{bakeryData.name}</span>
{/* Setup Summary Card */}
<div className="bg-gradient-to-r from-green-50 to-blue-50 rounded-3xl p-8 mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-6 text-center">
Lo que hemos configurado para ti
</h3>
<div className="grid md:grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<Store className="w-4 h-4 text-white" />
</div>
<div>
<div className="font-medium text-gray-900">{bakeryData.name}</div>
<div className="text-sm text-gray-600">Perfil de tu panadería</div>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<Upload className="w-4 h-4 text-white" />
</div>
<div>
<div className="font-medium text-gray-900">Datos históricos</div>
<div className="text-sm text-gray-600">
Archivo {bakeryData.csvFile?.name.split('.').pop()?.toUpperCase()} procesado
</div>
</div>
</div>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Productos:</span>
<span className="font-medium">{bakeryData.products.length} seleccionados</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Datos históricos:</span>
<span className="font-medium text-green-600">
{bakeryData.csvFile?.name.split('.').pop()?.toUpperCase()} subido
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Modelo IA:</span>
<span className="font-medium text-green-600"> Entrenado</span>
<div className="space-y-4">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<Brain className="w-4 h-4 text-white" />
</div>
<div>
<div className="font-medium text-gray-900">Modelo de IA</div>
<div className="text-sm text-gray-600">Entrenado y listo</div>
</div>
</div>
<div className="flex items-center space-x-3">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-white" />
</div>
<div>
<div className="font-medium text-gray-900">Predicciones inteligentes</div>
<div className="text-sm text-gray-600">Listo para usar</div>
</div>
</div>
</div>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 p-4 rounded-lg">
<p className="text-sm text-yellow-800">
💡 <strong>Próximo paso:</strong> Explora tu dashboard para ver las primeras predicciones y configurar alertas personalizadas.
{/* Next Steps Preview */}
<div className="bg-white border-2 border-blue-200 rounded-3xl p-8 mb-8">
<div className="text-center mb-6">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mx-auto mb-4">
<TrendingUp className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-2">
¿Qué sigue ahora?
</h3>
<p className="text-gray-600">
Tu panel de control está listo con todo lo que necesitas para tomar decisiones más inteligentes
</p>
</div>
<div className="grid md:grid-cols-3 gap-4 text-center">
<div className="p-4">
<div className="text-2xl mb-2">📊</div>
<div className="font-medium text-gray-900 mb-1">Ver Pronósticos</div>
<div className="text-sm text-gray-600">Ve la demanda prevista para cada producto</div>
</div>
<div className="p-4">
<div className="text-2xl mb-2">🎯</div>
<div className="font-medium text-gray-900 mb-1">Planificar Producción</div>
<div className="text-sm text-gray-600">Sabe exactamente cuánto hornear</div>
</div>
<div className="p-4">
<div className="text-2xl mb-2">📈</div>
<div className="font-medium text-gray-900 mb-1">Seguir Rendimiento</div>
<div className="text-sm text-gray-600">Monitorear precisión y ganancias</div>
</div>
</div>
</div>
{/* CTA Button */}
<div className="text-center">
<button
onClick={() => {
toast.success('¡Bienvenido a tu nuevo panel de control de panadería con IA! 🚀');
setTimeout(() => onComplete(), 1000); // Small delay for better UX
}}
className="bg-gradient-to-r from-primary-500 to-primary-600 text-white px-12 py-4 rounded-2xl text-lg font-semibold hover:from-primary-600 hover:to-primary-700 transform hover:scale-105 transition-all duration-200 shadow-lg"
>
Acceder al Panel de Control 🚀
</button>
<p className="text-sm text-gray-500 mt-3">
¡Listo para ver tus primeras predicciones!
</p>
</div>
</div>
@@ -1139,169 +1205,172 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
return (
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-blue-50 py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Configuración de PanIA
{/* Enhanced Header with Accessibility */}
<header className="text-center mb-12" role="banner">
<div
className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-r from-primary-500 to-primary-600 rounded-full mb-6"
aria-label="PanIA logo"
>
<Brain className="w-10 h-10 text-white" aria-hidden="true" />
</div>
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Bienvenido a PanIA
</h1>
<p className="text-gray-600">
Configuremos tu panadería para obtener predicciones precisas de demanda
<p className="text-xl text-gray-600 max-w-2xl mx-auto mb-6">
Configuremos tu asistente de IA para panadería en unos sencillos pasos
</p>
{/* Progress Summary */}
{progress && (
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
<div className="flex items-center justify-center space-x-4">
<div className="text-sm text-blue-700">
<span className="font-semibold">Progreso:</span> {Math.round(progress.completion_percentage)}%
{/* Simplified Progress Summary with Accessibility */}
{progress && currentStep < 4 && (
<div className="max-w-md mx-auto">
<div
className="bg-white rounded-2xl shadow-lg p-6"
role="progressbar"
aria-valuenow={Math.round(progress.completion_percentage)}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Onboarding setup progress"
>
<div className="flex items-center justify-between mb-3">
<span className="text-sm font-medium text-gray-700">Progreso de Configuración</span>
<span className="text-sm font-bold text-primary-600" aria-live="polite">
{Math.round(progress.completion_percentage)}%
</span>
</div>
<div className="text-sm text-blue-600">
({progress.steps.filter((s: any) => s.completed).length}/{progress.steps.length} pasos completados)
<div className="bg-gray-200 rounded-full h-2 overflow-hidden" aria-hidden="true">
<div
className="bg-gradient-to-r from-primary-500 to-primary-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${progress.completion_percentage}%` }}
/>
</div>
<div className="mt-2 text-xs text-gray-500" role="status">
Paso {currentStep} de 4 {progress.steps.filter((s: any) => s.completed).length} completados
</div>
</div>
</div>
)}
</div>
</header>
{/* Progress Indicator */}
<div className="mb-8">
<div className="flex justify-center items-center space-x-4 mb-6">
{steps.map((step, index) => (
<div key={step.id} className="flex flex-col items-center">
<div
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 transition-all duration-300 ${
currentStep > 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 ? (
<Check className="h-5 w-5" />
) : (
<step.icon className="h-5 w-5" />
)}
{/* Modern Step Indicator with Accessibility - Only show for steps 1-2, hide during training */}
{currentStep < 3 && (
<nav className="mb-12" aria-label="Onboarding progress steps" role="navigation">
<div className="max-w-2xl mx-auto">
<div className="flex justify-between items-center relative">
{/* Connection Line */}
<div className="absolute top-6 left-6 right-6 h-0.5 bg-gray-200" aria-hidden="true">
<div
className="h-full bg-gradient-to-r from-primary-500 to-primary-600 rounded-full transition-all duration-700"
style={{ width: `${((currentStep - 1) / (steps.length - 1)) * 100}%` }}
/>
</div>
<span className="mt-2 text-xs text-gray-500 text-center max-w-20">
{step.title}
</span>
{steps.map((step, index) => {
const isCompleted = currentStep > step.id;
const isCurrent = currentStep === step.id;
return (
<div key={step.id} className="flex flex-col items-center relative z-10">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all duration-300 shadow-lg ${
isCompleted
? 'bg-green-500 text-white scale-110'
: isCurrent
? 'bg-gradient-to-r from-primary-500 to-primary-600 text-white animate-pulse'
: 'bg-white border-2 border-gray-300 text-gray-400'
}`}
aria-label={`Step ${step.id}: ${step.title}${isCompleted ? ' (completed)' : isCurrent ? ' (current)' : ''}`}
role="img"
>
{isCompleted ? (
<CheckCircle className="h-6 w-6" aria-hidden="true" />
) : (
<step.icon className="h-6 w-6" aria-hidden="true" />
)}
</div>
<div
className={`mt-3 text-center ${
isCurrent ? 'text-primary-600 font-semibold' : 'text-gray-500'
}`}
aria-current={isCurrent ? 'step' : undefined}
>
<div className="text-sm font-medium">{step.title}</div>
{isCurrent && (
<div className="text-xs text-primary-500 mt-1">Paso actual</div>
)}
</div>
</div>
);
})}
</div>
))}
</div>
<div className="mt-4 bg-gray-200 rounded-full h-2">
<div
className="bg-primary-500 h-2 rounded-full transition-all duration-500"
style={{
width: `${progress ? progress.completion_percentage : (currentStep / steps.length) * 100}%`
}}
/>
</div>
</div>
</div>
</nav>
)}
{/* Step Content */}
<div className="bg-white rounded-2xl shadow-soft p-6 mb-8">
{/* Step Content with Accessibility */}
<main
className={`${currentStep === 3 ? '' : 'bg-white rounded-3xl shadow-lg p-8'} mb-8`}
role="main"
aria-live="polite"
aria-label={`Step ${currentStep} content`}
>
{renderStep()}
</div>
</main>
{/* Navigation */}
<div className="flex justify-between">
<button
onClick={handlePrevious}
disabled={currentStep === 1}
className="flex items-center px-4 py-2 text-gray-600 disabled:text-gray-400 disabled:cursor-not-allowed hover:text-gray-800 transition-colors"
{/* Navigation with Enhanced Accessibility - Hidden during training (step 3) and completion (step 4) */}
{currentStep < 3 && (
<nav
className="flex justify-between items-center bg-white rounded-3xl shadow-sm p-6"
role="navigation"
aria-label="Onboarding navigation"
>
<ChevronLeft className="h-5 w-5 mr-1" />
Anterior
</button>
<button
onClick={handlePrevious}
disabled={currentStep === 1}
className="flex items-center px-6 py-3 text-gray-600 disabled:text-gray-300 disabled:cursor-not-allowed hover:text-gray-800 hover:bg-gray-50 rounded-xl transition-all group focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
aria-label="Go to previous step"
tabIndex={currentStep === 1 ? -1 : 0}
>
<ChevronLeft className="h-5 w-5 mr-2 group-hover:-translate-x-0.5 transition-transform" aria-hidden="true" />
Paso Anterior
</button>
{/* Dynamic Next/Complete Button */}
{currentStep < 3 ? (
{/* Step info */}
<div className="text-center" role="status" aria-live="polite">
<div className="text-sm text-gray-500">Paso {currentStep} de 4</div>
<div className="text-xs text-gray-400 mt-1">
{currentStep === 1 && "Configura tu panadería"}
{currentStep === 2 && "Sube datos de ventas"}
</div>
</div>
{/* Dynamic Next Button with contextual text */}
<button
onClick={handleNext}
disabled={isLoading}
className="flex items-center px-6 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isLoading || (currentStep === 2 && validationStatus?.status !== 'valid')}
className="flex items-center px-8 py-3 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-xl hover:from-primary-600 hover:to-primary-700 transition-all hover:shadow-lg hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none group focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
aria-label={`${currentStep === 1 ? 'Save bakery information and continue' : currentStep === 2 && validationStatus?.status === 'valid' ? 'Start AI training' : 'Continue to next step'}`}
>
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Procesando...
<Loader className="h-5 w-5 mr-2 animate-spin" aria-hidden="true" />
<span aria-live="polite">
{currentStep === 1 ? "Creando panadería..." : "Procesando..."}
</span>
</>
) : (
<>
Siguiente
<ChevronRight className="h-5 w-5 ml-1" />
{currentStep === 1 && "Guardar y Continuar"}
{currentStep === 2 && (
<>
{validationStatus?.status === 'valid' ? "Iniciar Entrenamiento" : "Continuar"}
</>
)}
<ChevronRight className="h-5 w-5 ml-2 group-hover:translate-x-0.5 transition-transform" aria-hidden="true" />
</>
)}
</button>
) : currentStep === 3 ? (
// Training step - show different buttons based on status
<div className="flex space-x-3">
{trainingProgress.status === 'failed' ? (
<>
<button
onClick={handleRetryTraining}
disabled={isLoading}
className="flex items-center px-4 py-2 bg-primary-500 text-white rounded-xl hover:bg-primary-600 transition-all disabled:opacity-50"
>
{isLoading ? (
<Loader className="h-5 w-5 mr-2 animate-spin" />
) : (
<Brain className="h-5 w-5 mr-2" />
)}
Reintentar
</button>
<button
onClick={handleSkipTraining}
className="flex items-center px-4 py-2 border border-gray-300 text-gray-700 rounded-xl hover:bg-gray-50 transition-all"
>
Continuar sin entrenar
<ChevronRight className="h-5 w-5 ml-1" />
</button>
</>
) : trainingProgress.status === 'completed' ? (
<button
onClick={() => {
manualNavigation.current = true;
setCurrentStep(4);
}}
className="flex items-center px-6 py-2 bg-green-500 text-white rounded-xl hover:bg-green-600 transition-all hover:shadow-lg"
>
Continuar
<ChevronRight className="h-5 w-5 ml-1" />
</button>
) : (
// Training in progress - show status
<button
disabled
className="flex items-center px-6 py-2 bg-gray-400 text-white rounded-xl cursor-not-allowed"
>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Entrenando... {trainingProgress.progress}%
</button>
)}
</div>
) : (
// Final step - Complete button
<button
onClick={handleComplete}
disabled={isLoading}
className="flex items-center px-6 py-2 bg-success-500 text-white rounded-xl hover:bg-success-600 transition-all hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Completando...
</>
) : (
<>
<Check className="h-5 w-5 mr-2" />
Ir al Dashboard
</>
)}
</button>
)}
</div>
</nav>
)}
{/* Help Section */}
<div className="mt-8 text-center">