Add new API in the frontend

This commit is contained in:
Urtzi Alfaro
2025-09-11 18:21:32 +02:00
parent 523b926854
commit 55f31a3630
16 changed files with 2719 additions and 1806 deletions

View File

@@ -1,28 +1,68 @@
import React, { useState } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings } from 'lucide-react';
import React, { useState, useMemo } from 'react';
import { Calendar, TrendingUp, AlertTriangle, BarChart3, Download, Settings, Loader } from 'lucide-react';
import { Button, Card, Badge, Select, Table } from '../../../../components/ui';
import type { TableColumn } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { DemandChart, ForecastTable, SeasonalityIndicator, AlertsPanel } from '../../../../components/domain/forecasting';
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';
const ForecastingPage: React.FC = () => {
const [selectedProduct, setSelectedProduct] = useState('all');
const [forecastPeriod, setForecastPeriod] = useState('7');
const [viewMode, setViewMode] = useState<'chart' | 'table'>('chart');
const forecastData = {
accuracy: 92,
totalDemand: 1247,
growthTrend: 8.5,
seasonalityFactor: 1.15,
};
// Get tenant ID from auth user
const user = useAuthUser();
const tenantId = user?.tenant_id || '';
const products = [
{ id: 'all', name: 'Todos los productos' },
{ id: 'bread', name: 'Panes' },
{ id: 'pastry', name: 'Bollería' },
{ id: 'cake', name: 'Tartas' },
];
// 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]);
const periods = [
{ value: '7', label: '7 días' },
@@ -31,78 +71,51 @@ const ForecastingPage: React.FC = () => {
{ value: '90', label: '3 meses' },
];
const mockForecasts = [
{
id: '1',
product: 'Pan de Molde Integral',
currentStock: 25,
forecastDemand: 45,
recommendedProduction: 50,
confidence: 95,
trend: 'up',
stockoutRisk: 'low',
},
{
id: '2',
product: 'Croissants de Mantequilla',
currentStock: 18,
forecastDemand: 32,
recommendedProduction: 35,
confidence: 88,
trend: 'stable',
stockoutRisk: 'medium',
},
{
id: '3',
product: 'Baguettes Francesas',
currentStock: 12,
forecastDemand: 28,
recommendedProduction: 30,
confidence: 91,
trend: 'down',
stockoutRisk: 'high',
},
];
const alerts = [
{
id: '1',
type: 'stockout',
product: 'Baguettes Francesas',
message: 'Alto riesgo de agotamiento en las próximas 24h',
severity: 'high',
recommendation: 'Incrementar producción en 15 unidades',
},
{
id: '2',
type: 'overstock',
product: 'Magdalenas',
message: 'Probable exceso de stock para mañana',
severity: 'medium',
recommendation: 'Reducir producción en 20%',
},
{
id: '3',
type: 'weather',
product: 'Todos',
message: 'Lluvia prevista - incremento esperado en demanda de bollería',
severity: 'info',
recommendation: 'Aumentar producción de productos de interior en 10%',
},
];
const weatherImpact = {
today: 'sunny',
temperature: 22,
demandFactor: 0.95,
affectedCategories: ['helados', 'bebidas frías'],
// 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',
}));
};
const seasonalInsights = [
{ period: 'Mañana', factor: 1.2, products: ['Pan', 'Bollería'] },
{ period: 'Tarde', factor: 0.8, products: ['Tartas', 'Dulces'] },
{ period: 'Fin de semana', factor: 1.4, products: ['Tartas especiales'] },
];
// 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`
}));
};
// 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
};
};
const getTrendIcon = (trend: string) => {
switch (trend) {
@@ -177,6 +190,20 @@ const ForecastingPage: React.FC = () => {
},
];
// 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;
return (
<div className="p-6 space-y-6">
<PageHeader
@@ -196,57 +223,87 @@ const ForecastingPage: React.FC = () => {
}
/>
{/* 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)]">{forecastData.accuracy}%</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>
{isLoading && (
<Card className="p-6 flex items-center justify-center">
<Loader className="h-6 w-6 animate-spin mr-2" />
<span>Cargando predicciones...</span>
</Card>
)}
<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)]">{forecastData.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)]" />
{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>
</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">+{forecastData.growthTrend}%</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>
{!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>
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Factor Estacional</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{forecastData.seasonalityFactor}x</p>
<p className="text-xs text-[var(--text-tertiary)]">multiplicador actual</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>
<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>
</div>
</Card>
</div>
</>
)}
{/* Controls */}
<Card className="p-6">
@@ -308,7 +365,7 @@ const ForecastingPage: React.FC = () => {
period={forecastPeriod}
/>
) : (
<ForecastTable forecasts={mockForecasts} />
<ForecastTable forecasts={transformedForecasts} />
)}
</div>
@@ -317,67 +374,92 @@ const ForecastingPage: React.FC = () => {
<AlertsPanel alerts={alerts} />
{/* Weather Impact */}
<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>
{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>
<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>
</div>
<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 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>
</div>
</div>
</div>
</Card>
{/* Seasonal Insights */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Patrones Estacionales</h3>
<div className="space-y-3">
{seasonalInsights.map((insight, index) => (
<div key={index} className="p-3 bg-[var(--bg-secondary)] rounded-lg">
<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>
</div>
{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>
)}
</div>
</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">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">{insight.period}</span>
<span className="text-sm text-purple-600 font-medium">{insight.factor}x</span>
<span className="text-sm font-medium">Algoritmo principal</span>
<Badge variant="purple">{statisticsData.model_performance.most_used_algorithm}</Badge>
</div>
<div className="flex flex-wrap gap-1">
{insight.products.map((product, idx) => (
<Badge key={idx} variant="purple">{product}</Badge>
))}
<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>
</div>
</div>
))}
</div>
</Card>
</div>
</Card>
)}
</div>
</div>
{/* Detailed Forecasts Table */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Predicciones Detalladas</h3>
<Table
columns={forecastColumns}
data={mockForecasts}
rowKey="id"
hover={true}
variant="default"
size="md"
/>
</Card>
{!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>
)}
</div>
);
};