Imporve enterprise
This commit is contained in:
582
frontend/src/components/dashboard/DistributionTab.tsx
Normal file
582
frontend/src/components/dashboard/DistributionTab.tsx
Normal 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;
|
||||
340
frontend/src/components/dashboard/NetworkOverviewTab.tsx
Normal file
340
frontend/src/components/dashboard/NetworkOverviewTab.tsx
Normal 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;
|
||||
531
frontend/src/components/dashboard/NetworkPerformanceTab.tsx
Normal file
531
frontend/src/components/dashboard/NetworkPerformanceTab.tsx
Normal 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;
|
||||
603
frontend/src/components/dashboard/OutletFulfillmentTab.tsx
Normal file
603
frontend/src/components/dashboard/OutletFulfillmentTab.tsx
Normal 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;
|
||||
410
frontend/src/components/dashboard/ProductionTab.tsx
Normal file
410
frontend/src/components/dashboard/ProductionTab.tsx
Normal 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;
|
||||
@@ -71,9 +71,37 @@
|
||||
"items_needed": "items needed"
|
||||
},
|
||||
"production": {
|
||||
"title": "What needs to be produced today?",
|
||||
"title": "Production Status",
|
||||
"empty": "No production scheduled for today",
|
||||
"batches_pending": "batches pending"
|
||||
"batches_pending": "batches pending",
|
||||
"equipment_status": "Equipment Status",
|
||||
"temperature": "Temperature",
|
||||
"utilization": "Utilization",
|
||||
"next_maintenance": "Next Maintenance",
|
||||
"last_maintenance": "Last Maintenance",
|
||||
"view_details": "View Details",
|
||||
"status_critical": "Critical",
|
||||
"status_warning": "Warning",
|
||||
"status_normal": "Normal",
|
||||
"efficiency_metrics": "Production Efficiency Metrics",
|
||||
"on_time_start_rate": "On-time Start Rate",
|
||||
"batches_started_on_time": "Batches started on time",
|
||||
"efficiency_rate": "Efficiency Rate",
|
||||
"overall_efficiency": "Overall production efficiency",
|
||||
"active_alerts": "Active Alerts",
|
||||
"issues_require_attention": "Issues requiring attention",
|
||||
"ai_prevented": "AI Prevented Issues",
|
||||
"problems_prevented": "Problems prevented by AI",
|
||||
"quick_actions": "Quick Actions",
|
||||
"create_batch": "Create Production Batch",
|
||||
"create_batch_description": "Create a new production batch for the network",
|
||||
"maintenance": "Equipment Maintenance",
|
||||
"schedule_maintenance": "Schedule maintenance for production equipment",
|
||||
"manage_equipment": "Manage Equipment",
|
||||
"quality_checks": "Quality Checks",
|
||||
"manage_quality": "Manage quality control processes",
|
||||
"quality_management": "Quality Management",
|
||||
"event_message": "Message"
|
||||
},
|
||||
"po_approvals": {
|
||||
"title": "What purchase orders need approval?",
|
||||
@@ -351,6 +379,7 @@
|
||||
"network_sales": "Network Sales",
|
||||
"last_30_days": "last 30 days",
|
||||
"production_volume": "Production Volume",
|
||||
"production": "Production",
|
||||
"pending_orders": "Pending Orders",
|
||||
"internal_transfers": "internal transfers",
|
||||
"active_shipments": "Active Shipments",
|
||||
@@ -383,8 +412,61 @@
|
||||
"metrics": {
|
||||
"sales": "sales",
|
||||
"inventory_value": "inventory value",
|
||||
"order_frequency": "order frequency"
|
||||
"order_frequency": "order frequency",
|
||||
"on_time_delivery": "on-time delivery",
|
||||
"inventory_turnover": "inventory turnover"
|
||||
},
|
||||
"network_performance": "Network Performance",
|
||||
"performance_description": "Compare performance across all outlets in your network",
|
||||
"network_summary": "Network Summary",
|
||||
"performance_variance": "Performance Variance",
|
||||
"top_to_bottom_spread": "Top to bottom performance spread",
|
||||
"avg_sales": "Average Sales",
|
||||
"avg_inventory": "Average Inventory",
|
||||
"avg_orders": "Average Orders",
|
||||
"per_outlet": "per outlet",
|
||||
"total_outlets": "Total Outlets",
|
||||
"locations_in_network": "locations in network",
|
||||
"performance_insights": "Performance Insights",
|
||||
"top_performer": "Top Performer",
|
||||
"best_in_network": "Best performing outlet in network",
|
||||
"needs_attention": "Needs Attention",
|
||||
"improvement_opportunity": "Improvement opportunity identified",
|
||||
"network_insight": "Network Insight",
|
||||
"highly_balanced_network": "Highly balanced network",
|
||||
"moderate_variation": "Moderate performance variation",
|
||||
"high_variation": "High performance variation",
|
||||
"balanced_network_description": "Your network shows excellent balance with minimal performance gaps",
|
||||
"moderate_variation_description": "Some performance variation exists - consider knowledge sharing",
|
||||
"high_variation_description": "Significant performance gaps detected - targeted improvement needed",
|
||||
"analyze_performance": "Analyze Performance",
|
||||
"outlet_comparison": "Outlet Comparison",
|
||||
"chart_view": "Chart View",
|
||||
"card_view": "Card View",
|
||||
"improving": "Improving",
|
||||
"declining": "Declining",
|
||||
"stable": "Stable",
|
||||
"no_data": "No Data",
|
||||
"location": "Location",
|
||||
"location_id": "Location ID",
|
||||
"period": "Period",
|
||||
"days": "days",
|
||||
"of": "of",
|
||||
"performance_index": "Performance Index",
|
||||
"above_average": "Above Average",
|
||||
"below_average": "Below Average",
|
||||
"of_network_avg": "of network average",
|
||||
"view_details": "View Details",
|
||||
"performance_recommendations": "Performance Recommendations",
|
||||
"best_practices": "Best Practices Sharing",
|
||||
"learn_from_top_performer": "Learn from {name}'s best practices",
|
||||
"targeted_improvement": "Targeted Improvement",
|
||||
"focus_on_bottom_performer": "Focus improvement efforts on {name}",
|
||||
"network_goal": "Network Performance Goal",
|
||||
"reduce_variance_goal": "Reduce performance variance from {current}% to {target}%",
|
||||
"set_network_targets": "Set Network Targets",
|
||||
"schedule_knowledge_sharing": "Schedule Knowledge Sharing",
|
||||
"create_improvement_plan": "Create Improvement Plan",
|
||||
"route": "Route",
|
||||
"total_routes": "Total Routes",
|
||||
"total_distance": "Total Distance",
|
||||
@@ -407,7 +489,80 @@
|
||||
"in_transit": "In Transit",
|
||||
"delivered": "Delivered",
|
||||
"failed": "Failed",
|
||||
"distribution_routes": "Distribution Routes"
|
||||
"distribution_routes": "Distribution Routes",
|
||||
"network_status": "Network Status",
|
||||
"network_health": "Network Health Indicators",
|
||||
"on_time_delivery": "On-time Delivery Rate",
|
||||
"delivery_performance": "Delivery performance across network",
|
||||
"issue_prevention": "Issue Prevention Rate",
|
||||
"issues_prevented": "issues prevented by AI",
|
||||
"active_issues": "Active Issues",
|
||||
"action_required": "require immediate attention",
|
||||
"network_efficiency": "Network Efficiency",
|
||||
"operational_efficiency": "Operational efficiency score",
|
||||
"add_outlet": "Add Outlet",
|
||||
"add_outlet_description": "Add a new outlet to your bakery network",
|
||||
"create_outlet": "Create Outlet",
|
||||
"internal_transfers": "Internal Transfers",
|
||||
"manage_transfers": "Manage transfers between central bakery and outlets",
|
||||
"view_transfers": "View Transfers",
|
||||
"view_alerts": "View Alerts",
|
||||
"network_alerts_description": "View and manage network-wide alerts and issues",
|
||||
"view_all_alerts": "View All Alerts",
|
||||
"quick_actions": "Quick Actions",
|
||||
"real_time_events": "Real-time Network Events",
|
||||
"recent_activity": "Recent Activity",
|
||||
"live_updates": "Live updates enabled",
|
||||
"offline": "Offline mode",
|
||||
"no_recent_activity": "No recent network activity",
|
||||
"waiting_for_updates": "Waiting for real-time updates...",
|
||||
"show_all_events": "Show all {count} events",
|
||||
"show_less": "Show less",
|
||||
"distribution_summary": "Distribution Summary",
|
||||
"total_deliveries": "Total Deliveries",
|
||||
"all_shipments": "All shipments today",
|
||||
"on_time_deliveries": "On-time Deliveries",
|
||||
"on_time_rate": "on-time rate",
|
||||
"no_deliveries": "No deliveries yet",
|
||||
"delayed_deliveries": "Delayed Deliveries",
|
||||
"delay_rate": "delay rate",
|
||||
"no_delays": "No delays",
|
||||
"in_transit": "In Transit",
|
||||
"currently_en_route": "Currently en route",
|
||||
"route_optimization": "Route Optimization",
|
||||
"distance_saved": "Distance Saved",
|
||||
"total_distance_saved": "Total distance saved by optimization",
|
||||
"time_saved": "Time Saved",
|
||||
"total_time_saved": "Total time saved by optimization",
|
||||
"fuel_saved": "Fuel Saved",
|
||||
"estimated_fuel_savings": "Estimated fuel savings",
|
||||
"co2_saved": "CO2 Saved",
|
||||
"estimated_co2_reduction": "Estimated CO2 reduction",
|
||||
"active_routes": "Active Routes",
|
||||
"distance": "Distance",
|
||||
"estimated_duration": "Estimated Duration",
|
||||
"stops": "Stops",
|
||||
"optimization": "Optimization",
|
||||
"optimization_savings": "Optimization Savings",
|
||||
"vehicles": "Vehicles",
|
||||
"route_completed": "Route Completed",
|
||||
"route_delayed": "Route Delayed",
|
||||
"route_in_transit": "In Transit",
|
||||
"route_pending": "Pending",
|
||||
"track_route": "Track Route",
|
||||
"real_time_delivery_events": "Real-time Delivery Events",
|
||||
"recent_delivery_activity": "Recent Delivery Activity",
|
||||
"no_recent_delivery_activity": "No recent delivery activity",
|
||||
"route": "Route",
|
||||
"optimize_routes": "Optimize Routes",
|
||||
"optimize_routes_description": "Run route optimization for today's deliveries",
|
||||
"run_optimization": "Run Optimization",
|
||||
"manage_vehicles": "Manage Vehicles",
|
||||
"manage_vehicle_fleet": "Manage vehicle fleet and assignments",
|
||||
"view_vehicles": "View Vehicles",
|
||||
"live_tracking": "Live GPS Tracking",
|
||||
"real_time_gps_tracking": "Real-time GPS tracking of all vehicles",
|
||||
"open_tracking_map": "Open Tracking Map"
|
||||
},
|
||||
"ai_insights": {
|
||||
"title": "AI Insights",
|
||||
@@ -465,7 +620,55 @@
|
||||
"supplier_contract": "Contract with {supplier} for {products}",
|
||||
"seasonal_demand": "Seasonal increase of {increase}% in {products} for {season}",
|
||||
"forecast_demand": "Forecasted demand for {product} with {confidence}% confidence for next {period, plural, one {# day} other {# days}}"
|
||||
}
|
||||
},
|
||||
"outlet_fulfillment": "Outlet Fulfillment",
|
||||
"fulfillment_description": "Monitor inventory coverage, stockout risk, and fulfillment status across all outlets",
|
||||
"fulfillment_summary": "Fulfillment Summary",
|
||||
"network_health_score": "Network Health Score",
|
||||
"overall_fulfillment_health": "Overall fulfillment health score",
|
||||
"avg_inventory_coverage": "Avg Inventory Coverage",
|
||||
"across_all_outlets": "across all outlets",
|
||||
"fulfillment_rate": "Fulfillment Rate",
|
||||
"order_fulfillment_rate": "Order fulfillment rate",
|
||||
"critical_items": "Critical Items",
|
||||
"items_at_risk": "items at risk of stockout",
|
||||
"outlet_status_overview": "Outlet Status Overview",
|
||||
"inventory_coverage": "Inventory Coverage",
|
||||
"stockout_risk": "Stockout Risk",
|
||||
"last_updated": "Last Updated",
|
||||
"status_critical": "Critical Status",
|
||||
"status_normal": "Normal Status",
|
||||
"high_stockout_risk": "High Stockout Risk",
|
||||
"medium_stockout_risk": "Medium Stockout Risk",
|
||||
"risk_critical": "Critical",
|
||||
"risk_high": "High",
|
||||
"risk_medium": "Medium",
|
||||
"risk_low": "Low",
|
||||
"summary_view": "Summary View",
|
||||
"detailed_view": "Detailed View",
|
||||
"product_level_inventory": "Product Level Inventory",
|
||||
"back_to_summary": "Back to Summary",
|
||||
"current_stock": "Current Stock",
|
||||
"safety_stock": "Safety Stock",
|
||||
"coverage_of_safety": "Coverage of Safety Stock",
|
||||
"stock_above_safety": "Stock Above Safety",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"transfer_stock": "Transfer Stock",
|
||||
"fulfillment_recommendations": "Fulfillment Recommendations",
|
||||
"critical_outlets": "Critical Outlets",
|
||||
"critical_outlets_description": "{count} outlets require immediate attention for inventory issues",
|
||||
"inventory_optimization": "Inventory Optimization",
|
||||
"low_coverage_recommendation": "Low inventory coverage detected - consider replenishment",
|
||||
"good_coverage_recommendation": "Good inventory coverage - maintain current levels",
|
||||
"fulfillment_excellence": "Fulfillment Excellence",
|
||||
"high_fulfillment_congrats": "Excellent fulfillment rate of {rate}% - keep up the great work!",
|
||||
"maintain_excellence": "Maintain Excellence",
|
||||
"prioritize_transfers": "Prioritize Stock Transfers",
|
||||
"run_optimization": "Run Optimization",
|
||||
"real_time_inventory_alerts": "Real-time Inventory Alerts",
|
||||
"recent_inventory_events": "Recent Inventory Events",
|
||||
"all_outlets_healthy": "All outlets have healthy inventory levels"
|
||||
},
|
||||
"pending_deliveries": {
|
||||
"title": "Pending Deliveries",
|
||||
|
||||
@@ -71,9 +71,37 @@
|
||||
"items_needed": "artículos necesarios"
|
||||
},
|
||||
"production": {
|
||||
"title": "¿Qué necesito producir hoy?",
|
||||
"title": "Estado de Producción",
|
||||
"empty": "Sin producción programada para hoy",
|
||||
"batches_pending": "lotes pendientes"
|
||||
"batches_pending": "lotes pendientes",
|
||||
"equipment_status": "Estado del Equipo",
|
||||
"temperature": "Temperatura",
|
||||
"utilization": "Utilización",
|
||||
"next_maintenance": "Próximo Mantenimiento",
|
||||
"last_maintenance": "Último Mantenimiento",
|
||||
"view_details": "Ver Detalles",
|
||||
"status_critical": "Crítico",
|
||||
"status_warning": "Advertencia",
|
||||
"status_normal": "Normal",
|
||||
"efficiency_metrics": "Métricas de Eficiencia de Producción",
|
||||
"on_time_start_rate": "Tasa de Inicio a Tiempo",
|
||||
"batches_started_on_time": "Lotes iniciados a tiempo",
|
||||
"efficiency_rate": "Tasa de Eficiencia",
|
||||
"overall_efficiency": "Eficiencia general de producción",
|
||||
"active_alerts": "Alertas Activas",
|
||||
"issues_require_attention": "Problemas que requieren atención",
|
||||
"ai_prevented": "Problemas Prevenidos por IA",
|
||||
"problems_prevented": "Problemas prevenidos por IA",
|
||||
"quick_actions": "Acciones Rápidas",
|
||||
"create_batch": "Crear Lote de Producción",
|
||||
"create_batch_description": "Crear un nuevo lote de producción para la red",
|
||||
"maintenance": "Mantenimiento de Equipos",
|
||||
"schedule_maintenance": "Programar mantenimiento para equipos de producción",
|
||||
"manage_equipment": "Gestionar Equipos",
|
||||
"quality_checks": "Controles de Calidad",
|
||||
"manage_quality": "Gestionar procesos de control de calidad",
|
||||
"quality_management": "Gestión de Calidad",
|
||||
"event_message": "Mensaje"
|
||||
},
|
||||
"po_approvals": {
|
||||
"title": "¿Qué órdenes debo aprobar?",
|
||||
@@ -400,11 +428,36 @@
|
||||
"network_sales": "Ventas de Red",
|
||||
"last_30_days": "últimos 30 días",
|
||||
"production_volume": "Volumen de Producción",
|
||||
"production": "Producción",
|
||||
"pending_orders": "Órdenes Pendientes",
|
||||
"internal_transfers": "transferencias internas",
|
||||
"active_shipments": "Envíos Activos",
|
||||
"today": "hoy",
|
||||
"distribution_map": "Rutas de Distribución",
|
||||
"outlet_fulfillment": "Cumplimiento de Tiendas",
|
||||
"fulfillment_description": "Monitorea la cobertura de inventario, el riesgo de ruptura de stock y el estado de cumplimiento en todas las tiendas",
|
||||
"fulfillment_summary": "Resumen de Cumplimiento",
|
||||
"network_health_score": "Puntuación de Salud de la Red",
|
||||
"overall_fulfillment_health": "Puntuación general de salud de cumplimiento",
|
||||
"avg_inventory_coverage": "Cobertura Promedio de Inventario",
|
||||
"fulfillment_rate": "Tasa de Cumplimiento",
|
||||
"order_fulfillment_rate": "Tasa de cumplimiento de pedidos",
|
||||
"critical_items": "Artículos Críticos",
|
||||
"items_at_risk": "artículos en riesgo de ruptura de stock",
|
||||
"inventory_coverage": "Cobertura de Inventario",
|
||||
"stockout_risk": "Riesgo de Ruptura de Stock",
|
||||
"high_stockout_risk": "Alto Riesgo de Ruptura de Stock",
|
||||
"medium_stockout_risk": "Riesgo Medio de Ruptura de Stock",
|
||||
"fulfillment_recommendations": "Recomendaciones de Cumplimiento",
|
||||
"inventory_optimization": "Optimización de Inventario",
|
||||
"good_coverage_recommendation": "Buena cobertura de inventario - mantener niveles actuales",
|
||||
"real_time_inventory_alerts": "Alertas de Inventario en Tiempo Real",
|
||||
"recent_inventory_events": "Eventos Recientes de Inventario",
|
||||
"across_all_outlets": "en todas las tiendas",
|
||||
"outlet_status_overview": "Resumen del Estado de las Tiendas",
|
||||
"status_normal": "Estado Normal",
|
||||
"risk_low": "Bajo",
|
||||
"risk_medium": "Medio",
|
||||
"outlet_performance": "Rendimiento de Tiendas",
|
||||
"sales": "Ventas",
|
||||
"inventory_value": "Valor de Inventario",
|
||||
@@ -421,6 +474,57 @@
|
||||
"no_performance_data": "No hay datos de rendimiento disponibles",
|
||||
"no_distribution_data": "No hay datos de distribución disponibles",
|
||||
"performance_based_on": "Rendimiento basado en {{metric}} durante {{period}} días",
|
||||
"network_performance": "Rendimiento de Red",
|
||||
"performance_description": "Comparar rendimiento en todas las tiendas de tu red",
|
||||
"performance_variance": "Variación de Rendimiento",
|
||||
"top_to_bottom_spread": "Diferencia entre mejor y peor rendimiento",
|
||||
"avg_sales": "Ventas Promedio",
|
||||
"avg_inventory": "Inventario Promedio",
|
||||
"avg_orders": "Pedidos Promedio",
|
||||
"per_outlet": "por tienda",
|
||||
"total_outlets": "Total de Tiendas",
|
||||
"locations_in_network": "ubicaciones en red",
|
||||
"performance_insights": "Información de Rendimiento",
|
||||
"top_performer": "Mejor Rendimiento",
|
||||
"best_in_network": "Mejor tienda en la red",
|
||||
"needs_attention": "Necesita Atención",
|
||||
"improvement_opportunity": "Oportunidad de mejora identificada",
|
||||
"network_insight": "Información de Red",
|
||||
"high_variation": "Alta variación de rendimiento",
|
||||
"high_variation_description": "Se detectaron brechas significativas de rendimiento - se necesita mejora dirigida",
|
||||
"moderate_variation": "Variación moderada de rendimiento",
|
||||
"moderate_variation_description": "Existe cierta variación de rendimiento - considera compartir conocimientos",
|
||||
"highly_balanced_network": "Red altamente equilibrada",
|
||||
"balanced_network_description": "Tu red muestra un excelente equilibrio con mínimas brechas de rendimiento",
|
||||
"outlet_comparison": "Comparación de Tiendas",
|
||||
"performance_index": "Índice de Rendimiento",
|
||||
"above_average": "Por encima del promedio",
|
||||
"below_average": "Por debajo del promedio",
|
||||
"of_network_avg": "del promedio de la red",
|
||||
"location": "Ubicación",
|
||||
"location_id": "ID de Ubicación",
|
||||
"period": "Período",
|
||||
"days": "días",
|
||||
"of": "de",
|
||||
"no_data": "Sin Datos",
|
||||
"stable": "Estable",
|
||||
"improving": "Mejorando",
|
||||
"declining": "Empeorando",
|
||||
"view_details": "Ver Detalles",
|
||||
"analyze_performance": "Analizar Rendimiento",
|
||||
"card_view": "Vista de Tarjetas",
|
||||
"chart_view": "Vista de Gráficos",
|
||||
"performance_recommendations": "Recomendaciones de Rendimiento",
|
||||
"best_practices": "Compartir Mejores Prácticas",
|
||||
"learn_from_top_performer": "Aprender de las mejores prácticas de {name}",
|
||||
"targeted_improvement": "Mejora Dirigida",
|
||||
"focus_on_bottom_performer": "Enfocar esfuerzos de mejora en {name}",
|
||||
"network_goal": "Objetivo de Rendimiento de Red",
|
||||
"reduce_variance_goal": "Reducir variación de rendimiento de {current}% a {target}%",
|
||||
"set_network_targets": "Establecer Objetivos de Red",
|
||||
"schedule_knowledge_sharing": "Programar Compartición de Conocimientos",
|
||||
"create_improvement_plan": "Crear Plan de Mejora",
|
||||
"performance_based_on_period": "Rendimiento basado en {{metric}} durante {{period}} días",
|
||||
"ranking": "Clasificación",
|
||||
"rank": "Posición",
|
||||
"outlet": "Tienda",
|
||||
@@ -456,7 +560,80 @@
|
||||
"in_transit": "En Tránsito",
|
||||
"delivered": "Entregada",
|
||||
"failed": "Fallida",
|
||||
"distribution_routes": "Rutas de Distribución"
|
||||
"distribution_routes": "Rutas de Distribución",
|
||||
"network_status": "Estado de la Red",
|
||||
"network_health": "Indicadores de Salud de la Red",
|
||||
"on_time_delivery": "Tasa de Entrega a Tiempo",
|
||||
"delivery_performance": "Rendimiento de entrega en toda la red",
|
||||
"issue_prevention": "Tasa de Prevención de Problemas",
|
||||
"issues_prevented": "problemas prevenidos por IA",
|
||||
"active_issues": "Problemas Activos",
|
||||
"action_required": "requieren atención inmediata",
|
||||
"network_efficiency": "Eficiencia de la Red",
|
||||
"operational_efficiency": "Puntuación de eficiencia operativa",
|
||||
"add_outlet": "Agregar Punto de Venta",
|
||||
"add_outlet_description": "Agregar un nuevo punto de venta a tu red de panaderías",
|
||||
"create_outlet": "Crear Punto de Venta",
|
||||
"internal_transfers": "Transferencias Internas",
|
||||
"manage_transfers": "Gestionar transferencias entre obrador central y puntos de venta",
|
||||
"view_transfers": "Ver Transferencias",
|
||||
"view_alerts": "Ver Alertas",
|
||||
"network_alerts_description": "Ver y gestionar alertas e incidencias en toda la red",
|
||||
"view_all_alerts": "Ver Todas las Alertas",
|
||||
"quick_actions": "Acciones Rápidas",
|
||||
"real_time_events": "Eventos de Red en Tiempo Real",
|
||||
"recent_activity": "Actividad Reciente",
|
||||
"live_updates": "Actualizaciones en vivo activadas",
|
||||
"offline": "Modo fuera de línea",
|
||||
"no_recent_activity": "No hay actividad reciente en la red",
|
||||
"waiting_for_updates": "Esperando actualizaciones en tiempo real...",
|
||||
"show_all_events": "Mostrar todos los {count} eventos",
|
||||
"show_less": "Mostrar menos",
|
||||
"distribution_summary": "Resumen de Distribución",
|
||||
"total_deliveries": "Entregas Totales",
|
||||
"all_shipments": "Todos los envíos de hoy",
|
||||
"on_time_deliveries": "Entregas a Tiempo",
|
||||
"on_time_rate": "tasa de puntualidad",
|
||||
"no_deliveries": "No hay entregas aún",
|
||||
"delayed_deliveries": "Entregas Retrasadas",
|
||||
"delay_rate": "tasa de retraso",
|
||||
"no_delays": "No hay retrasos",
|
||||
"in_transit": "En Tránsito",
|
||||
"currently_en_route": "Actualmente en ruta",
|
||||
"route_optimization": "Optimización de Rutas",
|
||||
"distance_saved": "Distancia Ahorrada",
|
||||
"total_distance_saved": "Distancia total ahorrada por optimización",
|
||||
"time_saved": "Tiempo Ahorrado",
|
||||
"total_time_saved": "Tiempo total ahorrado por optimización",
|
||||
"fuel_saved": "Combustible Ahorrado",
|
||||
"estimated_fuel_savings": "Ahorro estimado de combustible",
|
||||
"co2_saved": "CO2 Ahorrado",
|
||||
"estimated_co2_reduction": "Reducción estimada de CO2",
|
||||
"active_routes": "Rutas Activas",
|
||||
"distance": "Distancia",
|
||||
"estimated_duration": "Duración Estimada",
|
||||
"stops": "Paradas",
|
||||
"optimization": "Optimización",
|
||||
"optimization_savings": "Ahorro de Optimización",
|
||||
"vehicles": "Vehículos",
|
||||
"route_completed": "Ruta Completada",
|
||||
"route_delayed": "Ruta Retrasada",
|
||||
"route_in_transit": "En Tránsito",
|
||||
"route_pending": "Pendiente",
|
||||
"track_route": "Seguir Ruta",
|
||||
"real_time_delivery_events": "Eventos de Entrega en Tiempo Real",
|
||||
"recent_delivery_activity": "Actividad Reciente de Entrega",
|
||||
"no_recent_delivery_activity": "No hay actividad reciente de entrega",
|
||||
"route": "Ruta",
|
||||
"optimize_routes": "Optimizar Rutas",
|
||||
"optimize_routes_description": "Ejecutar optimización de rutas para las entregas de hoy",
|
||||
"run_optimization": "Ejecutar Optimización",
|
||||
"manage_vehicles": "Gestionar Vehículos",
|
||||
"manage_vehicle_fleet": "Gestionar flota de vehículos y asignaciones",
|
||||
"view_vehicles": "Ver Vehículos",
|
||||
"live_tracking": "Seguimiento GPS en Vivo",
|
||||
"real_time_gps_tracking": "Seguimiento GPS en tiempo real de todos los vehículos",
|
||||
"open_tracking_map": "Abrir Mapa de Seguimiento"
|
||||
},
|
||||
"ai_insights": {
|
||||
"title": "Insights de IA",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../../api/hooks/useEnterpriseDashboard';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../../components/ui/Card';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../../components/ui/Tabs';
|
||||
import {
|
||||
TrendingUp,
|
||||
MapPin,
|
||||
@@ -27,7 +28,11 @@ import {
|
||||
PackageCheck,
|
||||
Building2,
|
||||
ArrowLeft,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
Target,
|
||||
Warehouse,
|
||||
ShoppingCart,
|
||||
ShieldCheck
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LoadingSpinner } from '../../components/ui/LoadingSpinner';
|
||||
@@ -35,11 +40,18 @@ import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { apiClient } from '../../api/client/apiClient';
|
||||
import { useEnterprise } from '../../contexts/EnterpriseContext';
|
||||
import { useTenant } from '../../stores/tenant.store';
|
||||
import { useSSEEvents } from '../../hooks/useSSE';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
// Components for enterprise dashboard
|
||||
const NetworkSummaryCards = React.lazy(() => import('../../components/dashboard/NetworkSummaryCards'));
|
||||
const DistributionMap = React.lazy(() => import('../../components/maps/DistributionMap'));
|
||||
const PerformanceChart = React.lazy(() => import('../../components/charts/PerformanceChart'));
|
||||
const NetworkOverviewTab = React.lazy(() => import('../../components/dashboard/NetworkOverviewTab'));
|
||||
const NetworkPerformanceTab = React.lazy(() => import('../../components/dashboard/NetworkPerformanceTab'));
|
||||
const OutletFulfillmentTab = React.lazy(() => import('../../components/dashboard/OutletFulfillmentTab'));
|
||||
const ProductionTab = React.lazy(() => import('../../components/dashboard/ProductionTab'));
|
||||
const DistributionTab = React.lazy(() => import('../../components/dashboard/DistributionTab'));
|
||||
|
||||
interface EnterpriseDashboardPageProps {
|
||||
tenantId?: string;
|
||||
@@ -56,6 +68,51 @@ const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenan
|
||||
const [selectedMetric, setSelectedMetric] = useState('sales');
|
||||
const [selectedPeriod, setSelectedPeriod] = useState(30);
|
||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// SSE Integration for real-time updates
|
||||
const { events: sseEvents } = useSSEEvents({
|
||||
channels: ['*.alerts', '*.notifications', 'recommendations']
|
||||
});
|
||||
|
||||
// Invalidate enterprise data on relevant SSE events
|
||||
useEffect(() => {
|
||||
if (sseEvents.length === 0 || !tenantId) return;
|
||||
|
||||
const latest = sseEvents[0];
|
||||
const relevantEventTypes = [
|
||||
'batch_completed', 'batch_started', 'batch_state_changed',
|
||||
'delivery_received', 'delivery_overdue', 'delivery_arriving_soon',
|
||||
'stock_receipt_incomplete', 'orchestration_run_completed',
|
||||
'production_delay', 'batch_start_delayed', 'equipment_maintenance',
|
||||
'network_alert', 'outlet_performance_update', 'distribution_route_update'
|
||||
];
|
||||
|
||||
if (relevantEventTypes.includes(latest.event_type)) {
|
||||
// Invalidate all enterprise dashboard queries
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['enterprise', 'network-summary', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['enterprise', 'children-performance', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['enterprise', 'distribution-overview', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['enterprise', 'forecast-summary', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['control-panel-data', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
}
|
||||
}, [sseEvents, tenantId, queryClient]);
|
||||
|
||||
// Check if tenantId is available at the start
|
||||
useEffect(() => {
|
||||
@@ -273,258 +330,187 @@ const EnterpriseDashboardPage: React.FC<EnterpriseDashboardPageProps> = ({ tenan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Network Summary Cards */}
|
||||
<div className="mb-8">
|
||||
<NetworkSummaryCards
|
||||
data={networkSummary}
|
||||
isLoading={isNetworkSummaryLoading}
|
||||
/>
|
||||
</div>
|
||||
{/* Main Tabs Structure */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
||||
<TabsList className="grid grid-cols-1 sm:grid-cols-3 lg:grid-cols-8 gap-2 mb-8">
|
||||
<TabsTrigger value="overview">
|
||||
<Network className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.network_status')}
|
||||
</TabsTrigger>
|
||||
|
||||
{/* Distribution Map and Performance Chart Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{/* Distribution Map */}
|
||||
<div>
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Truck className="w-5 h-5 text-[var(--color-info)]" />
|
||||
<CardTitle>{t('enterprise.distribution_map')}</CardTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4 text-[var(--text-secondary)]" />
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
className="border border-[var(--border-primary)] rounded-md px-2 py-1 text-sm bg-[var(--input-bg)] text-[var(--text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{distributionOverview ? (
|
||||
<DistributionMap
|
||||
routes={distributionOverview.route_sequences}
|
||||
shipments={distributionOverview.status_counts}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-96 flex items-center justify-center text-[var(--text-secondary)]">
|
||||
{t('enterprise.no_distribution_data')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<TabsTrigger value="network-performance">
|
||||
<Target className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.network_performance')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="fulfillment">
|
||||
<Warehouse className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.outlet_fulfillment')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="distribution">
|
||||
<Truck className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.distribution_map')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="forecast">
|
||||
<TrendingUp className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.network_forecast')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="production">
|
||||
<Package className="w-4 h-4 mr-2" />
|
||||
{t('enterprise.production')}
|
||||
</TabsTrigger>
|
||||
|
||||
{/* Performance Chart */}
|
||||
<div>
|
||||
<Card className="h-full">
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<BarChart3 className="w-5 h-5 text-[var(--color-success)]" />
|
||||
<CardTitle>{t('enterprise.outlet_performance')}</CardTitle>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
value={selectedMetric}
|
||||
onChange={(e) => setSelectedMetric(e.target.value)}
|
||||
className="border border-[var(--border-primary)] rounded-md px-2 py-1 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>
|
||||
</select>
|
||||
<select
|
||||
value={selectedPeriod}
|
||||
onChange={(e) => setSelectedPeriod(Number(e.target.value))}
|
||||
className="border border-[var(--border-primary)] rounded-md px-2 py-1 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>
|
||||
</select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{childrenPerformance ? (
|
||||
<PerformanceChart
|
||||
data={childrenPerformance.rankings}
|
||||
metric={selectedMetric}
|
||||
period={selectedPeriod}
|
||||
onOutletClick={handleOutletClick}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-96 flex items-center justify-center text-[var(--text-secondary)]">
|
||||
{t('enterprise.no_performance_data')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</TabsList>
|
||||
|
||||
{/* Forecast Summary */}
|
||||
<div className="mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
<CardTitle>{t('enterprise.network_forecast')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{forecastSummary && forecastSummary.aggregated_forecasts ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Total Demand Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-info-100)' }}
|
||||
>
|
||||
<Package className="w-5 h-5" style={{ color: 'var(--color-info-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-info-800)' }}>
|
||||
{t('enterprise.total_demand')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-info-900)' }}>
|
||||
{Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
|
||||
total + Object.values(day).reduce((dayTotal: number, product: any) =>
|
||||
dayTotal + (product.predicted_demand || 0), 0), 0
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Tab Content */}
|
||||
<TabsContent value="overview">
|
||||
<NetworkOverviewTab
|
||||
tenantId={tenantId!}
|
||||
onOutletClick={handleOutletClick}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Days Forecast Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-success-100)' }}
|
||||
>
|
||||
<Calendar className="w-5 h-5" style={{ color: 'var(--color-success-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-success-800)' }}>
|
||||
{t('enterprise.days_forecast')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
|
||||
{forecastSummary.days_forecast || 7}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Average Daily Demand Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-secondary-100)' }}
|
||||
>
|
||||
<Activity className="w-5 h-5" style={{ color: 'var(--color-secondary-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-secondary-800)' }}>
|
||||
{t('enterprise.avg_daily_demand')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-secondary-900)' }}>
|
||||
{forecastSummary.aggregated_forecasts
|
||||
? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
|
||||
total + Object.values(day).reduce((dayTotal: number, product: any) =>
|
||||
dayTotal + (product.predicted_demand || 0), 0), 0) /
|
||||
Object.keys(forecastSummary.aggregated_forecasts).length
|
||||
).toLocaleString()
|
||||
: 0}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Last Updated Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-warning-100)' }}
|
||||
>
|
||||
<Clock className="w-5 h-5" style={{ color: 'var(--color-warning-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-warning-800)' }}>
|
||||
{t('enterprise.last_updated')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
|
||||
{forecastSummary.last_updated ?
|
||||
new Date(forecastSummary.last_updated).toLocaleTimeString() :
|
||||
'N/A'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-48 text-[var(--text-secondary)]">
|
||||
{t('enterprise.no_forecast_data')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<TabsContent value="network-performance">
|
||||
<NetworkPerformanceTab
|
||||
tenantId={tenantId!}
|
||||
onOutletClick={handleOutletClick}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<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">
|
||||
<Building2 className="w-6 h-6 text-[var(--color-primary)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Agregar Punto de Venta</h3>
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)] mb-4">Añadir un nuevo outlet a la red enterprise</p>
|
||||
<Button
|
||||
onClick={() => navigate(`/app/tenants/${tenantId}/settings/organization`)}
|
||||
className="w-full"
|
||||
>
|
||||
Crear Outlet
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="distribution">
|
||||
<DistributionTab
|
||||
tenantId={tenantId!}
|
||||
selectedDate={selectedDate}
|
||||
onDateChange={setSelectedDate}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<PackageCheck className="w-6 h-6 text-[var(--color-success)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Transferencias Internas</h3>
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)] mb-4">Gestionar pedidos entre obrador central y outlets</p>
|
||||
<Button
|
||||
onClick={() => navigate(`/app/tenants/${tenantId}/procurement/internal-transfers`)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Ver Transferencias
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TabsContent value="forecast">
|
||||
{/* Forecast Summary */}
|
||||
<div className="mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
<CardTitle>{t('enterprise.network_forecast')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{forecastSummary && forecastSummary.aggregated_forecasts ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Total Demand Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-info-100)' }}
|
||||
>
|
||||
<Package className="w-5 h-5" style={{ color: 'var(--color-info-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-info-800)' }}>
|
||||
{t('enterprise.total_demand')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-info-900)' }}>
|
||||
{Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
|
||||
total + Object.values(day).reduce((dayTotal: number, product: any) =>
|
||||
dayTotal + (product.predicted_demand || 0), 0), 0
|
||||
).toLocaleString()}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<MapPin className="w-6 h-6 text-[var(--color-info)]" />
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Rutas de Distribución</h3>
|
||||
</div>
|
||||
<p className="text-[var(--text-secondary)] mb-4">Optimizar rutas de entrega entre ubicaciones</p>
|
||||
<Button
|
||||
onClick={() => navigate(`/app/tenants/${tenantId}/distribution/routes`)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Ver Rutas
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Days Forecast Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-success-100)' }}
|
||||
>
|
||||
<Calendar className="w-5 h-5" style={{ color: 'var(--color-success-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-success-800)' }}>
|
||||
{t('enterprise.days_forecast')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-success-900)' }}>
|
||||
{forecastSummary.days_forecast || 7}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Average Daily Demand Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-secondary-100)' }}
|
||||
>
|
||||
<Activity className="w-5 h-5" style={{ color: 'var(--color-secondary-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-secondary-800)' }}>
|
||||
{t('enterprise.avg_daily_demand')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-3xl font-bold" style={{ color: 'var(--color-secondary-900)' }}>
|
||||
{forecastSummary.aggregated_forecasts
|
||||
? Math.round(Object.values(forecastSummary.aggregated_forecasts).reduce((total: number, day: any) =>
|
||||
total + Object.values(day).reduce((dayTotal: number, product: any) =>
|
||||
dayTotal + (product.predicted_demand || 0), 0), 0) /
|
||||
Object.keys(forecastSummary.aggregated_forecasts).length
|
||||
).toLocaleString()
|
||||
: 0}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Last Updated Card */}
|
||||
<Card className="hover:shadow-lg transition-shadow duration-300">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center shadow-md"
|
||||
style={{ backgroundColor: 'var(--color-warning-100)' }}
|
||||
>
|
||||
<Clock className="w-5 h-5" style={{ color: 'var(--color-warning-600)' }} />
|
||||
</div>
|
||||
<h3 className="font-semibold text-sm" style={{ color: 'var(--color-warning-800)' }}>
|
||||
{t('enterprise.last_updated')}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-lg font-semibold" style={{ color: 'var(--color-warning-900)' }}>
|
||||
{forecastSummary.last_updated ?
|
||||
new Date(forecastSummary.last_updated).toLocaleTimeString() :
|
||||
'N/A'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-48 text-[var(--text-secondary)]">
|
||||
{t('enterprise.no_forecast_data')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="fulfillment">
|
||||
<OutletFulfillmentTab
|
||||
tenantId={tenantId!}
|
||||
onOutletClick={handleOutletClick}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="production">
|
||||
<ProductionTab tenantId={tenantId!} />
|
||||
</TabsContent>
|
||||
|
||||
|
||||
</Tabs>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user