fix demo session 3
This commit is contained in:
@@ -10,6 +10,8 @@ import { Package, AlertTriangle, CheckCircle2, Activity, Clock, Warehouse, Shopp
|
||||
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;
|
||||
@@ -21,67 +23,28 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
|
||||
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']
|
||||
});
|
||||
// Get child tenants data
|
||||
const { data: childTenants, isLoading: isChildTenantsLoading } = useChildTenants(tenantId);
|
||||
|
||||
// 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 }
|
||||
]
|
||||
}
|
||||
]);
|
||||
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) return;
|
||||
if (sseEvents.length === 0 || inventoryData.length === 0) return;
|
||||
|
||||
// Filter inventory-related events
|
||||
const inventoryEvents = sseEvents.filter(event =>
|
||||
event.event_type.includes('inventory_') ||
|
||||
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'
|
||||
@@ -93,8 +56,8 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
|
||||
setInventoryData(prevData => {
|
||||
return prevData.map(outlet => {
|
||||
// Find events for this outlet
|
||||
const outletEvents = inventoryEvents.filter(event =>
|
||||
event.entity_id === outlet.id ||
|
||||
const outletEvents = inventoryEvents.filter(event =>
|
||||
event.entity_id === outlet.id ||
|
||||
event.event_metadata?.outlet_id === outlet.id
|
||||
);
|
||||
|
||||
@@ -145,7 +108,84 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
|
||||
};
|
||||
});
|
||||
});
|
||||
}, [sseEvents]);
|
||||
}, [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 = () => {
|
||||
@@ -336,56 +376,83 @@ const OutletFulfillmentTab: React.FC<OutletFulfillmentTabProps> = ({ tenantId, o
|
||||
<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: () => {
|
||||
{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');
|
||||
onOutletClick(outlet.id, outlet.name);
|
||||
},
|
||||
priority: 'primary'
|
||||
}] : []}
|
||||
onClick={() => {
|
||||
setSelectedOutlet(outlet.id);
|
||||
setViewMode('detailed');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{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 */}
|
||||
|
||||
Reference in New Issue
Block a user