340 lines
14 KiB
TypeScript
340 lines
14 KiB
TypeScript
/*
|
|
* Network Overview Tab Component for Enterprise Dashboard
|
|
* Shows network-wide status and critical alerts
|
|
*/
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
|
|
import { Button } from '../ui/Button';
|
|
import { Network, AlertTriangle, CheckCircle2, Activity, TrendingUp, Bell, Clock } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import SystemStatusBlock from './blocks/SystemStatusBlock';
|
|
import NetworkSummaryCards from './NetworkSummaryCards';
|
|
import { useControlPanelData } from '../../api/hooks/useControlPanelData';
|
|
import { useNetworkSummary } from '../../api/hooks/useEnterpriseDashboard';
|
|
import { useSSEEvents } from '../../hooks/useSSE';
|
|
|
|
interface NetworkOverviewTabProps {
|
|
tenantId: string;
|
|
onOutletClick?: (outletId: string, outletName: string) => void;
|
|
}
|
|
|
|
const NetworkOverviewTab: React.FC<NetworkOverviewTabProps> = ({ tenantId, onOutletClick }) => {
|
|
const { t } = useTranslation('dashboard');
|
|
|
|
// Get network-wide control panel data (for system status)
|
|
const { data: controlPanelData, isLoading: isControlPanelLoading } = useControlPanelData(tenantId);
|
|
|
|
// Get network summary data
|
|
const { data: networkSummary, isLoading: isNetworkSummaryLoading } = useNetworkSummary(tenantId);
|
|
|
|
// Real-time SSE events
|
|
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
|
|
channels: ['*.alerts', '*.notifications', 'recommendations']
|
|
});
|
|
|
|
// State for real-time notifications
|
|
const [recentEvents, setRecentEvents] = useState<any[]>([]);
|
|
const [showAllEvents, setShowAllEvents] = useState(false);
|
|
|
|
// Process SSE events for real-time notifications
|
|
useEffect(() => {
|
|
if (sseEvents.length === 0) return;
|
|
|
|
// Filter relevant events for network overview
|
|
const relevantEventTypes = [
|
|
'network_alert', 'outlet_performance_update', 'distribution_route_update',
|
|
'batch_completed', 'batch_started', 'delivery_received', 'delivery_overdue',
|
|
'equipment_maintenance', 'production_delay', 'stock_receipt_incomplete'
|
|
];
|
|
|
|
const networkEvents = sseEvents.filter(event =>
|
|
relevantEventTypes.includes(event.event_type)
|
|
);
|
|
|
|
// Keep only the 5 most recent events
|
|
setRecentEvents(networkEvents.slice(0, 5));
|
|
}, [sseEvents]);
|
|
|
|
const isLoading = isControlPanelLoading || isNetworkSummaryLoading;
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Network Status Block - Reusing SystemStatusBlock with network-wide data */}
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<Network className="w-6 h-6 text-[var(--color-primary)]" />
|
|
{t('enterprise.network_status')}
|
|
</h2>
|
|
<SystemStatusBlock
|
|
data={controlPanelData}
|
|
loading={isControlPanelLoading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Network Summary Cards */}
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<Activity className="w-6 h-6 text-[var(--color-success)]" />
|
|
{t('enterprise.network_summary')}
|
|
</h2>
|
|
<NetworkSummaryCards
|
|
data={networkSummary}
|
|
isLoading={isNetworkSummaryLoading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Quick Actions */}
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<TrendingUp className="w-6 h-6 text-[var(--color-info)]" />
|
|
{t('enterprise.quick_actions')}
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<Network className="w-6 h-6 text-[var(--color-primary)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.add_outlet')}</h3>
|
|
</div>
|
|
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.add_outlet_description')}</p>
|
|
<Button
|
|
onClick={() => window.location.href = `/app/tenants/${tenantId}/settings/organization`}
|
|
className="w-full"
|
|
>
|
|
{t('enterprise.create_outlet')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<CheckCircle2 className="w-6 h-6 text-[var(--color-success)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.internal_transfers')}</h3>
|
|
</div>
|
|
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.manage_transfers')}</p>
|
|
<Button
|
|
onClick={() => window.location.href = `/app/tenants/${tenantId}/procurement/internal-transfers`}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
{t('enterprise.view_transfers')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<AlertTriangle className="w-6 h-6 text-[var(--color-warning)]" />
|
|
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.view_alerts')}</h3>
|
|
</div>
|
|
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.network_alerts_description')}</p>
|
|
<Button
|
|
onClick={() => window.location.href = `/app/tenants/${tenantId}/alerts`}
|
|
variant="outline"
|
|
className="w-full"
|
|
>
|
|
{t('enterprise.view_all_alerts')}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Network Health Indicators */}
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<CheckCircle2 className="w-6 h-6 text-[var(--color-success)]" />
|
|
{t('enterprise.network_health')}
|
|
</h2>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{/* On-time Delivery Rate */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
|
|
{t('enterprise.on_time_delivery')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-[var(--color-success)]">
|
|
{controlPanelData?.orchestrationSummary?.aiHandlingRate || 0}%
|
|
</div>
|
|
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
{t('enterprise.delivery_performance')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Issue Prevention Rate */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
|
|
{t('enterprise.issue_prevention')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-[var(--color-primary)]">
|
|
{controlPanelData?.issuesPreventedByAI || 0}
|
|
</div>
|
|
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
{t('enterprise.issues_prevented')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Active Issues */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
|
|
{t('enterprise.active_issues')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-[var(--color-warning)]">
|
|
{controlPanelData?.issuesRequiringAction || 0}
|
|
</div>
|
|
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
{t('enterprise.action_required')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Network Efficiency */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
|
|
{t('enterprise.network_efficiency')}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-3xl font-bold text-[var(--color-info)]">
|
|
{Math.round((controlPanelData?.issuesPreventedByAI || 0) /
|
|
Math.max(1, (controlPanelData?.issuesPreventedByAI || 0) + (controlPanelData?.issuesRequiringAction || 0)) * 100) || 0}%
|
|
</div>
|
|
<p className="text-xs text-[var(--text-secondary)] mt-1">
|
|
{t('enterprise.operational_efficiency')}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Real-time Events Notification */}
|
|
<div className="mb-8">
|
|
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
|
|
<Bell className="w-6 h-6 text-[var(--color-info)]" />
|
|
{t('enterprise.real_time_events')}
|
|
</h2>
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
|
|
{t('enterprise.recent_activity')}
|
|
</CardTitle>
|
|
{sseConnected ? (
|
|
<div className="flex items-center gap-1 text-xs text-[var(--color-success)]">
|
|
<span className="w-2 h-2 rounded-full bg-[var(--color-success)] animate-pulse"></span>
|
|
{t('enterprise.live_updates')}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center gap-1 text-xs text-[var(--color-warning)]">
|
|
<span className="w-2 h-2 rounded-full bg-[var(--color-warning)]"></span>
|
|
{t('enterprise.offline')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{recentEvents.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{recentEvents.slice(0, showAllEvents ? recentEvents.length : 3).map((event, index) => {
|
|
// Determine event icon and color based on type
|
|
const getEventConfig = () => {
|
|
switch (event.event_type) {
|
|
case 'network_alert':
|
|
case 'production_delay':
|
|
case 'equipment_maintenance':
|
|
return { icon: AlertTriangle, color: 'text-[var(--color-warning)]' };
|
|
case 'batch_completed':
|
|
case 'delivery_received':
|
|
return { icon: CheckCircle2, color: 'text-[var(--color-success)]' };
|
|
case 'batch_started':
|
|
case 'outlet_performance_update':
|
|
return { icon: Activity, color: 'text-[var(--color-info)]' };
|
|
case 'delivery_overdue':
|
|
case 'stock_receipt_incomplete':
|
|
return { icon: Clock, color: 'text-[var(--color-danger)]' };
|
|
default:
|
|
return { icon: Bell, color: 'text-[var(--color-primary)]' };
|
|
}
|
|
};
|
|
|
|
const { icon: EventIcon, color } = getEventConfig();
|
|
const eventTime = new Date(event.timestamp || event.created_at || Date.now());
|
|
|
|
return (
|
|
<div key={index} className="flex items-start gap-3 p-3 rounded-lg border border-[var(--border-primary)]">
|
|
<div className={`p-2 rounded-lg ${color.replace('text', 'bg')}`}>
|
|
<EventIcon className={`w-5 h-5 ${color}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<p className="font-medium text-[var(--text-primary)]">
|
|
{event.event_type.replace(/_/g, ' ')}
|
|
</p>
|
|
<p className="text-xs text-[var(--text-tertiary)] whitespace-nowrap">
|
|
{eventTime.toLocaleTimeString()}
|
|
</p>
|
|
</div>
|
|
{event.message && (
|
|
<p className="text-sm text-[var(--text-secondary)] mt-1">
|
|
{event.message}
|
|
</p>
|
|
)}
|
|
{event.entity_type && event.entity_id && (
|
|
<p className="text-xs text-[var(--text-tertiary)] mt-1">
|
|
{event.entity_type}: {event.entity_id}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
|
|
{recentEvents.length > 3 && !showAllEvents && (
|
|
<Button
|
|
onClick={() => setShowAllEvents(true)}
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full mt-2"
|
|
>
|
|
{t('enterprise.show_all_events', { count: recentEvents.length })}
|
|
</Button>
|
|
)}
|
|
|
|
{showAllEvents && recentEvents.length > 3 && (
|
|
<Button
|
|
onClick={() => setShowAllEvents(false)}
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full mt-2"
|
|
>
|
|
{t('enterprise.show_less')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8 text-[var(--text-secondary)]">
|
|
{sseConnected ? t('enterprise.no_recent_activity') : t('enterprise.waiting_for_updates')}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default NetworkOverviewTab; |