Files
bakery-ia/frontend/src/components/dashboard/DistributionTab.tsx
2026-01-02 12:18:46 +01:00

564 lines
23 KiB
TypeScript

/*
* 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';
import { useTenantCurrency } from '../../hooks/useTenantCurrency';
interface DistributionTabProps {
tenantId: string;
selectedDate: string;
onDateChange: (date: string) => void;
}
const DistributionTab: React.FC<DistributionTabProps> = ({ tenantId, selectedDate, onDateChange }) => {
const { t } = useTranslation('dashboard');
const { currencySymbol } = useTenantCurrency();
// 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: any) =>
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;
// No mockRoutes anymore, using distributionOverview.route_sequences
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)]">
{currencySymbol}{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">
{(distributionOverview?.route_sequences || []).map((route: any) => {
// Determine status configuration
const getStatusConfig = () => {
switch (route.status) {
case 'completed':
return {
color: '#10b981', // emerald-500
text: t('enterprise.route_completed'),
icon: CheckCircle2
};
case 'failed':
case 'cancelled':
return {
color: '#ef4444', // red-500
text: t('enterprise.route_delayed'),
icon: AlertTriangle,
isCritical: true
};
case 'in_progress':
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();
// Format optimization savings
const savings = route.vrp_optimization_savings || {};
const distanceSavedText = savings.distance_saved_km
? `${savings.distance_saved_km.toFixed(1)} km`
: '0 km';
return (
<StatusCard
key={route.id}
id={route.id}
statusIndicator={statusConfig}
title={`${t('enterprise.route')} #${route.route_number || 'N/A'}`}
subtitle={`${t('enterprise.distance')}: ${route.total_distance_km?.toFixed(1) || 0} km`}
primaryValue={`${route.estimated_duration_minutes || 0} min`}
primaryValueLabel={t('enterprise.estimated_duration')}
secondaryInfo={{
label: t('enterprise.stops'),
value: `${route.route_sequence?.length || 0}`
}}
progress={{
label: t('enterprise.optimization'),
percentage: route.status === 'completed' ? 100 :
route.status === 'in_progress' ? 75 :
route.status === 'failed' ? 50 : 25,
color: statusConfig.color
}}
metadata={[
`${t('enterprise.optimization_savings')}: ${distanceSavedText}`,
`${t('enterprise.vehicles')}: ${route.vehicle_id || t('common:not_available', 'N/A')}`
]}
actions={[
{
label: t('enterprise.track_route'),
icon: Map,
variant: 'outline',
onClick: () => {
console.log(`Track route ${route.route_number}`);
},
priority: 'primary'
}
]}
onClick={() => {
console.log(`View route ${route.route_number}`);
}}
/>
);
})}
{(!distributionOverview?.route_sequences || distributionOverview.route_sequences.length === 0) && (
<div className="col-span-1 sm:col-span-2 lg:col-span-3 py-12">
<div className="text-center text-[var(--text-secondary)]">
<Truck className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p>{t('enterprise.no_active_routes')}</p>
</div>
</div>
)}
</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;