Add frontend alerts imporvements

This commit is contained in:
Urtzi Alfaro
2025-12-29 08:11:29 +01:00
parent 96d8576103
commit 2e7e1f5557
7 changed files with 351 additions and 190 deletions

View File

@@ -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;