/* * Enterprise Dashboard Page * Main dashboard for enterprise parent tenants showing network-wide metrics */ import React, { useState, useEffect, useRef } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useNetworkSummary, useChildrenPerformance, useDistributionOverview, useForecastSummary } from '../../api/hooks/useEnterpriseDashboard'; import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card'; import { Button } from '../../components/ui/Button'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/Tabs'; import { TrendingUp, MapPin, Truck, Package, BarChart3, Network, Activity, Calendar, Clock, AlertTriangle, PackageCheck, Building2, ArrowLeft, ChevronRight, Target, Warehouse, ShoppingCart, ShieldCheck } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { LoadingSpinner } from '../../components/ui/LoadingSpinner'; import { ErrorBoundary } from 'react-error-boundary'; import { apiClient } from '../../api/client/apiClient'; import { useEnterprise } from '../../contexts/EnterpriseContext'; import { useTenant } from '../../stores/tenant.store'; import { useSSEEvents } from '../../hooks/useSSE'; import { useQueryClient } from '@tanstack/react-query'; // Components for enterprise dashboard const NetworkSummaryCards = React.lazy(() => import('../../components/dashboard/NetworkSummaryCards')); const DistributionMap = React.lazy(() => import('../../components/maps/DistributionMap')); const PerformanceChart = React.lazy(() => import('../../components/charts/PerformanceChart')); const NetworkOverviewTab = React.lazy(() => import('../../components/dashboard/NetworkOverviewTab')); const NetworkPerformanceTab = React.lazy(() => import('../../components/dashboard/NetworkPerformanceTab')); const OutletFulfillmentTab = React.lazy(() => import('../../components/dashboard/OutletFulfillmentTab')); const ProductionTab = React.lazy(() => import('../../components/dashboard/ProductionTab')); const DistributionTab = React.lazy(() => import('../../components/dashboard/DistributionTab')); interface EnterpriseDashboardPageProps { tenantId?: string; } const EnterpriseDashboardPage: React.FC = ({ tenantId: propTenantId }) => { const { tenantId: urlTenantId } = useParams<{ tenantId: string }>(); const tenantId = propTenantId || urlTenantId; const navigate = useNavigate(); const { t } = useTranslation('dashboard'); const { state: enterpriseState, drillDownToOutlet, returnToNetworkView, enterNetworkView } = useEnterprise(); const { switchTenant } = useTenant(); const [selectedMetric, setSelectedMetric] = useState('sales'); const [selectedPeriod, setSelectedPeriod] = useState(30); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const [activeTab, setActiveTab] = useState('overview'); const queryClient = useQueryClient(); // SSE Integration for real-time updates const { events: sseEvents } = useSSEEvents({ channels: ['*.alerts', '*.notifications', 'recommendations'] }); // Refs for debouncing SSE-triggered invalidations const invalidationTimeoutRef = useRef(null); const lastEventCountRef = useRef(0); // Invalidate enterprise data on relevant SSE events (debounced) useEffect(() => { // Skip if no new events since last check if (sseEvents.length === 0 || !tenantId || sseEvents.length === lastEventCountRef.current) { return; } const relevantEventTypes = [ 'batch_completed', 'batch_started', 'batch_state_changed', 'delivery_received', 'delivery_overdue', 'delivery_arriving_soon', 'stock_receipt_incomplete', 'orchestration_run_completed', 'production_delay', 'batch_start_delayed', 'equipment_maintenance', 'network_alert', 'outlet_performance_update', 'distribution_route_update' ]; // Check if any event is relevant const hasRelevantEvent = sseEvents.some(event => relevantEventTypes.includes(event.event_type) ); if (hasRelevantEvent) { // Clear existing timeout to debounce rapid events if (invalidationTimeoutRef.current) { clearTimeout(invalidationTimeoutRef.current); } // Debounce the invalidation to prevent multiple rapid refetches invalidationTimeoutRef.current = setTimeout(() => { lastEventCountRef.current = sseEvents.length; // Invalidate all enterprise dashboard queries in a single batch queryClient.invalidateQueries({ queryKey: ['enterprise', 'network-summary', tenantId], refetchType: 'active', }); queryClient.invalidateQueries({ queryKey: ['enterprise', 'children-performance', tenantId], refetchType: 'active', }); queryClient.invalidateQueries({ queryKey: ['enterprise', 'distribution-overview', tenantId], refetchType: 'active', }); queryClient.invalidateQueries({ queryKey: ['enterprise', 'forecast-summary', tenantId], refetchType: 'active', }); // Note: control-panel-data has its own debounced invalidation in useControlPanelData }, 500); // 500ms debounce } // Cleanup timeout on unmount or dependency change return () => { if (invalidationTimeoutRef.current) { clearTimeout(invalidationTimeoutRef.current); } }; }, [sseEvents, tenantId, queryClient]); // Check if tenantId is available at the start useEffect(() => { if (!tenantId) { console.error('No tenant ID available for enterprise dashboard'); navigate('/unauthorized'); } }, [tenantId, navigate]); // Initialize enterprise mode on mount useEffect(() => { if (tenantId && !enterpriseState.parentTenantId) { enterNetworkView(tenantId); } }, [tenantId, enterpriseState.parentTenantId, enterNetworkView]); // Check if user has enterprise tier access useEffect(() => { const checkAccess = async () => { if (!tenantId) { console.error('No tenant ID available for enterprise dashboard'); navigate('/unauthorized'); return; } try { const response = await apiClient.get<{ tenant_type: string }>(`/tenants/${tenantId}`); if (response.tenant_type !== 'parent') { navigate('/unauthorized'); } } catch (error) { console.error('Access check failed:', error); navigate('/unauthorized'); } }; checkAccess(); }, [tenantId, navigate]); // Fetch network summary data const { data: networkSummary, isLoading: isNetworkSummaryLoading, error: networkSummaryError } = useNetworkSummary(tenantId!, { refetchInterval: 60000, // Refetch every minute enabled: !!tenantId, // Only fetch if tenantId is available }); // Fetch children performance data const { data: childrenPerformance, isLoading: isChildrenPerformanceLoading, error: childrenPerformanceError } = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod, { enabled: !!tenantId, // Only fetch if tenantId is available }); // Fetch distribution overview data const { data: distributionOverview, isLoading: isDistributionLoading, error: distributionError } = useDistributionOverview(tenantId!, selectedDate, { refetchInterval: 60000, // Refetch every minute enabled: !!tenantId, // Only fetch if tenantId is available }); // Fetch enterprise forecast summary const { data: forecastSummary, isLoading: isForecastLoading, error: forecastError } = useForecastSummary(tenantId!, 7, { enabled: !!tenantId, // Only fetch if tenantId is available }); // Handle outlet drill-down const handleOutletClick = async (outletId: string, outletName: string) => { // Calculate network metrics if available const networkMetrics = childrenPerformance?.rankings ? { totalSales: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'sales' ? r.metric_value : 0), 0), totalProduction: 0, totalInventoryValue: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'inventory_value' ? r.metric_value : 0), 0), averageSales: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'sales' ? r.metric_value : 0), 0) / childrenPerformance.rankings.length, averageProduction: 0, averageInventoryValue: childrenPerformance.rankings.reduce((sum, r) => sum + (selectedMetric === 'inventory_value' ? r.metric_value : 0), 0) / childrenPerformance.rankings.length, childCount: childrenPerformance.rankings.length } : undefined; drillDownToOutlet(outletId, outletName, networkMetrics); await switchTenant(outletId); navigate('/app/dashboard'); }; // Handle return to network view const handleReturnToNetwork = async () => { if (enterpriseState.parentTenantId) { returnToNetworkView(); await switchTenant(enterpriseState.parentTenantId); navigate(`/app/enterprise/${enterpriseState.parentTenantId}`); } }; // Error boundary fallback const ErrorFallback = ({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) => (

