Files
bakery-ia/frontend/src/pages/onboarding/OnboardingPage.tsx

944 lines
36 KiB
TypeScript
Raw Normal View History

2025-08-04 16:19:00 +02:00
import React, { useState, useEffect, useCallback } from 'react';
2025-08-04 13:02:46 +02:00
import { ChevronLeft, ChevronRight, Upload, MapPin, Store, Factory, Check, Brain, Clock, CheckCircle, AlertTriangle, Loader } from 'lucide-react';
2025-08-03 19:23:20 +02:00
import toast from 'react-hot-toast';
2025-08-04 18:21:42 +02:00
import EnhancedTrainingProgress from '../../components/EnhancedTrainingProgress';
2025-08-04 08:42:35 +02:00
import {
useTenant,
useTraining,
2025-08-04 16:19:00 +02:00
useData,
2025-08-04 13:02:46 +02:00
useTrainingWebSocket,
TenantCreate,
TrainingJobRequest
2025-08-04 08:42:35 +02:00
} from '../../api';
2025-08-04 07:37:19 +02:00
2025-08-03 19:23:20 +02:00
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;
}
2025-08-04 13:02:46 +02:00
interface TrainingProgress {
progress: number;
status: string;
currentStep: string;
productsCompleted: number;
productsTotal: number;
estimatedTimeRemaining: number;
error?: string;
}
2025-08-03 19:23:20 +02:00
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<OnboardingPageProps> = ({ user, onComplete }) => {
const [currentStep, setCurrentStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [bakeryData, setBakeryData] = useState<BakeryData>({
name: '',
address: '',
businessType: 'individual',
products: [],
hasHistoricalData: false
});
2025-08-04 13:02:46 +02:00
// Training progress state
const [tenantId, setTenantId] = useState<string>('');
const [trainingJobId, setTrainingJobId] = useState<string>('');
2025-08-04 16:19:00 +02:00
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: 'Productos y Servicios', icon: Factory },
{ id: 3, title: 'Datos Históricos', icon: Upload },
{ id: 4, title: 'Entrenamiento IA', icon: Brain },
{ id: 5, title: 'Configuración Final', icon: Check }
];
2025-08-04 13:02:46 +02:00
const [trainingProgress, setTrainingProgress] = useState<TrainingProgress>({
progress: 0,
status: 'pending',
currentStep: 'Iniciando...',
productsCompleted: 0,
productsTotal: 0,
estimatedTimeRemaining: 0
});
2025-08-04 16:19:00 +02:00
2025-08-04 13:02:46 +02:00
// WebSocket connection for real-time training updates
2025-08-04 13:28:43 +02:00
const {
status,
jobUpdates,
connect,
disconnect,
isConnected,
lastMessage,
tenantId: resolvedTenantId,
wsUrl
} = useTrainingWebSocket(trainingJobId || 'pending', tenantId);
2025-08-04 07:37:19 +02:00
2025-08-04 13:02:46 +02:00
// Handle WebSocket job updates
2025-08-04 16:19:00 +02:00
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,
2025-08-04 18:58:12 +02:00
estimatedTimeRemaining: data.estimated_time_remaining_minutes ||
data.estimated_time_remaining ||
data.estimatedTimeRemaining ||
prev.estimatedTimeRemaining,
2025-08-04 16:19:00 +02:00
status: 'running'
}));
} else if (messageType === 'completed' || messageType === 'training_completed') {
setTrainingProgress(prev => ({
...prev,
progress: 100,
status: 'completed',
currentStep: 'Entrenamiento completado',
estimatedTimeRemaining: 0
}));
2025-08-04 13:02:46 +02:00
2025-08-04 16:19:00 +02:00
// Auto-advance to final step after 2 seconds
setTimeout(() => {
setCurrentStep(5);
}, 2000);
2025-08-04 13:28:43 +02:00
2025-08-04 16:19:00 +02:00
} 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'
}));
2025-08-04 13:28:43 +02:00
2025-08-04 16:19:00 +02:00
} 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);
2025-08-04 13:02:46 +02:00
}
2025-08-04 16:19:00 +02:00
}, [jobUpdates, processWebSocketMessage]);
2025-08-04 13:02:46 +02:00
// Connect to WebSocket when training starts
useEffect(() => {
if (tenantId && trainingJobId && currentStep === 4) {
connect();
}
return () => {
if (isConnected) {
disconnect();
}
};
}, [tenantId, trainingJobId, currentStep, connect, disconnect, isConnected]);
2025-08-03 19:23:20 +02:00
const handleNext = () => {
if (validateCurrentStep()) {
2025-08-04 13:02:46 +02:00
if (currentStep === 3) {
// Always proceed to training step after CSV upload
startTraining();
} else {
setCurrentStep(prev => Math.min(prev + 1, steps.length));
}
2025-08-03 19:23:20 +02:00
}
};
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.products.length === 0) {
toast.error('Selecciona al menos un producto');
return false;
}
return true;
case 3:
2025-08-04 13:02:46 +02:00
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');
2025-08-03 19:23:20 +02:00
return false;
}
2025-08-04 13:02:46 +02:00
2025-08-03 19:23:20 +02:00
return true;
default:
return true;
}
};
2025-08-04 13:02:46 +02:00
const startTraining = async () => {
setCurrentStep(4);
2025-08-03 19:23:20 +02:00
setIsLoading(true);
try {
2025-08-04 13:02:46 +02:00
const token = localStorage.getItem('auth_token');
if (!token) {
toast.error('Sesión expirada. Por favor, inicia sesión nuevamente.');
return;
}
// Create tenant first
2025-08-04 07:37:19 +02:00
const tenantData: TenantCreate = {
name: bakeryData.name,
address: bakeryData.address,
2025-08-04 13:02:46 +02:00
business_type: "bakery",
2025-08-04 08:42:35 +02:00
postal_code: "28010",
phone: "+34655334455",
2025-08-04 07:37:19 +02:00
coordinates: bakeryData.coordinates,
products: bakeryData.products,
has_historical_data: bakeryData.hasHistoricalData,
};
2025-08-04 08:42:35 +02:00
2025-08-04 13:02:46 +02:00
const tenant = await createTenant(tenantData);
setTenantId(tenant.id);
2025-08-04 07:37:19 +02:00
// Step 2: Validate and Upload CSV file if provided
2025-08-04 13:02:46 +02:00
if (bakeryData.csvFile) {
2025-08-04 07:37:19 +02:00
try {
2025-08-04 13:02:46 +02:00
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;
2025-08-04 07:37:19 +02:00
}
2025-08-04 13:02:46 +02:00
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;
2025-08-03 19:23:20 +02:00
}
}
2025-08-04 13:02:46 +02:00
// 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);
2025-08-04 07:37:19 +02:00
2025-08-04 13:02:46 +02:00
setTrainingProgress({
progress: 0,
status: 'running',
currentStep: 'Iniciando entrenamiento...',
productsCompleted: 0,
productsTotal: bakeryData.products.length,
estimatedTimeRemaining: 600 // 10 minutes
});
2025-08-04 08:42:35 +02:00
2025-08-04 13:02:46 +02:00
toast.success('Entrenamiento iniciado correctamente');
2025-08-04 08:42:35 +02:00
2025-08-04 13:02:46 +02:00
} 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'
}));
2025-08-03 19:23:20 +02:00
} finally {
setIsLoading(false);
}
};
2025-08-04 13:02:46 +02:00
const handleComplete = async () => {
if (!validateCurrentStep()) return;
if (currentStep < 4) {
// 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(5);
};
2025-08-04 18:21:42 +02:00
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:
<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}
/>
2025-08-04 13:02:46 +02:00
2025-08-03 19:23:20 +02:00
const renderStep = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
Información de tu Panadería
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Nombre de la panadería
</label>
<input
type="text"
value={bakeryData.name}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Dirección completa
</label>
<div className="relative">
<MapPin className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
<input
type="text"
value={bakeryData.address}
onChange={(e) => 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"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-3">
Tipo de negocio
</label>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'individual' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'individual'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
2025-08-04 13:02:46 +02:00
<Store className="h-5 w-5 mb-2" />
2025-08-03 19:23:20 +02:00
<div className="font-medium">Panadería Individual</div>
2025-08-04 13:02:46 +02:00
<div className="text-sm text-gray-500">Un solo punto de venta</div>
2025-08-03 19:23:20 +02:00
</button>
<button
type="button"
onClick={() => setBakeryData(prev => ({ ...prev, businessType: 'central_workshop' }))}
className={`p-4 border rounded-xl text-left transition-all ${
bakeryData.businessType === 'central_workshop'
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
2025-08-04 13:02:46 +02:00
<Factory className="h-5 w-5 mb-2" />
2025-08-03 19:23:20 +02:00
<div className="font-medium">Obrador Central</div>
2025-08-04 13:02:46 +02:00
<div className="text-sm text-gray-500">Múltiples puntos de venta</div>
2025-08-03 19:23:20 +02:00
</button>
</div>
</div>
</div>
</div>
</div>
);
case 2:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
2025-08-04 13:02:46 +02:00
Productos y Servicios
2025-08-03 19:23:20 +02:00
</h3>
<p className="text-gray-600 mb-6">
2025-08-04 13:02:46 +02:00
Selecciona los productos que vendes regularmente. Esto nos ayudará a crear predicciones más precisas.
2025-08-03 19:23:20 +02:00
</p>
2025-08-04 13:02:46 +02:00
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
2025-08-03 19:23:20 +02:00
{MADRID_PRODUCTS.map((product) => (
<button
key={product}
type="button"
onClick={() => {
setBakeryData(prev => ({
...prev,
products: prev.products.includes(product)
? prev.products.filter(p => p !== product)
: [...prev.products, product]
}));
}}
2025-08-04 13:02:46 +02:00
className={`p-3 text-sm rounded-xl border transition-all ${
2025-08-03 19:23:20 +02:00
bakeryData.products.includes(product)
? 'border-primary-500 bg-primary-50 text-primary-700'
: 'border-gray-300 hover:border-gray-400'
}`}
>
{product}
</button>
))}
</div>
2025-08-04 13:02:46 +02:00
{bakeryData.products.length > 0 && (
<div className="mt-4 p-4 bg-green-50 rounded-xl">
<p className="text-sm text-green-700">
{bakeryData.products.length} productos seleccionados
</p>
</div>
)}
2025-08-03 19:23:20 +02:00
</div>
</div>
);
case 3:
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-4">
2025-08-04 13:02:46 +02:00
Datos Históricos
2025-08-03 19:23:20 +02:00
</h3>
<p className="text-gray-600 mb-6">
2025-08-04 13:02:46 +02:00
Para obtener predicciones precisas, necesitamos tus datos históricos de ventas.
Puedes subir archivos en varios formatos.
2025-08-03 19:23:20 +02:00
</p>
2025-08-04 13:02:46 +02:00
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg mb-6">
<div className="flex items-start">
<div className="flex-shrink-0">
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-sm font-bold">!</span>
</div>
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-blue-900">
Formatos soportados y estructura de datos
</h4>
<div className="mt-2 text-sm text-blue-700">
<p className="mb-3"><strong>Formatos aceptados:</strong></p>
<div className="grid grid-cols-2 gap-4 mb-3">
<div>
<p className="font-medium">📊 Hojas de cálculo:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.xlsx (Excel moderno)</li>
<li>.xls (Excel clásico)</li>
</ul>
</div>
<div>
<p className="font-medium">📄 Datos estructurados:</p>
<ul className="list-disc list-inside text-xs space-y-1">
<li>.csv (Valores separados por comas)</li>
<li>.json (Formato JSON)</li>
</ul>
</div>
</div>
<p className="mb-2"><strong>Columnas requeridas (en cualquier idioma):</strong></p>
<ul className="list-disc list-inside text-xs space-y-1">
<li><strong>Fecha</strong>: fecha, date, datum (formato: YYYY-MM-DD, DD/MM/YYYY, etc.)</li>
<li><strong>Producto</strong>: producto, product, item, articulo, nombre</li>
<li><strong>Cantidad</strong>: cantidad, quantity, cantidad_vendida, qty</li>
</ul>
</div>
</div>
</div>
</div>
2025-08-03 19:23:20 +02:00
2025-08-04 13:02:46 +02:00
<div className="mt-6 p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-primary-300 transition-colors">
<div className="text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400" />
<div className="mt-4">
<label htmlFor="sales-file-upload" className="cursor-pointer">
<span className="mt-2 block text-sm font-medium text-gray-900">
Subir archivo de datos históricos
</span>
<span className="mt-1 block text-sm text-gray-500">
Arrastra y suelta tu archivo aquí, o haz clic para seleccionar
</span>
<span className="mt-1 block text-xs text-gray-400">
Máximo 10MB - CSV, Excel (.xlsx, .xls), JSON
</span>
</label>
2025-08-03 19:23:20 +02:00
<input
2025-08-04 13:02:46 +02:00
id="sales-file-upload"
type="file"
accept=".csv,.xlsx,.xls,.json"
required
onChange={(e) => {
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"
2025-08-03 19:23:20 +02:00
/>
2025-08-04 13:02:46 +02:00
</div>
2025-08-03 19:23:20 +02:00
</div>
2025-08-04 13:02:46 +02:00
{bakeryData.csvFile ? (
<div className="mt-4 p-4 bg-green-50 rounded-lg">
<div className="flex items-center justify-between">
<div className="flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-2" />
2025-08-03 19:23:20 +02:00
<div>
2025-08-04 13:02:46 +02:00
<p className="text-sm font-medium text-green-700">
2025-08-03 19:23:20 +02:00
{bakeryData.csvFile.name}
</p>
2025-08-04 13:02:46 +02:00
<p className="text-xs text-green-600">
{(bakeryData.csvFile.size / 1024).toFixed(1)} KB {bakeryData.csvFile.type || 'Archivo de datos'}
2025-08-03 19:23:20 +02:00
</p>
</div>
2025-08-04 13:02:46 +02:00
</div>
<button
onClick={() => setBakeryData(prev => ({ ...prev, csvFile: undefined }))}
className="text-red-600 hover:text-red-800 text-sm"
>
Quitar
</button>
2025-08-03 19:23:20 +02:00
</div>
2025-08-04 13:02:46 +02:00
</div>
) : (
<div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-center">
<AlertTriangle className="h-5 w-5 text-yellow-600 mr-2" />
<p className="text-sm text-yellow-800">
<strong>Archivo requerido:</strong> Selecciona un archivo con tus datos históricos de ventas
</p>
2025-08-03 19:23:20 +02:00
</div>
</div>
)}
2025-08-04 13:02:46 +02:00
</div>
2025-08-03 19:23:20 +02:00
2025-08-04 13:02:46 +02:00
{/* Sample formats examples */}
<div className="mt-6 space-y-4">
<h5 className="text-sm font-medium text-gray-900">
Ejemplos de formato:
</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* CSV Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">📄 CSV</span>
</div>
<div className="bg-white p-3 rounded border text-xs font-mono">
<div className="text-gray-600">fecha,producto,cantidad</div>
<div>2024-01-15,Croissants,45</div>
<div>2024-01-15,Pan de molde,32</div>
<div>2024-01-16,Baguettes,28</div>
</div>
2025-08-03 19:23:20 +02:00
</div>
2025-08-04 13:02:46 +02:00
{/* Excel Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">📊 Excel</span>
</div>
<div className="bg-white p-3 rounded border text-xs">
<table className="w-full">
<thead>
<tr className="bg-gray-100">
<th className="p-1 text-left">Fecha</th>
<th className="p-1 text-left">Producto</th>
<th className="p-1 text-left">Cantidad</th>
</tr>
</thead>
<tbody>
<tr><td className="p-1">15/01/2024</td><td className="p-1">Croissants</td><td className="p-1">45</td></tr>
<tr><td className="p-1">15/01/2024</td><td className="p-1">Pan molde</td><td className="p-1">32</td></tr>
</tbody>
</table>
</div>
</div>
</div>
{/* JSON Example */}
<div className="bg-gray-50 p-4 rounded-lg">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-900">🔧 JSON</span>
</div>
<div className="bg-white p-3 rounded border text-xs font-mono">
<div className="text-gray-600">[</div>
<div className="ml-2">{"{"}"fecha": "2024-01-15", "producto": "Croissants", "cantidad": 45{"}"},</div>
<div className="ml-2">{"{"}"fecha": "2024-01-15", "producto": "Pan de molde", "cantidad": 32{"}"}</div>
<div className="text-gray-600">]</div>
</div>
</div>
2025-08-03 19:23:20 +02:00
</div>
</div>
</div>
);
case 4:
2025-08-04 18:21:42 +02:00
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
}}
/>
);
2025-08-03 19:23:20 +02:00
2025-08-04 13:02:46 +02:00
case 5:
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.
</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>
</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>
</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.
</p>
</div>
</div>
);
2025-08-03 19:23:20 +02:00
default:
return null;
}
};
return (
2025-08-04 13:02:46 +02:00
<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">
2025-08-03 19:23:20 +02:00
{/* Header */}
<div className="text-center mb-8">
2025-08-04 13:02:46 +02:00
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Configuración de PanIA
2025-08-03 19:23:20 +02:00
</h1>
<p className="text-gray-600">
2025-08-04 13:02:46 +02:00
Configuremos tu panadería para obtener predicciones precisas de demanda
2025-08-03 19:23:20 +02:00
</p>
</div>
2025-08-04 13:02:46 +02:00
{/* Progress Indicator */}
2025-08-03 19:23:20 +02:00
<div className="mb-8">
2025-08-04 13:02:46 +02:00
<div className="flex justify-center items-center space-x-4 mb-6">
{steps.map((step, index) => (
2025-08-03 19:23:20 +02:00
<div key={step.id} className="flex flex-col items-center">
<div
2025-08-04 13:02:46 +02:00
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
2025-08-03 19:23:20 +02:00
? 'bg-primary-500 border-primary-500 text-white'
: 'border-gray-300 text-gray-500'
}`}
>
2025-08-04 13:02:46 +02:00
{currentStep > step.id ? (
<Check className="h-5 w-5" />
) : (
<step.icon className="h-5 w-5" />
)}
2025-08-03 19:23:20 +02:00
</div>
<span className="mt-2 text-xs text-gray-500 text-center max-w-20">
{step.title}
</span>
</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: `${(currentStep / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Step Content */}
<div className="bg-white rounded-2xl shadow-soft p-6 mb-8">
{renderStep()}
</div>
{/* 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"
>
<ChevronLeft className="h-5 w-5 mr-1" />
Anterior
</button>
2025-08-04 13:02:46 +02:00
{/* Dynamic Next/Complete Button */}
{currentStep < 4 ? (
2025-08-03 19:23:20 +02:00
<button
onClick={handleNext}
2025-08-04 13:02:46 +02:00
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"
2025-08-03 19:23:20 +02:00
>
2025-08-04 13:02:46 +02:00
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Procesando...
</>
) : (
<>
Siguiente
<ChevronRight className="h-5 w-5 ml-1" />
</>
)}
2025-08-03 19:23:20 +02:00
</button>
2025-08-04 13:02:46 +02:00
) : currentStep === 4 ? (
// 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={() => setCurrentStep(5)}
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>
2025-08-03 19:23:20 +02:00
) : (
2025-08-04 13:02:46 +02:00
// Final step - Complete button
2025-08-03 19:23:20 +02:00
<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"
>
2025-08-04 13:02:46 +02:00
{isLoading ? (
<>
<Loader className="h-5 w-5 mr-2 animate-spin" />
Completando...
</>
) : (
<>
<Check className="h-5 w-5 mr-2" />
Ir al Dashboard
</>
)}
2025-08-03 19:23:20 +02:00
</button>
)}
</div>
2025-08-04 13:02:46 +02:00
{/* Help Section */}
<div className="mt-8 text-center">
<div className="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
<h4 className="font-medium text-gray-900 mb-3">¿Necesitas ayuda?</h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span>📧 soporte@pania.es</span>
</div>
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>📞 +34 900 123 456</span>
</div>
<div className="flex items-center justify-center space-x-2 text-gray-600">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>💬 Chat en vivo</span>
</div>
</div>
</div>
</div>
2025-08-03 19:23:20 +02:00
</div>
</div>
);
};
export default OnboardingPage;