Imporve enterprise

This commit is contained in:
Urtzi Alfaro
2025-12-17 20:50:22 +01:00
parent e3ef47b879
commit f8591639a7
28 changed files with 6802 additions and 258 deletions

View File

@@ -0,0 +1,582 @@
/*
* Distribution Tab Component for Enterprise Dashboard
* Shows network-wide distribution status, route optimization, and delivery monitoring
*/
import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Button } from '../ui/Button';
import { Truck, AlertTriangle, CheckCircle2, Activity, Timer, Map, Route, Package, Clock, Bell, Calendar } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useDistributionOverview } from '../../api/hooks/useEnterpriseDashboard';
import { useSSEEvents } from '../../hooks/useSSE';
import StatusCard from '../ui/StatusCard/StatusCard';
interface DistributionTabProps {
tenantId: string;
selectedDate: string;
onDateChange: (date: string) => void;
}
const DistributionTab: React.FC<DistributionTabProps> = ({ tenantId, selectedDate, onDateChange }) => {
const { t } = useTranslation('dashboard');
// Get distribution data
const {
data: distributionOverview,
isLoading: isDistributionLoading,
error: distributionError
} = useDistributionOverview(tenantId, selectedDate, {
refetchInterval: 60000, // Refetch every minute
enabled: !!tenantId,
});
// Real-time SSE events
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
channels: ['*.alerts', '*.notifications', 'recommendations']
});
// State for real-time delivery status
const [deliveryStatus, setDeliveryStatus] = useState({
total: 0,
onTime: 0,
delayed: 0,
inTransit: 0,
completed: 0
});
// State for route optimization metrics
const [optimizationMetrics, setOptimizationMetrics] = useState({
distanceSaved: 0,
timeSaved: 0,
fuelSaved: 0,
co2Saved: 0
});
// State for real-time events
const [recentDeliveryEvents, setRecentDeliveryEvents] = useState<any[]>([]);
// Process SSE events for distribution updates
useEffect(() => {
if (sseEvents.length === 0) return;
// Filter delivery and distribution-related events
const deliveryEvents = sseEvents.filter(event =>
event.event_type.includes('delivery_') ||
event.event_type.includes('route_') ||
event.event_type.includes('shipment_') ||
event.entity_type === 'delivery' ||
event.entity_type === 'shipment'
);
if (deliveryEvents.length === 0) return;
// Update delivery status based on events
let newStatus = { ...deliveryStatus };
let newMetrics = { ...optimizationMetrics };
deliveryEvents.forEach(event => {
switch (event.event_type) {
case 'delivery_completed':
newStatus.completed += 1;
newStatus.inTransit = Math.max(0, newStatus.inTransit - 1);
break;
case 'delivery_started':
case 'delivery_in_transit':
newStatus.inTransit += 1;
break;
case 'delivery_delayed':
newStatus.delayed += 1;
break;
case 'route_optimized':
if (event.event_metadata?.distance_saved) {
newMetrics.distanceSaved += event.event_metadata.distance_saved;
}
if (event.event_metadata?.time_saved) {
newMetrics.timeSaved += event.event_metadata.time_saved;
}
if (event.event_metadata?.fuel_saved) {
newMetrics.fuelSaved += event.event_metadata.fuel_saved;
}
break;
}
});
setDeliveryStatus(newStatus);
setOptimizationMetrics(newMetrics);
setRecentDeliveryEvents(deliveryEvents.slice(0, 5));
}, [sseEvents]);
// Initialize status from API data
useEffect(() => {
if (distributionOverview) {
const statusCounts = distributionOverview.status_counts || {};
setDeliveryStatus({
total: Object.values(statusCounts).reduce((sum, count) => sum + count, 0),
onTime: statusCounts['delivered'] || 0,
delayed: statusCounts['overdue'] || 0,
inTransit: (statusCounts['in_transit'] || 0) + (statusCounts['pending'] || 0),
completed: statusCounts['delivered'] || 0
});
}
}, [distributionOverview]);
const isLoading = isDistributionLoading;
// Mock route data - in Phase 2 this will come from real API
const mockRoutes = [
{
id: 'route-1',
name: 'Madrid → Barcelona',
status: 'in_transit',
distance: '620 km',
duration: '6h 30m',
stops: 3,
optimizationSavings: '12 km (1.9%)',
vehicles: ['TRUCK-001', 'TRUCK-002']
},
{
id: 'route-2',
name: 'Barcelona → Valencia',
status: 'completed',
distance: '350 km',
duration: '4h 15m',
stops: 2,
optimizationSavings: '8 km (2.3%)',
vehicles: ['VAN-005']
},
{
id: 'route-3',
name: 'Central → Outlets (Daily)',
status: 'pending',
distance: '180 km',
duration: '3h 00m',
stops: 5,
optimizationSavings: '25 km (13.9%)',
vehicles: ['TRUCK-003', 'VAN-006', 'VAN-007']
}
];
return (
<div className="space-y-8">
{/* Distribution Summary */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Truck className="w-6 h-6 text-[var(--color-primary)]" />
{t('enterprise.distribution_summary')}
</h2>
{/* Date selector */}
<div className="mb-4 flex items-center gap-4">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-[var(--text-secondary)]" />
<input
type="date"
value={selectedDate}
onChange={(e) => onDateChange(e.target.value)}
className="border border-[var(--border-primary)] rounded-md px-3 py-2 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
/>
</div>
{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>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Total Deliveries */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.total_deliveries')}
</CardTitle>
<Package className="w-5 h-5 text-[var(--color-primary)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--text-primary)]">
{deliveryStatus.total}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.all_shipments')}
</p>
</CardContent>
</Card>
{/* On-time Deliveries */}
<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_deliveries')}
</CardTitle>
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{deliveryStatus.onTime}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{deliveryStatus.total > 0
? `${Math.round((deliveryStatus.onTime / deliveryStatus.total) * 100)}% ${t('enterprise.on_time_rate')}`
: t('enterprise.no_deliveries')}
</p>
</CardContent>
</Card>
{/* Delayed Deliveries */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.delayed_deliveries')}
</CardTitle>
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-warning)]">
{deliveryStatus.delayed}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{deliveryStatus.total > 0
? `${Math.round((deliveryStatus.delayed / deliveryStatus.total) * 100)}% ${t('enterprise.delay_rate')}`
: t('enterprise.no_delays')}
</p>
</CardContent>
</Card>
{/* In Transit */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.in_transit')}
</CardTitle>
<Activity className="w-5 h-5 text-[var(--color-info)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-info)]">
{deliveryStatus.inTransit}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.currently_en_route')}
</p>
</CardContent>
</Card>
</div>
</div>
{/* Route Optimization Metrics */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Route className="w-6 h-6 text-[var(--color-success)]" />
{t('enterprise.route_optimization')}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Distance Saved */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.distance_saved')}
</CardTitle>
<Map className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{optimizationMetrics.distanceSaved} km
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.total_distance_saved')}
</p>
</CardContent>
</Card>
{/* Time Saved */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.time_saved')}
</CardTitle>
<Timer className="w-5 h-5 text-[var(--color-primary)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-primary)]">
{optimizationMetrics.timeSaved} min
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.total_time_saved')}
</p>
</CardContent>
</Card>
{/* Fuel Saved */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.fuel_saved')}
</CardTitle>
<Package className="w-5 h-5 text-[var(--color-info)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-info)]">
{optimizationMetrics.fuelSaved.toFixed(2)}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.estimated_fuel_savings')}
</p>
</CardContent>
</Card>
{/* CO2 Saved */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.co2_saved')}
</CardTitle>
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{optimizationMetrics.co2Saved} kg
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.estimated_co2_reduction')}
</p>
</CardContent>
</Card>
</div>
</div>
{/* Active Routes */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Route className="w-6 h-6 text-[var(--color-primary)]" />
{t('enterprise.active_routes')}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{mockRoutes.map((route) => {
// Determine status configuration
const getStatusConfig = () => {
switch (route.status) {
case 'completed':
return {
color: '#10b981', // emerald-500
text: t('enterprise.route_completed'),
icon: CheckCircle2
};
case 'delayed':
case 'overdue':
return {
color: '#ef4444', // red-500
text: t('enterprise.route_delayed'),
icon: AlertTriangle,
isCritical: true
};
case 'in_transit':
return {
color: '#3b82f6', // blue-500
text: t('enterprise.route_in_transit'),
icon: Activity,
isHighlight: true
};
default: // pending, planned
return {
color: '#f59e0b', // amber-500
text: t('enterprise.route_pending'),
icon: Clock
};
}
};
const statusConfig = getStatusConfig();
return (
<StatusCard
key={route.id}
id={route.id}
statusIndicator={statusConfig}
title={route.name}
subtitle={`${t('enterprise.distance')}: ${route.distance}`}
primaryValue={route.duration}
primaryValueLabel={t('enterprise.estimated_duration')}
secondaryInfo={{
label: t('enterprise.stops'),
value: `${route.stops}`
}}
progress={{
label: t('enterprise.optimization'),
percentage: route.status === 'completed' ? 100 :
route.status === 'in_transit' ? 75 :
route.status === 'delayed' ? 50 : 25,
color: statusConfig.color
}}
metadata={[
`${t('enterprise.optimization_savings')}: ${route.optimizationSavings}`,
`${t('enterprise.vehicles')}: ${route.vehicles.join(', ')}`
]}
actions={[
{
label: t('enterprise.track_route'),
icon: Map,
variant: 'outline',
onClick: () => {
// In Phase 2, this will navigate to route tracking page
console.log(`Track route ${route.name}`);
},
priority: 'primary'
}
]}
onClick={() => {
// In Phase 2, this will navigate to route detail page
console.log(`View route ${route.name}`);
}}
/>
);
})}
</div>
</div>
{/* Real-time Delivery Events */}
<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_delivery_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_delivery_activity')}
</CardTitle>
</div>
</CardHeader>
<CardContent>
{recentDeliveryEvents.length > 0 ? (
<div className="space-y-4">
{recentDeliveryEvents.map((event, index) => {
// Determine event icon and color based on type
const getEventConfig = () => {
switch (event.event_type) {
case 'delivery_delayed':
case 'delivery_overdue':
return { icon: AlertTriangle, color: 'text-[var(--color-warning)]' };
case 'delivery_completed':
case 'delivery_received':
return { icon: CheckCircle2, color: 'text-[var(--color-success)]' };
case 'delivery_started':
case 'delivery_in_transit':
return { icon: Activity, color: 'text-[var(--color-info)]' };
case 'route_optimized':
return { icon: Route, color: 'text-[var(--color-primary)]' };
default:
return { icon: Bell, color: 'text-[var(--color-secondary)]' };
}
};
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>
)}
{event.event_metadata?.route_name && (
<p className="text-xs text-[var(--text-tertiary)] mt-1">
{t('enterprise.route')}: {event.event_metadata.route_name}
</p>
)}
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8 text-[var(--text-secondary)]">
{sseConnected ? t('enterprise.no_recent_delivery_activity') : t('enterprise.waiting_for_updates')}
</div>
)}
</CardContent>
</Card>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Timer 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">
<Route className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.optimize_routes')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.optimize_routes_description')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/distribution/routes/optimize`}
className="w-full"
>
{t('enterprise.run_optimization')}
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Truck className="w-6 h-6 text-[var(--color-success)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.manage_vehicles')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.manage_vehicle_fleet')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/distribution/vehicles`}
variant="outline"
className="w-full"
>
{t('enterprise.view_vehicles')}
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Map className="w-6 h-6 text-[var(--color-info)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.live_tracking')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('enterprise.real_time_gps_tracking')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/distribution/tracking`}
variant="outline"
className="w-full"
>
{t('enterprise.open_tracking_map')}
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default DistributionTab;

View File

@@ -0,0 +1,340 @@
/*
* 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;

View File

@@ -0,0 +1,531 @@
/*
* Network Performance Tab Component for Enterprise Dashboard
* Shows cross-location benchmarking and performance comparison
*/
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Button } from '../ui/Button';
import { BarChart3, TrendingUp, TrendingDown, Activity, CheckCircle2, AlertTriangle, Clock, Award, Target, LineChart, PieChart, Building2 } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useChildrenPerformance } from '../../api/hooks/useEnterpriseDashboard';
import PerformanceChart from '../charts/PerformanceChart';
import StatusCard from '../ui/StatusCard/StatusCard';
interface NetworkPerformanceTabProps {
tenantId: string;
onOutletClick?: (outletId: string, outletName: string) => void;
}
const NetworkPerformanceTab: React.FC<NetworkPerformanceTabProps> = ({ tenantId, onOutletClick }) => {
const { t } = useTranslation('dashboard');
const [selectedMetric, setSelectedMetric] = useState('sales');
const [selectedPeriod, setSelectedPeriod] = useState(30);
const [viewMode, setViewMode] = useState<'chart' | 'cards'>('chart');
// Get children performance data
const {
data: childrenPerformance,
isLoading: isChildrenPerformanceLoading,
error: childrenPerformanceError
} = useChildrenPerformance(tenantId!, selectedMetric, selectedPeriod, {
enabled: !!tenantId,
});
const isLoading = isChildrenPerformanceLoading;
// Calculate network-wide metrics
const calculateNetworkMetrics = () => {
if (!childrenPerformance?.rankings || childrenPerformance.rankings.length === 0) {
return null;
}
const rankings = childrenPerformance.rankings;
// Calculate averages
const totalSales = rankings.reduce((sum, r) => sum + (selectedMetric === 'sales' ? r.metric_value : 0), 0);
const totalInventory = rankings.reduce((sum, r) => sum + (selectedMetric === 'inventory_value' ? r.metric_value : 0), 0);
const totalOrders = rankings.reduce((sum, r) => sum + (selectedMetric === 'order_frequency' ? r.metric_value : 0), 0);
const avgSales = totalSales / rankings.length;
const avgInventory = totalInventory / rankings.length;
const avgOrders = totalOrders / rankings.length;
// Find top and bottom performers
const sortedByMetric = [...rankings].sort((a, b) => b.metric_value - a.metric_value);
const topPerformer = sortedByMetric[0];
const bottomPerformer = sortedByMetric[sortedByMetric.length - 1];
// Calculate performance variance
const variance = sortedByMetric.length > 1
? Math.round(((topPerformer.metric_value - bottomPerformer.metric_value) / topPerformer.metric_value) * 100)
: 0;
return {
totalOutlets: rankings.length,
avgSales,
avgInventory,
avgOrders,
totalSales,
totalInventory,
totalOrders,
topPerformer,
bottomPerformer,
variance,
networkEfficiency: Math.min(95, 85 + (100 - variance) / 2) // Cap at 95%
};
};
const networkMetrics = calculateNetworkMetrics();
// Get performance trend indicators
const getPerformanceIndicator = (outletId: string) => {
if (!childrenPerformance?.rankings) return null;
const outlet = childrenPerformance.rankings.find(r => r.outlet_id === outletId);
if (!outlet) return null;
// Simple trend calculation based on position
const position = childrenPerformance.rankings.findIndex(r => r.outlet_id === outletId) + 1;
const total = childrenPerformance.rankings.length;
if (position <= Math.ceil(total * 0.3)) {
return { icon: TrendingUp, color: '#10b981', trend: 'improving' };
} else if (position >= Math.floor(total * 0.7)) {
return { icon: TrendingDown, color: '#ef4444', trend: 'declining' };
} else {
return { icon: Activity, color: '#f59e0b', trend: 'stable' };
}
};
return (
<div className="space-y-8">
{/* Performance Header */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<BarChart3 className="w-6 h-6 text-[var(--color-primary)]" />
{t('enterprise.network_performance')}
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{t('enterprise.performance_description')}
</p>
{/* Metric and Period Selectors */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center gap-2">
<select
value={selectedMetric}
onChange={(e) => setSelectedMetric(e.target.value)}
className="border border-[var(--border-primary)] rounded-md px-3 py-2 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
>
<option value="sales">{t('enterprise.metrics.sales')}</option>
<option value="inventory_value">{t('enterprise.metrics.inventory_value')}</option>
<option value="order_frequency">{t('enterprise.metrics.order_frequency')}</option>
<option value="on_time_delivery">{t('enterprise.metrics.on_time_delivery')}</option>
<option value="inventory_turnover">{t('enterprise.metrics.inventory_turnover')}</option>
</select>
</div>
<div className="flex items-center gap-2">
<select
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(Number(e.target.value))}
className="border border-[var(--border-primary)] rounded-md px-3 py-2 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
>
<option value={7}>{t('enterprise.last_7_days')}</option>
<option value={30}>{t('enterprise.last_30_days')}</option>
<option value={90}>{t('enterprise.last_90_days')}</option>
<option value={180}>{t('enterprise.last_6_months')}</option>
</select>
</div>
<div className="flex items-center gap-2 ml-auto">
<Button
variant={viewMode === 'chart' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('chart')}
>
<LineChart className="w-4 h-4 mr-2" />
{t('enterprise.chart_view')}
</Button>
<Button
variant={viewMode === 'cards' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('cards')}
>
<PieChart className="w-4 h-4 mr-2" />
{t('enterprise.card_view')}
</Button>
</div>
</div>
</div>
{/* Network Performance Summary */}
{networkMetrics && (
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Target className="w-5 h-5 text-[var(--color-info)]" />
{t('enterprise.network_summary')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* 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>
<Activity className="w-5 h-5 text-[var(--color-primary)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-primary)]">
{networkMetrics.networkEfficiency}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.operational_efficiency')}
</p>
</CardContent>
</Card>
{/* Performance Variance */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.performance_variance')}
</CardTitle>
<BarChart3 className="w-5 h-5 text-[var(--color-warning)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-warning)]">
{networkMetrics.variance}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.top_to_bottom_spread')}
</p>
</CardContent>
</Card>
{/* Average Performance */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{selectedMetric === 'sales' ? t('enterprise.avg_sales') :
selectedMetric === 'inventory_value' ? t('enterprise.avg_inventory') :
t('enterprise.avg_orders')}
</CardTitle>
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{selectedMetric === 'sales' ? `${networkMetrics.avgSales.toLocaleString()}` :
selectedMetric === 'inventory_value' ? `${networkMetrics.avgInventory.toLocaleString()}` :
networkMetrics.avgOrders.toLocaleString()}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.per_outlet')}
</p>
</CardContent>
</Card>
{/* Total Outlets */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.total_outlets')}
</CardTitle>
<Building2 className="w-5 h-5 text-[var(--color-info)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-info)]">
{networkMetrics.totalOutlets}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.locations_in_network')}
</p>
</CardContent>
</Card>
</div>
</div>
)}
{/* Performance Insights */}
{networkMetrics && (
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Award className="w-5 h-5 text-[var(--color-success)]" />
{t('enterprise.performance_insights')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Top Performer */}
<StatusCard
id={`top-performer`}
statusIndicator={{
color: '#10b981',
text: t('enterprise.top_performer'),
icon: Award,
isHighlight: true
}}
title={networkMetrics.topPerformer.outlet_name}
subtitle={t('enterprise.best_in_network')}
primaryValue={selectedMetric === 'sales' ? `${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
selectedMetric === 'inventory_value' ? `${networkMetrics.topPerformer.metric_value.toLocaleString()}` :
networkMetrics.topPerformer.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
t('enterprise.orders')}
secondaryInfo={{
label: t('enterprise.performance_index'),
value: `${Math.round((networkMetrics.topPerformer.metric_value / networkMetrics.avgSales) * 100)}%`
}}
progress={{
label: t('enterprise.above_average'),
percentage: Math.min(100, Math.round(((networkMetrics.topPerformer.metric_value - networkMetrics.avgSales) / networkMetrics.avgSales) * 100)),
color: '#10b981'
}}
metadata={[
`${t('enterprise.location')}: ${networkMetrics.topPerformer.outlet_name}`,
`${t('enterprise.rank')}: #1`
]}
actions={onOutletClick ? [{
label: t('enterprise.view_details'),
icon: BarChart3,
variant: 'outline',
onClick: () => onOutletClick(networkMetrics.topPerformer.outlet_id, networkMetrics.topPerformer.outlet_name),
priority: 'primary'
}] : []}
/>
{/* Bottom Performer */}
<StatusCard
id={`bottom-performer`}
statusIndicator={{
color: '#ef4444',
text: t('enterprise.needs_attention'),
icon: AlertTriangle,
isCritical: true
}}
title={networkMetrics.bottomPerformer.outlet_name}
subtitle={t('enterprise.improvement_opportunity')}
primaryValue={selectedMetric === 'sales' ? `${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
selectedMetric === 'inventory_value' ? `${networkMetrics.bottomPerformer.metric_value.toLocaleString()}` :
networkMetrics.bottomPerformer.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
t('enterprise.orders')}
secondaryInfo={{
label: t('enterprise.performance_index'),
value: `${Math.round((networkMetrics.bottomPerformer.metric_value / networkMetrics.avgSales) * 100)}%`
}}
progress={{
label: t('enterprise.below_average'),
percentage: Math.round(((networkMetrics.avgSales - networkMetrics.bottomPerformer.metric_value) / networkMetrics.avgSales) * 100),
color: '#ef4444'
}}
metadata={[
`${t('enterprise.location')}: ${networkMetrics.bottomPerformer.outlet_name}`,
`${t('enterprise.rank')}: #${networkMetrics.totalOutlets}`
]}
actions={onOutletClick ? [{
label: t('enterprise.view_details'),
icon: BarChart3,
variant: 'outline',
onClick: () => onOutletClick(networkMetrics.bottomPerformer.outlet_id, networkMetrics.bottomPerformer.outlet_name),
priority: 'primary'
}] : []}
/>
{/* Network Insight */}
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Award className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.network_insight')}</h3>
</div>
<div className="space-y-3">
{networkMetrics.variance < 20 ? (
<div className="flex items-center gap-2 text-[var(--color-success)]">
<CheckCircle2 className="w-5 h-5" />
<span className="text-sm">{t('enterprise.highly_balanced_network')}</span>
</div>
) : networkMetrics.variance < 40 ? (
<div className="flex items-center gap-2 text-[var(--color-warning)]">
<AlertTriangle className="w-5 h-5" />
<span className="text-sm">{t('enterprise.moderate_variation')}</span>
</div>
) : (
<div className="flex items-center gap-2 text-[var(--color-danger)]">
<AlertTriangle className="w-5 h-5" />
<span className="text-sm">{t('enterprise.high_variation')}</span>
</div>
)}
<p className="text-sm text-[var(--text-secondary)] mt-3">
{networkMetrics.variance < 20
? t('enterprise.balanced_network_description')
: networkMetrics.variance < 40
? t('enterprise.moderate_variation_description')
: t('enterprise.high_variation_description')}
</p>
{networkMetrics.variance >= 40 && (
<Button
onClick={() => setSelectedMetric('sales')}
variant="outline"
size="sm"
className="mt-3"
>
{t('enterprise.analyze_performance')}
</Button>
)}
</div>
</CardContent>
</Card>
</div>
</div>
)}
{/* Main Performance Visualization */}
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-[var(--color-primary)]" />
{t('enterprise.outlet_comparison')}
</h3>
{viewMode === 'chart' ? (
<Card className="h-full">
<CardContent className="p-6">
{childrenPerformance && childrenPerformance.rankings ? (
<PerformanceChart
data={childrenPerformance.rankings}
metric={selectedMetric}
period={selectedPeriod}
onOutletClick={onOutletClick}
showTrendIndicators={true}
/>
) : (
<div className="h-96 flex items-center justify-center text-[var(--text-secondary)]">
{isLoading ? t('enterprise.loading_performance') : t('enterprise.no_performance_data')}
</div>
)}
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{childrenPerformance?.rankings?.map((outlet, index) => {
const performanceIndicator = getPerformanceIndicator(outlet.outlet_id);
return (
<StatusCard
key={outlet.outlet_id}
id={outlet.outlet_id}
statusIndicator={performanceIndicator ? {
color: performanceIndicator.color,
text: performanceIndicator.trend === 'improving' ? t('enterprise.improving') :
performanceIndicator.trend === 'declining' ? t('enterprise.declining') : t('enterprise.stable'),
icon: performanceIndicator.icon
} : {
color: '#6b7280',
text: t('enterprise.no_data'),
icon: Clock
}}
title={outlet.outlet_name}
subtitle={`#${index + 1} ${t('enterprise.of')} ${childrenPerformance.rankings.length}`}
primaryValue={selectedMetric === 'sales' ? `${outlet.metric_value.toLocaleString()}` :
selectedMetric === 'inventory_value' ? `${outlet.metric_value.toLocaleString()}` :
outlet.metric_value.toLocaleString()}
primaryValueLabel={selectedMetric === 'sales' ? t('enterprise.sales') :
selectedMetric === 'inventory_value' ? t('enterprise.inventory_value') :
t('enterprise.orders')}
secondaryInfo={{
label: t('enterprise.performance_index'),
value: `${Math.round((outlet.metric_value / networkMetrics.avgSales) * 100)}%`
}}
progress={{
label: t('enterprise.of_network_avg'),
percentage: Math.round((outlet.metric_value / networkMetrics.avgSales) * 100),
color: performanceIndicator?.color || '#6b7280'
}}
metadata={[
`${t('enterprise.location_id')}: ${outlet.outlet_id}`,
`${t('enterprise.period')}: ${selectedPeriod} ${t('enterprise.days')}`
]}
actions={onOutletClick ? [{
label: t('enterprise.view_details'),
icon: BarChart3,
variant: 'outline',
onClick: () => onOutletClick(outlet.outlet_id, outlet.outlet_name),
priority: 'primary'
}] : []}
/>
);
})}
</div>
)}
</div>
{/* Performance Recommendations */}
{networkMetrics && networkMetrics.variance >= 30 && (
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-[var(--color-success)]" />
{t('enterprise.performance_recommendations')}
</h3>
<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">
<Award className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.best_practices')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{t('enterprise.learn_from_top_performer', {
name: networkMetrics.topPerformer.outlet_name
})}
</p>
<Button variant="outline" size="sm">
{t('enterprise.schedule_knowledge_sharing')}
</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.targeted_improvement')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{t('enterprise.focus_on_bottom_performer', {
name: networkMetrics.bottomPerformer.outlet_name
})}
</p>
<Button variant="outline" size="sm">
{t('enterprise.create_improvement_plan')}
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Target className="w-6 h-6 text-[var(--color-success)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.network_goal')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{t('enterprise.reduce_variance_goal', {
current: networkMetrics.variance,
target: Math.max(10, networkMetrics.variance - 15)
})}
</p>
<Button variant="outline" size="sm">
{t('enterprise.set_network_targets')}
</Button>
</CardContent>
</Card>
</div>
</div>
)}
</div>
);
};
export default NetworkPerformanceTab;

