/* * 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 = ({ tenantId, onOutletClick }) => { const { t } = useTranslation('dashboard'); const [selectedOutlet, setSelectedOutlet] = useState(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([]); 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 (
{/* Fulfillment Header */}

{t('enterprise.outlet_fulfillment')}

{t('enterprise.fulfillment_description')}

{/* View Mode Selector */}
{/* Network Fulfillment Summary */}

{t('enterprise.fulfillment_summary')}

{/* Network Health Score */} {t('enterprise.network_health_score')}
{networkMetrics.networkHealth}%

{t('enterprise.overall_fulfillment_health')}

{/* Average Inventory Coverage */} {t('enterprise.avg_inventory_coverage')}
{networkMetrics.avgCoverage}%

{t('enterprise.across_all_outlets')}

{/* Fulfillment Rate */} {t('enterprise.fulfillment_rate')}
{networkMetrics.avgFulfillment}%

{t('enterprise.order_fulfillment_rate')}

{/* Critical Items */} {t('enterprise.critical_items')}
{networkMetrics.totalCriticalItems}

{t('enterprise.items_at_risk')}

{/* Outlet Status Overview */}

{t('enterprise.outlet_status_overview')}

{isLoading ? (
{[...Array(3)].map((_, index) => (
))}
) : (
{inventoryData.map((outlet) => { const statusConfig = getOutletStatusConfig(outlet.id); return ( { setSelectedOutlet(outlet.id); setViewMode('detailed'); onOutletClick(outlet.id, outlet.name); }, priority: 'primary' }] : []} onClick={() => { setSelectedOutlet(outlet.id); setViewMode('detailed'); }} /> ); })} {inventoryData.length === 0 && (

{t('enterprise.no_outlets')}

)}
)}
{/* Detailed View - Product Level Inventory */} {viewMode === 'detailed' && selectedOutlet && (

{t('enterprise.product_level_inventory')}

{inventoryData .find(outlet => outlet.id === selectedOutlet) ?.products.map((product) => { const riskConfig = getRiskConfig(product.risk); return ( = 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' } ]} /> ); })}
)} {/* Fulfillment Recommendations */}

{t('enterprise.fulfillment_recommendations')}

{/* Critical Outlets */} {networkMetrics.criticalOutlets > 0 && (

{t('enterprise.critical_outlets')}

{t('enterprise.critical_outlets_description', { count: networkMetrics.criticalOutlets })}

)} {/* Inventory Optimization */}

{t('enterprise.inventory_optimization')}

{networkMetrics.avgCoverage < 70 ? t('enterprise.low_coverage_recommendation') : t('enterprise.good_coverage_recommendation')}

{/* Fulfillment Excellence */} {networkMetrics.avgFulfillment > 95 && (

{t('enterprise.fulfillment_excellence')}

{t('enterprise.high_fulfillment_congrats', { rate: networkMetrics.avgFulfillment })}

)}
{/* Real-time Inventory Alerts */}

{t('enterprise.real_time_inventory_alerts')}

{t('enterprise.recent_inventory_events')} {sseConnected ? (
{t('enterprise.live_updates')}
) : (
{t('enterprise.offline')}
)}
{sseConnected ? (
{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 (

{outlet.name} - {statusConfig?.text}

{new Date(outlet.lastUpdated).toLocaleTimeString()}

{t('enterprise.inventory_coverage')}: {outlet.inventoryCoverage}% | {t('enterprise.critical_items')}: {outlet.criticalItems}

{t('enterprise.fulfillment_rate')}: {outlet.fulfillmentRate}%

); })} {inventoryData.filter(outlet => outlet.status !== 'normal').length === 0 && (

{t('enterprise.all_outlets_healthy')}

)}
) : (
{t('enterprise.waiting_for_updates')}
)}
); }; export default OutletFulfillmentTab;