From 67b8fcdf9f8ef99eddc1dcea5921e3e7d4e46005 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Sat, 20 Sep 2025 23:30:54 +0200 Subject: [PATCH] Imporve the AI models page --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- .../domain/forecasting/ModelDetailsModal.tsx | 404 ++++++++++++++++ .../components/domain/forecasting/index.ts | 22 +- .../app/database/models/ModelsConfigPage.tsx | 449 +++++------------- .../procurement/ProcurementPage.tsx | 377 +++++++-------- 6 files changed, 685 insertions(+), 571 deletions(-) create mode 100644 frontend/src/components/domain/forecasting/ModelDetailsModal.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f9c70db1..93d1eac4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -72,7 +72,7 @@ "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.0", "tailwindcss": "^3.3.0", - "typescript": "^5.3.0", + "typescript": "^5.9.2", "vite": "^5.0.0", "vite-plugin-pwa": "^0.17.0", "vitest": "^1.0.0" diff --git a/frontend/package.json b/frontend/package.json index d4b7d709..53a248d8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,7 +82,7 @@ "prettier": "^3.1.0", "prettier-plugin-tailwindcss": "^0.5.0", "tailwindcss": "^3.3.0", - "typescript": "^5.3.0", + "typescript": "^5.9.2", "vite": "^5.0.0", "vite-plugin-pwa": "^0.17.0", "vitest": "^1.0.0" diff --git a/frontend/src/components/domain/forecasting/ModelDetailsModal.tsx b/frontend/src/components/domain/forecasting/ModelDetailsModal.tsx new file mode 100644 index 00000000..a0ec9056 --- /dev/null +++ b/frontend/src/components/domain/forecasting/ModelDetailsModal.tsx @@ -0,0 +1,404 @@ +import React from 'react'; +import { StatusModal, StatusModalSection } from '@/components/ui/StatusModal/StatusModal'; +import { Badge } from '@/components/ui/Badge'; +import { Tooltip } from '@/components/ui/Tooltip'; +import { TrainedModelResponse, TrainingMetrics } from '@/types/training'; +import { Activity, TrendingUp, Calendar, Settings, Lightbulb, AlertTriangle, CheckCircle, Target } from 'lucide-react'; + +interface ModelDetailsModalProps { + isOpen: boolean; + onClose: () => void; + model: TrainedModelResponse; +} + +// Helper function to determine performance color based on accuracy +const getPerformanceColor = (accuracy: number): string => { + if (accuracy >= 90) return 'var(--color-success)'; + if (accuracy >= 80) return 'var(--color-info)'; + if (accuracy >= 70) return 'var(--color-warning)'; + if (accuracy >= 60) return 'var(--color-warning-dark)'; + return 'var(--color-error)'; +}; + + +// Helper function to determine performance message for bakery owners +const getPerformanceMessage = (accuracy: number): string => { + if (accuracy >= 90) return 'Excelente: Las predicciones son muy precisas, ideales para planificar producción'; + if (accuracy >= 80) return 'Bueno: Las predicciones son confiables para planificación diaria'; + if (accuracy >= 70) return 'Aceptable: Predicciones útiles pero considera monitorear de cerca'; + if (accuracy >= 60) return 'Necesita mejora: Predicciones menos precisas, considera reentrenar'; + return 'Poco confiable: El modelo necesita ser reentrenado con más datos'; +}; + +// Helper function to format date +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); +}; + +// Helper function to calculate accuracy from MAPE +const calculateAccuracy = (mape: number | undefined): number => { + if (mape === undefined || mape === null || isNaN(mape)) return 0; + // MAPE in the API response is already in percentage form (e.g., 8.5 = 8.5%) + const accuracy = Math.max(0, Math.min(100, 100 - mape)); + return accuracy; +}; + +// Feature tag component +const FeatureTag: React.FC<{ feature: string }> = ({ feature }) => { + const getIcon = (feature: string) => { + const featureLower = feature.toLowerCase(); + if (featureLower.includes('temperature')) return '☀️'; + if (featureLower.includes('weekend')) return '📅'; + if (featureLower.includes('holiday')) return '🎉'; + if (featureLower.includes('precipitation')) return '🌧️'; + if (featureLower.includes('traffic')) return '🚗'; + return '📊'; + }; + + const getTooltip = (feature: string) => { + const featureLower = feature.toLowerCase(); + if (featureLower.includes('temperature')) return 'El clima afecta lo que los clientes quieren comprar - días calurosos significan menos productos horneados calientes'; + if (featureLower.includes('weekend')) return 'Tus patrones de ventas son diferentes los fines de semana vs días de semana'; + if (featureLower.includes('holiday')) return 'Días especiales y festivos cambian cuánto compra la gente'; + if (featureLower.includes('precipitation')) return 'La lluvia o nieve afecta cuántos clientes visitan tu panadería'; + if (featureLower.includes('traffic')) return 'Calles transitadas usualmente significan más clientes'; + return `${feature}: Esto ayuda a predecir cuánto venderás`; + }; + + return ( + + + + {feature} + + + ); +}; + +const ModelDetailsModal: React.FC = ({ + isOpen, + onClose, + model +}) => { + // Early return if model is not provided + if (!model) { + return ( + + ); + } + + // Calculate business metrics from technical metrics + // The API response has metrics directly on the model object, not nested in training_metrics + const accuracy = calculateAccuracy((model as any).mape || model.training_metrics?.mape); + const performanceColor = getPerformanceColor(accuracy); + const performanceMessage = getPerformanceMessage(accuracy); + + // Prepare sections for StatusModal + const sections: StatusModalSection[] = [ + { + title: "Resumen", + icon: Activity, + fields: [ + { + label: "Rendimiento del Modelo", + value: ( +
+
+ Precisión + + {accuracy.toFixed(0)}% + +
+
+
+
+
+ {performanceMessage} +
+
+ ), + span: 2 + }, + { + label: "Estado", + value: ( + + + {(model as any).is_active ? 'Activo' : 'Inactivo'} + + ) + }, + { + label: "Última Actualización", + value: formatDate(model.created_at || new Date().toISOString()) + }, + { + label: "Período de Entrenamiento", + value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} - ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}` + } + ] + }, + { + title: "Métricas de Rendimiento", + icon: TrendingUp, + fields: [ + { + label: "Tasa de Precisión", + value: `${accuracy.toFixed(0)}%`, + highlight: true + }, + { + label: "Diferencia Típica", + value: (() => { + const maeValue = ((model as any).mae || model.training_metrics?.mae || 0).toFixed(0); + return ( +
+
±{maeValue} productos
+
+ En promedio, las predicciones se desvían por {maeValue} productos +
+
+ ); + })() + }, + { + label: "Confiabilidad General", + value: `${(((model as any).r2_score || model.training_metrics?.r2_score || 0) * 100).toFixed(0)}% confiable` + } + ] + }, + { + title: "Qué Significa Esto para Tu Panadería", + icon: Target, + fields: [ + { + label: "Precisión de las Predicciones", + value: (() => { + const mapeValue = Math.round((model as any).mape || model.training_metrics?.mape || 0); + return ( +
+
+ Error promedio: {mapeValue}% +
+
+ Ejemplo práctico: Si el modelo predice que venderás 100 panes, + es muy probable que vendas entre {100 - mapeValue} y {100 + mapeValue} panes. + Mientras menor sea este porcentaje, más precisas son las predicciones. +
+
+ ); + })(), + span: 2 + }, + { + label: "Potencial de Reducción de Desperdicio", + value: accuracy >= 80 + ? `Podría ayudar a reducir el desperdicio diario en ~${Math.round(accuracy / 8)}% mediante mejor planificación` + : accuracy >= 60 + ? 'Alguna reducción de desperdicio posible con monitoreo cuidadoso' + : 'Precisión del modelo demasiado baja para reducción confiable de desperdicio', + highlight: true + } + ] + }, + { + title: "Factores que Considera el Modelo", + icon: Settings, + fields: [ + { + label: "Información que Analiza", + value: (() => { + const features = ((model as any).features_used || model.features_used || []); + const featureCount = features.length; + + if (featureCount === 0) { + return ( +
+ Usando datos básicos de ventas e historial +
+ ); + } + + return ( +
+
+ El modelo analiza {featureCount} factores diferentes para hacer predicciones más precisas. +
+ +
+
+
📅 Temporales:
+
Días de semana, festivos, estaciones
+
+
+
🌤️ Clima:
+
Temperatura, lluvia, humedad
+
+
+
🚶 Tráfico:
+
Peatones, congestión, velocidad
+
+
+
📊 Ventas:
+
Historial, patrones, tendencias
+
+
+ +
+ 💡 En resumen: Cuantos más factores analiza el modelo, más precisas son las predicciones para tu panadería. +
+
+ ); + })(), + span: 2 + } + ] + }, + { + title: "Detalles Técnicos", + icon: Calendar, + fields: [ + { + label: "Método de Predicción", + value: model.model_type || 'Pronóstico con IA' + }, + { + label: "Última Actualización", + value: formatDate(model.created_at || new Date().toISOString()) + }, + { + label: "Período de Entrenamiento", + value: `${formatDate((model as any).training_start_date || model.training_period?.start_date || new Date().toISOString())} a ${formatDate((model as any).training_end_date || model.training_period?.end_date || new Date().toISOString())}`, + span: 2 + } + ] + }, + { + title: "Perspectivas de Negocio", + icon: Lightbulb, + fields: [ + { + label: "Qué funciona mejor", + value: ( +
+ + Este modelo te da las predicciones más precisas para días de semana regulares +
+ ), + span: 2 + }, + { + label: "Ten cuidado con", + value: ( +
+ + Las predicciones pueden ser menos confiables durante festivos y eventos especiales +
+ ), + span: 2 + }, + { + label: "Patrones descubiertos", + value: ((model as any).features_used || model.features_used || []).some((f: string) => f.toLowerCase().includes('weekend')) + ? "Tu negocio muestra patrones diferentes entre días de semana y fines de semana" + : "Este modelo ha aprendido tus patrones regulares de ventas", + span: 2 + }, + { + label: "Próximos pasos", + value: accuracy < 80 + ? "Considera reentrenar con datos más recientes para mejorar la precisión" + : "Tu modelo está funcionando bien. Continúa monitoreando las predicciones", + span: 2 + } + ] + } + ]; + + // Actions for the modal - removed console.log placeholders for production readiness + const actions = [ + { + label: 'Actualizar Modelo', + variant: 'primary' as const, + onClick: () => { + // TODO: Implement model retraining functionality + // This should trigger a new training job for the product + } + }, + { + label: 'Ver Predicciones', + variant: 'secondary' as const, + onClick: () => { + // TODO: Navigate to forecast history or predictions view + // This should show historical predictions vs actual sales + } + } + ]; + + // Only show deactivate if model is active + if ((model as any).is_active) { + actions.push({ + label: 'Desactivar', + variant: 'secondary' as const, + onClick: () => { + // TODO: Implement model deactivation + // This should set model status to inactive + } + }); + } + + return ( + + ); +}; + +export default ModelDetailsModal; \ No newline at end of file diff --git a/frontend/src/components/domain/forecasting/index.ts b/frontend/src/components/domain/forecasting/index.ts index d32d6784..32f3dc0e 100644 --- a/frontend/src/components/domain/forecasting/index.ts +++ b/frontend/src/components/domain/forecasting/index.ts @@ -3,29 +3,11 @@ export { default as DemandChart } from './DemandChart'; export { default as ForecastTable } from './ForecastTable'; export { default as SeasonalityIndicator } from './SeasonalityIndicator'; export { default as AlertsPanel } from './AlertsPanel'; +export { default as ModelDetailsModal } from './ModelDetailsModal'; // Export component props for type checking export type { DemandChartProps } from './DemandChart'; export type { ForecastTableProps } from './ForecastTable'; export type { SeasonalityIndicatorProps } from './SeasonalityIndicator'; export type { AlertsPanelProps } from './AlertsPanel'; - -// Re-export related types from forecasting types -export type { - ForecastResponse, - DemandTrend, - SeasonalPattern, - ForecastAlert, - TrendDirection, - AlertSeverity, - ForecastAlertType, - SeasonalComponent, - HolidayEffect, - WeeklyPattern, - YearlyTrend, - Season, - SeasonalPeriod, - DayOfWeek, - WeatherCondition, - EventType, -} from '../../../types/forecasting.types'; \ No newline at end of file +export type { ModelDetailsModalProps } from './ModelDetailsModal'; \ No newline at end of file diff --git a/frontend/src/pages/app/database/models/ModelsConfigPage.tsx b/frontend/src/pages/app/database/models/ModelsConfigPage.tsx index ac551f3d..85e09f34 100644 --- a/frontend/src/pages/app/database/models/ModelsConfigPage.tsx +++ b/frontend/src/pages/app/database/models/ModelsConfigPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useMemo } from 'react'; import { Brain, TrendingUp, AlertCircle, Play, RotateCcw, Eye, Loader, CheckCircle } from 'lucide-react'; -import { Button, Card, Badge, Modal, Table, Select, Input } from '../../../../components/ui'; +import { Button, Card, Badge, Modal, Table, Select, Input, StatsGrid, StatusCard } from '../../../../components/ui'; import { PageHeader } from '../../../../components/layout'; import { useToast } from '../../../../hooks/ui/useToast'; import { useCurrentTenant } from '../../../../stores/tenant.store'; @@ -13,6 +13,7 @@ import { useModelPerformance, useTenantTrainingStatistics } from '../../../../api/hooks/training'; +import { ModelDetailsModal } from '../../../../components/domain/forecasting'; import type { IngredientResponse } from '../../../../api/types/inventory'; import type { TrainedModelResponse, SingleProductTrainingRequest } from '../../../../api/types/training'; @@ -44,6 +45,7 @@ const ModelsConfigPage: React.FC = () => { const tenantId = currentTenant?.id || ''; const [selectedIngredient, setSelectedIngredient] = useState(null); + const [selectedModel, setSelectedModel] = useState(null); const [showTrainingModal, setShowTrainingModal] = useState(false); const [showModelDetailsModal, setShowModelDetailsModal] = useState(false); const [trainingSettings, setTrainingSettings] = useState>({ @@ -161,8 +163,15 @@ const ModelsConfigPage: React.FC = () => { } }; - const handleViewModelDetails = (ingredient: IngredientResponse) => { + const handleViewModelDetails = async (ingredient: IngredientResponse) => { setSelectedIngredient(ingredient); + + // Find the model for this ingredient + const model = modelStatuses.find(status => status.ingredient.id === ingredient.id)?.model; + if (model) { + setSelectedModel(model); + } + setShowModelDetailsModal(true); }; @@ -171,95 +180,9 @@ const ModelsConfigPage: React.FC = () => { setShowTrainingModal(true); }; - const getStatusBadge = (status: ModelStatus['status']) => { - switch (status) { - case 'no_model': - return Sin modelo; - case 'active': - return Activo; - case 'training': - return Entrenando; - case 'error': - return Error; - default: - return Desconocido; - } - }; + - const getAccuracyBadge = (accuracy?: number) => { - if (!accuracy) return null; - - const variant = accuracy >= 90 ? 'success' : accuracy >= 75 ? 'warning' : 'error'; - return {accuracy.toFixed(1)}%; - }; - - // Table columns configuration - const tableColumns = [ - { - key: 'ingredient', - title: 'Ingrediente', - render: (_: any, status: ModelStatus) => ( -
-
- {status.ingredient.name.charAt(0).toUpperCase()} -
-
-
{status.ingredient.name}
-
{status.ingredient.category}
-
-
- ), - }, - { - key: 'status', - title: 'Estado del Modelo', - render: (_: any, status: ModelStatus) => ( -
- {getStatusBadge(status.status)} - {status.accuracy && getAccuracyBadge(status.accuracy)} -
- ), - }, - { - key: 'lastTrained', - title: 'Último Entrenamiento', - render: (_: any, status: ModelStatus) => ( -
- {status.lastTrainingDate - ? new Date(status.lastTrainingDate).toLocaleDateString('es-ES') - : 'Nunca' - } -
- ), - }, - { - key: 'actions', - title: 'Acciones', - render: (_: any, status: ModelStatus) => ( -
- {status.hasModel && ( - - )} - -
- ), - }, - ]; + if (ingredientsLoading || modelsLoading) { return ( @@ -291,55 +214,35 @@ const ModelsConfigPage: React.FC = () => { {/* Statistics Cards */} -
- -
-
-
- {modelStatuses.filter(s => s.hasModel).length} -
-
Ingredientes con Modelo
-
- -
-
- - -
-
-
- {modelStatuses.filter(s => s.status === 'no_model').length} -
-
Sin Modelo
-
- -
-
- - -
-
-
- {orphanedModels.length} -
-
Modelos Huérfanos
-
- -
-
- - -
-
-
- {statsError ? 'N/A' : (statistics?.average_accuracy ? `${(100 - statistics.average_accuracy).toFixed(1)}%` : 'N/A')} -
-
Precisión Promedio
-
- -
-
-
+ s.hasModel).length, + icon: Brain, + variant: 'default', + }, + { + title: 'Sin Modelo', + value: modelStatuses.filter(s => s.status === 'no_model').length, + icon: AlertCircle, + variant: 'warning', + }, + { + title: 'Modelos Huérfanos', + value: orphanedModels.length, + icon: AlertCircle, + variant: 'info', + }, + { + title: 'Precisión Promedio', + value: statsError ? 'N/A' : (statistics?.average_accuracy ? `${(100 - statistics.average_accuracy).toFixed(1)}%` : 'N/A'), + icon: TrendingUp, + variant: 'success', + }, + ]} + columns={4} + /> {/* Orphaned Models Warning */} {orphanedModels.length > 0 && ( @@ -385,10 +288,10 @@ const ModelsConfigPage: React.FC = () => { - {/* Models Table */} - + {/* Models Grid */} +
{filteredStatuses.length === 0 ? ( -
+

No se encontraron ingredientes @@ -398,12 +301,70 @@ const ModelsConfigPage: React.FC = () => {

) : ( - + filteredStatuses.map((status) => { + // Get status configuration for the StatusCard + const statusConfig = { + color: status.status === 'active' + ? '#10B981' // green for active + : status.status === 'no_model' + ? '#6B7280' // gray for no model + : status.status === 'training' + ? '#F59E0B' // amber for training + : '#EF4444', // red for error + text: status.status === 'active' + ? 'Activo' + : status.status === 'no_model' + ? 'Sin Modelo' + : status.status === 'training' + ? 'Entrenando' + : 'Error', + icon: status.status === 'active' + ? CheckCircle + : status.status === 'no_model' + ? AlertCircle + : status.status === 'training' + ? Loader + : AlertCircle, + isCritical: status.status === 'error', + isHighlight: status.status === 'training' + }; + + return ( + status.hasModel + ? handleViewModelDetails(status.ingredient) + : handleStartTraining(status.ingredient), + priority: 'primary' as const + }, + // Secondary action - Retrain if model exists + ...(status.hasModel ? [{ + label: 'Reentrenar', + icon: RotateCcw, + onClick: () => handleStartTraining(status.ingredient), + priority: 'secondary' as const + }] : []) + ]} + /> + ); + }) )} - + {/* Training Modal */} { {/* Model Details Modal */} - setShowModelDetailsModal(false)} - title={`Detalles del Modelo - ${selectedIngredient?.name}`} - size="lg" - > -
- {selectedIngredient && ( - - )} -
-
- - ); -}; - -// Component for model details content -const ModelDetailsContent: React.FC<{ - tenantId: string; - ingredientId: string; -}> = ({ tenantId, ingredientId }) => { - const { data: activeModel } = useActiveModel(tenantId, ingredientId); - - if (!activeModel) { - return ( -
- -

No hay modelo disponible

-

- Este ingrediente no tiene un modelo entrenado disponible. Puedes entrenar uno nuevo usando el botón "Entrenar". -

-
- ); - } - - const precision = activeModel.training_metrics?.mape - ? (100 - activeModel.training_metrics.mape).toFixed(1) - : 'N/A'; - - return ( -
- {/* Model Overview */} -
-
-
-
- {precision}% -
-
Precisión
-
-
- -
-
-
- {activeModel.training_metrics?.mae?.toFixed(2) || 'N/A'} -
-
MAE
-
-
- -
-
-
- {activeModel.training_metrics?.rmse?.toFixed(2) || 'N/A'} -
-
RMSE
-
-
-
- - {/* Model Information */} - -

- - Información del Modelo -

-
-
-
- - Creado - - - {new Date(activeModel.created_at).toLocaleString('es-ES', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} - -
- -
- - Características usadas - - - {activeModel.features_used?.length || 0} variables - -
-
- -
-
- - Período de entrenamiento - - - {activeModel.training_period?.start_date && activeModel.training_period?.end_date - ? `${new Date(activeModel.training_period.start_date).toLocaleDateString('es-ES')} - ${new Date(activeModel.training_period.end_date).toLocaleDateString('es-ES')}` - : 'No disponible' - } - -
- -
- - Hiperparámetros - - - {Object.keys(activeModel.hyperparameters || {}).length} configurados - -
-
-
-
- - {/* Features Used */} - {activeModel.features_used && activeModel.features_used.length > 0 && ( - -

- - Características del Modelo -

-
- {activeModel.features_used.map((feature: string, index: number) => ( -
- - {feature.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())} - -
- ))} -
-
- )} - - {/* Training Performance */} - {activeModel.training_metrics && ( - -

- - Métricas de Rendimiento -

-
-
-
- {precision}% -
-
Precisión
-
-
-
- {activeModel.training_metrics.mae?.toFixed(2) || 'N/A'} -
-
MAE
-
-
-
- {activeModel.training_metrics.rmse?.toFixed(2) || 'N/A'} -
-
RMSE
-
-
-
- {activeModel.training_metrics.r2_score?.toFixed(3) || 'N/A'} -
-
-
-
-
+ {selectedModel && ( + setShowModelDetailsModal(false)} + model={selectedModel} + /> )}
); diff --git a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx index 2dc387fd..172778a3 100644 --- a/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx +++ b/frontend/src/pages/app/operations/procurement/ProcurementPage.tsx @@ -1,13 +1,11 @@ import React, { useState } from 'react'; -import { Plus, Search, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, Play, Pause, X, Save, Building2 } from 'lucide-react'; +import { Plus, Download, ShoppingCart, Truck, DollarSign, Calendar, Clock, CheckCircle, AlertCircle, Package, Eye, Loader, Edit, ArrowRight, X, Save, Building2, Play } from 'lucide-react'; import { Button, Input, Card, StatsGrid, StatusCard, getStatusColor, StatusModal } from '../../../../components/ui'; import { formatters } from '../../../../components/ui/Stats/StatsPresets'; import { PageHeader } from '../../../../components/layout'; import { useProcurementDashboard, useProcurementPlans, - useCurrentProcurementPlan, - useCriticalRequirements, usePlanRequirements, useGenerateProcurementPlan, useUpdateProcurementPlanStatus, @@ -16,7 +14,6 @@ import { import { useTenantStore } from '../../../../stores/tenant.store'; const ProcurementPage: React.FC = () => { - const [activeTab, setActiveTab] = useState('plans'); const [searchTerm, setSearchTerm] = useState(''); const [showForm, setShowForm] = useState(false); const [modalMode, setModalMode] = useState<'view' | 'edit'>('view'); @@ -36,17 +33,35 @@ const ProcurementPage: React.FC = () => { limit: 50, offset: 0 }); - const { data: currentPlan, isLoading: isCurrentPlanLoading } = useCurrentProcurementPlan(tenantId); - const { data: criticalRequirements, isLoading: isCriticalLoading } = useCriticalRequirements(tenantId); // Get plan requirements for selected plan - const { data: planRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({ + const { data: allPlanRequirements, isLoading: isPlanRequirementsLoading } = usePlanRequirements({ tenant_id: tenantId, - plan_id: selectedPlanForRequirements || '', - status: 'critical' // Only get critical requirements + plan_id: selectedPlanForRequirements || '' + // Remove status filter to get all requirements }, { enabled: !!selectedPlanForRequirements && !!tenantId }); + + // Filter critical requirements client-side + const planRequirements = allPlanRequirements?.filter(req => { + // Check various conditions that might make a requirement critical + const isLowStock = req.current_stock_level && req.required_quantity && + (req.current_stock_level / req.required_quantity) < 0.5; + const isNearDeadline = req.required_by_date && + (new Date(req.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24) < 7; + const hasHighPriority = req.priority === 'high'; + + return isLowStock || isNearDeadline || hasHighPriority; + }); + + // Debug logging + console.log('📊 Plan Requirements Debug:', { + selectedPlanId: selectedPlanForRequirements, + allRequirements: allPlanRequirements?.length || 0, + criticalRequirements: planRequirements?.length || 0, + sampleRequirement: allPlanRequirements?.[0] + }); const generatePlanMutation = useGenerateProcurementPlan(); const updatePlanStatusMutation = useUpdateProcurementPlanStatus(); @@ -120,6 +135,7 @@ const ProcurementPage: React.FC = () => { }; const handleShowCriticalRequirements = (planId: string) => { + console.log('🔍 Opening critical requirements for plan:', planId); setSelectedPlanForRequirements(planId); setShowCriticalRequirements(true); }; @@ -287,70 +303,32 @@ const ProcurementPage: React.FC = () => { /> )} - {/* Tabs Navigation */} -
- -
- {activeTab === 'plans' && ( - -
-
- setSearchTerm(e.target.value)} - className="w-full" - /> -
- + +
+
+ setSearchTerm(e.target.value)} + className="w-full" + />
- - )} + +
+
{/* Procurement Plans Grid - Mobile-Optimized */} - {activeTab === 'plans' && ( -
- {isPlansLoading ? ( -
- -
- ) : ( - filteredPlans.map((plan) => { +
+ {isPlansLoading ? ( +
+ +
+ ) : ( + filteredPlans.map((plan) => { const statusConfig = getPlanStatusConfig(plan.status); const nextStageConfig = getStageActionConfig(plan.status); const isEditing = editingPlan?.id === plan.id; @@ -522,12 +500,12 @@ const ProcurementPage: React.FC = () => {
); }) - )} -
- )} + ) + } +
{/* Empty State for Procurement Plans */} - {activeTab === 'plans' && !isPlansLoading && filteredPlans.length === 0 && ( + {!isPlansLoading && filteredPlans.length === 0 && (

@@ -558,152 +536,123 @@ const ProcurementPage: React.FC = () => {

)} - {activeTab === 'requirements' && ( -
- {isCriticalLoading ? ( -
- -
- ) : criticalRequirements && criticalRequirements.length > 0 ? ( -
- {criticalRequirements.map((requirement) => ( - = requirement.required_quantity ? '#10b981' : requirement.current_stock_level >= requirement.required_quantity * 0.5 ? '#f59e0b' : '#ef4444' - } : undefined} - metadata={[ - `Stock: ${requirement.current_stock_level || 0} ${requirement.unit_of_measure}`, - `Necesario: ${requirement.required_quantity - (requirement.current_stock_level || 0)} ${requirement.unit_of_measure}`, - `Costo: €${formatters.compact(requirement.estimated_total_cost || 0)}`, - `Días restantes: ${Math.ceil((new Date(requirement.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))}` - ]} - actions={[ - { - label: 'Ver Detalles', - icon: Eye, - variant: 'primary', - priority: 'primary', - onClick: () => console.log('View requirement details') - }, - { - label: 'Asignar Proveedor', - icon: Building2, - priority: 'secondary', - onClick: () => console.log('Assign supplier') - }, - { - label: 'Comprar Ahora', - icon: ShoppingCart, - priority: 'secondary', - onClick: () => console.log('Purchase now') - } - ]} - /> - ))} -
- ) : ( -
- -

- No hay requerimientos críticos -

-

- Todos los requerimientos están bajo control -

-
- )} -
- )} - - {activeTab === 'analytics' && ( -
-
- -

Costos de Procurement

-
-
- Costo Estimado Total - - {formatters.currency(stats.totalEstimatedCost)} - + {/* Critical Requirements Modal */} + {showCriticalRequirements && selectedPlanForRequirements && ( +
+
+ {/* Header */} +
+
+
+
-
- Costo Aprobado - - {formatters.currency(stats.totalApprovedCost)} - -
-
- Varianza - - {formatters.currency(stats.totalEstimatedCost - stats.totalApprovedCost)} - +
+

+ Requerimientos Críticos +

+

+ Plan {filteredPlans.find(p => p.id === selectedPlanForRequirements)?.plan_number || selectedPlanForRequirements} +

- + +
- -

Alertas Críticas

-
- {dashboardData?.low_stock_alerts?.slice(0, 5).map((alert: any, index: number) => ( -
-
- - {alert.product_name || `Alerta ${index + 1}`} -
- - Stock Bajo - -
- )) || ( -
- -

No hay alertas críticas

-
- )} -
-
+ {/* Content */} +
+ {isPlanRequirementsLoading ? ( +
+ +
+ ) : planRequirements && planRequirements.length > 0 ? ( +
+ {planRequirements.map((requirement) => ( + p.id === selectedPlanForRequirements)?.plan_number || 'N/A'}`} + primaryValue={requirement.required_quantity} + primaryValueLabel={requirement.unit_of_measure} + secondaryInfo={{ + label: 'Límite', + value: new Date(requirement.required_by_date).toLocaleDateString('es-ES', { day: '2-digit', month: '2-digit' }) + }} + progress={requirement.current_stock_level && requirement.required_quantity ? { + label: `${Math.round((requirement.current_stock_level / requirement.required_quantity) * 100)}% cubierto`, + percentage: Math.min((requirement.current_stock_level / requirement.required_quantity) * 100, 100), + color: requirement.current_stock_level >= requirement.required_quantity ? '#10b981' : requirement.current_stock_level >= requirement.required_quantity * 0.5 ? '#f59e0b' : '#ef4444' + } : undefined} + metadata={[ + `Stock: ${requirement.current_stock_level || 0} ${requirement.unit_of_measure}`, + `Necesario: ${requirement.required_quantity - (requirement.current_stock_level || 0)} ${requirement.unit_of_measure}`, + `Costo: €${formatters.compact(requirement.estimated_total_cost || 0)}`, + `Días restantes: ${Math.ceil((new Date(requirement.required_by_date).getTime() - Date.now()) / (1000 * 60 * 60 * 24))}`, + `Estado: ${requirement.status || 'N/A'}`, + `PO: ${requirement.purchase_order_number || 'Sin PO asignado'}`, + `Prioridad: ${requirement.priority || 'N/A'}` + ]} + actions={[ + { + label: 'Ver Detalles', + icon: Eye, + variant: 'primary', + priority: 'primary', + onClick: () => console.log('View requirement details', requirement) + }, + ...(requirement.purchase_order_number ? [ + { + label: 'Ver PO', + icon: Eye, + variant: 'outline' as const, + priority: 'secondary' as const, + onClick: () => console.log('View PO', requirement.purchase_order_number) + } + ] : [ + { + label: 'Crear PO', + icon: Plus, + variant: 'outline' as const, + priority: 'secondary' as const, + onClick: () => console.log('Create PO for', requirement) + } + ]), + { + label: requirement.supplier_name ? 'Cambiar Proveedor' : 'Asignar Proveedor', + icon: Building2, + variant: 'outline' as const, + priority: 'secondary' as const, + onClick: () => console.log('Assign supplier') + } + ]} + /> + ))} +
+ ) : ( +
+ +

+ No hay requerimientos críticos +

+

+ Este plan no tiene requerimientos críticos en este momento +

+
+ )} +
- - -

Resumen de Performance

-
-
-

{stats.totalPlans}

-

Planes Totales

-
-
-

{stats.activePlans}

-

Planes Activos

-
-
-

{stats.pendingRequirements}

-

Pendientes

-
-
-

{stats.criticalRequirements}

-

Críticos

-
-
-
)}