From ddf95958d2f73d1fed57fc78a41a9d5647af0ef8 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Fri, 2 Jan 2026 13:27:48 +0100 Subject: [PATCH] fix demo session 3 --- .../dashboard/OutletFulfillmentTab.tsx | 277 ++++++++++------- .../components/dashboard/ProductionTab.tsx | 286 ++++++++++-------- frontend/src/locales/en/dashboard.json | 25 +- frontend/src/locales/es/dashboard.json | 25 +- frontend/src/locales/eu/dashboard.json | 49 ++- 5 files changed, 419 insertions(+), 243 deletions(-) diff --git a/frontend/src/components/dashboard/OutletFulfillmentTab.tsx b/frontend/src/components/dashboard/OutletFulfillmentTab.tsx index 3d1fa757..deca59e1 100644 --- a/frontend/src/components/dashboard/OutletFulfillmentTab.tsx +++ b/frontend/src/components/dashboard/OutletFulfillmentTab.tsx @@ -10,6 +10,8 @@ import { Package, AlertTriangle, CheckCircle2, Activity, Clock, Warehouse, Shopp import { useTranslation } from 'react-i18next'; import StatusCard from '../ui/StatusCard/StatusCard'; import { useSSEEvents } from '../../hooks/useSSE'; +import { useChildTenants } from '../../api/hooks/useEnterpriseDashboard'; +import { inventoryService } from '../../api/services/inventory'; interface OutletFulfillmentTabProps { tenantId: string; @@ -21,67 +23,28 @@ const OutletFulfillmentTab: React.FC = ({ tenantId, o const [selectedOutlet, setSelectedOutlet] = useState(null); const [viewMode, setViewMode] = useState<'summary' | 'detailed'>('summary'); - // Real-time SSE events - const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({ - channels: ['*.alerts', '*.notifications', 'recommendations'] - }); + // Get child tenants data + const { data: childTenants, isLoading: isChildTenantsLoading } = useChildTenants(tenantId); // State for real-time inventory data - const [inventoryData, setInventoryData] = useState([ - { - id: 'outlet-madrid', - name: 'Madrid Central', - inventoryCoverage: 85, - stockoutRisk: 'low', - criticalItems: 2, - fulfillmentRate: 98, - lastUpdated: '2024-01-15T10:30:00', - status: 'normal', - products: [ - { id: 'baguette', name: 'Baguette', coverage: 92, risk: 'low', stock: 450, safetyStock: 300 }, - { id: 'croissant', name: 'Croissant', coverage: 78, risk: 'medium', stock: 280, safetyStock: 250 }, - { id: 'pain-au-chocolat', name: 'Pain au Chocolat', coverage: 65, risk: 'high', stock: 180, safetyStock: 200 } - ] - }, - { - id: 'outlet-barcelona', - name: 'Barcelona Coastal', - inventoryCoverage: 68, - stockoutRisk: 'medium', - criticalItems: 5, - fulfillmentRate: 92, - lastUpdated: '2024-01-15T10:25:00', - status: 'warning', - products: [ - { id: 'baguette', name: 'Baguette', coverage: 75, risk: 'medium', stock: 320, safetyStock: 300 }, - { id: 'croissant', name: 'Croissant', coverage: 58, risk: 'high', stock: 220, safetyStock: 250 }, - { id: 'ensaimada', name: 'Ensaimada', coverage: 45, risk: 'critical', stock: 120, safetyStock: 200 } - ] - }, - { - id: 'outlet-valencia', - name: 'Valencia Port', - inventoryCoverage: 72, - stockoutRisk: 'medium', - criticalItems: 3, - fulfillmentRate: 95, - lastUpdated: '2024-01-15T10:20:00', - status: 'warning', - products: [ - { id: 'baguette', name: 'Baguette', coverage: 88, risk: 'low', stock: 420, safetyStock: 300 }, - { id: 'croissant', name: 'Croissant', coverage: 65, risk: 'medium', stock: 240, safetyStock: 250 }, - { id: 'focaccia', name: 'Focaccia', coverage: 55, risk: 'high', stock: 160, safetyStock: 200 } - ] - } - ]); + const [inventoryData, setInventoryData] = useState([]); + const [loading, setLoading] = useState(true); + + // Combine loading states + const isLoading = isChildTenantsLoading || loading; + + // Real-time SSE events + const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({ + channels: ['*.alerts', '*.notifications', 'recommendations'] + }); // Process SSE events for inventory updates useEffect(() => { - if (sseEvents.length === 0) return; + if (sseEvents.length === 0 || inventoryData.length === 0) return; // Filter inventory-related events - const inventoryEvents = sseEvents.filter(event => - event.event_type.includes('inventory_') || + const inventoryEvents = sseEvents.filter(event => + event.event_type.includes('inventory_') || event.event_type.includes('stock_') || event.event_type === 'stock_receipt_incomplete' || event.entity_type === 'inventory' @@ -93,8 +56,8 @@ const OutletFulfillmentTab: React.FC = ({ tenantId, o setInventoryData(prevData => { return prevData.map(outlet => { // Find events for this outlet - const outletEvents = inventoryEvents.filter(event => - event.entity_id === outlet.id || + const outletEvents = inventoryEvents.filter(event => + event.entity_id === outlet.id || event.event_metadata?.outlet_id === outlet.id ); @@ -145,7 +108,84 @@ const OutletFulfillmentTab: React.FC = ({ tenantId, o }; }); }); - }, [sseEvents]); + }, [sseEvents, inventoryData]); + + // Fetch inventory data for each child tenant individually + useEffect(() => { + if (!childTenants) { + setInventoryData([]); + setLoading(true); + return; + } + + const fetchAllInventoryData = async () => { + setLoading(true); + try { + const promises = childTenants.map(async (tenant) => { + try { + // Using the imported service directly + const inventoryData = await inventoryService.getDashboardSummary(tenant.id); + return { tenant, inventoryData }; + } catch (error) { + console.error(`Error fetching inventory for tenant ${tenant.id}:`, error); + return { tenant, inventoryData: null }; + } + }); + + const results = await Promise.all(promises); + + const processedData = results.map(({ tenant, inventoryData }) => { + // Calculate inventory metrics + const totalValue = inventoryData?.total_value || 0; + const outOfStockCount = inventoryData?.out_of_stock_count || 0; + const lowStockCount = inventoryData?.low_stock_count || 0; + const adequateStockCount = inventoryData?.adequate_stock_count || 0; + const totalIngredients = inventoryData?.total_ingredients || 0; + + // Calculate coverage percentage (simplified calculation) + const coverage = totalIngredients > 0 + ? Math.min(100, Math.round(((adequateStockCount + lowStockCount) / totalIngredients) * 100)) + : 100; + + // Determine risk level based on out-of-stock and low-stock items + let riskLevel = 'low'; + if (outOfStockCount > 5 || (outOfStockCount > 0 && lowStockCount > 10)) { + riskLevel = 'critical'; + } else if (outOfStockCount > 0 || lowStockCount > 5) { + riskLevel = 'high'; + } else if (lowStockCount > 2) { + riskLevel = 'medium'; + } + + // Determine status based on risk level + let status = 'normal'; + if (riskLevel === 'critical') status = 'critical'; + else if (riskLevel === 'high' || riskLevel === 'medium') status = 'warning'; + + return { + id: tenant.id, + name: tenant.name, + inventoryCoverage: coverage, + stockoutRisk: riskLevel, + criticalItems: outOfStockCount, + fulfillmentRate: 95, // Placeholder - would come from actual fulfillment data + lastUpdated: new Date().toISOString(), + status: status, + products: [] // Will be populated if detailed view is needed + }; + }); + + setInventoryData(processedData); + } catch (error) { + console.error('Error fetching inventory data:', error); + setInventoryData([]); + } finally { + setLoading(false); + } + }; + + fetchAllInventoryData(); + }, [childTenants]); // Calculate network-wide fulfillment metrics const calculateNetworkMetrics = () => { @@ -336,56 +376,83 @@ const OutletFulfillmentTab: React.FC = ({ tenantId, o {t('enterprise.outlet_status_overview')} -
- {inventoryData.map((outlet) => { - const statusConfig = getOutletStatusConfig(outlet.id); - - return ( - { + {isLoading ? ( +
+ {[...Array(3)].map((_, index) => ( + + +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : ( +
+ {inventoryData.map((outlet) => { + const statusConfig = getOutletStatusConfig(outlet.id); + + return ( + { + setSelectedOutlet(outlet.id); + setViewMode('detailed'); + onOutletClick(outlet.id, outlet.name); + }, + priority: 'primary' + }] : []} + onClick={() => { setSelectedOutlet(outlet.id); setViewMode('detailed'); - onOutletClick(outlet.id, outlet.name); - }, - priority: 'primary' - }] : []} - onClick={() => { - setSelectedOutlet(outlet.id); - setViewMode('detailed'); - }} - /> - ); - })} -
+ }} + /> + ); + })} + {inventoryData.length === 0 && ( +
+
+ +

{t('enterprise.no_outlets')}

+
+
+ )} +
+ )} {/* Detailed View - Product Level Inventory */} diff --git a/frontend/src/components/dashboard/ProductionTab.tsx b/frontend/src/components/dashboard/ProductionTab.tsx index d927047d..b88115b1 100644 --- a/frontend/src/components/dashboard/ProductionTab.tsx +++ b/frontend/src/components/dashboard/ProductionTab.tsx @@ -12,6 +12,7 @@ import { ProductionStatusBlock } from './blocks/ProductionStatusBlock'; import StatusCard from '../ui/StatusCard/StatusCard'; import { useControlPanelData } from '../../api/hooks/useControlPanelData'; import { useSSEEvents } from '../../hooks/useSSE'; +import { equipmentService } from '../../api/services/equipment'; interface ProductionTabProps { tenantId: string; @@ -31,56 +32,46 @@ const ProductionTab: React.FC = ({ tenantId }) => { }); // State for equipment data with real-time updates - const [equipmentData, setEquipmentData] = useState([ - { - id: 'oven-1', - name: 'Oven #1', - status: 'normal', - temperature: '180°C', - utilization: 85, - lastMaintenance: '2024-01-15', - nextMaintenance: '2024-02-15', - lastEvent: null - }, - { - id: 'oven-2', - name: 'Oven #2', - status: 'warning', - temperature: '195°C', - utilization: 92, - lastMaintenance: '2024-01-10', - nextMaintenance: '2024-02-10', - lastEvent: null - }, - { - id: 'mixer-1', - name: 'Industrial Mixer', - status: 'normal', - temperature: 'N/A', - utilization: 78, - lastMaintenance: '2024-01-20', - nextMaintenance: '2024-03-20', - lastEvent: null - }, - { - id: 'proofer', - name: 'Proofing Chamber', - status: 'critical', - temperature: '32°C', - utilization: 65, - lastMaintenance: '2023-12-01', - nextMaintenance: '2024-01-31', - lastEvent: null - } - ]); + const [equipmentData, setEquipmentData] = useState([]); + const [equipmentLoading, setEquipmentLoading] = useState(true); + + // Fetch equipment data + useEffect(() => { + const fetchEquipmentData = async () => { + setEquipmentLoading(true); + try { + const equipmentList = await equipmentService.getEquipment(tenantId); + // Transform the equipment data to match the expected format + const transformedData = equipmentList.map(eq => ({ + id: eq.id, + name: eq.name, + status: eq.status.toLowerCase(), + temperature: eq.currentTemperature ? `${eq.currentTemperature}°C` : 'N/A', + utilization: eq.efficiency || eq.uptime || 0, + lastMaintenance: eq.lastMaintenance || new Date().toISOString().split('T')[0], + nextMaintenance: eq.nextMaintenance || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // Default to 30 days from now + lastEvent: null + })); + setEquipmentData(transformedData); + } catch (error) { + console.error('Error fetching equipment data:', error); + // Set empty array but still mark loading as false + setEquipmentData([]); + } finally { + setEquipmentLoading(false); + } + }; + + fetchEquipmentData(); + }, [tenantId]); // Process SSE events for equipment status updates useEffect(() => { - if (sseEvents.length === 0) return; + if (sseEvents.length === 0 || equipmentData.length === 0) return; // Filter equipment-related events - const equipmentEvents = sseEvents.filter(event => - event.event_type.includes('equipment_') || + const equipmentEvents = sseEvents.filter(event => + event.event_type.includes('equipment_') || event.event_type === 'equipment_maintenance' || event.entity_type === 'equipment' ); @@ -91,8 +82,8 @@ const ProductionTab: React.FC = ({ tenantId }) => { setEquipmentData(prevEquipment => { return prevEquipment.map(equipment => { // Find the latest event for this equipment - const equipmentEvent = equipmentEvents.find(event => - event.entity_id === equipment.id || + const equipmentEvent = equipmentEvents.find(event => + event.entity_id === equipment.id || event.event_metadata?.equipment_id === equipment.id ); @@ -143,7 +134,7 @@ const ProductionTab: React.FC = ({ tenantId }) => { return equipment; }); }); - }, [sseEvents]); + }, [sseEvents, equipmentData]); return (
@@ -170,97 +161,124 @@ const ProductionTab: React.FC = ({ tenantId }) => { {t('production.equipment_status')} -
- {equipmentData.map((equipment) => { - // Determine status configuration - const getStatusConfig = () => { - switch (equipment.status) { - case 'critical': - return { - color: '#ef4444', // red-500 - text: t('production.status_critical'), - icon: AlertTriangle, - isCritical: true - }; - case 'warning': - return { - color: '#f59e0b', // amber-500 - text: t('production.status_warning'), - icon: AlertTriangle, - isHighlight: true - }; - default: - return { - color: '#10b981', // emerald-500 - text: t('production.status_normal'), - icon: CheckCircle2 - }; + {equipmentLoading ? ( +
+ {[...Array(4)].map((_, index) => ( + + +
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : ( +
+ {equipmentData.map((equipment) => { + // Determine status configuration + const getStatusConfig = () => { + switch (equipment.status) { + case 'critical': + return { + color: '#ef4444', // red-500 + text: t('production.status_critical'), + icon: AlertTriangle, + isCritical: true + }; + case 'warning': + return { + color: '#f59e0b', // amber-500 + text: t('production.status_warning'), + icon: AlertTriangle, + isHighlight: true + }; + default: + return { + color: '#10b981', // emerald-500 + text: t('production.status_normal'), + icon: CheckCircle2 + }; + } + }; + + const statusConfig = getStatusConfig(); + + // Add real-time event indicator if there's a recent event + const eventMetadata = []; + if (equipment.lastEvent) { + const eventTime = new Date(equipment.lastEvent.timestamp); + eventMetadata.push(`🔔 ${equipment.lastEvent.type.replace(/_/g, ' ')} - ${eventTime.toLocaleTimeString()}`); + if (equipment.lastEvent.message) { + eventMetadata.push(`${t('production.event_message')}: ${equipment.lastEvent.message}`); + } } - }; - const statusConfig = getStatusConfig(); - - // Add real-time event indicator if there's a recent event - const eventMetadata = []; - if (equipment.lastEvent) { - const eventTime = new Date(equipment.lastEvent.timestamp); - eventMetadata.push(`🔔 ${equipment.lastEvent.type.replace(/_/g, ' ')} - ${eventTime.toLocaleTimeString()}`); - if (equipment.lastEvent.message) { - eventMetadata.push(`${t('production.event_message')}: ${equipment.lastEvent.message}`); + // Add SSE connection status to first card + const additionalMetadata = []; + if (equipment.id === equipmentData[0]?.id) { + additionalMetadata.push( + sseConnected + ? `🟢 ${t('enterprise.live_updates')}` + : `🟡 ${t('enterprise.offline')}` + ); } - } - // Add SSE connection status to first card - const additionalMetadata = []; - if (equipment.id === 'oven-1') { - additionalMetadata.push( - sseConnected - ? `🟢 ${t('enterprise.live_updates')}` - : `🟡 ${t('enterprise.offline')}` + return ( + { + // In Phase 2, this will navigate to equipment detail page + console.log(`View details for ${equipment.name}`); + }, + priority: 'primary' + } + ]} + onClick={() => { + // In Phase 2, this will navigate to equipment detail page + console.log(`Clicked ${equipment.name}`); + }} + /> ); - } - - return ( - { - // In Phase 2, this will navigate to equipment detail page - console.log(`View details for ${equipment.name}`); - }, - priority: 'primary' - } - ]} - onClick={() => { - // In Phase 2, this will navigate to equipment detail page - console.log(`Clicked ${equipment.name}`); - }} - /> - ); - })} -
+ })} + {equipmentData.length === 0 && !equipmentLoading && ( +
+
+ +

{t('production.no_equipment')}

+
+
+ )} +
+ )}
{/* Production Efficiency Metrics */} diff --git a/frontend/src/locales/en/dashboard.json b/frontend/src/locales/en/dashboard.json index 6b20aa46..2a7b1495 100644 --- a/frontend/src/locales/en/dashboard.json +++ b/frontend/src/locales/en/dashboard.json @@ -83,6 +83,7 @@ "status_critical": "Critical", "status_warning": "Warning", "status_normal": "Normal", + "no_equipment": "No equipment available", "efficiency_metrics": "Production Efficiency Metrics", "on_time_start_rate": "On-time Start Rate", "batches_started_on_time": "Batches started on time", @@ -562,7 +563,29 @@ "view_vehicles": "View Vehicles", "live_tracking": "Live GPS Tracking", "real_time_gps_tracking": "Real-time GPS tracking of all vehicles", - "open_tracking_map": "Open Tracking Map" + "open_tracking_map": "Open Tracking Map", + "no_outlets": "No outlets available", + "summary_view": "Summary View", + "detailed_view": "Detailed View", + "product_level_inventory": "Product-Level Inventory", + "back_to_summary": "Back to Summary", + "current_stock": "Current Stock", + "safety_stock": "Safety Stock", + "coverage_of_safety": "Coverage of Safety", + "risk_level": "Risk Level", + "stock_above_safety": "Stock Above Safety", + "yes": "Yes", + "no": "No", + "transfer_stock": "Transfer Stock", + "critical_outlets": "Critical Outlets", + "critical_outlets_description": "There are {count} outlets with critical inventory issues", + "prioritize_transfers": "Prioritize Transfers", + "low_coverage_recommendation": "Inventory coverage is low. Consider increasing stock levels.", + "good_coverage_recommendation": "Good inventory coverage - maintain current levels", + "fulfillment_excellence": "Fulfillment Excellence", + "high_fulfillment_congrats": "Congratulations! Your network has a {rate}% fulfillment rate", + "maintain_excellence": "Maintain Excellence", + "all_outlets_healthy": "All outlets are operating normally" }, "ai_insights": { "title": "AI Insights", diff --git a/frontend/src/locales/es/dashboard.json b/frontend/src/locales/es/dashboard.json index b930366f..0fc23dda 100644 --- a/frontend/src/locales/es/dashboard.json +++ b/frontend/src/locales/es/dashboard.json @@ -83,6 +83,7 @@ "status_critical": "Crítico", "status_warning": "Advertencia", "status_normal": "Normal", + "no_equipment": "No hay equipos disponibles", "efficiency_metrics": "Métricas de Eficiencia de Producción", "on_time_start_rate": "Tasa de Inicio a Tiempo", "batches_started_on_time": "Lotes iniciados a tiempo", @@ -633,7 +634,29 @@ "view_vehicles": "Ver Vehículos", "live_tracking": "Seguimiento GPS en Vivo", "real_time_gps_tracking": "Seguimiento GPS en tiempo real de todos los vehículos", - "open_tracking_map": "Abrir Mapa de Seguimiento" + "open_tracking_map": "Abrir Mapa de Seguimiento", + "no_outlets": "No hay tiendas disponibles", + "summary_view": "Vista de Resumen", + "detailed_view": "Vista Detallada", + "product_level_inventory": "Inventario a Nivel de Producto", + "back_to_summary": "Volver al Resumen", + "current_stock": "Stock Actual", + "safety_stock": "Stock de Seguridad", + "coverage_of_safety": "Cobertura del Stock de Seguridad", + "risk_level": "Nivel de Riesgo", + "stock_above_safety": "Stock por Encima del de Seguridad", + "yes": "Sí", + "no": "No", + "transfer_stock": "Transferir Stock", + "critical_outlets": "Tiendas Críticas", + "critical_outlets_description": "Hay {count} tiendas con problemas críticos de inventario", + "prioritize_transfers": "Priorizar Transferencias", + "low_coverage_recommendation": "La cobertura de inventario es baja. Considere aumentar los niveles de stock.", + "good_coverage_recommendation": "Buena cobertura de inventario - mantenga los niveles actuales", + "fulfillment_excellence": "Excelencia en Cumplimiento", + "high_fulfillment_congrats": "¡Felicitaciones! Su red tiene una tasa de cumplimiento del {rate}%", + "maintain_excellence": "Mantener Excelencia", + "all_outlets_healthy": "Todas las tiendas operan con normalidad" }, "ai_insights": { "title": "Insights de IA", diff --git a/frontend/src/locales/eu/dashboard.json b/frontend/src/locales/eu/dashboard.json index d2f41204..b4206dbb 100644 --- a/frontend/src/locales/eu/dashboard.json +++ b/frontend/src/locales/eu/dashboard.json @@ -134,7 +134,30 @@ "completed": "OSATUTA", "in_progress": "MARTXAN", "pending": "ITXAROTEAN" - } + }, + "status_critical": "Kritikoa", + "status_warning": "Abisua", + "status_normal": "Normala", + "no_equipment": "Ez dago ekipamendurik erabilgarri", + "efficiency_metrics": "Ekoizpen Eraginkortasunaren Metrikak", + "on_time_start_rate": "Denboraren Arabera Hasieraren Tasa", + "batches_started_on_time": "Denboraren arabera hasitako sortak", + "efficiency_rate": "Eraginkortasun Tasa", + "overall_efficiency": "Ekoizpen eraginkortasun orokorra", + "active_alerts": "Alerta Aktiboak", + "issues_require_attention": "Arreta behar duten arazoak", + "ai_prevented": "ADk Saihestutako Arazoak", + "problems_prevented": "ADk saihestutako arazoak", + "quick_actions": "Ekintza Azkarrak", + "create_batch": "Sortu Ekoizpen Sorta", + "create_batch_description": "Sortu ekoizpen sorta berria sarearentzat", + "maintenance": "Mantentzea", + "schedule_maintenance": "Programatu mantentzea ekoizpen ekipamendurako", + "manage_equipment": "Kudeatu Ekipamendua", + "quality_checks": "Kalitate Egiaztapenak", + "manage_quality": "Kudeatu kalitate kontrol prozesuak", + "quality_management": "Kalitate Kudeaketa", + "event_message": "Mezua" }, "messages": { "welcome": "Ongi etorri berriro", @@ -390,7 +413,29 @@ "in_transit": "Bidaiatzen", "delivered": "Entregatua", "failed": "Huts egin du", - "distribution_routes": "Banaketa Ibilbideak" + "distribution_routes": "Banaketa Ibilbideak", + "no_outlets": "Ez dago dendarik erabilgarri", + "summary_view": "Laburpen Ikuspegia", + "detailed_view": "Ikuspegi Xehatua", + "product_level_inventory": "Produktu Mailako Inbentarioa", + "back_to_summary": "Bueltatu Laburpena-ra", + "current_stock": "Uneko Stock-a", + "safety_stock": "Segurtasun Stock-a", + "coverage_of_safety": "Segurtasun Stockaren Estaldura", + "risk_level": "Arrisku Maila", + "stock_above_safety": "Segurtasunaren Gainetik Stock-a", + "yes": "Bai", + "no": "Ez", + "transfer_stock": "Stock-a Transferitu", + "critical_outlets": "Denda Kritikoak", + "critical_outlets_description": "{count} denda daude inbentario arazo kritikoekin", + "prioritize_transfers": "Transferentziak Lehentasun", + "low_coverage_recommendation": "Inbentario estaldura baxua da. Kontutan izan stock mailak handitzea.", + "good_coverage_recommendation": "Inbentario estaldura ona - mantendu uneko mailak", + "fulfillment_excellence": "Betetze Bikaintasuna", + "high_fulfillment_congrats": "Zorionak! Zure sareak %{rate}ko betetze tasa du", + "maintain_excellence": "Mantendu Bikaintasuna", + "all_outlets_healthy": "Denda guztiak arrunta lanean" }, "new_dashboard": { "system_status": {