Improve the UX and UI of the onboarding steps
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user