Start integrating the onboarding flow with backend 6
This commit is contained in:
@@ -1,621 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
@@ -1,520 +0,0 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
};
|
||||
@@ -4,23 +4,26 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { inventoryService } from '../../services/api/inventory.service';
|
||||
import { salesService } from '../../services/api/sales.service';
|
||||
import { authService } from '../../services/api/auth.service';
|
||||
import { tenantService } from '../../services/api/tenant.service';
|
||||
import { useAuthUser } from '../../stores/auth.store';
|
||||
import { useAlertActions } from '../../stores/alerts.store';
|
||||
import {
|
||||
ProductSuggestion,
|
||||
ProductSuggestionsResponse,
|
||||
InventoryCreationResponse
|
||||
} from '../../types/inventory.types';
|
||||
import {
|
||||
BusinessModelGuide,
|
||||
BusinessModelType,
|
||||
TemplateData
|
||||
} from '../../types/sales.types';
|
||||
import { OnboardingStatus } from '../../types/auth.types';
|
||||
import {
|
||||
// Auth hooks
|
||||
useAuthProfile,
|
||||
// Tenant hooks
|
||||
useRegisterBakery,
|
||||
// Sales hooks
|
||||
useValidateSalesRecord,
|
||||
// Inventory hooks
|
||||
useClassifyProductsBatch,
|
||||
useCreateIngredient,
|
||||
// Classification hooks
|
||||
useBusinessModelAnalysis,
|
||||
// Types
|
||||
type User,
|
||||
type BakeryRegistration,
|
||||
type ProductSuggestionResponse,
|
||||
type BusinessModelAnalysisResponse,
|
||||
type ProductClassificationRequest,
|
||||
} from '../../api';
|
||||
|
||||
export interface OnboardingStep {
|
||||
id: string;
|
||||
@@ -33,16 +36,7 @@ export interface OnboardingStep {
|
||||
|
||||
export interface OnboardingData {
|
||||
// Step 1: Setup
|
||||
bakery?: {
|
||||
name: string;
|
||||
business_model: BusinessModelType;
|
||||
address: string;
|
||||
city: string;
|
||||
postal_code: string;
|
||||
phone: string;
|
||||
email?: string;
|
||||
description?: string;
|
||||
};
|
||||
bakery?: BakeryRegistration;
|
||||
|
||||
// Step 2: Data Processing
|
||||
files?: {
|
||||
@@ -64,8 +58,8 @@ export interface OnboardingData {
|
||||
};
|
||||
|
||||
// Step 3: Review
|
||||
suggestions?: ProductSuggestion[];
|
||||
approvedSuggestions?: ProductSuggestion[];
|
||||
suggestions?: ProductSuggestionResponse[];
|
||||
approvedSuggestions?: ProductSuggestionResponse[];
|
||||
reviewCompleted?: boolean;
|
||||
|
||||
// Step 4: Inventory
|
||||
@@ -99,7 +93,7 @@ interface OnboardingState {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
isInitialized: boolean;
|
||||
onboardingStatus: OnboardingStatus | null;
|
||||
onboardingStatus: User['onboarding_status'] | null;
|
||||
}
|
||||
|
||||
interface OnboardingActions {
|
||||
@@ -113,12 +107,12 @@ interface OnboardingActions {
|
||||
validateCurrentStep: () => string | null;
|
||||
|
||||
// Step-specific Actions
|
||||
createTenant: (bakeryData: OnboardingData['bakery']) => Promise<boolean>;
|
||||
createTenant: (bakeryData: BakeryRegistration) => Promise<boolean>;
|
||||
processSalesFile: (file: File, onProgress: (progress: number, stage: string, message: string) => void) => Promise<boolean>;
|
||||
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionsResponse | null>;
|
||||
createInventoryFromSuggestions: (suggestions: ProductSuggestion[]) => Promise<InventoryCreationResponse | null>;
|
||||
getBusinessModelGuide: (model: BusinessModelType) => Promise<BusinessModelGuide | null>;
|
||||
downloadTemplate: (templateData: TemplateData, filename: string, format?: 'csv' | 'json') => void;
|
||||
generateInventorySuggestions: (productList: string[]) => Promise<ProductSuggestionResponse[] | null>;
|
||||
createInventoryFromSuggestions: (suggestions: ProductSuggestionResponse[]) => Promise<any | null>;
|
||||
getBusinessModelGuide: (model: string) => Promise<BusinessModelAnalysisResponse | null>;
|
||||
downloadTemplate: (templateData: any, filename: string, format?: 'csv' | 'json') => void;
|
||||
|
||||
// Completion
|
||||
completeOnboarding: () => Promise<boolean>;
|
||||
@@ -138,7 +132,7 @@ const DEFAULT_STEPS: OnboardingStep[] = [
|
||||
isCompleted: false,
|
||||
validation: (data: OnboardingData) => {
|
||||
if (!data.bakery?.name) return 'El nombre de la panadería es requerido';
|
||||
if (!data.bakery?.business_model) return 'El modelo de negocio es requerido';
|
||||
if (!data.bakery?.business_type) return 'El tipo de negocio es requerido';
|
||||
if (!data.bakery?.address) return 'La dirección es requerida';
|
||||
if (!data.bakery?.city) return 'La ciudad es requerida';
|
||||
if (!data.bakery?.postal_code) return 'El código postal es requerido';
|
||||
@@ -221,7 +215,13 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
|
||||
const navigate = useNavigate();
|
||||
const user = useAuthUser();
|
||||
const { createAlert } = useAlertActions();
|
||||
|
||||
// React Query hooks
|
||||
const { data: profile } = useAuthProfile();
|
||||
const registerBakeryMutation = useRegisterBakery();
|
||||
const validateSalesMutation = useValidateSalesRecord();
|
||||
const classifyProductsMutation = useClassifyProductsBatch();
|
||||
const businessModelMutation = useBusinessModelAnalysis();
|
||||
|
||||
// Initialize onboarding status
|
||||
useEffect(() => {
|
||||
@@ -236,14 +236,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
const validation = validateCurrentStep();
|
||||
|
||||
if (validation) {
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'validation',
|
||||
priority: 'high',
|
||||
title: 'Validación fallida',
|
||||
message: validation,
|
||||
source: 'onboarding'
|
||||
});
|
||||
setState(prev => ({ ...prev, error: validation }));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -302,54 +295,25 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
}, [state.currentStep, state.steps, state.data]);
|
||||
|
||||
// Step-specific Actions
|
||||
const createTenant = useCallback(async (bakeryData: OnboardingData['bakery']): Promise<boolean> => {
|
||||
const createTenant = useCallback(async (bakeryData: BakeryRegistration): Promise<boolean> => {
|
||||
if (!bakeryData) return false;
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tenantService.createTenant({
|
||||
name: bakeryData.name,
|
||||
description: bakeryData.description || '',
|
||||
business_type: bakeryData.business_model,
|
||||
settings: {
|
||||
address: bakeryData.address,
|
||||
city: bakeryData.city,
|
||||
postal_code: bakeryData.postal_code,
|
||||
phone: bakeryData.phone,
|
||||
email: bakeryData.email,
|
||||
}
|
||||
await registerBakeryMutation.mutateAsync({
|
||||
bakeryData
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
updateStepData('setup', { bakery: bakeryData });
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'medium',
|
||||
title: 'Tenant creado',
|
||||
message: 'Tu panadería ha sido configurada exitosamente',
|
||||
source: 'onboarding'
|
||||
});
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error creating tenant');
|
||||
}
|
||||
|
||||
updateStepData('setup', { bakery: bakeryData });
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error desconocido';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: 'Error al crear tenant',
|
||||
message: errorMessage,
|
||||
source: 'onboarding'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}, [updateStepData, createAlert]);
|
||||
}, [updateStepData, registerBakeryMutation]);
|
||||
|
||||
const processSalesFile = useCallback(async (
|
||||
file: File,
|
||||
@@ -360,16 +324,19 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
try {
|
||||
// Stage 1: Validate file
|
||||
onProgress(20, 'validating', 'Validando estructura del archivo...');
|
||||
const validationResult = await salesService.validateSalesData(file);
|
||||
|
||||
const validationResult = await validateSalesMutation.mutateAsync({
|
||||
// Convert file to the expected format for validation
|
||||
data: {
|
||||
// This would need to be adapted based on the actual API structure
|
||||
file_data: file
|
||||
}
|
||||
});
|
||||
|
||||
onProgress(40, 'validating', 'Verificando integridad de datos...');
|
||||
|
||||
if (!validationResult.is_valid) {
|
||||
throw new Error('Archivo de datos inválido');
|
||||
}
|
||||
|
||||
if (!validationResult.product_list || validationResult.product_list.length === 0) {
|
||||
throw new Error('No se encontraron productos en el archivo');
|
||||
if (!validationResult || !validationResult.product_list || validationResult.product_list.length === 0) {
|
||||
throw new Error('No se encontraron productos válidos en el archivo');
|
||||
}
|
||||
|
||||
// Stage 2: Generate AI suggestions
|
||||
@@ -384,7 +351,7 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
files: { salesData: file },
|
||||
processingStage: 'completed',
|
||||
processingResults: validationResult,
|
||||
suggestions: suggestions?.suggestions || []
|
||||
suggestions: suggestions || []
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
@@ -402,36 +369,48 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
}, [updateStepData]);
|
||||
}, [updateStepData, validateSalesMutation]);
|
||||
|
||||
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionsResponse | null> => {
|
||||
const generateInventorySuggestions = useCallback(async (productList: string[]): Promise<ProductSuggestionResponse[] | null> => {
|
||||
try {
|
||||
const response = await inventoryService.generateInventorySuggestions(productList);
|
||||
return response.success ? response.data : null;
|
||||
const response = await classifyProductsMutation.mutateAsync({
|
||||
products: productList.map(name => ({ name, description: '' }))
|
||||
});
|
||||
return response.suggestions || [];
|
||||
} catch (error) {
|
||||
console.error('Error generating inventory suggestions:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
}, [classifyProductsMutation]);
|
||||
|
||||
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestion[]): Promise<InventoryCreationResponse | null> => {
|
||||
const createInventoryFromSuggestions = useCallback(async (suggestions: ProductSuggestionResponse[]): Promise<any | null> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await inventoryService.createInventoryFromSuggestions(suggestions);
|
||||
// Create ingredients from approved suggestions
|
||||
const createdItems = [];
|
||||
const inventoryMapping: { [key: string]: string } = {};
|
||||
|
||||
if (response.success) {
|
||||
updateStepData('inventory', {
|
||||
inventoryItems: response.data.created_items,
|
||||
inventoryMapping: response.data.inventory_mapping,
|
||||
inventoryConfigured: true
|
||||
for (const suggestion of suggestions) {
|
||||
// This would need to be adapted based on actual API structure
|
||||
const createdItem = await useCreateIngredient().mutateAsync({
|
||||
name: suggestion.name,
|
||||
category: suggestion.category,
|
||||
// Map other suggestion properties to ingredient properties
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error creating inventory');
|
||||
createdItems.push(createdItem);
|
||||
inventoryMapping[suggestion.name] = createdItem.id;
|
||||
}
|
||||
|
||||
updateStepData('inventory', {
|
||||
inventoryItems: createdItems,
|
||||
inventoryMapping,
|
||||
inventoryConfigured: true
|
||||
});
|
||||
|
||||
setState(prev => ({ ...prev, isLoading: false }));
|
||||
return { created_items: createdItems, inventory_mapping: inventoryMapping };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error creating inventory';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
@@ -439,28 +418,49 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
}
|
||||
}, [updateStepData]);
|
||||
|
||||
const getBusinessModelGuide = useCallback(async (model: BusinessModelType): Promise<BusinessModelGuide | null> => {
|
||||
const getBusinessModelGuide = useCallback(async (model: string): Promise<BusinessModelAnalysisResponse | null> => {
|
||||
try {
|
||||
const response = await salesService.getBusinessModelGuide(model);
|
||||
return response.success ? response.data : null;
|
||||
const response = await businessModelMutation.mutateAsync({
|
||||
business_model: model,
|
||||
// Include any other required parameters
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error getting business model guide:', error);
|
||||
return null;
|
||||
}
|
||||
}, [businessModelMutation]);
|
||||
|
||||
const downloadTemplate = useCallback((templateData: any, filename: string, format: 'csv' | 'json' = 'csv') => {
|
||||
// Create and download template file
|
||||
const content = format === 'json' ? JSON.stringify(templateData, null, 2) : convertToCSV(templateData);
|
||||
const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${filename}.${format}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}, []);
|
||||
|
||||
const downloadTemplate = useCallback((templateData: TemplateData, filename: string, format: 'csv' | 'json' = 'csv') => {
|
||||
salesService.downloadTemplate(templateData, filename, format);
|
||||
}, []);
|
||||
const convertToCSV = (data: any): string => {
|
||||
// Simple CSV conversion - this should be adapted based on the actual data structure
|
||||
if (Array.isArray(data)) {
|
||||
const headers = Object.keys(data[0] || {}).join(',');
|
||||
const rows = data.map(item => Object.values(item).join(',')).join('\n');
|
||||
return `${headers}\n${rows}`;
|
||||
}
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
const checkOnboardingStatus = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, isLoading: true }));
|
||||
|
||||
try {
|
||||
const response = await authService.checkOnboardingStatus();
|
||||
// Use the profile data to get onboarding status
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
onboardingStatus: response.success ? response.data : null,
|
||||
onboardingStatus: profile?.onboarding_status || null,
|
||||
isInitialized: true,
|
||||
isLoading: false
|
||||
}));
|
||||
@@ -471,58 +471,33 @@ export const useOnboarding = (): OnboardingState & OnboardingActions => {
|
||||
isLoading: false
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
}, [profile]);
|
||||
|
||||
const completeOnboarding = useCallback(async (): Promise<boolean> => {
|
||||
setState(prev => ({ ...prev, isLoading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await authService.completeOnboarding({
|
||||
completedAt: new Date().toISOString(),
|
||||
data: state.data
|
||||
});
|
||||
// Mark onboarding as completed - this would typically involve an API call
|
||||
// For now, we'll simulate success and navigate to dashboard
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
|
||||
}));
|
||||
|
||||
if (response.success) {
|
||||
createAlert({
|
||||
type: 'success',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: '¡Onboarding completado!',
|
||||
message: 'Has completado exitosamente la configuración inicial',
|
||||
source: 'onboarding'
|
||||
});
|
||||
// Navigate to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
navigate('/app/dashboard');
|
||||
}, 2000);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isLoading: false,
|
||||
steps: prev.steps.map(step => ({ ...step, isCompleted: true }))
|
||||
}));
|
||||
|
||||
// Navigate to dashboard after a short delay
|
||||
setTimeout(() => {
|
||||
navigate('/app/dashboard');
|
||||
}, 2000);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Error(response.error || 'Error completing onboarding');
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Error completing onboarding';
|
||||
setState(prev => ({ ...prev, isLoading: false, error: errorMessage }));
|
||||
|
||||
createAlert({
|
||||
type: 'error',
|
||||
category: 'system',
|
||||
priority: 'high',
|
||||
title: 'Error al completar onboarding',
|
||||
message: errorMessage,
|
||||
source: 'onboarding'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}, [state.data, createAlert, navigate]);
|
||||
}, [state.data, navigate]);
|
||||
|
||||
const clearError = useCallback(() => {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
|
||||
@@ -1,655 +0,0 @@
|
||||
/**
|
||||
* 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