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