/** * 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; staffLimitations: Record; }; } interface ProductionScheduleActions { // Schedule Management loadSchedule: (date: string) => Promise; createSchedule: (date: string) => Promise; updateSchedule: (schedule: Partial) => Promise; // Schedule Items addScheduleItem: (item: Omit) => Promise; updateScheduleItem: (id: string, item: Partial) => Promise; removeScheduleItem: (id: string) => Promise; moveScheduleItem: (id: string, newStartTime: Date) => Promise; // Automatic Scheduling autoSchedule: (date: string, items: Omit[]) => Promise; optimizeSchedule: (date: string) => Promise; generateFromForecast: (date: string) => Promise; // Capacity Management checkCapacity: (date: string, newItem: Omit) => Promise<{ canSchedule: boolean; suggestedTime?: Date; conflicts?: string[] }>; getAvailableSlots: (date: string, duration: number) => Promise; calculateUtilization: (date: string) => Promise; // Resource Management checkIngredientAvailability: (items: ScheduleItem[]) => Promise<{ available: boolean; shortages: any[] }>; checkEquipmentAvailability: (date: string, equipment: string[], timeSlot: { start: Date; end: Date }) => Promise; checkStaffAvailability: (date: string, staff: string[], timeSlot: { start: Date; end: Date }) => Promise; // Analytics and Optimization getScheduleAnalytics: (startDate: string, endDate: string) => Promise; getBottleneckAnalysis: (date: string) => Promise; getEfficiencyReport: (period: string) => Promise; predictDelays: (date: string) => Promise; // Templates and Presets saveScheduleTemplate: (name: string, template: Omit[]) => Promise; loadScheduleTemplate: (templateId: string, date: string) => Promise; getScheduleTemplates: () => Promise; // Utilities clearError: () => void; refresh: () => Promise; } export const useProductionSchedule = (): ProductionScheduleState & ProductionScheduleActions => { const [state, setState] = useState({ 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): Promise => { 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): Promise => { 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 => { 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 => { 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[]): Promise => { 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[]): Promise => { 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) => { 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 => { 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 => { 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[] => { 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 = {}; 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 => { // This would fetch from actual API return null; }; // Placeholder implementations for remaining functions const updateSchedule = useCallback(async (schedule: Partial): Promise => { return true; }, []); const optimizeSchedule = useCallback(async (date: string): Promise => { return true; }, []); const calculateUtilization = useCallback(async (date: string): Promise => { return state.currentSchedule?.totalUtilization || 0; }, [state.currentSchedule]); const checkEquipmentAvailability = useCallback(async (date: string, equipment: string[], timeSlot: { start: Date; end: Date }): Promise => { return true; }, []); const checkStaffAvailability = useCallback(async (date: string, staff: string[], timeSlot: { start: Date; end: Date }): Promise => { return true; }, []); const getScheduleAnalytics = useCallback(async (startDate: string, endDate: string): Promise => { return {}; }, []); const getBottleneckAnalysis = useCallback(async (date: string): Promise => { return {}; }, []); const getEfficiencyReport = useCallback(async (period: string): Promise => { return {}; }, []); const predictDelays = useCallback(async (date: string): Promise => { return {}; }, []); const saveScheduleTemplate = useCallback(async (name: string, template: Omit[]): Promise => { return true; }, []); const loadScheduleTemplate = useCallback(async (templateId: string, date: string): Promise => { return true; }, []); const getScheduleTemplates = useCallback(async (): Promise => { 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, }; };