2025-08-04 13:02:46 +02:00
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
|
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 08:42:35 +02:00
|
|
|
|
import {
|
|
|
|
|
|
useTenant,
|
|
|
|
|
|
useTraining,
|
|
|
|
|
|
useData,
|
|
|
|
|
|
useAuth,
|
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>('');
|
|
|
|
|
|
const [trainingProgress, setTrainingProgress] = useState<TrainingProgress>({
|
|
|
|
|
|
progress: 0,
|
|
|
|
|
|
status: 'pending',
|
|
|
|
|
|
currentStep: 'Iniciando...',
|
|
|
|
|
|
productsCompleted: 0,
|
|
|
|
|
|
productsTotal: 0,
|
|
|
|
|
|
estimatedTimeRemaining: 0
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-04 07:37:19 +02:00
|
|
|
|
const { createTenant, isLoading: tenantLoading } = useTenant();
|
2025-08-04 13:02:46 +02:00
|
|
|
|
const { startTrainingJob, getTrainingJobStatus } = useTraining();
|
2025-08-04 08:42:35 +02:00
|
|
|
|
const { uploadSalesHistory, validateSalesData } = useData();
|
2025-08-04 13:02:46 +02:00
|
|
|
|
|
|
|
|
|
|
// WebSocket connection for real-time training updates
|
|
|
|
|
|
const { status, jobUpdates, connect, disconnect, isConnected } = useTrainingWebSocket(trainingJobId || 'pending');
|
2025-08-04 07:37:19 +02:00
|
|
|
|
|
2025-08-03 19:23:20 +02:00
|
|
|
|
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 },
|
2025-08-04 13:02:46 +02:00
|
|
|
|
{ id: 4, title: 'Entrenamiento IA', icon: Brain },
|
|
|
|
|
|
{ id: 5, title: 'Configuración Final', icon: Check }
|
2025-08-03 19:23:20 +02:00
|
|
|
|
];
|
|
|
|
|
|
|
2025-08-04 13:02:46 +02:00
|
|
|
|
// Handle WebSocket job updates
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (jobUpdates.length > 0) {
|
|
|
|
|
|
const latestUpdate = jobUpdates[0];
|
|
|
|
|
|
|
|
|
|
|
|
// Update training progress based on WebSocket messages
|
|
|
|
|
|
if (latestUpdate.type === 'training_progress') {
|
|
|
|
|
|
setTrainingProgress(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
progress: latestUpdate.progress || 0,
|
|
|
|
|
|
currentStep: latestUpdate.current_step || 'Procesando...',
|
|
|
|
|
|
productsCompleted: latestUpdate.products_completed || 0,
|
|
|
|
|
|
productsTotal: latestUpdate.products_total || prev.productsTotal,
|
|
|
|
|
|
estimatedTimeRemaining: latestUpdate.estimated_time_remaining || 0,
|
|
|
|
|
|
status: 'running'
|
|
|
|
|
|
}));
|
|
|
|
|
|
} else if (latestUpdate.type === 'training_completed') {
|
|
|
|
|
|
setTrainingProgress(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
progress: 100,
|
|
|
|
|
|
status: 'completed',
|
|
|
|
|
|
currentStep: 'Entrenamiento completado',
|
|
|
|
|
|
estimatedTimeRemaining: 0
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-advance to final step after 2 seconds
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
setCurrentStep(5);
|
|
|
|
|
|
}, 2000);
|
|
|
|
|
|
|
|
|
|
|
|
} else if (latestUpdate.type === 'training_failed' || latestUpdate.type === 'training_error') {
|
|
|
|
|
|
setTrainingProgress(prev => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
status: 'failed',
|
|
|
|
|
|
error: latestUpdate.error || 'Error en el entrenamiento',
|
|
|
|
|
|
currentStep: 'Error en el entrenamiento'
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [jobUpdates]);
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatTimeRemaining = (seconds: number): string => {
|
|
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
|
|
const secs = seconds % 60;
|
|
|
|
|
|
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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:
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
|
<div className="text-center">
|
2025-08-04 13:02:46 +02:00
|
|
|
|
<div className="mx-auto w-16 h-16 bg-primary-100 rounded-full flex items-center justify-center mb-4">
|
|
|
|
|
|
<Brain className="h-8 w-8 text-primary-600" />
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
2025-08-04 13:02:46 +02:00
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
|
|
|
|
🧠 Entrenando tu modelo de predicción
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</h3>
|
2025-08-04 13:02:46 +02:00
|
|
|
|
<p className="text-gray-600 mb-8">
|
|
|
|
|
|
Estamos procesando tus datos históricos para crear predicciones personalizadas
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-04 13:02:46 +02:00
|
|
|
|
{/* WebSocket Connection Status */}
|
|
|
|
|
|
{tenantId && trainingJobId && (
|
|
|
|
|
|
<div className="mb-4 text-xs text-gray-500 flex items-center">
|
|
|
|
|
|
<div className={`w-2 h-2 rounded-full mr-2 ${
|
|
|
|
|
|
isConnected ? 'bg-green-500' : 'bg-red-500'
|
|
|
|
|
|
}`} />
|
|
|
|
|
|
{isConnected ? 'Conectado a actualizaciones en tiempo real' : 'Reconectando...'}
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
2025-08-04 13:02:46 +02:00
|
|
|
|
)}
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="flex justify-between items-center text-sm">
|
|
|
|
|
|
<span className="text-gray-600">{trainingProgress.currentStep}</span>
|
|
|
|
|
|
<span className="text-gray-500">
|
|
|
|
|
|
{trainingProgress.progress}% completado
|
|
|
|
|
|
</span>
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-04 13:02:46 +02:00
|
|
|
|
<div className="w-full bg-gray-200 rounded-full h-3">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="bg-gradient-to-r from-primary-500 to-primary-600 h-3 rounded-full transition-all duration-1000 ease-out"
|
|
|
|
|
|
style={{ width: `${trainingProgress.progress}%` }}
|
|
|
|
|
|
/>
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
2025-08-04 13:02:46 +02:00
|
|
|
|
|
|
|
|
|
|
{trainingProgress.productsTotal > 0 && (
|
|
|
|
|
|
<div className="flex justify-between items-center text-sm text-gray-600">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
📦 Productos: {trainingProgress.productsCompleted}/{trainingProgress.productsTotal}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{trainingProgress.estimatedTimeRemaining > 0 && (
|
|
|
|
|
|
<span className="flex items-center">
|
|
|
|
|
|
<Clock className="h-4 w-4 mr-1" />
|
|
|
|
|
|
{formatTimeRemaining(trainingProgress.estimatedTimeRemaining)} restante
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Training Status */}
|
|
|
|
|
|
<div className="bg-gray-50 rounded-xl p-6">
|
|
|
|
|
|
{trainingProgress.status === 'running' && (
|
|
|
|
|
|
<div className="flex items-center text-blue-700">
|
|
|
|
|
|
<Loader className="h-5 w-5 mr-3 animate-spin" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="font-medium">Entrenamiento en progreso</div>
|
|
|
|
|
|
<div className="text-sm text-blue-600">
|
|
|
|
|
|
Tu modelo está aprendiendo de los patrones históricos de ventas
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{trainingProgress.status === 'completed' && (
|
|
|
|
|
|
<div className="flex items-center text-green-700">
|
|
|
|
|
|
<CheckCircle className="h-5 w-5 mr-3" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="font-medium">¡Entrenamiento completado!</div>
|
|
|
|
|
|
<div className="text-sm text-green-600">
|
|
|
|
|
|
Tu modelo está listo para generar predicciones precisas
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{trainingProgress.status === 'failed' && (
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
<div className="flex items-center text-red-700">
|
|
|
|
|
|
<AlertTriangle className="h-5 w-5 mr-3" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="font-medium">Error en el entrenamiento</div>
|
|
|
|
|
|
<div className="text-sm text-red-600">
|
|
|
|
|
|
{trainingProgress.error || 'Ha ocurrido un error durante el entrenamiento'}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex space-x-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleRetryTraining}
|
|
|
|
|
|
className="flex-1 px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Reintentar entrenamiento
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={handleSkipTraining}
|
|
|
|
|
|
className="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Continuar sin entrenar
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Educational Content */}
|
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
|
|
|
|
|
<div className="bg-blue-50 p-4 rounded-lg">
|
|
|
|
|
|
<div className="font-medium text-blue-900 mb-2">¿Qué está pasando?</div>
|
|
|
|
|
|
<div className="text-blue-700">
|
|
|
|
|
|
Nuestro sistema está analizando patrones estacionales, tendencias de demanda y factores externos para crear un modelo personalizado para tu panadería.
|
|
|
|
|
|
</div>
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-08-04 13:02:46 +02:00
|
|
|
|
<div className="bg-green-50 p-4 rounded-lg">
|
|
|
|
|
|
<div className="font-medium text-green-900 mb-2">Beneficios esperados</div>
|
|
|
|
|
|
<div className="text-green-700">
|
|
|
|
|
|
Predicciones de demanda precisas, reducción de desperdicio, optimización de stock y mejor planificación de producción.
|
|
|
|
|
|
</div>
|
2025-08-03 19:23:20 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
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;
|