View File

@@ -0,0 +1,603 @@
/*
* Outlet Fulfillment Tab Component for Enterprise Dashboard
* Shows outlet inventory coverage, stockout risk, and fulfillment status
*/
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Button } from '../ui/Button';
import { Package, AlertTriangle, CheckCircle2, Activity, Clock, Warehouse, ShoppingCart, Truck, BarChart3, AlertCircle, ShieldCheck, PackageCheck, ArrowLeft } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import StatusCard from '../ui/StatusCard/StatusCard';
import { useSSEEvents } from '../../hooks/useSSE';
interface OutletFulfillmentTabProps {
tenantId: string;
onOutletClick?: (outletId: string, outletName: string) => void;
}
const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, onOutletClick }) => {
const { t } = useTranslation('dashboard');
const [selectedOutlet, setSelectedOutlet] = useState<string | null>(null);
const [viewMode, setViewMode] = useState<'summary' | 'detailed'>('summary');
// Real-time SSE events
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
channels: ['*.alerts', '*.notifications', 'recommendations']
});
// 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 }
]
}
]);
// Process SSE events for inventory updates
useEffect(() => {
if (sseEvents.length === 0) return;
// Filter inventory-related events
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'
);
if (inventoryEvents.length === 0) return;
// Update inventory data based on events
setInventoryData(prevData => {
return prevData.map(outlet => {
// Find events for this outlet
const outletEvents = inventoryEvents.filter(event =>
event.entity_id === outlet.id ||
event.event_metadata?.outlet_id === outlet.id
);
if (outletEvents.length === 0) return outlet;
// Calculate new inventory coverage based on events
let newCoverage = outlet.inventoryCoverage;
let newRisk = outlet.stockoutRisk;
let newStatus = outlet.status;
let newCriticalItems = outlet.criticalItems;
outletEvents.forEach(event => {
switch (event.event_type) {
case 'inventory_low':
case 'stock_receipt_incomplete':
newCoverage = Math.max(0, newCoverage - 10);
if (newCoverage < 50) newRisk = 'high';
if (newCoverage < 30) newRisk = 'critical';
newStatus = 'critical';
newCriticalItems += 1;
break;
case 'inventory_replenished':
case 'stock_received':
newCoverage = Math.min(100, newCoverage + 15);
if (newCoverage > 70) newRisk = 'low';
if (newCoverage > 50) newRisk = 'medium';
newStatus = newCoverage > 80 ? 'normal' : 'warning';
newCriticalItems = Math.max(0, newCriticalItems - 1);
break;
case 'inventory_adjustment':
// Adjust coverage based on event metadata
if (event.event_metadata?.coverage_change) {
newCoverage = Math.min(100, Math.max(0, newCoverage + event.event_metadata.coverage_change));
}
break;
}
});
return {
...outlet,
inventoryCoverage: newCoverage,
stockoutRisk: newRisk,
status: newStatus,
criticalItems: newCriticalItems,
lastUpdated: new Date().toISOString()
};
});
});
}, [sseEvents]);
// Calculate network-wide fulfillment metrics
const calculateNetworkMetrics = () => {
const totalOutlets = inventoryData.length;
const avgCoverage = inventoryData.reduce((sum, outlet) => sum + outlet.inventoryCoverage, 0) / totalOutlets;
const avgFulfillment = inventoryData.reduce((sum, outlet) => sum + outlet.fulfillmentRate, 0) / totalOutlets;
const criticalOutlets = inventoryData.filter(outlet => outlet.status === 'critical').length;
const warningOutlets = inventoryData.filter(outlet => outlet.status === 'warning').length;
const normalOutlets = inventoryData.filter(outlet => outlet.status === 'normal').length;
const totalCriticalItems = inventoryData.reduce((sum, outlet) => sum + outlet.criticalItems, 0);
return {
totalOutlets,
avgCoverage,
avgFulfillment,
criticalOutlets,
warningOutlets,
normalOutlets,
totalCriticalItems,
networkHealth: Math.round(avgCoverage * 0.6 + avgFulfillment * 0.4)
};
};
const networkMetrics = calculateNetworkMetrics();
// Get status configuration for outlets
const getOutletStatusConfig = (outletId: string) => {
const outlet = inventoryData.find(o => o.id === outletId);
if (!outlet) return null;
switch (outlet.status) {
case 'critical':
return {
color: '#ef4444', // red-500
text: t('enterprise.status_critical'),
icon: AlertCircle,
isCritical: true
};
case 'warning':
return {
color: '#f59e0b', // amber-500
text: outlet.stockoutRisk === 'high' ? t('enterprise.high_stockout_risk') : t('enterprise.medium_stockout_risk'),
icon: AlertTriangle,
isHighlight: true
};
default:
return {
color: '#10b981', // emerald-500
text: t('enterprise.status_normal'),
icon: CheckCircle2
};
}
};
// Get risk level configuration
const getRiskConfig = (riskLevel: string) => {
switch (riskLevel) {
case 'critical':
return { color: '#ef4444', text: t('enterprise.risk_critical'), icon: AlertCircle };
case 'high':
return { color: '#f59e0b', text: t('enterprise.risk_high'), icon: AlertTriangle };
case 'medium':
return { color: '#fbbf24', text: t('enterprise.risk_medium'), icon: AlertTriangle };
default:
return { color: '#10b981', text: t('enterprise.risk_low'), icon: CheckCircle2 };
}
};
return (
<div className="space-y-8">
{/* Fulfillment Header */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Warehouse className="w-6 h-6 text-[var(--color-primary)]" />
{t('enterprise.outlet_fulfillment')}
</h2>
<p className="text-[var(--text-secondary)] mb-6">
{t('enterprise.fulfillment_description')}
</p>
{/* View Mode Selector */}
<div className="flex gap-2 mb-6">
<Button
variant={viewMode === 'summary' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('summary')}
>
<BarChart3 className="w-4 h-4 mr-2" />
{t('enterprise.summary_view')}
</Button>
<Button
variant={viewMode === 'detailed' ? 'default' : 'outline'}
size="sm"
onClick={() => setViewMode('detailed')}
>
<PackageCheck className="w-4 h-4 mr-2" />
{t('enterprise.detailed_view')}
</Button>
</div>
</div>
{/* Network Fulfillment Summary */}
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-[var(--color-success)]" />
{t('enterprise.fulfillment_summary')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* Network Health Score */}
<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_health_score')}
</CardTitle>
<Activity className="w-5 h-5 text-[var(--color-primary)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-primary)]">
{networkMetrics.networkHealth}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.overall_fulfillment_health')}
</p>
</CardContent>
</Card>
{/* Average Inventory Coverage */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.avg_inventory_coverage')}
</CardTitle>
<Package className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{networkMetrics.avgCoverage}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.across_all_outlets')}
</p>
</CardContent>
</Card>
{/* Fulfillment 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.fulfillment_rate')}
</CardTitle>
<ShoppingCart className="w-5 h-5 text-[var(--color-info)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-info)]">
{networkMetrics.avgFulfillment}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.order_fulfillment_rate')}
</p>
</CardContent>
</Card>
{/* Critical Items */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('enterprise.critical_items')}
</CardTitle>
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-warning)]">
{networkMetrics.totalCriticalItems}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('enterprise.items_at_risk')}
</p>
</CardContent>
</Card>
</div>
</div>
{/* Outlet Status Overview */}
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Warehouse className="w-5 h-5 text-[var(--color-primary)]" />
{t('enterprise.outlet_status_overview')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{inventoryData.map((outlet) => {
const statusConfig = getOutletStatusConfig(outlet.id);
return (
<StatusCard
key={outlet.id}
id={outlet.id}
statusIndicator={statusConfig || {
color: '#6b7280',
text: t('enterprise.no_data'),
icon: Clock
}}
title={outlet.name}
subtitle={`${t('enterprise.inventory_coverage')}: ${outlet.inventoryCoverage}%`}
primaryValue={`${outlet.fulfillmentRate}%`}
primaryValueLabel={t('enterprise.fulfillment_rate')}
secondaryInfo={{
label: t('enterprise.critical_items'),
value: `${outlet.criticalItems}`
}}
progress={{
label: t('enterprise.inventory_coverage'),
percentage: outlet.inventoryCoverage,
color: statusConfig?.color || '#6b7280'
}}
metadata={[
`${t('enterprise.stockout_risk')}: ${t(`enterprise.risk_${outlet.stockoutRisk}`)}`,
`${t('enterprise.last_updated')}: ${new Date(outlet.lastUpdated).toLocaleTimeString()}`,
sseConnected ? `🟢 ${t('enterprise.live_updates')}` : `🟡 ${t('enterprise.offline')}`
]}
actions={onOutletClick ? [{
label: t('enterprise.view_details'),
icon: PackageCheck,
variant: 'outline',
onClick: () => {
setSelectedOutlet(outlet.id);
setViewMode('detailed');
onOutletClick(outlet.id, outlet.name);
},
priority: 'primary'
}] : []}
onClick={() => {
setSelectedOutlet(outlet.id);
setViewMode('detailed');
}}
/>
);
})}
</div>
</div>
{/* Detailed View - Product Level Inventory */}
{viewMode === 'detailed' && selectedOutlet && (
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<PackageCheck className="w-5 h-5 text-[var(--color-info)]" />
{t('enterprise.product_level_inventory')}
</h3>
<div className="mb-4">
<Button
onClick={() => setViewMode('summary')}
variant="outline"
size="sm"
>
<ArrowLeft className="w-4 h-4 mr-2" />
{t('enterprise.back_to_summary')}
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{inventoryData
.find(outlet => outlet.id === selectedOutlet)
?.products.map((product) => {
const riskConfig = getRiskConfig(product.risk);
return (
<StatusCard
key={product.id}
id={product.id}
statusIndicator={riskConfig}
title={product.name}
subtitle={`${t('enterprise.current_stock')}: ${product.stock} units`}
primaryValue={`${product.coverage}%`}
primaryValueLabel={t('enterprise.inventory_coverage')}
secondaryInfo={{
label: t('enterprise.safety_stock'),
value: `${product.safetyStock} units`
}}
progress={{
label: t('enterprise.coverage_of_safety'),
percentage: Math.min(100, Math.round((product.stock / product.safetyStock) * 100)),
color: riskConfig.color
}}
metadata={[
`${t('enterprise.risk_level')}: ${riskConfig.text}`,
`${t('enterprise.stock_above_safety')}: ${product.stock >= product.safetyStock ? t('enterprise.yes') : t('enterprise.no')}`
]}
actions={[
{
label: t('enterprise.transfer_stock'),
icon: Truck,
variant: 'outline',
onClick: () => {
// In Phase 2, this will navigate to transfer page
console.log(`Transfer stock for ${product.name}`);
},
priority: 'primary'
}
]}
/>
);
})}
</div>
</div>
)}
{/* Fulfillment Recommendations */}
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)]" />
{t('enterprise.fulfillment_recommendations')}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Critical Outlets */}
{networkMetrics.criticalOutlets > 0 && (
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<AlertCircle className="w-6 h-6 text-[var(--color-danger)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.critical_outlets')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{t('enterprise.critical_outlets_description', {
count: networkMetrics.criticalOutlets
})}
</p>
<Button variant="outline" size="sm">
{t('enterprise.prioritize_transfers')}
</Button>
</CardContent>
</Card>
)}
{/* Inventory Optimization */}
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Package className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.inventory_optimization')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{networkMetrics.avgCoverage < 70
? t('enterprise.low_coverage_recommendation')
: t('enterprise.good_coverage_recommendation')}
</p>
<Button variant="outline" size="sm">
{t('enterprise.run_optimization')}
</Button>
</CardContent>
</Card>
{/* Fulfillment Excellence */}
{networkMetrics.avgFulfillment > 95 && (
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<ShieldCheck className="w-6 h-6 text-[var(--color-success)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('enterprise.fulfillment_excellence')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">
{t('enterprise.high_fulfillment_congrats', {
rate: networkMetrics.avgFulfillment
})}
</p>
<Button variant="outline" size="sm">
{t('enterprise.maintain_excellence')}
</Button>
</CardContent>
</Card>
)}
</div>
</div>
{/* Real-time Inventory Alerts */}
<div className="mb-8">
<h3 className="text-xl font-semibold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-[var(--color-info)]" />
{t('enterprise.real_time_inventory_alerts')}
</h3>
<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_inventory_events')}
</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>
{sseConnected ? (
<div className="space-y-4">
{inventoryData
.filter(outlet => outlet.status !== 'normal')
.map((outlet, index) => {
const statusConfig = getOutletStatusConfig(outlet.id);
const EventIcon = statusConfig?.icon || AlertTriangle;
const color = statusConfig?.color || 'text-[var(--color-warning)]';
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)]">
{outlet.name} - {statusConfig?.text}
</p>
<p className="text-xs text-[var(--text-tertiary)] whitespace-nowrap">
{new Date(outlet.lastUpdated).toLocaleTimeString()}
</p>
</div>
<p className="text-sm text-[var(--text-secondary)] mt-1">
{t('enterprise.inventory_coverage')}: {outlet.inventoryCoverage}% | {t('enterprise.critical_items')}: {outlet.criticalItems}
</p>
<p className="text-xs text-[var(--text-tertiary)] mt-1">
{t('enterprise.fulfillment_rate')}: {outlet.fulfillmentRate}%
</p>
</div>
</div>
);
})}
{inventoryData.filter(outlet => outlet.status !== 'normal').length === 0 && (
<div className="text-center py-8 text-[var(--color-success)]">
<CheckCircle2 className="w-8 h-8 mx-auto mb-2" />
<p>{t('enterprise.all_outlets_healthy')}</p>
</div>
)}
</div>
) : (
<div className="text-center py-8 text-[var(--text-secondary)]">
{t('enterprise.waiting_for_updates')}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
};
export default OutletFulfillmentTab;