Something went wrong

{error.message}

); if (isNetworkSummaryLoading || isChildrenPerformanceLoading || isDistributionLoading || isForecastLoading) { return (
); } if (networkSummaryError || childrenPerformanceError || distributionError || forecastError) { return (

Error Loading Dashboard

{networkSummaryError?.message || childrenPerformanceError?.message || distributionError?.message || forecastError?.message}

); } return (
{/* Breadcrumb / Return to Network Banner */} {enterpriseState.selectedOutletId && !enterpriseState.isNetworkView && (
Network Overview {enterpriseState.selectedOutletName}
{enterpriseState.networkMetrics && (
Network Average Sales: €{enterpriseState.networkMetrics.averageSales.toLocaleString()}
Total Outlets: {enterpriseState.networkMetrics.childCount}
Network Total: €{enterpriseState.networkMetrics.totalSales.toLocaleString()}
)}
)} {/* Enhanced Header */}
{/* Title Section with Gradient Icon */}

{t('enterprise.network_dashboard')}

{t('enterprise.network_summary_description')}

{/* Main Tabs Structure */} {t('enterprise.network_status')} {t('enterprise.network_performance')} {t('enterprise.outlet_fulfillment')} {t('enterprise.distribution_map')} {t('enterprise.network_forecast')} {t('enterprise.production')} {/* Tab Content */} {/* Forecast Summary */}
{t('enterprise.network_forecast')} {forecastSummary && forecastSummary.aggregated_forecasts ? (
{/* Total Demand Card */}

{t('enterprise.total_demand')}

{Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => total + Object.values(day).reduce((dayTotal: number, product: any) => dayTotal + (product.predicted_demand || 0), 0), 0 ).toLocaleString()}

{/* Days Forecast Card */}

{t('enterprise.days_forecast')}

{forecastSummary.days_forecast || 7}

{/* Average Daily Demand Card */}

{t('enterprise.avg_daily_demand')}

{forecastSummary.aggregated_forecasts ? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) => total + Object.values(day).reduce((dayTotal: number, product: any) => dayTotal + (product.predicted_demand || 0), 0), 0) / Object.keys(forecastSummary.aggregated_forecasts).length ).toLocaleString() : 0}

{/* Last Updated Card */}

{t('enterprise.last_updated')}

{forecastSummary.last_updated ? new Date(forecastSummary.last_updated).toLocaleTimeString() : 'N/A'}

) : (
{t('enterprise.no_forecast_data')}
)}
); }; export default EnterpriseDashboardPage;