2025-09-11 18:21:32 +02:00
|
|
|
import React, { useState, useMemo } from 'react';
|
|
|
|
|
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings, Loader } from 'lucide-react';
|
2025-08-28 18:07:16 +02:00
|
|
|
import { Button, Card, Badge, Select, Table } from '../../../../components/ui';
|
|
|
|
|
import type { TableColumn } from '../../../../components/ui';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
|
|
|
|
import { DemandChart, ForecastTable, SeasonalityIndicator, AlertsPanel } from '../../../../components/domain/forecasting';
|
2025-09-11 18:21:32 +02:00
|
|
|
import { useTenantForecasts, useForecastStatistics } from '../../../../api/hooks/forecasting';
|
|
|
|
|
import { useIngredients } from '../../../../api/hooks/inventory';
|
|
|
|
|
import { useAuthUser } from '../../../../stores/auth.store';
|
|
|
|
|
import { ForecastResponse } from '../../../../api/types/forecasting';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const ForecastingPage: React.FC = () => {
|
|
|
|
|
const [selectedProduct, setSelectedProduct] = useState('all');
|
|
|
|
|
const [forecastPeriod, setForecastPeriod] = useState('7');
|
|
|
|
|
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Get tenant ID from auth user
|
|
|
|
|
const user = useAuthUser();
|
|
|
|
|
const tenantId = user?.tenant_id || '';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Calculate date range based on selected period
|
|
|
|
|
const endDate = new Date();
|
|
|
|
|
const startDate = new Date();
|
|
|
|
|
startDate.setDate(startDate.getDate() - parseInt(forecastPeriod));
|
|
|
|
|
|
|
|
|
|
// API hooks
|
|
|
|
|
const {
|
|
|
|
|
data: forecastsData,
|
|
|
|
|
isLoading: forecastsLoading,
|
|
|
|
|
error: forecastsError
|
|
|
|
|
} = useTenantForecasts(tenantId, {
|
|
|
|
|
start_date: startDate.toISOString().split('T')[0],
|
|
|
|
|
end_date: endDate.toISOString().split('T')[0],
|
|
|
|
|
...(selectedProduct !== 'all' && { inventory_product_id: selectedProduct }),
|
|
|
|
|
limit: 100
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
data: statisticsData,
|
|
|
|
|
isLoading: statisticsLoading,
|
|
|
|
|
error: statisticsError
|
|
|
|
|
} = useForecastStatistics(tenantId);
|
|
|
|
|
|
|
|
|
|
// Fetch real inventory data
|
|
|
|
|
const {
|
|
|
|
|
data: ingredientsData,
|
|
|
|
|
isLoading: ingredientsLoading,
|
|
|
|
|
error: ingredientsError
|
|
|
|
|
} = useIngredients(tenantId);
|
|
|
|
|
|
|
|
|
|
// Build products list from real inventory data
|
|
|
|
|
const products = useMemo(() => {
|
|
|
|
|
const productList = [{ id: 'all', name: 'Todos los productos' }];
|
|
|
|
|
|
|
|
|
|
if (ingredientsData && ingredientsData.length > 0) {
|
|
|
|
|
const inventoryProducts = ingredientsData.map(ingredient => ({
|
|
|
|
|
id: ingredient.id,
|
|
|
|
|
name: ingredient.name,
|
|
|
|
|
category: ingredient.category,
|
|
|
|
|
}));
|
|
|
|
|
productList.push(...inventoryProducts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return productList;
|
|
|
|
|
}, [ingredientsData]);
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const periods = [
|
|
|
|
|
{ value: '7', label: '7 días' },
|
|
|
|
|
{ value: '14', label: '14 días' },
|
|
|
|
|
{ value: '30', label: '30 días' },
|
|
|
|
|
{ value: '90', label: '3 meses' },
|
|
|
|
|
];
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Transform forecast data for table display
|
|
|
|
|
const transformForecastsForTable = (forecasts: ForecastResponse[]) => {
|
|
|
|
|
return forecasts.map(forecast => ({
|
|
|
|
|
id: forecast.id,
|
|
|
|
|
product: forecast.inventory_product_id, // Will need to map to product name
|
|
|
|
|
currentStock: 'N/A', // Not available in forecast data
|
|
|
|
|
forecastDemand: forecast.predicted_demand,
|
|
|
|
|
recommendedProduction: Math.ceil(forecast.predicted_demand * 1.1), // Simple calculation
|
|
|
|
|
confidence: Math.round(forecast.confidence_level * 100),
|
|
|
|
|
trend: forecast.predicted_demand > 0 ? 'up' : 'stable',
|
|
|
|
|
stockoutRisk: forecast.confidence_level > 0.8 ? 'low' : forecast.confidence_level > 0.6 ? 'medium' : 'high',
|
|
|
|
|
}));
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Generate alerts based on forecast data
|
|
|
|
|
const generateAlertsFromForecasts = (forecasts: ForecastResponse[]) => {
|
|
|
|
|
return forecasts
|
|
|
|
|
.filter(forecast => forecast.confidence_level < 0.7 || forecast.predicted_demand > 50)
|
|
|
|
|
.slice(0, 3) // Limit to 3 alerts
|
|
|
|
|
.map((forecast, index) => ({
|
|
|
|
|
id: (index + 1).toString(),
|
|
|
|
|
type: forecast.confidence_level < 0.7 ? 'low-confidence' : 'high-demand',
|
|
|
|
|
product: forecast.inventory_product_id,
|
|
|
|
|
message: forecast.confidence_level < 0.7
|
|
|
|
|
? `Baja confianza en predicción (${Math.round(forecast.confidence_level * 100)}%)`
|
|
|
|
|
: `Alta demanda prevista: ${forecast.predicted_demand} unidades`,
|
|
|
|
|
severity: forecast.confidence_level < 0.5 ? 'high' : 'medium',
|
|
|
|
|
recommendation: forecast.confidence_level < 0.7
|
|
|
|
|
? 'Revisar datos históricos y factores externos'
|
|
|
|
|
: `Considerar aumentar producción a ${Math.ceil(forecast.predicted_demand * 1.2)} unidades`
|
|
|
|
|
}));
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Extract weather data from first forecast (if available)
|
|
|
|
|
const getWeatherImpact = (forecasts: ForecastResponse[]) => {
|
|
|
|
|
const firstForecast = forecasts?.[0];
|
|
|
|
|
if (!firstForecast) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
today: firstForecast.weather_description || 'N/A',
|
|
|
|
|
temperature: firstForecast.weather_temperature || 0,
|
|
|
|
|
demandFactor: 1.0, // Could be calculated based on weather
|
|
|
|
|
affectedCategories: [], // Could be derived from business logic
|
|
|
|
|
};
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const getTrendIcon = (trend: string) => {
|
|
|
|
|
switch (trend) {
|
|
|
|
|
case 'up':
|
|
|
|
|
return <TrendingUp className="h-4 w-4 text-[var(--color-success)]" />;
|
|
|
|
|
case 'down':
|
|
|
|
|
return <TrendingUp className="h-4 w-4 text-[var(--color-error)] rotate-180" />;
|
|
|
|
|
default:
|
|
|
|
|
return <div className="h-4 w-4 bg-gray-400 rounded-full" />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getRiskBadge = (risk: string) => {
|
|
|
|
|
const riskConfig = {
|
|
|
|
|
low: { color: 'green', text: 'Bajo' },
|
|
|
|
|
medium: { color: 'yellow', text: 'Medio' },
|
|
|
|
|
high: { color: 'red', text: 'Alto' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const config = riskConfig[risk as keyof typeof riskConfig];
|
|
|
|
|
return <Badge variant={config?.color as any}>{config?.text}</Badge>;
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-28 18:07:16 +02:00
|
|
|
const forecastColumns: TableColumn[] = [
|
|
|
|
|
{
|
|
|
|
|
key: 'product',
|
|
|
|
|
title: 'Producto',
|
|
|
|
|
dataIndex: 'product',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'currentStock',
|
|
|
|
|
title: 'Stock Actual',
|
|
|
|
|
dataIndex: 'currentStock',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'forecastDemand',
|
|
|
|
|
title: 'Demanda Prevista',
|
|
|
|
|
dataIndex: 'forecastDemand',
|
|
|
|
|
render: (value) => (
|
|
|
|
|
<span className="font-medium text-[var(--color-info)]">{value}</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'recommendedProduction',
|
|
|
|
|
title: 'Producción Recomendada',
|
|
|
|
|
dataIndex: 'recommendedProduction',
|
|
|
|
|
render: (value) => (
|
|
|
|
|
<span className="font-medium text-[var(--color-success)]">{value}</span>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'confidence',
|
|
|
|
|
title: 'Confianza',
|
|
|
|
|
dataIndex: 'confidence',
|
|
|
|
|
render: (value) => `${value}%`,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'trend',
|
|
|
|
|
title: 'Tendencia',
|
|
|
|
|
dataIndex: 'trend',
|
|
|
|
|
render: (value) => (
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
{getTrendIcon(value)}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'stockoutRisk',
|
|
|
|
|
title: 'Riesgo Agotamiento',
|
|
|
|
|
dataIndex: 'stockoutRisk',
|
|
|
|
|
render: (value) => getRiskBadge(value),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
// Derived data from API responses
|
|
|
|
|
const forecasts = forecastsData?.forecasts || [];
|
|
|
|
|
const transformedForecasts = transformForecastsForTable(forecasts);
|
|
|
|
|
const alerts = generateAlertsFromForecasts(forecasts);
|
|
|
|
|
const weatherImpact = getWeatherImpact(forecasts);
|
|
|
|
|
const isLoading = forecastsLoading || statisticsLoading || ingredientsLoading;
|
|
|
|
|
const hasError = forecastsError || statisticsError || ingredientsError;
|
|
|
|
|
|
|
|
|
|
// Calculate metrics from real data
|
|
|
|
|
const totalDemand = forecasts.reduce((sum, f) => sum + f.predicted_demand, 0);
|
|
|
|
|
const averageConfidence = forecasts.length > 0
|
|
|
|
|
? Math.round((forecasts.reduce((sum, f) => sum + f.confidence_level, 0) / forecasts.length) * 100)
|
|
|
|
|
: 0;
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
return (
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="Predicción de Demanda"
|
|
|
|
|
description="Predicciones inteligentes basadas en IA para optimizar tu producción"
|
|
|
|
|
action={
|
|
|
|
|
<div className="flex space-x-2">
|
|
|
|
|
<Button variant="outline">
|
|
|
|
|
<Settings className="w-4 h-4 mr-2" />
|
|
|
|
|
Configurar
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline">
|
|
|
|
|
<Download className="w-4 h-4 mr-2" />
|
|
|
|
|
Exportar
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
{isLoading && (
|
|
|
|
|
<Card className="p-6 flex items-center justify-center">
|
|
|
|
|
<Loader className="h-6 w-6 animate-spin mr-2" />
|
|
|
|
|
<span>Cargando predicciones...</span>
|
2025-08-28 10:41:04 +02:00
|
|
|
</Card>
|
2025-09-11 18:21:32 +02:00
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
{hasError && (
|
|
|
|
|
<Card className="p-6 bg-red-50 border-red-200">
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<AlertTriangle className="h-5 w-5 text-red-600 mr-2" />
|
|
|
|
|
<span className="text-red-800">Error al cargar las predicciones. Por favor, inténtalo de nuevo.</span>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
2025-09-11 18:21:32 +02:00
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
{!isLoading && !hasError && (
|
|
|
|
|
<>
|
|
|
|
|
{/* Key Metrics */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Precisión del Modelo</p>
|
|
|
|
|
<p className="text-3xl font-bold text-[var(--color-success)]">
|
|
|
|
|
{statisticsData?.accuracy_metrics?.average_accuracy
|
|
|
|
|
? Math.round(statisticsData.accuracy_metrics.average_accuracy * 100)
|
|
|
|
|
: averageConfidence}%
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="h-12 w-12 bg-[var(--color-success)]/10 rounded-full flex items-center justify-center">
|
|
|
|
|
<BarChart3 className="h-6 w-6 text-[var(--color-success)]" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Demanda Prevista</p>
|
|
|
|
|
<p className="text-3xl font-bold text-[var(--color-info)]">{Math.round(totalDemand)}</p>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)]">próximos {forecastPeriod} días</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Calendar className="h-12 w-12 text-[var(--color-info)]" />
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Tendencia</p>
|
|
|
|
|
<p className="text-3xl font-bold text-purple-600">
|
|
|
|
|
+{statisticsData?.accuracy_metrics?.accuracy_trend
|
|
|
|
|
? Math.round(statisticsData.accuracy_metrics.accuracy_trend * 100)
|
|
|
|
|
: 5}%
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)]">vs período anterior</p>
|
|
|
|
|
</div>
|
|
|
|
|
<TrendingUp className="h-12 w-12 text-purple-600" />
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium text-[var(--text-secondary)]">Total Predicciones</p>
|
|
|
|
|
<p className="text-3xl font-bold text-[var(--color-primary)]">
|
|
|
|
|
{statisticsData?.total_forecasts || forecasts.length}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)]">generadas</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
|
|
|
|
|
<svg className="h-6 w-6 text-[var(--color-primary)]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-09-11 18:21:32 +02:00
|
|
|
</>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
{/* Controls */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
|
|
|
<div className="flex-1 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Producto</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedProduct}
|
|
|
|
|
onChange={(e) => setSelectedProduct(e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
|
|
|
>
|
|
|
|
|
{products.map(product => (
|
|
|
|
|
<option key={product.id} value={product.id}>{product.name}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Período</label>
|
|
|
|
|
<select
|
|
|
|
|
value={forecastPeriod}
|
|
|
|
|
onChange={(e) => setForecastPeriod(e.target.value)}
|
|
|
|
|
className="w-full px-3 py-2 border border-[var(--border-secondary)] rounded-md"
|
|
|
|
|
>
|
|
|
|
|
{periods.map(period => (
|
|
|
|
|
<option key={period.value} value={period.value}>{period.label}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-2">Vista</label>
|
|
|
|
|
<div className="flex rounded-md border border-[var(--border-secondary)]">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setViewMode('chart')}
|
|
|
|
|
className={`px-3 py-2 text-sm ${viewMode === 'chart' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-l-md`}
|
|
|
|
|
>
|
|
|
|
|
Gráfico
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setViewMode('table')}
|
|
|
|
|
className={`px-3 py-2 text-sm ${viewMode === 'table' ? 'bg-blue-600 text-white' : 'bg-white text-[var(--text-secondary)]'} rounded-r-md border-l`}
|
|
|
|
|
>
|
|
|
|
|
Tabla
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
|
{/* Main Forecast Display */}
|
|
|
|
|
<div className="lg:col-span-2">
|
|
|
|
|
{viewMode === 'chart' ? (
|
|
|
|
|
<DemandChart
|
|
|
|
|
product={selectedProduct}
|
|
|
|
|
period={forecastPeriod}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
2025-09-11 18:21:32 +02:00
|
|
|
<ForecastTable forecasts={transformedForecasts} />
|
2025-08-28 10:41:04 +02:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Alerts Panel */}
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<AlertsPanel alerts={alerts} />
|
|
|
|
|
|
|
|
|
|
{/* Weather Impact */}
|
2025-09-11 18:21:32 +02:00
|
|
|
{weatherImpact && (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Impacto Meteorológico</h3>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Hoy:</span>
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<span className="text-sm font-medium">{weatherImpact.temperature}°C</span>
|
|
|
|
|
<div className="ml-2 w-6 h-6 bg-yellow-400 rounded-full"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Condiciones:</span>
|
|
|
|
|
<span className="text-sm font-medium text-[var(--color-info)]">{weatherImpact.today}</span>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-11 18:21:32 +02:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm text-[var(--text-secondary)]">Factor de demanda:</span>
|
|
|
|
|
<span className="text-sm font-medium text-[var(--color-info)]">{weatherImpact.demandFactor}x</span>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-09-11 18:21:32 +02:00
|
|
|
|
|
|
|
|
{weatherImpact.affectedCategories.length > 0 && (
|
|
|
|
|
<div className="mt-4">
|
|
|
|
|
<p className="text-xs text-[var(--text-tertiary)] mb-2">Categorías afectadas:</p>
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{weatherImpact.affectedCategories.map((category, index) => (
|
|
|
|
|
<Badge key={index} variant="blue">{category}</Badge>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-09-11 18:21:32 +02:00
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Model Performance */}
|
|
|
|
|
{statisticsData?.model_performance && (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Rendimiento del Modelo</h3>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="p-3 bg-[var(--bg-secondary)] rounded-lg">
|
2025-08-28 10:41:04 +02:00
|
|
|
<div className="flex items-center justify-between mb-2">
|
2025-09-11 18:21:32 +02:00
|
|
|
<span className="text-sm font-medium">Algoritmo principal</span>
|
|
|
|
|
<Badge variant="purple">{statisticsData.model_performance.most_used_algorithm}</Badge>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
2025-09-11 18:21:32 +02:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-xs text-[var(--text-tertiary)]">Tiempo de procesamiento promedio</span>
|
|
|
|
|
<span className="text-xs text-[var(--text-secondary)]">
|
|
|
|
|
{Math.round(statisticsData.model_performance.average_processing_time)}ms
|
|
|
|
|
</span>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-09-11 18:21:32 +02:00
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Detailed Forecasts Table */}
|
2025-09-11 18:21:32 +02:00
|
|
|
{!isLoading && !hasError && transformedForecasts.length > 0 && (
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Predicciones Detalladas</h3>
|
|
|
|
|
<Table
|
|
|
|
|
columns={forecastColumns}
|
|
|
|
|
data={transformedForecasts}
|
|
|
|
|
rowKey="id"
|
|
|
|
|
hover={true}
|
|
|
|
|
variant="default"
|
|
|
|
|
size="md"
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{!isLoading && !hasError && transformedForecasts.length === 0 && (
|
|
|
|
|
<Card className="p-6 text-center">
|
|
|
|
|
<div className="py-8">
|
|
|
|
|
<BarChart3 className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-gray-600 mb-2">No hay predicciones disponibles</h3>
|
|
|
|
|
<p className="text-gray-500">
|
|
|
|
|
No se encontraron predicciones para el período seleccionado.
|
|
|
|
|
Prueba ajustando los filtros o genera nuevas predicciones.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default ForecastingPage;
|