import React, { useState, useMemo } from 'react'; import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings, Loader, Zap, Brain, Target, CloudRain, Sun, Thermometer } from 'lucide-react'; import { Button, Card, Badge, Select, Table, StatsGrid } from '../../../../components/ui'; import type { TableColumn } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; import { DemandChart, ForecastTable } from '../../../../components/domain/forecasting'; import { useTenantForecasts, useCreateSingleForecast } from '../../../../api/hooks/forecasting'; import { useIngredients } from '../../../../api/hooks/inventory'; import { useModels } from '../../../../api/hooks/training'; import { useCurrentTenant } from '../../../../stores/tenant.store'; import { ForecastResponse } from '../../../../api/types/forecasting'; import { forecastingService } from '../../../../api/services/forecasting'; const ForecastingPage: React.FC = () => { const [selectedProduct, setSelectedProduct] = useState(''); const [forecastPeriod, setForecastPeriod] = useState('7'); const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart'); const [isGenerating, setIsGenerating] = useState(false); const [hasGeneratedForecast, setHasGeneratedForecast] = useState(false); const [currentForecastData, setCurrentForecastData] = useState([]); // Get tenant ID from tenant store const currentTenant = useCurrentTenant(); const tenantId = currentTenant?.id || ''; // Calculate date range based on selected period const endDate = new Date(); const startDate = new Date(); startDate.setDate(startDate.getDate() - parseInt(forecastPeriod)); // Fetch existing forecasts const { data: forecastsData, isLoading: forecastsLoading, error: forecastsError } = useTenantForecasts(tenantId, { start_date: startDate.toISOString().split('T')[0], end_date: endDate.toISOString().split('T')[0], ...(selectedProduct && { inventory_product_id: selectedProduct }), limit: 100 }, { enabled: !!tenantId && hasGeneratedForecast && !!selectedProduct }); // Fetch real inventory data const { data: ingredientsData, isLoading: ingredientsLoading, error: ingredientsError } = useIngredients(tenantId); // Fetch trained models to filter products const { data: modelsData, isLoading: modelsLoading, error: modelsError } = useModels(tenantId, { active_only: true }); // Forecast generation mutation const createForecastMutation = useCreateSingleForecast({ onSuccess: (data) => { setIsGenerating(false); setHasGeneratedForecast(true); // Store the generated forecast data locally for immediate display setCurrentForecastData([data]); }, onError: (error) => { setIsGenerating(false); console.error('Error generating forecast:', error); }, }); // Build products list from ingredients that have trained models const products = useMemo(() => { if (!ingredientsData || !modelsData?.models) { return []; } // Get inventory product IDs that have trained models const modelProductIds = new Set(modelsData.models.map(model => model.inventory_product_id)); // Filter ingredients to only those with models const ingredientsWithModels = ingredientsData.filter(ingredient => modelProductIds.has(ingredient.id) ); return ingredientsWithModels.map(ingredient => ({ id: ingredient.id, name: ingredient.name, category: ingredient.category, hasModel: true })); }, [ingredientsData, modelsData]); 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' }, ]; // Handle forecast generation const handleGenerateForecast = async () => { if (!tenantId || !selectedProduct) { alert('Por favor, selecciona un ingrediente para generar predicciones.'); return; } setIsGenerating(true); try { const today = new Date(); const forecastRequest = { inventory_product_id: selectedProduct, forecast_date: today.toISOString().split('T')[0], forecast_days: parseInt(forecastPeriod), location: 'default', confidence_level: 0.8, }; // Use the new multi-day endpoint for all forecasts const multiDayResult = await forecastingService.createMultiDayForecast(tenantId, forecastRequest); setIsGenerating(false); setHasGeneratedForecast(true); // Use the forecasts from the multi-day response setCurrentForecastData(multiDayResult.forecasts); } catch (error) { console.error('Failed to generate forecast:', error); setIsGenerating(false); } }; // Transform forecast data for table display - only real data const transformForecastsForTable = (forecasts: ForecastResponse[]) => { return forecasts.map(forecast => ({ id: forecast.id, product: forecast.inventory_product_id, forecastDate: forecast.forecast_date, forecastDemand: forecast.predicted_demand, confidence: Math.round(forecast.confidence_level * 100), confidenceRange: `${forecast.confidence_lower?.toFixed(1) || 'N/A'} - ${forecast.confidence_upper?.toFixed(1) || 'N/A'}`, algorithm: forecast.algorithm, })); }; // Extract weather data from all forecasts for 7-day view const getWeatherImpact = (forecasts: ForecastResponse[]) => { if (!forecasts || forecasts.length === 0) return null; // Calculate average temperature across all forecast days const avgTemp = forecasts.reduce((sum, f) => sum + (f.weather_temperature || 0), 0) / forecasts.length; const tempRange = { min: Math.min(...forecasts.map(f => f.weather_temperature || 0)), max: Math.max(...forecasts.map(f => f.weather_temperature || 0)) }; // Aggregate weather descriptions const weatherTypes = forecasts .map(f => f.weather_description) .filter(Boolean) .reduce((acc, desc) => { acc[desc] = (acc[desc] || 0) + 1; return acc; }, {} as Record); const dominantWeather = Object.entries(weatherTypes) .sort(([,a], [,b]) => b - a)[0]?.[0] || 'N/A'; return { avgTemperature: Math.round(avgTemp), tempRange, dominantWeather, forecastDays: forecasts.length, dailyForecasts: forecasts.map(f => ({ date: f.forecast_date, temperature: f.weather_temperature, description: f.weather_description, predicted_demand: f.predicted_demand })) }; }; const forecastColumns: TableColumn[] = [ { key: 'product', title: 'Producto ID', dataIndex: 'product', }, { key: 'forecastDate', title: 'Fecha', dataIndex: 'forecastDate', render: (value) => new Date(value).toLocaleDateString('es-ES'), }, { key: 'forecastDemand', title: 'Demanda Prevista', dataIndex: 'forecastDemand', render: (value) => ( {value?.toFixed(2) || 'N/A'} ), }, { key: 'confidence', title: 'Confianza', dataIndex: 'confidence', render: (value) => `${value}%`, }, { key: 'confidenceRange', title: 'Rango de Confianza', dataIndex: 'confidenceRange', }, { key: 'algorithm', title: 'Algoritmo', dataIndex: 'algorithm', }, ]; // Use either current forecast data or fetched data const forecasts = currentForecastData.length > 0 ? currentForecastData : (forecastsData?.forecasts || []); const transformedForecasts = transformForecastsForTable(forecasts); const weatherImpact = getWeatherImpact(forecasts); const isLoading = forecastsLoading || ingredientsLoading || modelsLoading || isGenerating; const hasError = forecastsError || ingredientsError || modelsError; // 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; // Get forecast insights from the latest forecast - only real backend data const getForecastInsights = (forecast: ForecastResponse) => { const insights = []; // Weather data (only factual) if (forecast.weather_temperature) { insights.push({ type: 'weather', icon: Thermometer, title: 'Temperatura', description: `${forecast.weather_temperature}°C`, impact: 'info' }); } if (forecast.weather_description) { insights.push({ type: 'weather', icon: CloudRain, title: 'Condición Climática', description: forecast.weather_description, impact: 'info' }); } // Temporal factors (only factual) if (forecast.is_weekend) { insights.push({ type: 'temporal', icon: Calendar, title: 'Fin de Semana', description: 'Día de fin de semana', impact: 'info' }); } if (forecast.is_holiday) { insights.push({ type: 'temporal', icon: Calendar, title: 'Día Festivo', description: 'Día festivo', impact: 'info' }); } // Model confidence (factual) insights.push({ type: 'model', icon: Target, title: 'Confianza del Modelo', description: `${Math.round(forecast.confidence_level * 100)}%`, impact: forecast.confidence_level > 0.8 ? 'positive' : forecast.confidence_level > 0.6 ? 'moderate' : 'high' }); return insights; }; const currentInsights = forecasts.length > 0 ? getForecastInsights(forecasts[0]) : []; return (
} /> {isLoading && ( {isGenerating ? 'Generando nuevas predicciones...' : 'Cargando predicciones...'} )} {hasError && (
Error al cargar las predicciones. Por favor, inténtalo de nuevo.
)} {!isLoading && !hasError && ( <> )} {/* Forecast Configuration */}

