655 lines
20 KiB
TypeScript
655 lines
20 KiB
TypeScript
|
|
/**
|
||
|
|
* 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,
|
||
|
|
};
|
||
|
|
};
|