2025-12-17 20:50:22 +01:00
|
|
|
/*
|
|
|
|
|
* 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';
|
2025-12-30 14:40:20 +01:00
|
|
|
import { useTenantCurrency } from '../../hooks/useTenantCurrency';
|
2025-12-17 20:50:22 +01:00
|
|
|
|
|
|
|
|
interface DistributionTabProps {
|
|
|
|
|
tenantId: string;
|
|
|
|
|
selectedDate: string;
|
|
|
|
|
onDateChange: (date: string) => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const DistributionTab: React.FC<DistributionTabProps> = ({ tenantId, selectedDate, onDateChange }) => {
|
|
|
|
|
const { t } = useTranslation('dashboard');
|
2025-12-30 14:40:20 +01:00
|
|
|
const { currencySymbol } = useTenantCurrency();
|
2025-12-17 20:50:22 +01:00
|
|
|
|
|
|
|
|
// 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)]">
|
2025-12-30 14:40:20 +01:00
|
|
|
{currencySymbol}{optimizationMetrics.fuelSaved.toFixed(2)}
|
2025-12-17 20:50:22 +01:00
|
|
|
</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;
|