603 lines
24 KiB
TypeScript
603 lines
24 KiB
TypeScript
/*
|
|
* 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; |