670 lines
27 KiB
TypeScript
670 lines
27 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';
|
|
import { useChildTenants } from '../../api/hooks/useEnterpriseDashboard';
|
|
import { inventoryService } from '../../api/services/inventory';
|
|
|
|
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');
|
|
|
|
// Get child tenants data
|
|
const { data: childTenants, isLoading: isChildTenantsLoading } = useChildTenants(tenantId);
|
|
|
|
// State for real-time inventory data
|
|
const [inventoryData, setInventoryData] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Combine loading states
|
|
const isLoading = isChildTenantsLoading || loading;
|
|
|
|
// Real-time SSE events
|
|
const { events: sseEvents, isConnected: sseConnected } = useSSEEvents({
|
|
channels: ['*.alerts', '*.notifications', 'recommendations']
|
|
});
|
|
|
|
// Process SSE events for inventory updates
|
|
useEffect(() => {
|
|
if (sseEvents.length === 0 || inventoryData.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, inventoryData]);
|
|
|
|
// Fetch inventory data for each child tenant individually
|
|
useEffect(() => {
|
|
if (!childTenants) {
|
|
setInventoryData([]);
|
|
setLoading(true);
|
|
return;
|
|
}
|
|
|
|
const fetchAllInventoryData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const promises = childTenants.map(async (tenant) => {
|
|
try {
|
|
// Using the imported service directly
|
|
const inventoryData = await inventoryService.getDashboardSummary(tenant.id);
|
|
return { tenant, inventoryData };
|
|
} catch (error) {
|
|
console.error(`Error fetching inventory for tenant ${tenant.id}:`, error);
|
|
return { tenant, inventoryData: null };
|
|
}
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
const processedData = results.map(({ tenant, inventoryData }) => {
|
|
// Calculate inventory metrics
|
|
const totalValue = inventoryData?.total_value || 0;
|
|
const outOfStockCount = inventoryData?.out_of_stock_count || 0;
|
|
const lowStockCount = inventoryData?.low_stock_count || 0;
|
|
const adequateStockCount = inventoryData?.adequate_stock_count || 0;
|
|
const totalIngredients = inventoryData?.total_ingredients || 0;
|
|
|
|
// Calculate coverage percentage (simplified calculation)
|
|
const coverage = totalIngredients > 0
|
|
? Math.min(100, Math.round(((adequateStockCount + lowStockCount) / totalIngredients) * 100))
|
|
: 100;
|
|
|
|
// Determine risk level based on out-of-stock and low-stock items
|
|
let riskLevel = 'low';
|
|
if (outOfStockCount > 5 || (outOfStockCount > 0 && lowStockCount > 10)) {
|
|
riskLevel = 'critical';
|
|
} else if (outOfStockCount > 0 || lowStockCount > 5) {
|
|
riskLevel = 'high';
|
|
} else if (lowStockCount > 2) {
|
|
riskLevel = 'medium';
|
|
}
|
|
|
|
// Determine status based on risk level
|
|
let status = 'normal';
|
|
if (riskLevel === 'critical') status = 'critical';
|
|
else if (riskLevel === 'high' || riskLevel === 'medium') status = 'warning';
|
|
|
|
return {
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
inventoryCoverage: coverage,
|
|
stockoutRisk: riskLevel,
|
|
criticalItems: outOfStockCount,
|
|
fulfillmentRate: 95, // Placeholder - would come from actual fulfillment data
|
|
lastUpdated: new Date().toISOString(),
|
|
status: status,
|
|
products: [] // Will be populated if detailed view is needed
|
|
};
|
|
});
|
|
|
|
setInventoryData(processedData);
|
|
} catch (error) {
|
|
console.error('Error fetching inventory data:', error);
|
|
setInventoryData([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchAllInventoryData();
|
|
}, [childTenants]);
|
|
|
|
// 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>
|
|
{isLoading ? (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{[...Array(3)].map((_, index) => (
|
|
<Card key={index} className="animate-pulse">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
|
|
<div className="w-8 h-8 rounded-full bg-[var(--bg-tertiary)]"></div>
|
|
</div>
|
|
<div className="h-6 bg-[var(--bg-tertiary)] rounded w-1/2 mb-2"></div>
|
|
<div className="h-4 bg-[var(--bg-tertiary)] rounded w-full mb-4"></div>
|
|
<div className="h-2 bg-[var(--bg-tertiary)] rounded w-full mb-2"></div>
|
|
<div className="h-2 bg-[var(--bg-tertiary)] rounded w-3/4"></div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<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');
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
{inventoryData.length === 0 && (
|
|
<div className="col-span-full py-12">
|
|
<div className="text-center text-[var(--text-secondary)]">
|
|
<Warehouse className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
|
<p>{t('enterprise.no_outlets')}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</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; |