Configurar Predicción

{/* Step 1: Select Ingredient */}
{products.length === 0 && (

No hay ingredientes con modelos entrenados

)}
{/* Step 2: Select Period */}
{/* Step 3: Generate */}
{selectedProduct && (

Ingrediente seleccionado: {products.find(p => p.id === selectedProduct)?.name}

Se generará una predicción de demanda para los próximos {forecastPeriod} días usando IA

)}
{/* Results Section - Only show after generating forecast */} {hasGeneratedForecast && forecasts.length > 0 && ( <> {/* Enhanced Layout Structure */}
{/* Key Metrics Row - Using StatsGrid */} f.predicted_demand)) - Math.min(...forecasts.map(f => f.predicted_demand))).toFixed(1), icon: BarChart3, variant: "warning", size: "sm" } ]} /> {/* Main Content Grid */}
{/* Chart Section - Takes most space */}

Predicción de Demanda

{products.find(p => p.id === selectedProduct)?.name} • {forecastPeriod} días • {forecasts.length} puntos

{viewMode === 'chart' ? ( ) : ( )}
{/* Right Sidebar - Insights */}
{/* Weather & External Factors */}
{/* Forecast Insights */} {currentInsights.length > 0 && (

Factores que Afectan la Predicción

{currentInsights.map((insight, index) => { const IconComponent = insight.icon; return (

{insight.title}

{insight.description}

); })}
)} {/* Weather Impact */} {weatherImpact && (

Clima ({weatherImpact.forecastDays} días)

Impacto meteorológico en la demanda

{/* Temperature Overview */}
{weatherImpact.avgTemperature}°C

Promedio

{weatherImpact.dominantWeather}

Condición

{/* Daily forecast - compact */}

Pronóstico detallado:

{weatherImpact.dailyForecasts.slice(0, 5).map((day, index) => (
{new Date(day.date).toLocaleDateString('es-ES', { weekday: 'short', day: 'numeric' })}
{day.temperature}°C {day.predicted_demand?.toFixed(0)}
))}
)} {/* Model Information */} {forecasts.length > 0 && (

Modelo IA

Información técnica del algoritmo

Algoritmo {forecasts[0]?.algorithm || 'N/A'}
Versión {forecasts[0]?.model_version || 'N/A'}
Tiempo {forecasts[0]?.processing_time_ms || 'N/A'}ms
)}
)} {/* Detailed Forecasts Table */} {!isLoading && !hasError && transformedForecasts.length > 0 && (

Predicciones Detalladas

)} {/* Empty States */} {!isLoading && !hasError && products.length === 0 && (

No hay ingredientes con modelos entrenados

Para generar predicciones, primero necesitas entrenar modelos de IA para tus ingredientes. Ve a la página de Modelos IA para entrenar modelos para tus ingredientes.

)} {!isLoading && !hasError && products.length > 0 && !hasGeneratedForecast && (

Listo para Generar Predicciones

Tienes {products.length} ingrediente{products.length > 1 ? 's' : ''} con modelos entrenados disponibles. Selecciona un ingrediente y período para comenzar.

1

Selecciona Ingrediente

Elige un ingrediente con modelo IA

2

Define Período

Establece días a predecir

3

Generar Predicción

Obtén insights de IA

)} ); }; export default ForecastingPage;