View File

@@ -0,0 +1,410 @@
/*
* Production Tab Component for Enterprise Dashboard
* Shows network-wide production status and equipment monitoring
*/
import React, { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/Card';
import { Button } from '../ui/Button';
import { Factory, AlertTriangle, CheckCircle2, Activity, Timer, Brain, Cog, Wrench, Bell, Clock } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { ProductionStatusBlock } from './blocks/ProductionStatusBlock';
import StatusCard from '../ui/StatusCard/StatusCard';
import { useControlPanelData } from '../../api/hooks/useControlPanelData';
import { useSSEEvents } from '../../hooks/useSSE';
interface ProductionTabProps {
tenantId: string;
}
const ProductionTab: React.FC<ProductionTabProps> = ({ tenantId }) => {
const { t } = useTranslation('dashboard');
// Get control panel data for production information
const { data: controlPanelData, isLoading: isControlPanelLoading } = useControlPanelData(tenantId);
const isLoading = isControlPanelLoading;
// Real-time SSE events
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
channels: ['*.alerts', '*.notifications', 'recommendations']
});
// 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
}
]);
// Process SSE events for equipment status updates
useEffect(() => {
if (sseEvents.length === 0) return;
// Filter equipment-related events
const equipmentEvents = sseEvents.filter(event =>
event.event_type.includes('equipment_') ||
event.event_type === 'equipment_maintenance' ||
event.entity_type === 'equipment'
);
if (equipmentEvents.length === 0) return;
// Update equipment status based on events
setEquipmentData(prevEquipment => {
return prevEquipment.map(equipment => {
// Find the latest event for this equipment
const equipmentEvent = equipmentEvents.find(event =>
event.entity_id === equipment.id ||
event.event_metadata?.equipment_id === equipment.id
);
if (equipmentEvent) {
// Update status based on event type
let newStatus = equipment.status;
let temperature = equipment.temperature;
let utilization = equipment.utilization;
switch (equipmentEvent.event_type) {
case 'equipment_maintenance_required':
case 'equipment_failure':
newStatus = 'critical';
break;
case 'equipment_warning':
case 'temperature_variance':
newStatus = 'warning';
break;
case 'equipment_normal':
case 'maintenance_completed':
newStatus = 'normal';
break;
}
// Update temperature if available in event metadata
if (equipmentEvent.event_metadata?.temperature) {
temperature = `${equipmentEvent.event_metadata.temperature}°C`;
}
// Update utilization if available in event metadata
if (equipmentEvent.event_metadata?.utilization) {
utilization = equipmentEvent.event_metadata.utilization;
}
return {
...equipment,
status: newStatus,
temperature: temperature,
utilization: utilization,
lastEvent: {
type: equipmentEvent.event_type,
timestamp: equipmentEvent.timestamp || new Date().toISOString(),
message: equipmentEvent.message
}
};
}
return equipment;
});
});
}, [sseEvents]);
return (
<div className="space-y-8">
{/* Production Status Block - Reusing existing component */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Factory className="w-6 h-6 text-[var(--color-primary)]" />
{t('production.title')}
</h2>
<ProductionStatusBlock
lateToStartBatches={controlPanelData?.lateToStartBatches || []}
runningBatches={controlPanelData?.runningBatches || []}
pendingBatches={controlPanelData?.pendingBatches || []}
alerts={controlPanelData?.alerts || []}
equipmentAlerts={controlPanelData?.equipmentAlerts || []}
productionAlerts={controlPanelData?.productionAlerts || []}
loading={isLoading}
/>
</div>
{/* Equipment Status Grid */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Cog className="w-6 h-6 text-[var(--color-secondary)]" />
{t('production.equipment_status')}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{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}`);
}
}
// 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 (
<StatusCard
key={equipment.id}
id={equipment.id}
statusIndicator={statusConfig}
title={equipment.name}
subtitle={equipment.temperature ? `${t('production.temperature')}: ${equipment.temperature}` : undefined}
primaryValue={`${equipment.utilization}%`}
primaryValueLabel={t('production.utilization')}
secondaryInfo={{
label: t('production.next_maintenance'),
value: new Date(equipment.nextMaintenance).toLocaleDateString()
}}
progress={{
label: t('production.utilization'),
percentage: equipment.utilization,
color: statusConfig.color
}}
metadata={[...eventMetadata, ...additionalMetadata,
`${t('production.last_maintenance')}: ${new Date(equipment.lastMaintenance).toLocaleDateString()}`
]}
actions={[
{
label: t('production.view_details'),
icon: Wrench,
variant: 'outline',
onClick: () => {
// 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}`);
}}
/>
);
})}
</div>
</div>
{/* Production Efficiency Metrics */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Brain className="w-6 h-6 text-[var(--color-info)]" />
{t('production.efficiency_metrics')}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{/* On-time Batch Start Rate */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('production.on_time_start_rate')}
</CardTitle>
<Timer className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{controlPanelData?.orchestrationSummary?.aiHandlingRate || 85}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('production.batches_started_on_time')}
</p>
</CardContent>
</Card>
{/* Production Efficiency */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('production.efficiency_rate')}
</CardTitle>
<Activity className="w-5 h-5 text-[var(--color-primary)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-primary)]">
{Math.round((controlPanelData?.issuesPreventedByAI || 0) /
Math.max(1, (controlPanelData?.issuesPreventedByAI || 0) + (controlPanelData?.issuesRequiringAction || 0)) * 100) || 92}%
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('production.overall_efficiency')}
</p>
</CardContent>
</Card>
{/* Active Production Alerts */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('production.active_alerts')}
</CardTitle>
<AlertTriangle className="w-5 h-5 text-[var(--color-warning)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-warning)]">
{(controlPanelData?.productionAlerts?.length || 0) + (controlPanelData?.equipmentAlerts?.length || 0)}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('production.issues_require_attention')}
</p>
</CardContent>
</Card>
{/* AI Prevented Issues */}
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-[var(--text-secondary)]">
{t('production.ai_prevented')}
</CardTitle>
<CheckCircle2 className="w-5 h-5 text-[var(--color-success)]" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-[var(--color-success)]">
{controlPanelData?.issuesPreventedByAI || 12}
</div>
<p className="text-xs text-[var(--text-secondary)] mt-1">
{t('production.problems_prevented')}
</p>
</CardContent>
</Card>
</div>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-[var(--text-primary)] mb-4 flex items-center gap-2">
<Timer className="w-6 h-6 text-[var(--color-info)]" />
{t('production.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">
<Factory className="w-6 h-6 text-[var(--color-primary)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('production.create_batch')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('production.create_batch_description')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/production/batches/create`}
className="w-full"
>
{t('production.create_batch')}
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Wrench className="w-6 h-6 text-[var(--color-success)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('production.maintenance')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('production.schedule_maintenance')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/equipment`}
variant="outline"
className="w-full"
>
{t('production.manage_equipment')}
</Button>
</CardContent>
</Card>
<Card>
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Cog className="w-6 h-6 text-[var(--color-info)]" />
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{t('production.quality_checks')}</h3>
</div>
<p className="text-[var(--text-secondary)] mb-4">{t('production.manage_quality')}</p>
<Button
onClick={() => window.location.href = `/app/tenants/${tenantId}/production/quality`}
variant="outline"
className="w-full"
>
{t('production.quality_management')}
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default ProductionTab;