New alert service
This commit is contained in:
74
frontend/src/contexts/AlertContext.tsx
Normal file
74
frontend/src/contexts/AlertContext.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Unified Alert Context
|
||||
*
|
||||
* Context provider for sharing alert state across components
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, ReactNode, useState, useEffect, useCallback } from 'react';
|
||||
import { Alert } from '../api/types/events';
|
||||
import { useUnifiedAlerts } from '../api/hooks/useUnifiedAlerts';
|
||||
import { AlertFilterOptions } from '../utils/alertManagement';
|
||||
|
||||
// Define context type
|
||||
interface AlertContextType {
|
||||
alerts: Alert[];
|
||||
filteredAlerts: Alert[];
|
||||
stats: any;
|
||||
filters: AlertFilterOptions;
|
||||
setFilters: (filters: AlertFilterOptions) => void;
|
||||
search: string;
|
||||
setSearch: (search: string) => void;
|
||||
isLoading: boolean;
|
||||
isRefetching: boolean;
|
||||
error: Error | null;
|
||||
refetch: () => void;
|
||||
acknowledgeAlert: (alertId: string) => Promise<void>;
|
||||
resolveAlert: (alertId: string) => Promise<void>;
|
||||
cancelAutoAction: (alertId: string) => Promise<void>;
|
||||
acknowledgeAlertsByMetadata: (alertType: string, metadata: any) => Promise<void>;
|
||||
resolveAlertsByMetadata: (alertType: string, metadata: any) => Promise<void>;
|
||||
isSSEConnected: boolean;
|
||||
sseError: Error | null;
|
||||
}
|
||||
|
||||
// Create context with default values
|
||||
const AlertContext = createContext<AlertContextType | undefined>(undefined);
|
||||
|
||||
// Props for the provider
|
||||
interface AlertProviderProps {
|
||||
children: ReactNode;
|
||||
tenantId: string;
|
||||
initialFilters?: AlertFilterOptions;
|
||||
}
|
||||
|
||||
// Alert Provider Component
|
||||
export const AlertProvider: React.FC<AlertProviderProps> = ({
|
||||
children,
|
||||
tenantId,
|
||||
initialFilters = {}
|
||||
}) => {
|
||||
// Use the unified hook
|
||||
const unifiedAlerts = useUnifiedAlerts(tenantId, initialFilters, {
|
||||
refetchInterval: 60000, // 1 minute
|
||||
enableSSE: true,
|
||||
sseChannels: [`*.alerts`, `*.notifications`]
|
||||
});
|
||||
|
||||
return (
|
||||
<AlertContext.Provider value={unifiedAlerts}>
|
||||
{children}
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Custom hook to use the alert context
|
||||
export const useAlertContext = (): AlertContextType => {
|
||||
const context = useContext(AlertContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAlertContext must be used within an AlertProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Export the context for use in components
|
||||
export default AlertContext;
|
||||
117
frontend/src/contexts/EnterpriseContext.tsx
Normal file
117
frontend/src/contexts/EnterpriseContext.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
export interface NetworkMetrics {
|
||||
totalSales: number;
|
||||
totalProduction: number;
|
||||
totalInventoryValue: number;
|
||||
averageSales: number;
|
||||
averageProduction: number;
|
||||
averageInventoryValue: number;
|
||||
childCount: number;
|
||||
}
|
||||
|
||||
export interface EnterpriseModeState {
|
||||
isNetworkView: boolean;
|
||||
selectedOutletId: string | null;
|
||||
selectedOutletName: string | null;
|
||||
parentTenantId: string | null;
|
||||
networkMetrics: NetworkMetrics | null;
|
||||
networkViewPath: string | null;
|
||||
}
|
||||
|
||||
interface EnterpriseContextType {
|
||||
state: EnterpriseModeState;
|
||||
enterNetworkView: (parentTenantId: string) => void;
|
||||
drillDownToOutlet: (outletId: string, outletName: string, metrics?: NetworkMetrics) => void;
|
||||
returnToNetworkView: () => void;
|
||||
updateNetworkMetrics: (metrics: NetworkMetrics) => void;
|
||||
clearEnterpriseMode: () => void;
|
||||
}
|
||||
|
||||
const EnterpriseContext = createContext<EnterpriseContextType | undefined>(undefined);
|
||||
|
||||
export const useEnterprise = () => {
|
||||
const context = useContext(EnterpriseContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useEnterprise must be used within an EnterpriseProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
interface EnterpriseProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const EnterpriseProvider: React.FC<EnterpriseProviderProps> = ({ children }) => {
|
||||
const [state, setState] = useState<EnterpriseModeState>({
|
||||
isNetworkView: false,
|
||||
selectedOutletId: null,
|
||||
selectedOutletName: null,
|
||||
parentTenantId: null,
|
||||
networkMetrics: null,
|
||||
networkViewPath: null,
|
||||
});
|
||||
|
||||
const enterNetworkView = (parentTenantId: string) => {
|
||||
setState({
|
||||
isNetworkView: true,
|
||||
selectedOutletId: null,
|
||||
selectedOutletName: null,
|
||||
parentTenantId,
|
||||
networkMetrics: null,
|
||||
networkViewPath: window.location.pathname,
|
||||
});
|
||||
};
|
||||
|
||||
const drillDownToOutlet = (outletId: string, outletName: string, metrics?: NetworkMetrics) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isNetworkView: false,
|
||||
selectedOutletId: outletId,
|
||||
selectedOutletName: outletName,
|
||||
networkMetrics: metrics || prev.networkMetrics,
|
||||
}));
|
||||
};
|
||||
|
||||
const returnToNetworkView = () => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isNetworkView: true,
|
||||
selectedOutletId: null,
|
||||
selectedOutletName: null,
|
||||
}));
|
||||
};
|
||||
|
||||
const updateNetworkMetrics = (metrics: NetworkMetrics) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
networkMetrics: metrics,
|
||||
}));
|
||||
};
|
||||
|
||||
const clearEnterpriseMode = () => {
|
||||
setState({
|
||||
isNetworkView: false,
|
||||
selectedOutletId: null,
|
||||
selectedOutletName: null,
|
||||
parentTenantId: null,
|
||||
networkMetrics: null,
|
||||
networkViewPath: null,
|
||||
});
|
||||
};
|
||||
|
||||
const contextValue: EnterpriseContextType = {
|
||||
state,
|
||||
enterNetworkView,
|
||||
drillDownToOutlet,
|
||||
returnToNetworkView,
|
||||
updateNetworkMetrics,
|
||||
clearEnterpriseMode,
|
||||
};
|
||||
|
||||
return (
|
||||
<EnterpriseContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</EnterpriseContext.Provider>
|
||||
);
|
||||
};
|
||||
154
frontend/src/contexts/EventContext.tsx
Normal file
154
frontend/src/contexts/EventContext.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Clean Event Context for Global State Management
|
||||
*
|
||||
* NO BACKWARD COMPATIBILITY - Complete rewrite with i18n parameterized content
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
|
||||
import { Event, Alert, Notification, Recommendation } from '../api/types/events';
|
||||
|
||||
// Action types
|
||||
type EventAction =
|
||||
| { type: 'ADD_EVENT'; payload: Event }
|
||||
| { type: 'UPDATE_EVENT'; payload: Event }
|
||||
| { type: 'REMOVE_EVENT'; payload: string } // event ID
|
||||
| { type: 'SET_EVENTS'; payload: Event[] }
|
||||
| { type: 'CLEAR_EVENTS' }
|
||||
| { type: 'MARK_AS_READ'; payload: string } // event ID
|
||||
| { type: 'ACKNOWLEDGE_EVENT'; payload: string } // event ID
|
||||
| { type: 'RESOLVE_EVENT'; payload: string }; // event ID
|
||||
|
||||
// State type
|
||||
interface EventState {
|
||||
events: Event[];
|
||||
unreadCount: number;
|
||||
activeAlerts: Alert[];
|
||||
recentNotifications: Notification[];
|
||||
recommendations: Recommendation[];
|
||||
}
|
||||
|
||||
// Initial state
|
||||
const initialState: EventState = {
|
||||
events: [],
|
||||
unreadCount: 0,
|
||||
activeAlerts: [],
|
||||
recentNotifications: [],
|
||||
recommendations: [],
|
||||
};
|
||||
|
||||
// Reducer
|
||||
const eventReducer = (state: EventState, action: EventAction): EventState => {
|
||||
switch (action.type) {
|
||||
case 'SET_EVENTS':
|
||||
const alerts = action.payload.filter(e => e.event_class === 'alert') as Alert[];
|
||||
const notifications = action.payload.filter(e => e.event_class === 'notification') as Notification[];
|
||||
const recommendations = action.payload.filter(e => e.event_class === 'recommendation') as Recommendation[];
|
||||
|
||||
return {
|
||||
...state,
|
||||
events: action.payload,
|
||||
activeAlerts: alerts.filter(a => a.status === 'active'),
|
||||
recentNotifications: notifications.slice(0, 10), // Most recent 10
|
||||
recommendations: recommendations,
|
||||
unreadCount: action.payload.filter(e => !e.event_metadata?.read).length,
|
||||
};
|
||||
|
||||
case 'ADD_EVENT':
|
||||
const existingIndex = state.events.findIndex(e => e.id === action.payload.id);
|
||||
if (existingIndex !== -1) {
|
||||
// Update existing event
|
||||
const updatedEvents = [...state.events];
|
||||
updatedEvents[existingIndex] = action.payload;
|
||||
return eventReducer({ ...state, events: updatedEvents }, { type: 'SET_EVENTS', payload: updatedEvents });
|
||||
} else {
|
||||
// Add new event
|
||||
const newEvents = [...state.events, action.payload];
|
||||
return eventReducer({ ...state, events: newEvents }, { type: 'SET_EVENTS', payload: newEvents });
|
||||
}
|
||||
|
||||
case 'UPDATE_EVENT':
|
||||
const updatedEvents = state.events.map(e =>
|
||||
e.id === action.payload.id ? action.payload : e
|
||||
);
|
||||
return eventReducer({ ...state, events: updatedEvents }, { type: 'SET_EVENTS', payload: updatedEvents });
|
||||
|
||||
case 'REMOVE_EVENT':
|
||||
const filteredEvents = state.events.filter(e => e.id !== action.payload);
|
||||
return eventReducer({ ...state, events: filteredEvents }, { type: 'SET_EVENTS', payload: filteredEvents });
|
||||
|
||||
case 'MARK_AS_READ':
|
||||
const eventsWithRead = state.events.map(e =>
|
||||
e.id === action.payload ? { ...e, event_metadata: { ...e.event_metadata, read: true } } : e
|
||||
);
|
||||
return eventReducer({ ...state, events: eventsWithRead }, { type: 'SET_EVENTS', payload: eventsWithRead });
|
||||
|
||||
case 'ACKNOWLEDGE_EVENT':
|
||||
const eventsWithAck = state.events.map(e =>
|
||||
e.id === action.payload && e.event_class === 'alert'
|
||||
? { ...e, status: 'acknowledged' as const } as Event
|
||||
: e
|
||||
);
|
||||
return eventReducer({ ...state, events: eventsWithAck }, { type: 'SET_EVENTS', payload: eventsWithAck });
|
||||
|
||||
case 'RESOLVE_EVENT':
|
||||
const eventsWithResolved = state.events.map(e =>
|
||||
e.id === action.payload && e.event_class === 'alert'
|
||||
? { ...e, status: 'resolved' as const, resolved_at: new Date().toISOString() } as Event
|
||||
: e
|
||||
);
|
||||
return eventReducer({ ...state, events: eventsWithResolved }, { type: 'SET_EVENTS', payload: eventsWithResolved });
|
||||
|
||||
case 'CLEAR_EVENTS':
|
||||
return initialState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
// Context types
|
||||
interface EventContextType extends EventState {
|
||||
addEvent: (event: Event) => void;
|
||||
updateEvent: (event: Event) => void;
|
||||
removeEvent: (eventId: string) => void;
|
||||
setEvents: (events: Event[]) => void;
|
||||
markAsRead: (eventId: string) => void;
|
||||
acknowledgeEvent: (eventId: string) => void;
|
||||
resolveEvent: (eventId: string) => void;
|
||||
clearEvents: () => void;
|
||||
}
|
||||
|
||||
// Create context
|
||||
const EventContext = createContext<EventContextType | undefined>(undefined);
|
||||
|
||||
// Provider component
|
||||
interface EventProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const EventProvider: React.FC<EventProviderProps> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(eventReducer, initialState);
|
||||
|
||||
const value = {
|
||||
...state,
|
||||
addEvent: (event: Event) => dispatch({ type: 'ADD_EVENT', payload: event }),
|
||||
updateEvent: (event: Event) => dispatch({ type: 'UPDATE_EVENT', payload: event }),
|
||||
removeEvent: (eventId: string) => dispatch({ type: 'REMOVE_EVENT', payload: eventId }),
|
||||
setEvents: (events: Event[]) => dispatch({ type: 'SET_EVENTS', payload: events }),
|
||||
markAsRead: (eventId: string) => dispatch({ type: 'MARK_AS_READ', payload: eventId }),
|
||||
acknowledgeEvent: (eventId: string) => dispatch({ type: 'ACKNOWLEDGE_EVENT', payload: eventId }),
|
||||
resolveEvent: (eventId: string) => dispatch({ type: 'RESOLVE_EVENT', payload: eventId }),
|
||||
clearEvents: () => dispatch({ type: 'CLEAR_EVENTS' }),
|
||||
};
|
||||
|
||||
return <EventContext.Provider value={value}>{children}</EventContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook to use the context
|
||||
export const useEventContext = (): EventContextType => {
|
||||
const context = useContext(EventContext);
|
||||
if (!context) {
|
||||
throw new Error('useEventContext must be used within an EventProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user