Add frontend alerts imporvements
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { alertService } from '../services/alertService';
|
||||
import { getPendingApprovalPurchaseOrders } from '../services/purchase_orders';
|
||||
import { productionService } from '../services/production';
|
||||
@@ -17,6 +17,9 @@ import { aiInsightsService } from '../services/aiInsights';
|
||||
import { useSSEEvents } from '../../hooks/useSSE';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
// Debounce delay for SSE-triggered query invalidations (ms)
|
||||
const SSE_INVALIDATION_DEBOUNCE_MS = 500;
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
@@ -228,13 +231,15 @@ export function useControlPanelData(tenantId: string) {
|
||||
supplierMap.set(supplier.id, supplier.name || supplier.supplier_name);
|
||||
});
|
||||
|
||||
// Merge SSE events with API data
|
||||
const allAlerts = [...alerts];
|
||||
// Merge SSE events with API data (deduplicate by ID, prioritizing SSE events as they're newer)
|
||||
let allAlerts: any[];
|
||||
if (sseEvents.length > 0) {
|
||||
// Merge SSE events, prioritizing newer events
|
||||
const sseEventIds = new Set(sseEvents.map(e => e.id));
|
||||
const mergedAlerts = alerts.filter(alert => !sseEventIds.has(alert.id));
|
||||
allAlerts.push(...sseEvents);
|
||||
// Filter out API alerts that also exist in SSE (SSE has newer data)
|
||||
const uniqueApiAlerts = alerts.filter((alert: any) => !sseEventIds.has(alert.id));
|
||||
allAlerts = [...uniqueApiAlerts, ...sseEvents];
|
||||
} else {
|
||||
allAlerts = [...alerts];
|
||||
}
|
||||
|
||||
// Apply data priority rules for POs
|
||||
@@ -327,6 +332,32 @@ export function useControlPanelData(tenantId: string) {
|
||||
!a.hidden_from_ui &&
|
||||
a.status === 'active'
|
||||
);
|
||||
|
||||
// Debug: Log alert counts by type_class
|
||||
console.log('📊 [useControlPanelData] Alert analysis:', {
|
||||
totalAlerts: allAlerts.length,
|
||||
fromAPI: alerts.length,
|
||||
fromSSE: sseEvents.length,
|
||||
preventedIssuesCount: preventedIssues.length,
|
||||
actionNeededCount: actionNeededAlerts.length,
|
||||
typeClassBreakdown: allAlerts.reduce((acc: Record<string, number>, a: any) => {
|
||||
const typeClass = a.type_class || 'unknown';
|
||||
acc[typeClass] = (acc[typeClass] || 0) + 1;
|
||||
return acc;
|
||||
}, {}),
|
||||
apiAlertsSample: alerts.slice(0, 3).map((a: any) => ({
|
||||
id: a.id,
|
||||
event_type: a.event_type,
|
||||
type_class: a.type_class,
|
||||
status: a.status,
|
||||
})),
|
||||
sseEventsSample: sseEvents.slice(0, 3).map((a: any) => ({
|
||||
id: a.id,
|
||||
event_type: a.event_type,
|
||||
type_class: a.type_class,
|
||||
status: a.status,
|
||||
})),
|
||||
});
|
||||
|
||||
// Calculate total issues requiring action:
|
||||
// 1. Action needed alerts
|
||||
@@ -387,24 +418,51 @@ export function useControlPanelData(tenantId: string) {
|
||||
retry: 2,
|
||||
});
|
||||
|
||||
// SSE integration - invalidate query on relevant events
|
||||
// Ref for debouncing SSE-triggered invalidations
|
||||
const invalidationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const lastEventCountRef = useRef<number>(0);
|
||||
|
||||
// SSE integration - invalidate query on relevant events (debounced)
|
||||
useEffect(() => {
|
||||
if (sseAlerts.length > 0 && tenantId) {
|
||||
const relevantEvents = sseAlerts.filter(event =>
|
||||
event.event_type.includes('production.') ||
|
||||
event.event_type.includes('batch_') ||
|
||||
event.event_type.includes('delivery') ||
|
||||
event.event_type.includes('purchase_order') ||
|
||||
event.event_type.includes('equipment_')
|
||||
);
|
||||
|
||||
if (relevantEvents.length > 0) {
|
||||
// Skip if no new events since last check
|
||||
if (sseAlerts.length === 0 || !tenantId || sseAlerts.length === lastEventCountRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relevantEvents = sseAlerts.filter(event =>
|
||||
event.event_type?.includes('production.') ||
|
||||
event.event_type?.includes('batch_') ||
|
||||
event.event_type?.includes('delivery') ||
|
||||
event.event_type?.includes('purchase_order') ||
|
||||
event.event_type?.includes('equipment_') ||
|
||||
event.event_type?.includes('insight') ||
|
||||
event.event_type?.includes('recommendation') ||
|
||||
event.event_type?.includes('ai_') || // Match ai_yield_prediction, ai_*, etc.
|
||||
event.event_class === 'recommendation'
|
||||
);
|
||||
|
||||
if (relevantEvents.length > 0) {
|
||||
// Clear existing timeout to debounce rapid events
|
||||
if (invalidationTimeoutRef.current) {
|
||||
clearTimeout(invalidationTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Debounce the invalidation to prevent multiple rapid refetches
|
||||
invalidationTimeoutRef.current = setTimeout(() => {
|
||||
lastEventCountRef.current = sseAlerts.length;
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['control-panel-data', tenantId],
|
||||
refetchType: 'active',
|
||||
});
|
||||
}
|
||||
}, SSE_INVALIDATION_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
// Cleanup timeout on unmount or dependency change
|
||||
return () => {
|
||||
if (invalidationTimeoutRef.current) {
|
||||
clearTimeout(invalidationTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [sseAlerts, tenantId, queryClient]);
|
||||
|
||||
return query;
|
||||
|
||||
Reference in New Issue
Block a user