664 lines
29 KiB
TypeScript
664 lines
29 KiB
TypeScript
// Fixed Frontend Onboarding with Auto-Training
|
|
// frontend/src/pages/onboarding.tsx
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { CheckIcon, ArrowRightIcon, ArrowLeftIcon, CloudArrowUpIcon } from '@heroicons/react/24/outline';
|
|
import onboardingApi from '../api/onboardingApi';
|
|
|
|
const OnboardingPage = () => {
|
|
const router = useRouter();
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [completedSteps, setCompletedSteps] = useState<number[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [authToken, setAuthToken] = useState<string | null>(null);
|
|
const [tenantId, setTenantId] = useState<string | null>(null);
|
|
const [trainingTaskId, setTrainingTaskId] = useState<string | null>(null);
|
|
const [trainingStarted, setTrainingStarted] = useState(false); // New state to track training start
|
|
|
|
const [formData, setFormData] = useState({
|
|
// Step 1: User Registration
|
|
full_name: '',
|
|
email: '',
|
|
password: '',
|
|
confirm_password: '',
|
|
|
|
// Step 2: Bakery Information
|
|
bakery_name: '',
|
|
address: '',
|
|
city: 'Madrid',
|
|
postal_code: '',
|
|
has_nearby_schools: false,
|
|
has_nearby_offices: false,
|
|
|
|
// Step 3: Sales History File
|
|
salesFile: null as File | null,
|
|
|
|
// Step 4: Model Training
|
|
trainingStatus: 'pending'
|
|
});
|
|
|
|
const [errors, setErrors] = useState<{ [key: string]: string }>({});
|
|
const [trainingProgress, setTrainingProgress] = useState({
|
|
currentTask: '',
|
|
progress: 0,
|
|
tasks: [
|
|
{ id: 1, name: 'Procesando archivo de ventas históricas...', completed: false },
|
|
{ id: 2, name: 'Preparando datos para el entrenamiento...', completed: false },
|
|
{ id: 3, name: 'Entrenando modelos de pronóstico (esto puede tardar unos minutos)...', completed: false },
|
|
{ id: 4, name: 'Evaluando y optimizando modelos...', completed: false },
|
|
{ id: 5, name: 'Desplegando modelos en producción...', completed: false },
|
|
{ id: 6, name: 'Entrenamiento completado.', completed: false },
|
|
]
|
|
});
|
|
|
|
// Load auth token and tenantId on component mount
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('access_token');
|
|
const storedTenantId = localStorage.getItem('tenant_id');
|
|
if (token) {
|
|
setAuthToken(token);
|
|
onboardingApi.setAuthToken(token);
|
|
}
|
|
if (storedTenantId) {
|
|
setTenantId(storedTenantId);
|
|
}
|
|
}, []);
|
|
|
|
// Utility function to extract error message from FastAPI response
|
|
const getErrorMessage = (error: any): string => {
|
|
if (error.response && error.response.data && error.response.data.detail) {
|
|
const detail = error.response.data.detail;
|
|
if (typeof detail === 'string') {
|
|
return detail;
|
|
}
|
|
if (Array.isArray(detail)) {
|
|
return detail.map((err: any) => err.msg || JSON.stringify(err)).join(', ');
|
|
}
|
|
if (typeof detail === 'object') {
|
|
return detail.msg || JSON.stringify(detail);
|
|
}
|
|
}
|
|
return error.message || 'Ocurrió un error inesperado.';
|
|
};
|
|
|
|
const startModelTraining = useCallback(async () => {
|
|
console.log('Starting model training...');
|
|
setLoading(true);
|
|
setErrors({});
|
|
setTrainingStarted(true); // Mark training as started
|
|
|
|
try {
|
|
const response = await onboardingApi.startTraining();
|
|
console.log('Training API response:', response);
|
|
|
|
setFormData(prev => ({ ...prev, trainingStatus: 'in_progress' }));
|
|
setTrainingTaskId(response.data.task_id);
|
|
setCompletedSteps(prev => [...prev, 4]);
|
|
|
|
console.log('Training started successfully with task ID:', response.data.task_id);
|
|
} catch (err: any) {
|
|
console.error('Error starting training:', err);
|
|
setErrors({ general: getErrorMessage(err) });
|
|
setTrainingStarted(false); // Reset if failed
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Auto-start training when entering step 4
|
|
useEffect(() => {
|
|
if (currentStep === 4 && !trainingStarted && !trainingTaskId && !loading) {
|
|
console.log('Auto-starting training on step 4...');
|
|
startModelTraining();
|
|
}
|
|
}, [currentStep, trainingStarted, trainingTaskId, loading, startModelTraining]);
|
|
|
|
// Polling for training status
|
|
useEffect(() => {
|
|
let interval: NodeJS.Timeout;
|
|
|
|
if (currentStep === 4 && trainingTaskId) {
|
|
console.log(`Starting to poll for training status with task ID: ${trainingTaskId}`);
|
|
interval = setInterval(async () => {
|
|
try {
|
|
const statusResponse = await onboardingApi.getTrainingStatus(trainingTaskId);
|
|
console.log("Polling status:", statusResponse);
|
|
|
|
const { status, progress, current_step, error: trainingError } = statusResponse.data;
|
|
|
|
setTrainingProgress(prev => ({
|
|
...prev,
|
|
currentTask: current_step || 'Procesando...',
|
|
progress: progress || 0,
|
|
tasks: prev.tasks.map(task =>
|
|
task.name === current_step ? { ...task, completed: true } : task
|
|
),
|
|
}));
|
|
|
|
setFormData(prev => ({ ...prev, trainingStatus: status }));
|
|
|
|
if (status === 'completed') {
|
|
clearInterval(interval);
|
|
setLoading(false);
|
|
setCompletedSteps(prev => [...prev.filter(s => s !== 4), 4]);
|
|
console.log('Training completed successfully!');
|
|
} else if (status === 'failed') {
|
|
clearInterval(interval);
|
|
setLoading(false);
|
|
setTrainingStarted(false); // Allow retry
|
|
setErrors({ general: trainingError || 'El entrenamiento falló.' });
|
|
console.error('Training failed:', trainingError);
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error fetching training status:', error);
|
|
clearInterval(interval);
|
|
setLoading(false);
|
|
setTrainingStarted(false); // Allow retry
|
|
setErrors({ general: getErrorMessage(error) });
|
|
}
|
|
}, 3000); // Poll every 3 seconds
|
|
}
|
|
|
|
return () => clearInterval(interval);
|
|
}, [currentStep, trainingTaskId]);
|
|
|
|
// Use useCallback for memoized functions
|
|
const handleNext = useCallback(async () => {
|
|
setLoading(true);
|
|
setErrors({});
|
|
|
|
try {
|
|
if (currentStep === 1) {
|
|
if (formData.password !== formData.confirm_password) {
|
|
setErrors({ confirmPassword: 'Las contraseñas no coinciden' });
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
const response = await onboardingApi.registerUser(formData);
|
|
console.log('User registered:', response);
|
|
|
|
if (response.data?.access_token) {
|
|
localStorage.setItem('access_token', response.data.access_token);
|
|
onboardingApi.setAuthToken(response.data.access_token);
|
|
setAuthToken(response.data.access_token);
|
|
}
|
|
if (response.data?.tenant_id) {
|
|
localStorage.setItem('tenant_id', response.data.tenant_id);
|
|
setTenantId(response.data.tenant_id);
|
|
}
|
|
setCompletedSteps(prev => [...prev, 1]);
|
|
setCurrentStep(2);
|
|
|
|
} else if (currentStep === 2) {
|
|
const response = await onboardingApi.registerBakery(formData);
|
|
console.log('Bakery registered:', response);
|
|
setCompletedSteps(prev => [...prev, 2]);
|
|
setCurrentStep(3);
|
|
|
|
} else if (currentStep === 3) {
|
|
if (!formData.salesFile) {
|
|
setErrors({ salesFile: 'Por favor, suba el archivo de historial de ventas.' });
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
const response = await onboardingApi.uploadSalesHistory(formData.salesFile);
|
|
console.log('Sales history uploaded:', response);
|
|
setCompletedSteps(prev => [...prev, 3]);
|
|
setCurrentStep(4); // This will trigger auto-training via useEffect
|
|
}
|
|
} catch (err: any) {
|
|
console.error('Error in step', currentStep, err);
|
|
setErrors({ general: getErrorMessage(err) });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [currentStep, formData]);
|
|
|
|
const handlePrevious = useCallback(() => {
|
|
setCurrentStep(prev => Math.max(1, prev - 1));
|
|
}, []);
|
|
|
|
const handleFileChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (event.target.files && event.target.files[0]) {
|
|
setFormData(prev => ({ ...prev, salesFile: event.target.files![0] }));
|
|
setErrors(prev => ({ ...prev, salesFile: '' }));
|
|
}
|
|
}, []);
|
|
|
|
const handleSubmitFinal = async () => {
|
|
setLoading(true);
|
|
setErrors({});
|
|
try {
|
|
const response = await onboardingApi.completeOnboarding();
|
|
console.log('Onboarding completed:', response);
|
|
router.push('/dashboard');
|
|
} catch (err: any) {
|
|
console.error('Error completing onboarding:', err);
|
|
setErrors({ general: getErrorMessage(err) });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const renderStep = () => {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Paso 1: Datos de Usuario</h2>
|
|
<p className="text-gray-600 mb-6">Regístrate para comenzar a usar BakeryForecast.</p>
|
|
{errors.general && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
|
<span className="block sm:inline">{errors.general}</span>
|
|
</div>
|
|
)}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700">Nombre Completo</label>
|
|
<input
|
|
type="text"
|
|
id="fullName"
|
|
value={formData.full_name}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, full_name: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="Tu nombre completo"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700">Email</label>
|
|
<input
|
|
type="email"
|
|
id="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="tu@email.com"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700">Contraseña</label>
|
|
<input
|
|
type="password"
|
|
id="password"
|
|
value={formData.password}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="••••••••"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">Confirmar Contraseña</label>
|
|
<input
|
|
type="password"
|
|
id="confirmPassword"
|
|
value={formData.confirm_password}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, confirm_password: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="••••••••"
|
|
/>
|
|
{errors.confirmPassword && <p className="text-red-500 text-xs mt-1">{errors.confirmPassword}</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 2:
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Paso 2: Información de la Panadería</h2>
|
|
<p className="text-gray-600 mb-6">Cuéntanos sobre tu panadería para personalizar las predicciones.</p>
|
|
{errors.general && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
|
<span className="block sm:inline">{errors.general}</span>
|
|
</div>
|
|
)}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label htmlFor="bakeryName" className="block text-sm font-medium text-gray-700">Nombre de la Panadería</label>
|
|
<input
|
|
type="text"
|
|
id="bakeryName"
|
|
value={formData.bakery_name}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, bakery_name: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="Panadería San José"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="address" className="block text-sm font-medium text-gray-700">Dirección</label>
|
|
<input
|
|
type="text"
|
|
id="address"
|
|
value={formData.address}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, address: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="Calle Mayor 123, Madrid"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="postalCode" className="block text-sm font-medium text-gray-700">Código Postal</label>
|
|
<input
|
|
type="text"
|
|
id="postalCode"
|
|
value={formData.postal_code}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, postal_code: e.target.value }))}
|
|
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-orange-500 focus:ring-orange-500"
|
|
placeholder="28001"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.has_nearby_schools}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, has_nearby_schools: e.target.checked }))}
|
|
className="rounded border-gray-300 text-orange-600 focus:ring-orange-500"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">Hay colegios cerca</span>
|
|
</label>
|
|
<label className="flex items-center">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.has_nearby_offices}
|
|
onChange={(e) => setFormData(prev => ({ ...prev, has_nearby_offices: e.target.checked }))}
|
|
className="rounded border-gray-300 text-orange-600 focus:ring-orange-500"
|
|
/>
|
|
<span className="ml-2 text-sm text-gray-700">Hay oficinas cerca</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
case 3:
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Paso 3: Historial de Ventas</h2>
|
|
<p className="text-gray-600 mb-6">Sube un archivo CSV con tu historial de ventas para entrenar el modelo de predicción.</p>
|
|
{errors.general && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
|
<span className="block sm:inline">{errors.general}</span>
|
|
</div>
|
|
)}
|
|
{errors.salesFile && <p className="text-red-500 text-xs mb-4">{errors.salesFile}</p>}
|
|
|
|
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
|
<div className="space-y-1 text-center">
|
|
<CloudArrowUpIcon className="mx-auto h-12 w-12 text-gray-400" />
|
|
<div className="flex text-sm text-gray-600">
|
|
<label
|
|
htmlFor="file-upload"
|
|
className="relative cursor-pointer bg-white rounded-md font-medium text-orange-600 hover:text-orange-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-orange-500"
|
|
>
|
|
<span>Sube un archivo</span>
|
|
<input id="file-upload" name="file-upload" type="file" className="sr-only" onChange={handleFileChange} accept=".csv,.xlsx" />
|
|
</label>
|
|
<p className="pl-1">o arrastra y suelta</p>
|
|
</div>
|
|
<p className="text-xs text-gray-500">Archivo CSV o Excel</p>
|
|
{formData.salesFile && (
|
|
<p className="text-sm text-gray-700 mt-2">Archivo seleccionado: <strong>{formData.salesFile.name}</strong></p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<p className="mt-4 text-sm text-gray-500">
|
|
Asegúrate de que tu archivo contiene columnas como: <strong>date</strong>, <strong>product_name</strong>, <strong>quantity_sold</strong>, <strong>revenue</strong> (opcional).
|
|
</p>
|
|
</div>
|
|
);
|
|
|
|
case 4:
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-4">Paso 4: Entrenamiento del Modelo</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
{trainingStarted ?
|
|
'Estamos entrenando los modelos de predicción con tus datos de ventas. Esto puede tardar unos minutos.' :
|
|
'Preparándose para entrenar los modelos de predicción...'
|
|
}
|
|
</p>
|
|
|
|
{errors.general && (
|
|
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative mb-4" role="alert">
|
|
<span className="block sm:inline">{errors.general}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
|
|
<div className="px-4 py-5 sm:px-6">
|
|
<h3 className="text-lg leading-6 font-medium text-gray-900">Estado del Entrenamiento</h3>
|
|
<p className="mt-1 max-w-2xl text-sm text-gray-500">Progreso del entrenamiento del modelo de IA</p>
|
|
</div>
|
|
<div className="border-t border-gray-200">
|
|
<dl>
|
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt className="text-sm font-medium text-gray-500">Estado</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
formData.trainingStatus === 'pending' ? 'bg-yellow-100 text-yellow-800' :
|
|
formData.trainingStatus === 'in_progress' ? 'bg-blue-100 text-blue-800' :
|
|
formData.trainingStatus === 'completed' ? 'bg-green-100 text-green-800' :
|
|
'bg-red-100 text-red-800'
|
|
}`}>
|
|
{formData.trainingStatus === 'pending' ? 'En espera' :
|
|
formData.trainingStatus === 'in_progress' ? 'En progreso' :
|
|
formData.trainingStatus === 'completed' ? 'Completado' :
|
|
formData.trainingStatus === 'failed' ? 'Fallido' : 'Desconocido'}
|
|
</span>
|
|
</dd>
|
|
</div>
|
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt className="text-sm font-medium text-gray-500">Progreso</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div className="bg-orange-600 h-2.5 rounded-full" style={{ width: `${trainingProgress.progress}%` }}></div>
|
|
</div>
|
|
<p className="mt-1 text-right text-xs text-gray-500">{trainingProgress.progress}%</p>
|
|
</dd>
|
|
</div>
|
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt className="text-sm font-medium text-gray-500">Paso Actual</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">{trainingProgress.currentTask || 'Iniciando...'}</dd>
|
|
</div>
|
|
<div className="py-4 sm:py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
|
<dt className="text-sm font-medium text-gray-500">Historial de Tareas</dt>
|
|
<dd className="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
|
|
<ul className="divide-y divide-gray-200">
|
|
{trainingProgress.tasks.map(task => (
|
|
<li key={task.id} className="py-2 flex items-center justify-between">
|
|
<span className="text-sm">{task.name}</span>
|
|
{task.completed && <CheckIcon className="h-5 w-5 text-green-500" />}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Manual start button (only shown if auto-start failed) */}
|
|
{!trainingStarted && formData.trainingStatus === 'pending' && !loading && (
|
|
<div className="mt-6 text-center">
|
|
<button
|
|
onClick={startModelTraining}
|
|
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500"
|
|
>
|
|
Iniciar Entrenamiento
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Retry button if training failed */}
|
|
{formData.trainingStatus === 'failed' && !loading && (
|
|
<div className="mt-6 text-center">
|
|
<button
|
|
onClick={() => {
|
|
setTrainingStarted(false);
|
|
setTrainingTaskId(null);
|
|
setFormData(prev => ({ ...prev, trainingStatus: 'pending' }));
|
|
setErrors({});
|
|
}}
|
|
className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
|
|
>
|
|
Reintentar Entrenamiento
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
case 5:
|
|
return (
|
|
<div>
|
|
<h2 className="text-2xl font-semibold text-gray-800 mb-4">¡Enhorabuena!</h2>
|
|
<p className="text-gray-600 mb-6">Has completado el proceso de configuración. Tu sistema de predicción está listo para usar.</p>
|
|
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
|
<div className="flex">
|
|
<CheckIcon className="h-5 w-5 text-green-400" />
|
|
<div className="ml-3">
|
|
<h3 className="text-sm font-medium text-green-800">Sistema Configurado</h3>
|
|
<div className="mt-2 text-sm text-green-700">
|
|
<p>Tu modelo de predicción ha sido entrenado y está listo para generar pronósticos precisos para tu panadería.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const steps = [
|
|
{ id: 1, name: 'Registro', status: currentStep > 1 || completedSteps.includes(1) ? 'complete' : currentStep === 1 ? 'current' : 'upcoming' },
|
|
{ id: 2, name: 'Panadería', status: currentStep > 2 || completedSteps.includes(2) ? 'complete' : currentStep === 2 ? 'current' : 'upcoming' },
|
|
{ id: 3, name: 'Historial de Ventas', status: currentStep > 3 || completedSteps.includes(3) ? 'complete' : currentStep === 3 ? 'current' : 'upcoming' },
|
|
{ id: 4, name: 'Entrenamiento ML', status: currentStep > 4 || completedSteps.includes(4) ? 'complete' : currentStep === 4 ? 'current' : 'upcoming' },
|
|
{ id: 5, name: 'Completar', status: currentStep > 5 || completedSteps.includes(5) ? 'complete' : currentStep === 5 ? 'current' : 'upcoming' },
|
|
];
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-4 sm:p-6 lg:p-8">
|
|
<div className="bg-white shadow-xl rounded-lg p-6 sm:p-8 w-full max-w-4xl">
|
|
{/* Progress Stepper */}
|
|
<nav aria-label="Progress" className="mb-8">
|
|
<ol role="list" className="flex items-center justify-center">
|
|
{steps.map((step, stepIdx) => (
|
|
<li key={step.name} className="relative flex-1">
|
|
<div className="flex flex-col items-center">
|
|
<span className={`flex h-10 w-10 items-center justify-center rounded-full ${
|
|
step.status === 'complete' ? 'bg-orange-600' :
|
|
step.status === 'current' ? 'border-2 border-orange-600' :
|
|
'bg-gray-200'
|
|
}`}>
|
|
{step.status === 'complete' ? (
|
|
<CheckIcon className="h-6 w-6 text-white" aria-hidden="true" />
|
|
) : (
|
|
<span className={`text-sm font-medium ${step.status === 'current' ? 'text-orange-600' : 'text-gray-500'}`}>
|
|
{step.id}
|
|
</span>
|
|
)}
|
|
</span>
|
|
<span className={`mt-2 text-sm font-medium ${step.status === 'current' ? 'text-orange-600' : 'text-gray-500'}`}>
|
|
{step.name}
|
|
</span>
|
|
</div>
|
|
{stepIdx !== steps.length - 1 && (
|
|
<div
|
|
className={`absolute right-0 top-5 h-0.5 w-1/2 translate-x-1/2 transform ${
|
|
step.status === 'complete' ? 'bg-orange-600' : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
|
|
{/* Step Content */}
|
|
<div className="mt-8">
|
|
{renderStep()}
|
|
</div>
|
|
|
|
{/* Navigation Buttons */}
|
|
<div className="mt-8 flex justify-between">
|
|
<button
|
|
onClick={handlePrevious}
|
|
disabled={currentStep === 1 || loading}
|
|
className={`flex items-center px-6 py-3 rounded-lg font-medium transition-colors ${
|
|
currentStep === 1 || loading
|
|
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
|
: 'bg-orange-100 hover:bg-orange-200 text-orange-700'
|
|
}`}
|
|
>
|
|
<ArrowLeftIcon className="w-5 h-5 mr-2" />
|
|
Anterior
|
|
</button>
|
|
|
|
{currentStep < 4 && (
|
|
<button
|
|
onClick={handleNext}
|
|
disabled={loading}
|
|
className={`flex items-center px-6 py-3 rounded-lg font-medium transition-colors ${
|
|
loading
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-orange-600 hover:bg-orange-700'
|
|
} text-white`}
|
|
>
|
|
{loading ? (
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
|
) : (
|
|
'Siguiente'
|
|
)}
|
|
{!loading && <ArrowRightIcon className="w-5 h-5 ml-2" />}
|
|
</button>
|
|
)}
|
|
|
|
{currentStep === 4 && (
|
|
<button
|
|
onClick={handleSubmitFinal}
|
|
disabled={loading || formData.trainingStatus !== 'completed'}
|
|
className={`flex items-center px-6 py-3 rounded-lg font-medium transition-colors ${
|
|
loading || formData.trainingStatus !== 'completed'
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-green-600 hover:bg-green-700'
|
|
} text-white`}
|
|
>
|
|
{loading ? (
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
|
) : (
|
|
'Ir al Dashboard'
|
|
)}
|
|
{!loading && <ArrowRightIcon className="w-5 h-5 ml-2" />}
|
|
</button>
|
|
)}
|
|
|
|
{currentStep === 5 && (
|
|
<button
|
|
onClick={handleSubmitFinal}
|
|
disabled={loading}
|
|
className={`flex items-center px-6 py-3 rounded-lg font-medium transition-colors ${
|
|
loading
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-green-600 hover:bg-green-700'
|
|
} text-white`}
|
|
>
|
|
{loading ? (
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
|
) : (
|
|
'Ir al Dashboard'
|
|
)}
|
|
{!loading && <ArrowRightIcon className="w-5 h-5 ml-2" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default OnboardingPage; |