ADD new frontend
This commit is contained in:
621
frontend/src/hooks/business/useAlerts.ts
Normal file
621
frontend/src/hooks/business/useAlerts.ts
Normal file
@@ -0,0 +1,621 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
520
frontend/src/hooks/business/useBakeryWorkflow.ts
Normal file
520
frontend/src/hooks/business/useBakeryWorkflow.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
/**
|
||||
* Bakery workflow hook for managing daily bakery operations and processes
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ProductionService } from '../../services/api/production.service';
|
||||
import { InventoryService } from '../../services/api/inventory.service';
|
||||
import { SalesService } from '../../services/api/sales.service';
|
||||
import { OrderService } from '../../services/api/order.service';
|
||||
|
||||
export interface WorkflowStep {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
estimatedDuration: number; // in minutes
|
||||
actualDuration?: number;
|
||||
dependencies?: string[];
|
||||
assignedTo?: string;
|
||||
startTime?: Date;
|
||||
endTime?: Date;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface DailyWorkflow {
|
||||
date: string;
|
||||
steps: WorkflowStep[];
|
||||
totalEstimatedTime: number;
|
||||
totalActualTime: number;
|
||||
completionRate: number;
|
||||
status: 'not_started' | 'in_progress' | 'completed' | 'behind_schedule';
|
||||
}
|
||||
|
||||
interface BakeryWorkflowState {
|
||||
currentWorkflow: DailyWorkflow | null;
|
||||
workflowHistory: DailyWorkflow[];
|
||||
activeStep: WorkflowStep | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface BakeryWorkflowActions {
|
||||
// Workflow Management
|
||||
initializeDailyWorkflow: (date: string) => Promise<void>;
|
||||
generateWorkflowFromForecast: (date: string) => Promise<void>;
|
||||
updateWorkflow: (workflow: Partial<DailyWorkflow>) => Promise<boolean>;
|
||||
|
||||
// Step Management
|
||||
startStep: (stepId: string) => Promise<boolean>;
|
||||
completeStep: (stepId: string, notes?: string) => Promise<boolean>;
|
||||
failStep: (stepId: string, reason: string) => Promise<boolean>;
|
||||
skipStep: (stepId: string, reason: string) => Promise<boolean>;
|
||||
updateStepProgress: (stepId: string, progress: Partial<WorkflowStep>) => Promise<boolean>;
|
||||
|
||||
// Workflow Templates
|
||||
createWorkflowTemplate: (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]) => Promise<boolean>;
|
||||
loadWorkflowTemplate: (templateId: string, date: string) => Promise<boolean>;
|
||||
getWorkflowTemplates: () => Promise<any[]>;
|
||||
|
||||
// Analytics and Optimization
|
||||
getWorkflowAnalytics: (startDate: string, endDate: string) => Promise<any>;
|
||||
getBottleneckAnalysis: (period: string) => Promise<any>;
|
||||
getEfficiencyReport: (date: string) => Promise<any>;
|
||||
|
||||
// Real-time Updates
|
||||
subscribeToWorkflowUpdates: (callback: (workflow: DailyWorkflow) => void) => () => void;
|
||||
|
||||
// Utilities
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useBakeryWorkflow = (): BakeryWorkflowState & BakeryWorkflowActions => {
|
||||
const [state, setState] = useState<BakeryWorkflowState>({
|
||||
currentWorkflow: null,
|
||||
workflowHistory: [],
|
||||
activeStep: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
const productionService = new ProductionService();
|
||||
const inventoryService = new InventoryService();
|
||||
const salesService = new SalesService();
|
||||
const orderService = new OrderService();
|
||||
|
||||
// Initialize daily workflow
|
||||
const initializeDailyWorkflow = useCallback(async (date: string) => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Get existing workflow or create new one
|
||||
const existingWorkflow = await getWorkflowForDate(date);
|
||||
|
||||
if (existingWorkflow) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: existingWorkflow,
|
||||
activeStep: findActiveStep(existingWorkflow.steps),
|
||||
isLoading: false,
|
||||
}));
|
||||
} else {
|
||||
await generateWorkflowFromForecast(date);
|
||||
}
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al inicializar flujo de trabajo diario',
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Generate workflow from forecast and orders
|
||||
const generateWorkflowFromForecast = useCallback(async (date: string) => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Get daily forecast and orders
|
||||
const [forecastData, ordersData, inventoryData] = await Promise.all([
|
||||
productionService.getDailyForecast?.(date),
|
||||
orderService.getOrdersByDate?.(date),
|
||||
inventoryService.getStockLevels(),
|
||||
]);
|
||||
|
||||
const workflow = generateWorkflowSteps(forecastData, ordersData, inventoryData);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: workflow,
|
||||
activeStep: workflow.steps.find(step => step.status === 'pending') || null,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al generar flujo de trabajo desde predicción',
|
||||
}));
|
||||
}
|
||||
}, [productionService, orderService, inventoryService]);
|
||||
|
||||
// Generate workflow steps based on data
|
||||
const generateWorkflowSteps = (forecastData: any, ordersData: any, inventoryData: any): DailyWorkflow => {
|
||||
const steps: WorkflowStep[] = [
|
||||
// Morning prep
|
||||
{
|
||||
id: 'morning_prep',
|
||||
name: 'Preparación matutina',
|
||||
description: 'Verificar equipos, ingredientes y planificación del día',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedDuration: 30,
|
||||
},
|
||||
|
||||
// Inventory check
|
||||
{
|
||||
id: 'inventory_check',
|
||||
name: 'Control de inventario',
|
||||
description: 'Verificar niveles de stock y calidad de ingredientes',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedDuration: 20,
|
||||
dependencies: ['morning_prep'],
|
||||
},
|
||||
|
||||
// Production preparation
|
||||
{
|
||||
id: 'production_prep',
|
||||
name: 'Preparación de producción',
|
||||
description: 'Preparar ingredientes y configurar equipos',
|
||||
status: 'pending',
|
||||
priority: 'high',
|
||||
estimatedDuration: 45,
|
||||
dependencies: ['inventory_check'],
|
||||
},
|
||||
|
||||
// Production batches (generated from forecast)
|
||||
...generateProductionSteps(forecastData),
|
||||
|
||||
// Quality control
|
||||
{
|
||||
id: 'quality_control',
|
||||
name: 'Control de calidad',
|
||||
description: 'Verificar calidad de productos terminados',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedDuration: 30,
|
||||
dependencies: ['production_prep'],
|
||||
},
|
||||
|
||||
// Order fulfillment
|
||||
...generateOrderSteps(ordersData),
|
||||
|
||||
// End of day cleanup
|
||||
{
|
||||
id: 'cleanup',
|
||||
name: 'Limpieza final',
|
||||
description: 'Limpieza de equipos y área de trabajo',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
estimatedDuration: 45,
|
||||
},
|
||||
|
||||
// Daily reporting
|
||||
{
|
||||
id: 'daily_report',
|
||||
name: 'Reporte diario',
|
||||
description: 'Completar reportes de producción y ventas',
|
||||
status: 'pending',
|
||||
priority: 'low',
|
||||
estimatedDuration: 15,
|
||||
dependencies: ['cleanup'],
|
||||
},
|
||||
];
|
||||
|
||||
const totalEstimatedTime = steps.reduce((total, step) => total + step.estimatedDuration, 0);
|
||||
|
||||
return {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
steps,
|
||||
totalEstimatedTime,
|
||||
totalActualTime: 0,
|
||||
completionRate: 0,
|
||||
status: 'not_started',
|
||||
};
|
||||
};
|
||||
|
||||
// Generate production steps from forecast
|
||||
const generateProductionSteps = (forecastData: any): WorkflowStep[] => {
|
||||
if (!forecastData || !forecastData.products) return [];
|
||||
|
||||
return forecastData.products.map((product: any, index: number) => ({
|
||||
id: `production_${product.id}`,
|
||||
name: `Producir ${product.name}`,
|
||||
description: `Producir ${product.estimated_quantity} unidades de ${product.name}`,
|
||||
status: 'pending' as const,
|
||||
priority: product.priority || 'medium' as const,
|
||||
estimatedDuration: product.production_time || 60,
|
||||
dependencies: ['production_prep'],
|
||||
}));
|
||||
};
|
||||
|
||||
// Generate order fulfillment steps
|
||||
const generateOrderSteps = (ordersData: any): WorkflowStep[] => {
|
||||
if (!ordersData || !ordersData.length) return [];
|
||||
|
||||
const specialOrders = ordersData.filter((order: any) => order.type === 'special' || order.priority === 'high');
|
||||
|
||||
return specialOrders.map((order: any) => ({
|
||||
id: `order_${order.id}`,
|
||||
name: `Preparar pedido especial`,
|
||||
description: `Preparar pedido #${order.id} - ${order.items?.length || 0} items`,
|
||||
status: 'pending' as const,
|
||||
priority: order.priority || 'medium' as const,
|
||||
estimatedDuration: order.preparation_time || 30,
|
||||
dependencies: ['production_prep'],
|
||||
}));
|
||||
};
|
||||
|
||||
// Find active step
|
||||
const findActiveStep = (steps: WorkflowStep[]): WorkflowStep | null => {
|
||||
return steps.find(step => step.status === 'in_progress') ||
|
||||
steps.find(step => step.status === 'pending') ||
|
||||
null;
|
||||
};
|
||||
|
||||
// Get workflow for specific date
|
||||
const getWorkflowForDate = async (date: string): Promise<DailyWorkflow | null> => {
|
||||
// This would typically fetch from an API
|
||||
// For now, return null to force generation
|
||||
return null;
|
||||
};
|
||||
|
||||
// Start a workflow step
|
||||
const startStep = useCallback(async (stepId: string): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const updatedWorkflow = {
|
||||
...state.currentWorkflow,
|
||||
steps: state.currentWorkflow.steps.map(step =>
|
||||
step.id === stepId
|
||||
? { ...step, status: 'in_progress' as const, startTime: new Date() }
|
||||
: step
|
||||
),
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
activeStep: updatedWorkflow.steps.find(step => step.id === stepId) || null,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al iniciar paso del flujo de trabajo' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Complete a workflow step
|
||||
const completeStep = useCallback(async (stepId: string, notes?: string): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const step = state.currentWorkflow.steps.find(s => s.id === stepId);
|
||||
if (!step || !step.startTime) return false;
|
||||
|
||||
const endTime = new Date();
|
||||
const actualDuration = Math.round((endTime.getTime() - step.startTime.getTime()) / 60000);
|
||||
|
||||
const updatedWorkflow = {
|
||||
...state.currentWorkflow,
|
||||
steps: state.currentWorkflow.steps.map(s =>
|
||||
s.id === stepId
|
||||
? {
|
||||
...s,
|
||||
status: 'completed' as const,
|
||||
endTime,
|
||||
actualDuration,
|
||||
notes: notes || s.notes,
|
||||
}
|
||||
: s
|
||||
),
|
||||
};
|
||||
|
||||
// Update completion rate
|
||||
const completedSteps = updatedWorkflow.steps.filter(s => s.status === 'completed');
|
||||
updatedWorkflow.completionRate = (completedSteps.length / updatedWorkflow.steps.length) * 100;
|
||||
updatedWorkflow.totalActualTime = updatedWorkflow.steps
|
||||
.filter(s => s.actualDuration)
|
||||
.reduce((total, s) => total + (s.actualDuration || 0), 0);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
activeStep: findActiveStep(updatedWorkflow.steps),
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al completar paso del flujo de trabajo' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Fail a workflow step
|
||||
const failStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const updatedWorkflow = {
|
||||
...state.currentWorkflow,
|
||||
steps: state.currentWorkflow.steps.map(step =>
|
||||
step.id === stepId
|
||||
? { ...step, status: 'failed' as const, notes: reason }
|
||||
: step
|
||||
),
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
activeStep: findActiveStep(updatedWorkflow.steps),
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al marcar paso como fallido' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Skip a workflow step
|
||||
const skipStep = useCallback(async (stepId: string, reason: string): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const updatedWorkflow = {
|
||||
...state.currentWorkflow,
|
||||
steps: state.currentWorkflow.steps.map(step =>
|
||||
step.id === stepId
|
||||
? { ...step, status: 'completed' as const, notes: `Omitido: ${reason}` }
|
||||
: step
|
||||
),
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
activeStep: findActiveStep(updatedWorkflow.steps),
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al omitir paso del flujo de trabajo' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Update workflow
|
||||
const updateWorkflow = useCallback(async (workflow: Partial<DailyWorkflow>): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const updatedWorkflow = { ...state.currentWorkflow, ...workflow };
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al actualizar flujo de trabajo' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Update step progress
|
||||
const updateStepProgress = useCallback(async (stepId: string, progress: Partial<WorkflowStep>): Promise<boolean> => {
|
||||
if (!state.currentWorkflow) return false;
|
||||
|
||||
try {
|
||||
const updatedWorkflow = {
|
||||
...state.currentWorkflow,
|
||||
steps: state.currentWorkflow.steps.map(step =>
|
||||
step.id === stepId ? { ...step, ...progress } : step
|
||||
),
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentWorkflow: updatedWorkflow,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al actualizar progreso del paso' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentWorkflow]);
|
||||
|
||||
// Placeholder implementations for template and analytics functions
|
||||
const createWorkflowTemplate = useCallback(async (name: string, steps: Omit<WorkflowStep, 'id' | 'status'>[]): Promise<boolean> => {
|
||||
// Implementation would save template to backend
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const loadWorkflowTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
|
||||
// Implementation would load template from backend
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const getWorkflowTemplates = useCallback(async (): Promise<any[]> => {
|
||||
// Implementation would fetch templates from backend
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const getWorkflowAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
|
||||
// Implementation would fetch analytics from backend
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const getBottleneckAnalysis = useCallback(async (period: string): Promise<any> => {
|
||||
// Implementation would analyze bottlenecks
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const getEfficiencyReport = useCallback(async (date: string): Promise<any> => {
|
||||
// Implementation would generate efficiency report
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const subscribeToWorkflowUpdates = useCallback((callback: (workflow: DailyWorkflow) => void): (() => void) => {
|
||||
// Implementation would set up real-time subscription
|
||||
return () => {
|
||||
// Cleanup subscription
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (state.currentWorkflow) {
|
||||
await initializeDailyWorkflow(state.currentWorkflow.date);
|
||||
}
|
||||
}, [state.currentWorkflow, initializeDailyWorkflow]);
|
||||
|
||||
// Initialize workflow for today on mount
|
||||
useEffect(() => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
initializeDailyWorkflow(today);
|
||||
}, [initializeDailyWorkflow]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
initializeDailyWorkflow,
|
||||
generateWorkflowFromForecast,
|
||||
updateWorkflow,
|
||||
startStep,
|
||||
completeStep,
|
||||
failStep,
|
||||
skipStep,
|
||||
updateStepProgress,
|
||||
createWorkflowTemplate,
|
||||
loadWorkflowTemplate,
|
||||
getWorkflowTemplates,
|
||||
getWorkflowAnalytics,
|
||||
getBottleneckAnalysis,
|
||||
getEfficiencyReport,
|
||||
subscribeToWorkflowUpdates,
|
||||
clearError,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
655
frontend/src/hooks/business/useProductionSchedule.ts
Normal file
655
frontend/src/hooks/business/useProductionSchedule.ts
Normal file
@@ -0,0 +1,655 @@
|
||||
/**
|
||||
* Production schedule hook for managing bakery production scheduling and capacity planning
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { ProductionService } from '../../services/api/production.service';
|
||||
import { InventoryService } from '../../services/api/inventory.service';
|
||||
import { ForecastingService } from '../../services/api/forecasting.service';
|
||||
|
||||
export interface ScheduleItem {
|
||||
id: string;
|
||||
recipeId: string;
|
||||
recipeName: string;
|
||||
quantity: number;
|
||||
priority: 'low' | 'medium' | 'high' | 'urgent';
|
||||
estimatedStartTime: Date;
|
||||
estimatedEndTime: Date;
|
||||
actualStartTime?: Date;
|
||||
actualEndTime?: Date;
|
||||
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'delayed';
|
||||
assignedEquipment?: string[];
|
||||
assignedStaff?: string[];
|
||||
requiredIngredients: {
|
||||
ingredientId: string;
|
||||
ingredientName: string;
|
||||
requiredQuantity: number;
|
||||
availableQuantity: number;
|
||||
unit: string;
|
||||
}[];
|
||||
notes?: string;
|
||||
dependencies?: string[];
|
||||
}
|
||||
|
||||
export interface ProductionSlot {
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
isAvailable: boolean;
|
||||
assignedItems: ScheduleItem[];
|
||||
capacity: number;
|
||||
utilizationRate: number;
|
||||
}
|
||||
|
||||
export interface DailySchedule {
|
||||
date: string;
|
||||
items: ScheduleItem[];
|
||||
slots: ProductionSlot[];
|
||||
totalCapacity: number;
|
||||
totalUtilization: number;
|
||||
efficiency: number;
|
||||
bottlenecks: string[];
|
||||
}
|
||||
|
||||
interface ProductionScheduleState {
|
||||
currentSchedule: DailySchedule | null;
|
||||
scheduleHistory: DailySchedule[];
|
||||
availableRecipes: any[];
|
||||
equipmentStatus: any[];
|
||||
staffAvailability: any[];
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
constraints: {
|
||||
maxDailyCapacity: number;
|
||||
workingHours: { start: string; end: string };
|
||||
equipmentLimitations: Record<string, number>;
|
||||
staffLimitations: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProductionScheduleActions {
|
||||
// Schedule Management
|
||||
loadSchedule: (date: string) => Promise<void>;
|
||||
createSchedule: (date: string) => Promise<void>;
|
||||
updateSchedule: (schedule: Partial<DailySchedule>) => Promise<boolean>;
|
||||
|
||||
// Schedule Items
|
||||
addScheduleItem: (item: Omit<ScheduleItem, 'id'>) => Promise<boolean>;
|
||||
updateScheduleItem: (id: string, item: Partial<ScheduleItem>) => Promise<boolean>;
|
||||
removeScheduleItem: (id: string) => Promise<boolean>;
|
||||
moveScheduleItem: (id: string, newStartTime: Date) => Promise<boolean>;
|
||||
|
||||
// Automatic Scheduling
|
||||
autoSchedule: (date: string, items: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
|
||||
optimizeSchedule: (date: string) => Promise<boolean>;
|
||||
generateFromForecast: (date: string) => Promise<boolean>;
|
||||
|
||||
// Capacity Management
|
||||
checkCapacity: (date: string, newItem: Omit<ScheduleItem, 'id'>) => Promise<{ canSchedule: boolean; suggestedTime?: Date; conflicts?: string[] }>;
|
||||
getAvailableSlots: (date: string, duration: number) => Promise<ProductionSlot[]>;
|
||||
calculateUtilization: (date: string) => Promise<number>;
|
||||
|
||||
// Resource Management
|
||||
checkIngredientAvailability: (items: ScheduleItem[]) => Promise<{ available: boolean; shortages: any[] }>;
|
||||
checkEquipmentAvailability: (date: string, equipment: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
|
||||
checkStaffAvailability: (date: string, staff: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
|
||||
|
||||
// Analytics and Optimization
|
||||
getScheduleAnalytics: (startDate: string, endDate: string) => Promise<any>;
|
||||
getBottleneckAnalysis: (date: string) => Promise<any>;
|
||||
getEfficiencyReport: (period: string) => Promise<any>;
|
||||
predictDelays: (date: string) => Promise<any>;
|
||||
|
||||
// Templates and Presets
|
||||
saveScheduleTemplate: (name: string, template: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
|
||||
loadScheduleTemplate: (templateId: string, date: string) => Promise<boolean>;
|
||||
getScheduleTemplates: () => Promise<any[]>;
|
||||
|
||||
// Utilities
|
||||
clearError: () => void;
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useProductionSchedule = (): ProductionScheduleState & ProductionScheduleActions => {
|
||||
const [state, setState] = useState<ProductionScheduleState>({
|
||||
currentSchedule: null,
|
||||
scheduleHistory: [],
|
||||
availableRecipes: [],
|
||||
equipmentStatus: [],
|
||||
staffAvailability: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
constraints: {
|
||||
maxDailyCapacity: 8 * 60, // 8 hours in minutes
|
||||
workingHours: { start: '06:00', end: '20:00' },
|
||||
equipmentLimitations: {},
|
||||
staffLimitations: {},
|
||||
},
|
||||
});
|
||||
|
||||
const productionService = new ProductionService();
|
||||
const inventoryService = new InventoryService();
|
||||
const forecastingService = new ForecastingService();
|
||||
|
||||
// Load schedule for specific date
|
||||
const loadSchedule = useCallback(async (date: string) => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Get schedule data from API
|
||||
const scheduleData = await getScheduleFromAPI(date);
|
||||
|
||||
if (scheduleData) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: scheduleData,
|
||||
isLoading: false,
|
||||
}));
|
||||
} else {
|
||||
// Create new schedule if none exists
|
||||
await createSchedule(date);
|
||||
}
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al cargar programación de producción',
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Create new schedule
|
||||
const createSchedule = useCallback(async (date: string) => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
|
||||
const slots = generateTimeSlots(workingHours, 30); // 30-minute slots
|
||||
|
||||
const newSchedule: DailySchedule = {
|
||||
date,
|
||||
items: [],
|
||||
slots,
|
||||
totalCapacity: state.constraints.maxDailyCapacity,
|
||||
totalUtilization: 0,
|
||||
efficiency: 0,
|
||||
bottlenecks: [],
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: newSchedule,
|
||||
isLoading: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al crear nueva programación',
|
||||
}));
|
||||
}
|
||||
}, [state.constraints]);
|
||||
|
||||
// Generate working hours for a date
|
||||
const generateWorkingHours = (date: string, workingHours: { start: string; end: string }) => {
|
||||
const startTime = new Date(`${date}T${workingHours.start}`);
|
||||
const endTime = new Date(`${date}T${workingHours.end}`);
|
||||
return { startTime, endTime };
|
||||
};
|
||||
|
||||
// Generate time slots
|
||||
const generateTimeSlots = (workingHours: { startTime: Date; endTime: Date }, slotDuration: number): ProductionSlot[] => {
|
||||
const slots: ProductionSlot[] = [];
|
||||
const current = new Date(workingHours.startTime);
|
||||
|
||||
while (current < workingHours.endTime) {
|
||||
const slotEnd = new Date(current.getTime() + slotDuration * 60000);
|
||||
|
||||
slots.push({
|
||||
startTime: new Date(current),
|
||||
endTime: slotEnd,
|
||||
isAvailable: true,
|
||||
assignedItems: [],
|
||||
capacity: 1,
|
||||
utilizationRate: 0,
|
||||
});
|
||||
|
||||
current.setTime(slotEnd.getTime());
|
||||
}
|
||||
|
||||
return slots;
|
||||
};
|
||||
|
||||
// Add schedule item
|
||||
const addScheduleItem = useCallback(async (item: Omit<ScheduleItem, 'id'>): Promise<boolean> => {
|
||||
if (!state.currentSchedule) return false;
|
||||
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
|
||||
try {
|
||||
// Check capacity and resources
|
||||
const capacityCheck = await checkCapacity(state.currentSchedule.date, item);
|
||||
|
||||
if (!capacityCheck.canSchedule) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: `No se puede programar: ${capacityCheck.conflicts?.join(', ')}`,
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
const newItem: ScheduleItem = {
|
||||
...item,
|
||||
id: generateScheduleItemId(),
|
||||
};
|
||||
|
||||
const updatedSchedule = {
|
||||
...state.currentSchedule,
|
||||
items: [...state.currentSchedule.items, newItem],
|
||||
};
|
||||
|
||||
// Recalculate utilization and efficiency
|
||||
recalculateScheduleMetrics(updatedSchedule);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: updatedSchedule,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al agregar item a la programación' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
// Update schedule item
|
||||
const updateScheduleItem = useCallback(async (id: string, item: Partial<ScheduleItem>): Promise<boolean> => {
|
||||
if (!state.currentSchedule) return false;
|
||||
|
||||
try {
|
||||
const updatedSchedule = {
|
||||
...state.currentSchedule,
|
||||
items: state.currentSchedule.items.map(scheduleItem =>
|
||||
scheduleItem.id === id ? { ...scheduleItem, ...item } : scheduleItem
|
||||
),
|
||||
};
|
||||
|
||||
recalculateScheduleMetrics(updatedSchedule);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: updatedSchedule,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al actualizar item de programación' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
// Remove schedule item
|
||||
const removeScheduleItem = useCallback(async (id: string): Promise<boolean> => {
|
||||
if (!state.currentSchedule) return false;
|
||||
|
||||
try {
|
||||
const updatedSchedule = {
|
||||
...state.currentSchedule,
|
||||
items: state.currentSchedule.items.filter(item => item.id !== id),
|
||||
};
|
||||
|
||||
recalculateScheduleMetrics(updatedSchedule);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: updatedSchedule,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({ ...prev, error: 'Error al eliminar item de programación' }));
|
||||
return false;
|
||||
}
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
// Move schedule item to new time
|
||||
const moveScheduleItem = useCallback(async (id: string, newStartTime: Date): Promise<boolean> => {
|
||||
if (!state.currentSchedule) return false;
|
||||
|
||||
const item = state.currentSchedule.items.find(item => item.id === id);
|
||||
if (!item) return false;
|
||||
|
||||
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
|
||||
const newEndTime = new Date(newStartTime.getTime() + duration);
|
||||
|
||||
return updateScheduleItem(id, {
|
||||
estimatedStartTime: newStartTime,
|
||||
estimatedEndTime: newEndTime,
|
||||
});
|
||||
}, [state.currentSchedule, updateScheduleItem]);
|
||||
|
||||
// Auto-schedule items
|
||||
const autoSchedule = useCallback(async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Sort items by priority and estimated duration
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
|
||||
const schedule = await createOptimalSchedule(date, sortedItems);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
currentSchedule: schedule,
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al programar automáticamente',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Create optimal schedule
|
||||
const createOptimalSchedule = async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<DailySchedule> => {
|
||||
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
|
||||
const slots = generateTimeSlots(workingHours, 30);
|
||||
|
||||
const scheduledItems: ScheduleItem[] = [];
|
||||
let currentTime = new Date(workingHours.startTime);
|
||||
|
||||
for (const item of items) {
|
||||
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
|
||||
const endTime = new Date(currentTime.getTime() + duration);
|
||||
|
||||
// Check if item fits in remaining time
|
||||
if (endTime <= workingHours.endTime) {
|
||||
scheduledItems.push({
|
||||
...item,
|
||||
id: generateScheduleItemId(),
|
||||
estimatedStartTime: new Date(currentTime),
|
||||
estimatedEndTime: endTime,
|
||||
});
|
||||
|
||||
currentTime = endTime;
|
||||
}
|
||||
}
|
||||
|
||||
const schedule: DailySchedule = {
|
||||
date,
|
||||
items: scheduledItems,
|
||||
slots,
|
||||
totalCapacity: state.constraints.maxDailyCapacity,
|
||||
totalUtilization: 0,
|
||||
efficiency: 0,
|
||||
bottlenecks: [],
|
||||
};
|
||||
|
||||
recalculateScheduleMetrics(schedule);
|
||||
return schedule;
|
||||
};
|
||||
|
||||
// Check capacity for new item
|
||||
const checkCapacity = useCallback(async (date: string, newItem: Omit<ScheduleItem, 'id'>) => {
|
||||
const conflicts: string[] = [];
|
||||
let canSchedule = true;
|
||||
|
||||
// Check time conflicts
|
||||
if (state.currentSchedule) {
|
||||
const hasTimeConflict = state.currentSchedule.items.some(item => {
|
||||
return (newItem.estimatedStartTime < item.estimatedEndTime &&
|
||||
newItem.estimatedEndTime > item.estimatedStartTime);
|
||||
});
|
||||
|
||||
if (hasTimeConflict) {
|
||||
conflicts.push('Conflicto de horario');
|
||||
canSchedule = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check ingredient availability
|
||||
const ingredientCheck = await checkIngredientAvailability([newItem as ScheduleItem]);
|
||||
if (!ingredientCheck.available) {
|
||||
conflicts.push('Ingredientes insuficientes');
|
||||
canSchedule = false;
|
||||
}
|
||||
|
||||
return { canSchedule, conflicts };
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
// Get available slots
|
||||
const getAvailableSlots = useCallback(async (date: string, duration: number): Promise<ProductionSlot[]> => {
|
||||
if (!state.currentSchedule) return [];
|
||||
|
||||
return state.currentSchedule.slots.filter(slot => {
|
||||
const slotDuration = slot.endTime.getTime() - slot.startTime.getTime();
|
||||
return slot.isAvailable && slotDuration >= duration * 60000;
|
||||
});
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
// Check ingredient availability
|
||||
const checkIngredientAvailability = useCallback(async (items: ScheduleItem[]) => {
|
||||
try {
|
||||
const stockLevels = await inventoryService.getStockLevels();
|
||||
const shortages: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
for (const ingredient of item.requiredIngredients) {
|
||||
const stock = stockLevels.data?.find((s: any) => s.ingredient_id === ingredient.ingredientId);
|
||||
if (!stock || stock.current_quantity < ingredient.requiredQuantity) {
|
||||
shortages.push({
|
||||
ingredientName: ingredient.ingredientName,
|
||||
required: ingredient.requiredQuantity,
|
||||
available: stock?.current_quantity || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { available: shortages.length === 0, shortages };
|
||||
} catch (error) {
|
||||
return { available: false, shortages: [] };
|
||||
}
|
||||
}, [inventoryService]);
|
||||
|
||||
// Generate from forecast
|
||||
const generateFromForecast = useCallback(async (date: string): Promise<boolean> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
// Get forecast data
|
||||
const forecast = await forecastingService.generateDemandForecast('default', 1);
|
||||
|
||||
if (!forecast) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'No se pudo obtener predicción de demanda',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert forecast to schedule items
|
||||
const items = convertForecastToScheduleItems(forecast);
|
||||
|
||||
// Auto-schedule the items
|
||||
return await autoSchedule(date, items);
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
error: 'Error al generar programación desde predicción',
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}, [forecastingService, autoSchedule]);
|
||||
|
||||
// Convert forecast to schedule items
|
||||
const convertForecastToScheduleItems = (forecast: any): Omit<ScheduleItem, 'id'>[] => {
|
||||
if (!forecast.products) return [];
|
||||
|
||||
return forecast.products.map((product: any) => ({
|
||||
recipeId: product.recipe_id || `recipe_${product.id}`,
|
||||
recipeName: product.name,
|
||||
quantity: product.estimated_quantity || 1,
|
||||
priority: 'medium' as const,
|
||||
estimatedStartTime: new Date(),
|
||||
estimatedEndTime: new Date(Date.now() + (product.production_time || 60) * 60000),
|
||||
status: 'scheduled' as const,
|
||||
requiredIngredients: product.ingredients || [],
|
||||
}));
|
||||
};
|
||||
|
||||
// Recalculate schedule metrics
|
||||
const recalculateScheduleMetrics = (schedule: DailySchedule) => {
|
||||
const totalScheduledTime = schedule.items.reduce((total, item) => {
|
||||
return total + (item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime());
|
||||
}, 0);
|
||||
|
||||
schedule.totalUtilization = (totalScheduledTime / (schedule.totalCapacity * 60000)) * 100;
|
||||
schedule.efficiency = calculateEfficiency(schedule.items);
|
||||
schedule.bottlenecks = identifyBottlenecks(schedule.items);
|
||||
};
|
||||
|
||||
// Calculate efficiency
|
||||
const calculateEfficiency = (items: ScheduleItem[]): number => {
|
||||
if (items.length === 0) return 0;
|
||||
|
||||
const completedItems = items.filter(item => item.status === 'completed');
|
||||
return (completedItems.length / items.length) * 100;
|
||||
};
|
||||
|
||||
// Identify bottlenecks
|
||||
const identifyBottlenecks = (items: ScheduleItem[]): string[] => {
|
||||
const bottlenecks: string[] = [];
|
||||
|
||||
// Check for equipment conflicts
|
||||
const equipmentUsage: Record<string, number> = {};
|
||||
items.forEach(item => {
|
||||
item.assignedEquipment?.forEach(equipment => {
|
||||
equipmentUsage[equipment] = (equipmentUsage[equipment] || 0) + 1;
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(equipmentUsage).forEach(([equipment, usage]) => {
|
||||
if (usage > 1) {
|
||||
bottlenecks.push(`Conflicto de equipamiento: ${equipment}`);
|
||||
}
|
||||
});
|
||||
|
||||
return bottlenecks;
|
||||
};
|
||||
|
||||
// Generate unique ID
|
||||
const generateScheduleItemId = (): string => {
|
||||
return `schedule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
};
|
||||
|
||||
// Get schedule from API (placeholder)
|
||||
const getScheduleFromAPI = async (date: string): Promise<DailySchedule | null> => {
|
||||
// This would fetch from actual API
|
||||
return null;
|
||||
};
|
||||
|
||||
// Placeholder implementations for remaining functions
|
||||
const updateSchedule = useCallback(async (schedule: Partial<DailySchedule>): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const optimizeSchedule = useCallback(async (date: string): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const calculateUtilization = useCallback(async (date: string): Promise<number> => {
|
||||
return state.currentSchedule?.totalUtilization || 0;
|
||||
}, [state.currentSchedule]);
|
||||
|
||||
const checkEquipmentAvailability = useCallback(async (date: string, equipment: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const checkStaffAvailability = useCallback(async (date: string, staff: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const getScheduleAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const getBottleneckAnalysis = useCallback(async (date: string): Promise<any> => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const getEfficiencyReport = useCallback(async (period: string): Promise<any> => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const predictDelays = useCallback(async (date: string): Promise<any> => {
|
||||
return {};
|
||||
}, []);
|
||||
|
||||
const saveScheduleTemplate = useCallback(async (name: string, template: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const loadScheduleTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const getScheduleTemplates = useCallback(async (): Promise<any[]> => {
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
}, []);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (state.currentSchedule) {
|
||||
await loadSchedule(state.currentSchedule.date);
|
||||
}
|
||||
}, [state.currentSchedule, loadSchedule]);
|
||||
|
||||
// Load today's schedule on mount
|
||||
useEffect(() => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
loadSchedule(today);
|
||||
}, [loadSchedule]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
loadSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
addScheduleItem,
|
||||
updateScheduleItem,
|
||||
removeScheduleItem,
|
||||
moveScheduleItem,
|
||||
autoSchedule,
|
||||
optimizeSchedule,
|
||||
generateFromForecast,
|
||||
checkCapacity,
|
||||
getAvailableSlots,
|
||||
calculateUtilization,
|
||||
checkIngredientAvailability,
|
||||
checkEquipmentAvailability,
|
||||
checkStaffAvailability,
|
||||
getScheduleAnalytics,
|
||||
getBottleneckAnalysis,
|
||||
getEfficiencyReport,
|
||||
predictDelays,
|
||||
saveScheduleTemplate,
|
||||
loadScheduleTemplate,
|
||||
getScheduleTemplates,
|
||||
clearError,
|
||||
refresh,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user