621 lines
19 KiB
TypeScript
621 lines
19 KiB
TypeScript
/**
|
|
* Alerts hook for managing bakery alerts and notifications
|
|
*/
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { InventoryService } from '../../services/api/inventory.service';
|
|
import { ProductionService } from '../../services/api/production.service';
|
|
import { NotificationService } from '../../services/api/notification.service';
|
|
|
|
export type AlertType = 'inventory' | 'production' | 'quality' | 'maintenance' | 'safety' | 'system' | 'custom';
|
|
export type AlertPriority = 'low' | 'medium' | 'high' | 'critical';
|
|
export type AlertStatus = 'active' | 'acknowledged' | 'resolved' | 'dismissed';
|
|
|
|
export interface Alert {
|
|
id: string;
|
|
type: AlertType;
|
|
priority: AlertPriority;
|
|
status: AlertStatus;
|
|
title: string;
|
|
message: string;
|
|
details?: Record<string, any>;
|
|
source: string;
|
|
sourceId?: string;
|
|
createdAt: Date;
|
|
updatedAt?: Date;
|
|
acknowledgedAt?: Date;
|
|
acknowledgedBy?: string;
|
|
resolvedAt?: Date;
|
|
resolvedBy?: string;
|
|
dismissedAt?: Date;
|
|
dismissedBy?: string;
|
|
expiresAt?: Date;
|
|
actions?: {
|
|
id: string;
|
|
label: string;
|
|
action: string;
|
|
parameters?: Record<string, any>;
|
|
}[];
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export interface AlertRule {
|
|
id: string;
|
|
name: string;
|
|
type: AlertType;
|
|
priority: AlertPriority;
|
|
condition: {
|
|
field: string;
|
|
operator: 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'greater_or_equal' | 'less_or_equal' | 'contains' | 'not_contains';
|
|
value: any;
|
|
}[];
|
|
actions: string[];
|
|
enabled: boolean;
|
|
cooldownPeriod?: number; // in minutes
|
|
lastTriggered?: Date;
|
|
}
|
|
|
|
interface AlertsState {
|
|
alerts: Alert[];
|
|
rules: AlertRule[];
|
|
unreadCount: number;
|
|
criticalCount: number;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
filters: {
|
|
types: AlertType[];
|
|
priorities: AlertPriority[];
|
|
statuses: AlertStatus[];
|
|
sources: string[];
|
|
};
|
|
}
|
|
|
|
interface AlertsActions {
|
|
// Alert Management
|
|
fetchAlerts: (filters?: Partial<AlertsState['filters']>) => Promise<void>;
|
|
acknowledgeAlert: (id: string) => Promise<boolean>;
|
|
resolveAlert: (id: string, resolution?: string) => Promise<boolean>;
|
|
dismissAlert: (id: string, reason?: string) => Promise<boolean>;
|
|
bulkAcknowledge: (ids: string[]) => Promise<boolean>;
|
|
bulkResolve: (ids: string[], resolution?: string) => Promise<boolean>;
|
|
bulkDismiss: (ids: string[], reason?: string) => Promise<boolean>;
|
|
|
|
// Alert Creation
|
|
createAlert: (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>) => Promise<boolean>;
|
|
createCustomAlert: (title: string, message: string, priority?: AlertPriority, details?: Record<string, any>) => Promise<boolean>;
|
|
|
|
// Alert Rules
|
|
fetchAlertRules: () => Promise<void>;
|
|
createAlertRule: (rule: Omit<AlertRule, 'id'>) => Promise<boolean>;
|
|
updateAlertRule: (id: string, rule: Partial<AlertRule>) => Promise<boolean>;
|
|
deleteAlertRule: (id: string) => Promise<boolean>;
|
|
testAlertRule: (rule: AlertRule) => Promise<boolean>;
|
|
|
|
// Monitoring and Checks
|
|
checkInventoryAlerts: () => Promise<void>;
|
|
checkProductionAlerts: () => Promise<void>;
|
|
checkQualityAlerts: () => Promise<void>;
|
|
checkMaintenanceAlerts: () => Promise<void>;
|
|
checkSystemAlerts: () => Promise<void>;
|
|
|
|
// Analytics
|
|
getAlertAnalytics: (period: string) => Promise<any>;
|
|
getAlertTrends: (days: number) => Promise<any>;
|
|
getMostFrequentAlerts: (period: string) => Promise<any>;
|
|
|
|
// Filters and Search
|
|
setFilters: (filters: Partial<AlertsState['filters']>) => void;
|
|
searchAlerts: (query: string) => Promise<Alert[]>;
|
|
|
|
// Real-time Updates
|
|
subscribeToAlerts: (callback: (alert: Alert) => void) => () => void;
|
|
|
|
// Utilities
|
|
clearError: () => void;
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
export const useAlerts = (): AlertsState & AlertsActions => {
|
|
const [state, setState] = useState<AlertsState>({
|
|
alerts: [],
|
|
rules: [],
|
|
unreadCount: 0,
|
|
criticalCount: 0,
|
|
isLoading: false,
|
|
error: null,
|
|
filters: {
|
|
types: [],
|
|
priorities: [],
|
|
statuses: [],
|
|
sources: [],
|
|
},
|
|
});
|
|
|
|
const inventoryService = new InventoryService();
|
|
const productionService = new ProductionService();
|
|
const notificationService = new NotificationService();
|
|
|
|
// Fetch alerts
|
|
const fetchAlerts = useCallback(async (filters?: Partial<AlertsState['filters']>) => {
|
|
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
// Combine filters
|
|
const activeFilters = { ...state.filters, ...filters };
|
|
|
|
// Fetch alerts from different sources
|
|
const [inventoryAlerts, productionAlerts, systemAlerts] = await Promise.all([
|
|
getInventoryAlerts(activeFilters),
|
|
getProductionAlerts(activeFilters),
|
|
getSystemAlerts(activeFilters),
|
|
]);
|
|
|
|
const allAlerts = [...inventoryAlerts, ...productionAlerts, ...systemAlerts]
|
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
|
|
// Calculate counts
|
|
const unreadCount = allAlerts.filter(alert => alert.status === 'active').length;
|
|
const criticalCount = allAlerts.filter(alert => alert.priority === 'critical' && alert.status === 'active').length;
|
|
|
|
setState(prev => ({
|
|
...prev,
|
|
alerts: allAlerts,
|
|
unreadCount,
|
|
criticalCount,
|
|
isLoading: false,
|
|
filters: activeFilters,
|
|
}));
|
|
} catch (error) {
|
|
setState(prev => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: 'Error al cargar alertas',
|
|
}));
|
|
}
|
|
}, [state.filters]);
|
|
|
|
// Get inventory alerts
|
|
const getInventoryAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
|
|
try {
|
|
const response = await inventoryService.getAlerts();
|
|
|
|
if (response.success && response.data) {
|
|
return response.data.map((alert: any) => convertToAlert(alert, 'inventory'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching inventory alerts:', error);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
// Get production alerts
|
|
const getProductionAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
|
|
try {
|
|
const response = await productionService.getAlerts?.();
|
|
|
|
if (response?.success && response.data) {
|
|
return response.data.map((alert: any) => convertToAlert(alert, 'production'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching production alerts:', error);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
// Get system alerts
|
|
const getSystemAlerts = async (filters: Partial<AlertsState['filters']>): Promise<Alert[]> => {
|
|
try {
|
|
const response = await notificationService.getNotifications();
|
|
|
|
if (response.success && response.data) {
|
|
return response.data
|
|
.filter((notif: any) => notif.type === 'alert')
|
|
.map((alert: any) => convertToAlert(alert, 'system'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching system alerts:', error);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
// Convert API response to Alert format
|
|
const convertToAlert = (apiAlert: any, source: string): Alert => {
|
|
return {
|
|
id: apiAlert.id,
|
|
type: apiAlert.type || (source as AlertType),
|
|
priority: mapPriority(apiAlert.priority || apiAlert.severity),
|
|
status: mapStatus(apiAlert.status),
|
|
title: apiAlert.title || apiAlert.message?.substring(0, 50) || 'Alert',
|
|
message: apiAlert.message || apiAlert.description || '',
|
|
details: apiAlert.details || apiAlert.data,
|
|
source,
|
|
sourceId: apiAlert.source_id,
|
|
createdAt: new Date(apiAlert.created_at || Date.now()),
|
|
updatedAt: apiAlert.updated_at ? new Date(apiAlert.updated_at) : undefined,
|
|
acknowledgedAt: apiAlert.acknowledged_at ? new Date(apiAlert.acknowledged_at) : undefined,
|
|
acknowledgedBy: apiAlert.acknowledged_by,
|
|
resolvedAt: apiAlert.resolved_at ? new Date(apiAlert.resolved_at) : undefined,
|
|
resolvedBy: apiAlert.resolved_by,
|
|
dismissedAt: apiAlert.dismissed_at ? new Date(apiAlert.dismissed_at) : undefined,
|
|
dismissedBy: apiAlert.dismissed_by,
|
|
expiresAt: apiAlert.expires_at ? new Date(apiAlert.expires_at) : undefined,
|
|
actions: apiAlert.actions || [],
|
|
metadata: apiAlert.metadata || {},
|
|
};
|
|
};
|
|
|
|
// Map priority from different sources
|
|
const mapPriority = (priority: string): AlertPriority => {
|
|
const priorityMap: Record<string, AlertPriority> = {
|
|
'low': 'low',
|
|
'medium': 'medium',
|
|
'high': 'high',
|
|
'critical': 'critical',
|
|
'urgent': 'critical',
|
|
'warning': 'medium',
|
|
'error': 'high',
|
|
'info': 'low',
|
|
};
|
|
|
|
return priorityMap[priority?.toLowerCase()] || 'medium';
|
|
};
|
|
|
|
// Map status from different sources
|
|
const mapStatus = (status: string): AlertStatus => {
|
|
const statusMap: Record<string, AlertStatus> = {
|
|
'active': 'active',
|
|
'new': 'active',
|
|
'open': 'active',
|
|
'acknowledged': 'acknowledged',
|
|
'ack': 'acknowledged',
|
|
'resolved': 'resolved',
|
|
'closed': 'resolved',
|
|
'dismissed': 'dismissed',
|
|
'ignored': 'dismissed',
|
|
};
|
|
|
|
return statusMap[status?.toLowerCase()] || 'active';
|
|
};
|
|
|
|
// Acknowledge alert
|
|
const acknowledgeAlert = useCallback(async (id: string): Promise<boolean> => {
|
|
setState(prev => ({ ...prev, error: null }));
|
|
|
|
try {
|
|
const alert = state.alerts.find(a => a.id === id);
|
|
if (!alert) return false;
|
|
|
|
// Call appropriate service based on source
|
|
let success = false;
|
|
if (alert.source === 'inventory') {
|
|
const response = await inventoryService.markAlertAsRead(id);
|
|
success = response.success;
|
|
}
|
|
|
|
if (success) {
|
|
setState(prev => ({
|
|
...prev,
|
|
alerts: prev.alerts.map(a =>
|
|
a.id === id
|
|
? { ...a, status: 'acknowledged', acknowledgedAt: new Date() }
|
|
: a
|
|
),
|
|
unreadCount: Math.max(0, prev.unreadCount - 1),
|
|
}));
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
setState(prev => ({ ...prev, error: 'Error al confirmar alerta' }));
|
|
return false;
|
|
}
|
|
}, [state.alerts, inventoryService]);
|
|
|
|
// Resolve alert
|
|
const resolveAlert = useCallback(async (id: string, resolution?: string): Promise<boolean> => {
|
|
setState(prev => ({ ...prev, error: null }));
|
|
|
|
try {
|
|
const alert = state.alerts.find(a => a.id === id);
|
|
if (!alert) return false;
|
|
|
|
// Call appropriate service based on source
|
|
let success = false;
|
|
if (alert.source === 'inventory') {
|
|
const response = await inventoryService.dismissAlert(id);
|
|
success = response.success;
|
|
}
|
|
|
|
if (success) {
|
|
setState(prev => ({
|
|
...prev,
|
|
alerts: prev.alerts.map(a =>
|
|
a.id === id
|
|
? { ...a, status: 'resolved', resolvedAt: new Date(), metadata: { ...a.metadata, resolution } }
|
|
: a
|
|
),
|
|
unreadCount: a.status === 'active' ? Math.max(0, prev.unreadCount - 1) : prev.unreadCount,
|
|
criticalCount: a.priority === 'critical' && a.status === 'active' ? Math.max(0, prev.criticalCount - 1) : prev.criticalCount,
|
|
}));
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
setState(prev => ({ ...prev, error: 'Error al resolver alerta' }));
|
|
return false;
|
|
}
|
|
}, [state.alerts, inventoryService]);
|
|
|
|
// Dismiss alert
|
|
const dismissAlert = useCallback(async (id: string, reason?: string): Promise<boolean> => {
|
|
setState(prev => ({ ...prev, error: null }));
|
|
|
|
try {
|
|
const alert = state.alerts.find(a => a.id === id);
|
|
if (!alert) return false;
|
|
|
|
// Call appropriate service based on source
|
|
let success = false;
|
|
if (alert.source === 'inventory') {
|
|
const response = await inventoryService.dismissAlert(id);
|
|
success = response.success;
|
|
}
|
|
|
|
if (success) {
|
|
setState(prev => ({
|
|
...prev,
|
|
alerts: prev.alerts.map(a =>
|
|
a.id === id
|
|
? { ...a, status: 'dismissed', dismissedAt: new Date(), metadata: { ...a.metadata, dismissReason: reason } }
|
|
: a
|
|
),
|
|
}));
|
|
}
|
|
|
|
return success;
|
|
} catch (error) {
|
|
setState(prev => ({ ...prev, error: 'Error al descartar alerta' }));
|
|
return false;
|
|
}
|
|
}, [state.alerts, inventoryService]);
|
|
|
|
// Bulk acknowledge
|
|
const bulkAcknowledge = useCallback(async (ids: string[]): Promise<boolean> => {
|
|
const results = await Promise.all(ids.map(id => acknowledgeAlert(id)));
|
|
return results.every(result => result);
|
|
}, [acknowledgeAlert]);
|
|
|
|
// Bulk resolve
|
|
const bulkResolve = useCallback(async (ids: string[], resolution?: string): Promise<boolean> => {
|
|
const results = await Promise.all(ids.map(id => resolveAlert(id, resolution)));
|
|
return results.every(result => result);
|
|
}, [resolveAlert]);
|
|
|
|
// Bulk dismiss
|
|
const bulkDismiss = useCallback(async (ids: string[], reason?: string): Promise<boolean> => {
|
|
const results = await Promise.all(ids.map(id => dismissAlert(id, reason)));
|
|
return results.every(result => result);
|
|
}, [dismissAlert]);
|
|
|
|
// Create alert
|
|
const createAlert = useCallback(async (alert: Omit<Alert, 'id' | 'createdAt' | 'status'>): Promise<boolean> => {
|
|
setState(prev => ({ ...prev, error: null }));
|
|
|
|
try {
|
|
const newAlert: Alert = {
|
|
...alert,
|
|
id: generateAlertId(),
|
|
status: 'active',
|
|
createdAt: new Date(),
|
|
};
|
|
|
|
setState(prev => ({
|
|
...prev,
|
|
alerts: [newAlert, ...prev.alerts],
|
|
unreadCount: prev.unreadCount + 1,
|
|
criticalCount: newAlert.priority === 'critical' ? prev.criticalCount + 1 : prev.criticalCount,
|
|
}));
|
|
|
|
return true;
|
|
} catch (error) {
|
|
setState(prev => ({ ...prev, error: 'Error al crear alerta' }));
|
|
return false;
|
|
}
|
|
}, []);
|
|
|
|
// Create custom alert
|
|
const createCustomAlert = useCallback(async (
|
|
title: string,
|
|
message: string,
|
|
priority: AlertPriority = 'medium',
|
|
details?: Record<string, any>
|
|
): Promise<boolean> => {
|
|
return createAlert({
|
|
type: 'custom',
|
|
priority,
|
|
title,
|
|
message,
|
|
details,
|
|
source: 'user',
|
|
});
|
|
}, [createAlert]);
|
|
|
|
// Check inventory alerts
|
|
const checkInventoryAlerts = useCallback(async () => {
|
|
try {
|
|
// Check for low stock
|
|
const stockResponse = await inventoryService.getStockLevels();
|
|
if (stockResponse.success && stockResponse.data) {
|
|
const lowStockItems = stockResponse.data.filter((item: any) =>
|
|
item.current_quantity <= item.reorder_point
|
|
);
|
|
|
|
for (const item of lowStockItems) {
|
|
await createAlert({
|
|
type: 'inventory',
|
|
priority: item.current_quantity === 0 ? 'critical' : 'high',
|
|
title: 'Stock bajo',
|
|
message: `Stock bajo para ${item.ingredient?.name}: ${item.current_quantity} ${item.unit}`,
|
|
source: 'inventory',
|
|
sourceId: item.id,
|
|
details: { item },
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for expiring items
|
|
const expirationResponse = await inventoryService.getExpirationReport();
|
|
if (expirationResponse?.success && expirationResponse.data) {
|
|
const expiringItems = expirationResponse.data.expiring_soon || [];
|
|
|
|
for (const item of expiringItems) {
|
|
await createAlert({
|
|
type: 'inventory',
|
|
priority: 'medium',
|
|
title: 'Producto próximo a expirar',
|
|
message: `${item.name} expira el ${item.expiration_date}`,
|
|
source: 'inventory',
|
|
sourceId: item.id,
|
|
details: { item },
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking inventory alerts:', error);
|
|
}
|
|
}, [inventoryService, createAlert]);
|
|
|
|
// Generate unique ID
|
|
const generateAlertId = (): string => {
|
|
return `alert_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
};
|
|
|
|
// Set filters
|
|
const setFilters = useCallback((filters: Partial<AlertsState['filters']>) => {
|
|
setState(prev => ({
|
|
...prev,
|
|
filters: { ...prev.filters, ...filters },
|
|
}));
|
|
}, []);
|
|
|
|
// Clear error
|
|
const clearError = useCallback(() => {
|
|
setState(prev => ({ ...prev, error: null }));
|
|
}, []);
|
|
|
|
// Refresh alerts
|
|
const refresh = useCallback(async () => {
|
|
await fetchAlerts(state.filters);
|
|
}, [fetchAlerts, state.filters]);
|
|
|
|
// Placeholder implementations for remaining functions
|
|
const fetchAlertRules = useCallback(async () => {
|
|
setState(prev => ({ ...prev, rules: [] }));
|
|
}, []);
|
|
|
|
const createAlertRule = useCallback(async (rule: Omit<AlertRule, 'id'>): Promise<boolean> => {
|
|
return true;
|
|
}, []);
|
|
|
|
const updateAlertRule = useCallback(async (id: string, rule: Partial<AlertRule>): Promise<boolean> => {
|
|
return true;
|
|
}, []);
|
|
|
|
const deleteAlertRule = useCallback(async (id: string): Promise<boolean> => {
|
|
return true;
|
|
}, []);
|
|
|
|
const testAlertRule = useCallback(async (rule: AlertRule): Promise<boolean> => {
|
|
return true;
|
|
}, []);
|
|
|
|
const checkProductionAlerts = useCallback(async () => {
|
|
// Implementation for production alerts
|
|
}, []);
|
|
|
|
const checkQualityAlerts = useCallback(async () => {
|
|
// Implementation for quality alerts
|
|
}, []);
|
|
|
|
const checkMaintenanceAlerts = useCallback(async () => {
|
|
// Implementation for maintenance alerts
|
|
}, []);
|
|
|
|
const checkSystemAlerts = useCallback(async () => {
|
|
// Implementation for system alerts
|
|
}, []);
|
|
|
|
const getAlertAnalytics = useCallback(async (period: string): Promise<any> => {
|
|
return {};
|
|
}, []);
|
|
|
|
const getAlertTrends = useCallback(async (days: number): Promise<any> => {
|
|
return {};
|
|
}, []);
|
|
|
|
const getMostFrequentAlerts = useCallback(async (period: string): Promise<any> => {
|
|
return {};
|
|
}, []);
|
|
|
|
const searchAlerts = useCallback(async (query: string): Promise<Alert[]> => {
|
|
return state.alerts.filter(alert =>
|
|
alert.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
alert.message.toLowerCase().includes(query.toLowerCase())
|
|
);
|
|
}, [state.alerts]);
|
|
|
|
const subscribeToAlerts = useCallback((callback: (alert: Alert) => void): (() => void) => {
|
|
// Implementation would set up real-time subscription
|
|
return () => {
|
|
// Cleanup subscription
|
|
};
|
|
}, []);
|
|
|
|
// Initialize alerts on mount
|
|
useEffect(() => {
|
|
fetchAlerts();
|
|
}, []);
|
|
|
|
// Set up periodic checks
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
checkInventoryAlerts();
|
|
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
|
|
return () => clearInterval(interval);
|
|
}, [checkInventoryAlerts]);
|
|
|
|
return {
|
|
...state,
|
|
fetchAlerts,
|
|
acknowledgeAlert,
|
|
resolveAlert,
|
|
dismissAlert,
|
|
bulkAcknowledge,
|
|
bulkResolve,
|
|
bulkDismiss,
|
|
createAlert,
|
|
createCustomAlert,
|
|
fetchAlertRules,
|
|
createAlertRule,
|
|
updateAlertRule,
|
|
deleteAlertRule,
|
|
testAlertRule,
|
|
checkInventoryAlerts,
|
|
checkProductionAlerts,
|
|
checkQualityAlerts,
|
|
checkMaintenanceAlerts,
|
|
checkSystemAlerts,
|
|
getAlertAnalytics,
|
|
getAlertTrends,
|
|
getMostFrequentAlerts,
|
|
setFilters,
|
|
searchAlerts,
|
|
subscribeToAlerts,
|
|
clearError,
|
|
refresh,
|
|
};
|
|
}; |