Fix new services implementation 5
This commit is contained in:
@@ -141,7 +141,7 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
toast.success('¡Pago procesado correctamente!');
|
||||
|
||||
setFormData(prev => ({ ...prev, paymentCompleted: true }));
|
||||
setPaymentStep('completed');
|
||||
// Skip intermediate page and proceed directly to registration
|
||||
onPaymentSuccess();
|
||||
|
||||
} catch (error) {
|
||||
@@ -299,14 +299,11 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
|
||||
|
||||
// Move to payment step, or bypass if in development mode
|
||||
if (bypassPayment) {
|
||||
// Development bypass: simulate payment completion
|
||||
// Development bypass: simulate payment completion and proceed directly to registration
|
||||
setFormData(prev => ({ ...prev, paymentCompleted: true }));
|
||||
setPaymentStep('completed');
|
||||
toast.success('🚀 Modo desarrollo: Pago omitido');
|
||||
// Proceed directly to registration
|
||||
setTimeout(() => {
|
||||
handleRegistrationComplete();
|
||||
}, 1500);
|
||||
// Proceed directly to registration without intermediate page
|
||||
handleRegistrationComplete();
|
||||
} else {
|
||||
setPaymentStep('payment');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrendingUp, TrendingDown, Calendar, Cloud, AlertTriangle, Info } from 'lucide-react';
|
||||
import { TrendingUp, TrendingDown, Calendar, Cloud, AlertTriangle, Info, RefreshCw } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { useForecast } from '../../api/hooks/useForecast';
|
||||
import { useInventory } from '../../api/hooks/useInventory';
|
||||
import { useTenantId } from '../../hooks/useTenantId';
|
||||
import type { ForecastResponse } from '../../api/types/forecasting';
|
||||
|
||||
interface ForecastData {
|
||||
date: string;
|
||||
@@ -9,6 +13,9 @@ interface ForecastData {
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
factors: string[];
|
||||
weatherImpact?: string;
|
||||
inventory_product_id?: string;
|
||||
confidence_lower?: number;
|
||||
confidence_upper?: number;
|
||||
}
|
||||
|
||||
interface WeatherAlert {
|
||||
@@ -20,95 +27,270 @@ interface WeatherAlert {
|
||||
const ForecastPage: React.FC = () => {
|
||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [selectedProduct, setSelectedProduct] = useState('all');
|
||||
const [forecasts, setForecasts] = useState<ForecastData[]>([]);
|
||||
const [forecastData, setForecastData] = useState<ForecastData[]>([]);
|
||||
const [weatherAlert, setWeatherAlert] = useState<WeatherAlert | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
|
||||
// Hooks
|
||||
const { tenantId } = useTenantId();
|
||||
const {
|
||||
forecasts,
|
||||
isLoading: forecastLoading,
|
||||
error: forecastError,
|
||||
createSingleForecast,
|
||||
getForecasts,
|
||||
getForecastAlerts,
|
||||
exportForecasts
|
||||
} = useForecast();
|
||||
const {
|
||||
items: inventoryItems,
|
||||
isLoading: inventoryLoading,
|
||||
loadItems
|
||||
} = useInventory(false); // Disable auto-load, we'll load manually
|
||||
|
||||
const products = [
|
||||
'Croissants', 'Pan de molde', 'Baguettes', 'Napolitanas',
|
||||
'Café', 'Magdalenas', 'Donuts', 'Bocadillos'
|
||||
];
|
||||
// Debug logging
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('ForecastPage - inventoryItems:', inventoryItems);
|
||||
console.log('ForecastPage - inventoryLoading:', inventoryLoading);
|
||||
console.log('ForecastPage - tenantId:', tenantId);
|
||||
}
|
||||
|
||||
// Sample forecast data for the next 7 days
|
||||
const sampleForecastData = [
|
||||
{ date: '2024-11-04', croissants: 48, pan: 35, cafe: 72 },
|
||||
{ date: '2024-11-05', croissants: 52, pan: 38, cafe: 78 },
|
||||
{ date: '2024-11-06', croissants: 45, pan: 32, cafe: 65 },
|
||||
{ date: '2024-11-07', croissants: 41, pan: 29, cafe: 58 },
|
||||
{ date: '2024-11-08', croissants: 56, pan: 42, cafe: 82 },
|
||||
{ date: '2024-11-09', croissants: 61, pan: 45, cafe: 89 },
|
||||
{ date: '2024-11-10', croissants: 38, pan: 28, cafe: 55 },
|
||||
];
|
||||
// Derived state
|
||||
const isLoading = forecastLoading || inventoryLoading;
|
||||
const products = (inventoryItems || []).map(item => ({
|
||||
id: item.id,
|
||||
name: item.name || 'Unknown Product'
|
||||
}));
|
||||
|
||||
// Sample forecast data for the next 7 days - will be populated by real data
|
||||
const [sampleForecastData, setSampleForecastData] = useState<any[]>(() => {
|
||||
// Generate 7 days starting from today
|
||||
const data = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + i);
|
||||
data.push({
|
||||
date: date.toISOString().split('T')[0],
|
||||
croissants: 0,
|
||||
pan: 0,
|
||||
cafe: 0
|
||||
});
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
// Load inventory items on component mount
|
||||
useEffect(() => {
|
||||
const loadForecasts = async () => {
|
||||
setIsLoading(true);
|
||||
if (tenantId) {
|
||||
loadItems();
|
||||
}
|
||||
}, [tenantId, loadItems]);
|
||||
|
||||
// Transform API forecasts to our local format
|
||||
const transformForecastResponse = (forecast: ForecastResponse): ForecastData => {
|
||||
// Find product name from inventory items
|
||||
const inventoryItem = (inventoryItems || []).find(item => item.id === forecast.inventory_product_id);
|
||||
const productName = inventoryItem?.name || 'Unknown Product';
|
||||
|
||||
// Determine confidence level based on confidence_level number
|
||||
let confidence: 'high' | 'medium' | 'low' = 'medium';
|
||||
if (forecast.confidence_level) {
|
||||
if (forecast.confidence_level >= 0.8) confidence = 'high';
|
||||
else if (forecast.confidence_level >= 0.6) confidence = 'medium';
|
||||
else confidence = 'low';
|
||||
}
|
||||
|
||||
// Extract factors from features_used or provide defaults
|
||||
const factors = [];
|
||||
if (forecast.features_used) {
|
||||
if (forecast.features_used.is_weekend === false) factors.push('Día laboral');
|
||||
else if (forecast.features_used.is_weekend === true) factors.push('Fin de semana');
|
||||
|
||||
if (forecast.features_used.is_holiday === false) factors.push('Sin eventos especiales');
|
||||
else if (forecast.features_used.is_holiday === true) factors.push('Día festivo');
|
||||
|
||||
if (forecast.features_used.weather_description) factors.push(`Clima: ${forecast.features_used.weather_description}`);
|
||||
else factors.push('Clima estable');
|
||||
} else {
|
||||
factors.push('Día laboral', 'Clima estable', 'Sin eventos especiales');
|
||||
}
|
||||
|
||||
// Determine weather impact
|
||||
let weatherImpact = 'Sin impacto significativo';
|
||||
if (forecast.features_used?.temperature) {
|
||||
const temp = forecast.features_used.temperature;
|
||||
if (temp < 10) weatherImpact = 'Temperatura baja - posible aumento en bebidas calientes';
|
||||
else if (temp > 25) weatherImpact = 'Temperatura alta - posible reducción en productos horneados';
|
||||
}
|
||||
|
||||
return {
|
||||
date: forecast.forecast_date.split('T')[0], // Convert to YYYY-MM-DD
|
||||
product: productName,
|
||||
predicted: Math.round(forecast.predicted_demand),
|
||||
confidence,
|
||||
factors,
|
||||
weatherImpact,
|
||||
inventory_product_id: forecast.inventory_product_id,
|
||||
confidence_lower: forecast.confidence_lower,
|
||||
confidence_upper: forecast.confidence_upper,
|
||||
};
|
||||
};
|
||||
|
||||
// Generate forecasts for available products
|
||||
const generateForecasts = async () => {
|
||||
if (!tenantId || !inventoryItems || inventoryItems.length === 0) return;
|
||||
|
||||
setIsGenerating(true);
|
||||
try {
|
||||
// Generate forecasts for top 3 products for the next 7 days
|
||||
const productsToForecast = inventoryItems.slice(0, 3);
|
||||
const chartData = [];
|
||||
|
||||
// Generate data for the next 7 days
|
||||
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
const forecastDate = new Date();
|
||||
forecastDate.setDate(forecastDate.getDate() + dayOffset);
|
||||
const dateStr = forecastDate.toISOString().split('T')[0];
|
||||
|
||||
const dayData = {
|
||||
date: dateStr,
|
||||
croissants: 0,
|
||||
pan: 0,
|
||||
cafe: 0
|
||||
};
|
||||
|
||||
// Generate forecasts for each product for this day
|
||||
const dayForecasts = await Promise.all(
|
||||
productsToForecast.map(async (item) => {
|
||||
try {
|
||||
const forecastResponses = await createSingleForecast(tenantId, {
|
||||
inventory_product_id: item.id,
|
||||
forecast_date: dateStr,
|
||||
forecast_days: 1,
|
||||
location: 'Madrid, Spain',
|
||||
include_external_factors: true,
|
||||
confidence_intervals: true,
|
||||
});
|
||||
|
||||
return forecastResponses.map(transformForecastResponse);
|
||||
} catch (error) {
|
||||
console.error(`Failed to generate forecast for ${item.name} on ${dateStr}:`, error);
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Process forecasts for this day
|
||||
const flatDayForecasts = dayForecasts.flat();
|
||||
flatDayForecasts.forEach((forecast) => {
|
||||
const key = forecast.product.toLowerCase();
|
||||
if (key.includes('croissant')) dayData.croissants = forecast.predicted;
|
||||
else if (key.includes('pan')) dayData.pan = forecast.predicted;
|
||||
else if (key.includes('cafe')) dayData.cafe = forecast.predicted;
|
||||
});
|
||||
|
||||
chartData.push(dayData);
|
||||
|
||||
// Store forecasts for selected date display
|
||||
if (dateStr === selectedDate) {
|
||||
setForecastData(flatDayForecasts);
|
||||
}
|
||||
}
|
||||
|
||||
// Update chart with 7 days of data
|
||||
setSampleForecastData(chartData);
|
||||
|
||||
// Set a sample weather alert
|
||||
setWeatherAlert({
|
||||
type: 'rain',
|
||||
impact: 'Condiciones climáticas estables para el día seleccionado',
|
||||
recommendation: 'Mantener la producción según las predicciones'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error generating forecasts:', error);
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load existing forecasts when component mounts or date changes
|
||||
useEffect(() => {
|
||||
const loadExistingForecasts = async () => {
|
||||
if (!tenantId) return;
|
||||
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// Try to get existing forecasts first
|
||||
const existingForecasts = await getForecasts(tenantId);
|
||||
|
||||
// Mock weather alert
|
||||
setWeatherAlert({
|
||||
type: 'rain',
|
||||
impact: 'Se esperan lluvias moderadas mañana',
|
||||
recommendation: 'Reduce la producción de productos frescos en un 20%'
|
||||
});
|
||||
|
||||
// Mock forecast data
|
||||
const mockForecasts: ForecastData[] = [
|
||||
{
|
||||
date: selectedDate,
|
||||
product: 'Croissants',
|
||||
predicted: 48,
|
||||
confidence: 'high',
|
||||
factors: ['Día laboral', 'Clima estable', 'Sin eventos especiales'],
|
||||
weatherImpact: 'Sin impacto significativo'
|
||||
},
|
||||
{
|
||||
date: selectedDate,
|
||||
product: 'Pan de molde',
|
||||
predicted: 35,
|
||||
confidence: 'high',
|
||||
factors: ['Demanda constante', 'Histórico estable'],
|
||||
weatherImpact: 'Sin impacto'
|
||||
},
|
||||
{
|
||||
date: selectedDate,
|
||||
product: 'Café',
|
||||
predicted: 72,
|
||||
confidence: 'medium',
|
||||
factors: ['Temperatura fresca', 'Día laboral'],
|
||||
weatherImpact: 'Aumento del 10% por temperatura'
|
||||
},
|
||||
{
|
||||
date: selectedDate,
|
||||
product: 'Baguettes',
|
||||
predicted: 28,
|
||||
confidence: 'medium',
|
||||
factors: ['Día entre semana', 'Demanda normal'],
|
||||
weatherImpact: 'Sin impacto'
|
||||
},
|
||||
{
|
||||
date: selectedDate,
|
||||
product: 'Napolitanas',
|
||||
predicted: 23,
|
||||
confidence: 'low',
|
||||
factors: ['Variabilidad alta', 'Datos limitados'],
|
||||
weatherImpact: 'Posible reducción del 5%'
|
||||
console.log('🔍 ForecastPage - existingForecasts:', existingForecasts);
|
||||
console.log('🔍 ForecastPage - existingForecasts type:', typeof existingForecasts);
|
||||
console.log('🔍 ForecastPage - existingForecasts isArray:', Array.isArray(existingForecasts));
|
||||
|
||||
if (Array.isArray(existingForecasts) && existingForecasts.length > 0) {
|
||||
// Filter forecasts for selected date
|
||||
const dateForecasts = existingForecasts
|
||||
.filter(f => f.forecast_date && f.forecast_date.split('T')[0] === selectedDate)
|
||||
.map(transformForecastResponse);
|
||||
|
||||
if (dateForecasts.length > 0) {
|
||||
setForecastData(dateForecasts);
|
||||
}
|
||||
];
|
||||
|
||||
setForecasts(mockForecasts);
|
||||
// Update 7-day chart with existing forecasts
|
||||
const chartData = [];
|
||||
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
|
||||
const forecastDate = new Date();
|
||||
forecastDate.setDate(forecastDate.getDate() + dayOffset);
|
||||
const dateStr = forecastDate.toISOString().split('T')[0];
|
||||
|
||||
const dayData = {
|
||||
date: dateStr,
|
||||
croissants: 0,
|
||||
pan: 0,
|
||||
cafe: 0
|
||||
};
|
||||
|
||||
// Find existing forecasts for this day
|
||||
const dayForecasts = existingForecasts
|
||||
.filter(f => f.forecast_date && f.forecast_date.split('T')[0] === dateStr)
|
||||
.map(transformForecastResponse);
|
||||
|
||||
dayForecasts.forEach((forecast) => {
|
||||
const key = forecast.product.toLowerCase();
|
||||
if (key.includes('croissant')) dayData.croissants = forecast.predicted;
|
||||
else if (key.includes('pan')) dayData.pan = forecast.predicted;
|
||||
else if (key.includes('cafe')) dayData.cafe = forecast.predicted;
|
||||
});
|
||||
|
||||
chartData.push(dayData);
|
||||
}
|
||||
|
||||
setSampleForecastData(chartData);
|
||||
} else {
|
||||
console.log('🔍 ForecastPage - No existing forecasts found or invalid format');
|
||||
}
|
||||
|
||||
// Load alerts
|
||||
const alerts = await getForecastAlerts(tenantId);
|
||||
if (Array.isArray(alerts) && alerts.length > 0) {
|
||||
// Convert first alert to weather alert format
|
||||
const alert = alerts[0];
|
||||
setWeatherAlert({
|
||||
type: 'rain', // Default type
|
||||
impact: alert.message || 'Alert information not available',
|
||||
recommendation: 'Revisa las recomendaciones del sistema'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading forecasts:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
console.error('Error loading existing forecasts:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadForecasts();
|
||||
}, [selectedDate]);
|
||||
if (inventoryItems && inventoryItems.length > 0) {
|
||||
loadExistingForecasts();
|
||||
}
|
||||
}, [tenantId, selectedDate, inventoryItems, getForecasts, getForecastAlerts]);
|
||||
|
||||
const getConfidenceColor = (confidence: string) => {
|
||||
switch (confidence) {
|
||||
@@ -137,8 +319,8 @@ const ForecastPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const filteredForecasts = selectedProduct === 'all'
|
||||
? forecasts
|
||||
: forecasts.filter(f => f.product.toLowerCase().includes(selectedProduct.toLowerCase()));
|
||||
? forecastData
|
||||
: forecastData.filter(f => f.product.toLowerCase().includes(selectedProduct.toLowerCase()));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -185,7 +367,7 @@ const ForecastPage: React.FC = () => {
|
||||
|
||||
{/* Controls */}
|
||||
<div className="bg-white p-6 rounded-xl shadow-soft">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Fecha de predicción
|
||||
@@ -214,11 +396,54 @@ const ForecastPage: React.FC = () => {
|
||||
>
|
||||
<option value="all">Todos los productos</option>
|
||||
{products.map(product => (
|
||||
<option key={product} value={product.toLowerCase()}>{product}</option>
|
||||
<option key={product.id} value={product.name.toLowerCase()}>{product.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Generar predicciones
|
||||
</label>
|
||||
<button
|
||||
onClick={generateForecasts}
|
||||
disabled={isGenerating || !tenantId || !(inventoryItems && inventoryItems.length > 0)}
|
||||
className="w-full px-4 py-3 bg-primary-500 hover:bg-primary-600 disabled:bg-gray-300 text-white rounded-xl transition-colors flex items-center justify-center"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
||||
Generando...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingUp className="h-4 w-4 mr-2" />
|
||||
Generar Predicciones
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{forecastError && (
|
||||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<AlertTriangle className="h-5 w-5 text-red-600 mr-2" />
|
||||
<span className="text-red-800 text-sm">Error: {forecastError}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{forecastData.length === 0 && !isLoading && !isGenerating && (
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-center">
|
||||
<Info className="h-5 w-5 text-blue-600 mr-2" />
|
||||
<span className="text-blue-800 text-sm">
|
||||
No hay predicciones para la fecha seleccionada. Haz clic en "Generar Predicciones" para crear nuevas predicciones.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Forecast Cards */}
|
||||
@@ -388,19 +613,31 @@ const ForecastPage: React.FC = () => {
|
||||
Acciones Rápidas
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<button
|
||||
onClick={() => tenantId && exportForecasts(tenantId, 'csv')}
|
||||
disabled={!tenantId || forecastData.length === 0}
|
||||
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
|
||||
>
|
||||
<div className="font-medium text-gray-900">Exportar Predicciones</div>
|
||||
<div className="text-sm text-gray-500 mt-1">Descargar en formato CSV</div>
|
||||
</button>
|
||||
|
||||
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<div className="font-medium text-gray-900">Configurar Alertas</div>
|
||||
<div className="text-sm text-gray-500 mt-1">Recibir notificaciones automáticas</div>
|
||||
<button
|
||||
onClick={generateForecasts}
|
||||
disabled={isGenerating || !tenantId || !(inventoryItems && inventoryItems.length > 0)}
|
||||
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
|
||||
>
|
||||
<div className="font-medium text-gray-900">Actualizar Predicciones</div>
|
||||
<div className="text-sm text-gray-500 mt-1">Generar nuevas predicciones</div>
|
||||
</button>
|
||||
|
||||
<button className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 transition-all text-left">
|
||||
<div className="font-medium text-gray-900">Ver Precisión Histórica</div>
|
||||
<div className="text-sm text-gray-500 mt-1">Analizar rendimiento del modelo</div>
|
||||
<button
|
||||
onClick={() => tenantId && getForecastAlerts(tenantId)}
|
||||
disabled={!tenantId}
|
||||
className="p-4 border border-gray-300 rounded-lg hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 disabled:cursor-not-allowed transition-all text-left"
|
||||
>
|
||||
<div className="font-medium text-gray-900">Ver Alertas</div>
|
||||
<div className="text-sm text-gray-500 mt-1">Revisar notificaciones del sistema</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,13 @@ import SmartHistoricalDataImport from '../../components/onboarding/SmartHistoric
|
||||
|
||||
import {
|
||||
useTenant,
|
||||
useTraining,
|
||||
useSales,
|
||||
useTrainingWebSocket,
|
||||
useOnboarding,
|
||||
TenantCreate,
|
||||
TrainingJobRequest
|
||||
} from '../../api';
|
||||
import { useTraining } from '../../api/hooks/useTraining';
|
||||
|
||||
import { OnboardingRouter } from '../../utils/onboardingRouter';
|
||||
|
||||
@@ -134,7 +134,7 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
fetchTenantIdFromBackend();
|
||||
}, [tenantId, user, getUserTenants]);
|
||||
|
||||
// WebSocket connection for real-time training updates
|
||||
// Enhanced WebSocket connection for real-time training updates
|
||||
const {
|
||||
status,
|
||||
jobUpdates,
|
||||
@@ -143,7 +143,11 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
isConnected,
|
||||
lastMessage,
|
||||
tenantId: resolvedTenantId,
|
||||
wsUrl
|
||||
wsUrl,
|
||||
connectionError,
|
||||
isAuthenticationError,
|
||||
refreshConnection,
|
||||
retryWithAuth
|
||||
} = useTrainingWebSocket(trainingJobId || 'pending', tenantId);
|
||||
|
||||
// Handle WebSocket job updates
|
||||
@@ -203,12 +207,19 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
currentStep: 'Error en el entrenamiento'
|
||||
}));
|
||||
|
||||
} else if (messageType === 'initial_status') {
|
||||
} else if (messageType === 'initial_status' || messageType === 'current_status') {
|
||||
console.log('Received training status update:', messageType, data);
|
||||
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
|
||||
currentStep: data.current_step || data.currentStep || prev.currentStep,
|
||||
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
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
@@ -228,10 +239,94 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
}
|
||||
}, [jobUpdates, processWebSocketMessage]);
|
||||
|
||||
// Connect to WebSocket when training starts
|
||||
// Enhanced WebSocket connection management with polling fallback
|
||||
useEffect(() => {
|
||||
if (tenantId && trainingJobId && currentStep === 3) {
|
||||
console.log('Connecting to training WebSocket:', { tenantId, trainingJobId, wsUrl });
|
||||
connect();
|
||||
|
||||
// Simple polling fallback for training completion detection (now that we fixed the 404 issue)
|
||||
const pollingInterval = setInterval(async () => {
|
||||
if (trainingProgress.status === 'running' || trainingProgress.status === 'pending') {
|
||||
try {
|
||||
// Check training job status via REST API as fallback
|
||||
const response = await fetch(`http://localhost:8000/api/v1/tenants/${tenantId}/training/jobs/${trainingJobId}/status`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
'X-Tenant-ID': tenantId
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const jobStatus = await response.json();
|
||||
|
||||
// If the job is completed but we haven't received WebSocket notification
|
||||
if (jobStatus.status === 'completed' && (trainingProgress.status === 'running' || trainingProgress.status === 'pending')) {
|
||||
console.log('Training completed detected via REST polling fallback');
|
||||
|
||||
setTrainingProgress(prev => ({
|
||||
...prev,
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
currentStep: 'Entrenamiento completado',
|
||||
estimatedTimeRemaining: 0
|
||||
}));
|
||||
|
||||
// Mark training step as completed in onboarding API
|
||||
completeStep('training_completed', {
|
||||
training_completed_at: new Date().toISOString(),
|
||||
user_id: user?.id,
|
||||
tenant_id: tenantId,
|
||||
completion_detected_via: 'rest_polling_fallback'
|
||||
}).catch(error => {
|
||||
console.warn('Failed to mark training as completed in API:', error);
|
||||
});
|
||||
|
||||
// Show celebration and auto-advance to final step after 3 seconds
|
||||
toast.success('🎉 Training completed! Your AI model is ready to use.', {
|
||||
duration: 5000,
|
||||
icon: '🤖'
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
manualNavigation.current = true;
|
||||
setCurrentStep(4);
|
||||
}, 3000);
|
||||
|
||||
// Clear the polling interval
|
||||
clearInterval(pollingInterval);
|
||||
}
|
||||
|
||||
// If job failed, update status
|
||||
if (jobStatus.status === 'failed' && (trainingProgress.status === 'running' || trainingProgress.status === 'pending')) {
|
||||
console.log('Training failure detected via REST polling fallback');
|
||||
|
||||
setTrainingProgress(prev => ({
|
||||
...prev,
|
||||
status: 'failed',
|
||||
error: jobStatus.error_message || 'Error en el entrenamiento',
|
||||
currentStep: 'Error en el entrenamiento'
|
||||
}));
|
||||
|
||||
clearInterval(pollingInterval);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore polling errors to avoid noise
|
||||
console.debug('REST polling error (expected if training not started):', error);
|
||||
}
|
||||
} else if (trainingProgress.status === 'completed' || trainingProgress.status === 'failed') {
|
||||
// Clear polling if training is finished
|
||||
clearInterval(pollingInterval);
|
||||
}
|
||||
}, 15000); // Poll every 15 seconds (less aggressive than before)
|
||||
|
||||
return () => {
|
||||
if (isConnected) {
|
||||
disconnect();
|
||||
}
|
||||
clearInterval(pollingInterval);
|
||||
};
|
||||
}
|
||||
|
||||
return () => {
|
||||
@@ -239,7 +334,35 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
disconnect();
|
||||
}
|
||||
};
|
||||
}, [tenantId, trainingJobId, currentStep, connect, disconnect, isConnected]);
|
||||
}, [tenantId, trainingJobId, currentStep]); // Removed problematic dependencies that cause reconnection loops
|
||||
|
||||
// Handle connection errors with user feedback
|
||||
useEffect(() => {
|
||||
if (connectionError) {
|
||||
if (isAuthenticationError) {
|
||||
toast.error('Sesión expirada. Reintentando conexión...');
|
||||
// Auto-retry authentication errors after 3 seconds
|
||||
setTimeout(() => {
|
||||
retryWithAuth();
|
||||
}, 3000);
|
||||
} else {
|
||||
console.warn('WebSocket connection error:', connectionError);
|
||||
// Don't show error toast for non-auth errors as they auto-retry
|
||||
}
|
||||
}
|
||||
}, [connectionError, isAuthenticationError, retryWithAuth]);
|
||||
|
||||
// Enhanced WebSocket status logging
|
||||
useEffect(() => {
|
||||
console.log('WebSocket status changed:', {
|
||||
status,
|
||||
isConnected,
|
||||
jobId: trainingJobId,
|
||||
tenantId,
|
||||
connectionError,
|
||||
isAuthenticationError
|
||||
});
|
||||
}, [status, isConnected, trainingJobId, tenantId, connectionError, isAuthenticationError]);
|
||||
|
||||
|
||||
const storeTenantId = (tenantId: string) => {
|
||||
@@ -632,6 +755,10 @@ const OnboardingPage: React.FC<OnboardingPageProps> = ({ user, onComplete }) =>
|
||||
estimatedTimeRemaining: trainingProgress.estimatedTimeRemaining,
|
||||
error: trainingProgress.error
|
||||
}}
|
||||
websocketStatus={status}
|
||||
connectionError={connectionError}
|
||||
isConnected={isConnected}
|
||||
onRetryConnection={refreshConnection}
|
||||
onTimeout={() => {
|
||||
toast.success('El entrenamiento continuará en segundo plano. ¡Puedes empezar a explorar!');
|
||||
onComplete(); // Navigate to dashboard
|
||||
|
||||
Reference in New Issue
Block a user