ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -0,0 +1,655 @@
/**
* Production schedule hook for managing bakery production scheduling and capacity planning
*/
import { useState, useEffect, useCallback } from 'react';
import { ProductionService } from '../../services/api/production.service';
import { InventoryService } from '../../services/api/inventory.service';
import { ForecastingService } from '../../services/api/forecasting.service';
export interface ScheduleItem {
id: string;
recipeId: string;
recipeName: string;
quantity: number;
priority: 'low' | 'medium' | 'high' | 'urgent';
estimatedStartTime: Date;
estimatedEndTime: Date;
actualStartTime?: Date;
actualEndTime?: Date;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'delayed';
assignedEquipment?: string[];
assignedStaff?: string[];
requiredIngredients: {
ingredientId: string;
ingredientName: string;
requiredQuantity: number;
availableQuantity: number;
unit: string;
}[];
notes?: string;
dependencies?: string[];
}
export interface ProductionSlot {
startTime: Date;
endTime: Date;
isAvailable: boolean;
assignedItems: ScheduleItem[];
capacity: number;
utilizationRate: number;
}
export interface DailySchedule {
date: string;
items: ScheduleItem[];
slots: ProductionSlot[];
totalCapacity: number;
totalUtilization: number;
efficiency: number;
bottlenecks: string[];
}
interface ProductionScheduleState {
currentSchedule: DailySchedule | null;
scheduleHistory: DailySchedule[];
availableRecipes: any[];
equipmentStatus: any[];
staffAvailability: any[];
isLoading: boolean;
error: string | null;
constraints: {
maxDailyCapacity: number;
workingHours: { start: string; end: string };
equipmentLimitations: Record<string, number>;
staffLimitations: Record<string, number>;
};
}
interface ProductionScheduleActions {
// Schedule Management
loadSchedule: (date: string) => Promise<void>;
createSchedule: (date: string) => Promise<void>;
updateSchedule: (schedule: Partial<DailySchedule>) => Promise<boolean>;
// Schedule Items
addScheduleItem: (item: Omit<ScheduleItem, 'id'>) => Promise<boolean>;
updateScheduleItem: (id: string, item: Partial<ScheduleItem>) => Promise<boolean>;
removeScheduleItem: (id: string) => Promise<boolean>;
moveScheduleItem: (id: string, newStartTime: Date) => Promise<boolean>;
// Automatic Scheduling
autoSchedule: (date: string, items: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
optimizeSchedule: (date: string) => Promise<boolean>;
generateFromForecast: (date: string) => Promise<boolean>;
// Capacity Management
checkCapacity: (date: string, newItem: Omit<ScheduleItem, 'id'>) => Promise<{ canSchedule: boolean; suggestedTime?: Date; conflicts?: string[] }>;
getAvailableSlots: (date: string, duration: number) => Promise<ProductionSlot[]>;
calculateUtilization: (date: string) => Promise<number>;
// Resource Management
checkIngredientAvailability: (items: ScheduleItem[]) => Promise<{ available: boolean; shortages: any[] }>;
checkEquipmentAvailability: (date: string, equipment: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
checkStaffAvailability: (date: string, staff: string[], timeSlot: { start: Date; end: Date }) => Promise<boolean>;
// Analytics and Optimization
getScheduleAnalytics: (startDate: string, endDate: string) => Promise<any>;
getBottleneckAnalysis: (date: string) => Promise<any>;
getEfficiencyReport: (period: string) => Promise<any>;
predictDelays: (date: string) => Promise<any>;
// Templates and Presets
saveScheduleTemplate: (name: string, template: Omit<ScheduleItem, 'id'>[]) => Promise<boolean>;
loadScheduleTemplate: (templateId: string, date: string) => Promise<boolean>;
getScheduleTemplates: () => Promise<any[]>;
// Utilities
clearError: () => void;
refresh: () => Promise<void>;
}
export const useProductionSchedule = (): ProductionScheduleState & ProductionScheduleActions => {
const [state, setState] = useState<ProductionScheduleState>({
currentSchedule: null,
scheduleHistory: [],
availableRecipes: [],
equipmentStatus: [],
staffAvailability: [],
isLoading: false,
error: null,
constraints: {
maxDailyCapacity: 8 * 60, // 8 hours in minutes
workingHours: { start: '06:00', end: '20:00' },
equipmentLimitations: {},
staffLimitations: {},
},
});
const productionService = new ProductionService();
const inventoryService = new InventoryService();
const forecastingService = new ForecastingService();
// Load schedule for specific date
const loadSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get schedule data from API
const scheduleData = await getScheduleFromAPI(date);
if (scheduleData) {
setState(prev => ({
...prev,
currentSchedule: scheduleData,
isLoading: false,
}));
} else {
// Create new schedule if none exists
await createSchedule(date);
}
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al cargar programación de producción',
}));
}
}, []);
// Create new schedule
const createSchedule = useCallback(async (date: string) => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30); // 30-minute slots
const newSchedule: DailySchedule = {
date,
items: [],
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
setState(prev => ({
...prev,
currentSchedule: newSchedule,
isLoading: false,
}));
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al crear nueva programación',
}));
}
}, [state.constraints]);
// Generate working hours for a date
const generateWorkingHours = (date: string, workingHours: { start: string; end: string }) => {
const startTime = new Date(`${date}T${workingHours.start}`);
const endTime = new Date(`${date}T${workingHours.end}`);
return { startTime, endTime };
};
// Generate time slots
const generateTimeSlots = (workingHours: { startTime: Date; endTime: Date }, slotDuration: number): ProductionSlot[] => {
const slots: ProductionSlot[] = [];
const current = new Date(workingHours.startTime);
while (current < workingHours.endTime) {
const slotEnd = new Date(current.getTime() + slotDuration * 60000);
slots.push({
startTime: new Date(current),
endTime: slotEnd,
isAvailable: true,
assignedItems: [],
capacity: 1,
utilizationRate: 0,
});
current.setTime(slotEnd.getTime());
}
return slots;
};
// Add schedule item
const addScheduleItem = useCallback(async (item: Omit<ScheduleItem, 'id'>): Promise<boolean> => {
if (!state.currentSchedule) return false;
setState(prev => ({ ...prev, error: null }));
try {
// Check capacity and resources
const capacityCheck = await checkCapacity(state.currentSchedule.date, item);
if (!capacityCheck.canSchedule) {
setState(prev => ({
...prev,
error: `No se puede programar: ${capacityCheck.conflicts?.join(', ')}`,
}));
return false;
}
const newItem: ScheduleItem = {
...item,
id: generateScheduleItemId(),
};
const updatedSchedule = {
...state.currentSchedule,
items: [...state.currentSchedule.items, newItem],
};
// Recalculate utilization and efficiency
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al agregar item a la programación' }));
return false;
}
}, [state.currentSchedule]);
// Update schedule item
const updateScheduleItem = useCallback(async (id: string, item: Partial<ScheduleItem>): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.map(scheduleItem =>
scheduleItem.id === id ? { ...scheduleItem, ...item } : scheduleItem
),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al actualizar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Remove schedule item
const removeScheduleItem = useCallback(async (id: string): Promise<boolean> => {
if (!state.currentSchedule) return false;
try {
const updatedSchedule = {
...state.currentSchedule,
items: state.currentSchedule.items.filter(item => item.id !== id),
};
recalculateScheduleMetrics(updatedSchedule);
setState(prev => ({
...prev,
currentSchedule: updatedSchedule,
}));
return true;
} catch (error) {
setState(prev => ({ ...prev, error: 'Error al eliminar item de programación' }));
return false;
}
}, [state.currentSchedule]);
// Move schedule item to new time
const moveScheduleItem = useCallback(async (id: string, newStartTime: Date): Promise<boolean> => {
if (!state.currentSchedule) return false;
const item = state.currentSchedule.items.find(item => item.id === id);
if (!item) return false;
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const newEndTime = new Date(newStartTime.getTime() + duration);
return updateScheduleItem(id, {
estimatedStartTime: newStartTime,
estimatedEndTime: newEndTime,
});
}, [state.currentSchedule, updateScheduleItem]);
// Auto-schedule items
const autoSchedule = useCallback(async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Sort items by priority and estimated duration
const sortedItems = [...items].sort((a, b) => {
const priorityOrder = { urgent: 4, high: 3, medium: 2, low: 1 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
});
const schedule = await createOptimalSchedule(date, sortedItems);
setState(prev => ({
...prev,
currentSchedule: schedule,
isLoading: false,
}));
return true;
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al programar automáticamente',
}));
return false;
}
}, []);
// Create optimal schedule
const createOptimalSchedule = async (date: string, items: Omit<ScheduleItem, 'id'>[]): Promise<DailySchedule> => {
const workingHours = generateWorkingHours(date, state.constraints.workingHours);
const slots = generateTimeSlots(workingHours, 30);
const scheduledItems: ScheduleItem[] = [];
let currentTime = new Date(workingHours.startTime);
for (const item of items) {
const duration = item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime();
const endTime = new Date(currentTime.getTime() + duration);
// Check if item fits in remaining time
if (endTime <= workingHours.endTime) {
scheduledItems.push({
...item,
id: generateScheduleItemId(),
estimatedStartTime: new Date(currentTime),
estimatedEndTime: endTime,
});
currentTime = endTime;
}
}
const schedule: DailySchedule = {
date,
items: scheduledItems,
slots,
totalCapacity: state.constraints.maxDailyCapacity,
totalUtilization: 0,
efficiency: 0,
bottlenecks: [],
};
recalculateScheduleMetrics(schedule);
return schedule;
};
// Check capacity for new item
const checkCapacity = useCallback(async (date: string, newItem: Omit<ScheduleItem, 'id'>) => {
const conflicts: string[] = [];
let canSchedule = true;
// Check time conflicts
if (state.currentSchedule) {
const hasTimeConflict = state.currentSchedule.items.some(item => {
return (newItem.estimatedStartTime < item.estimatedEndTime &&
newItem.estimatedEndTime > item.estimatedStartTime);
});
if (hasTimeConflict) {
conflicts.push('Conflicto de horario');
canSchedule = false;
}
}
// Check ingredient availability
const ingredientCheck = await checkIngredientAvailability([newItem as ScheduleItem]);
if (!ingredientCheck.available) {
conflicts.push('Ingredientes insuficientes');
canSchedule = false;
}
return { canSchedule, conflicts };
}, [state.currentSchedule]);
// Get available slots
const getAvailableSlots = useCallback(async (date: string, duration: number): Promise<ProductionSlot[]> => {
if (!state.currentSchedule) return [];
return state.currentSchedule.slots.filter(slot => {
const slotDuration = slot.endTime.getTime() - slot.startTime.getTime();
return slot.isAvailable && slotDuration >= duration * 60000;
});
}, [state.currentSchedule]);
// Check ingredient availability
const checkIngredientAvailability = useCallback(async (items: ScheduleItem[]) => {
try {
const stockLevels = await inventoryService.getStockLevels();
const shortages: any[] = [];
for (const item of items) {
for (const ingredient of item.requiredIngredients) {
const stock = stockLevels.data?.find((s: any) => s.ingredient_id === ingredient.ingredientId);
if (!stock || stock.current_quantity < ingredient.requiredQuantity) {
shortages.push({
ingredientName: ingredient.ingredientName,
required: ingredient.requiredQuantity,
available: stock?.current_quantity || 0,
});
}
}
}
return { available: shortages.length === 0, shortages };
} catch (error) {
return { available: false, shortages: [] };
}
}, [inventoryService]);
// Generate from forecast
const generateFromForecast = useCallback(async (date: string): Promise<boolean> => {
setState(prev => ({ ...prev, isLoading: true, error: null }));
try {
// Get forecast data
const forecast = await forecastingService.generateDemandForecast('default', 1);
if (!forecast) {
setState(prev => ({
...prev,
isLoading: false,
error: 'No se pudo obtener predicción de demanda',
}));
return false;
}
// Convert forecast to schedule items
const items = convertForecastToScheduleItems(forecast);
// Auto-schedule the items
return await autoSchedule(date, items);
} catch (error) {
setState(prev => ({
...prev,
isLoading: false,
error: 'Error al generar programación desde predicción',
}));
return false;
}
}, [forecastingService, autoSchedule]);
// Convert forecast to schedule items
const convertForecastToScheduleItems = (forecast: any): Omit<ScheduleItem, 'id'>[] => {
if (!forecast.products) return [];
return forecast.products.map((product: any) => ({
recipeId: product.recipe_id || `recipe_${product.id}`,
recipeName: product.name,
quantity: product.estimated_quantity || 1,
priority: 'medium' as const,
estimatedStartTime: new Date(),
estimatedEndTime: new Date(Date.now() + (product.production_time || 60) * 60000),
status: 'scheduled' as const,
requiredIngredients: product.ingredients || [],
}));
};
// Recalculate schedule metrics
const recalculateScheduleMetrics = (schedule: DailySchedule) => {
const totalScheduledTime = schedule.items.reduce((total, item) => {
return total + (item.estimatedEndTime.getTime() - item.estimatedStartTime.getTime());
}, 0);
schedule.totalUtilization = (totalScheduledTime / (schedule.totalCapacity * 60000)) * 100;
schedule.efficiency = calculateEfficiency(schedule.items);
schedule.bottlenecks = identifyBottlenecks(schedule.items);
};
// Calculate efficiency
const calculateEfficiency = (items: ScheduleItem[]): number => {
if (items.length === 0) return 0;
const completedItems = items.filter(item => item.status === 'completed');
return (completedItems.length / items.length) * 100;
};
// Identify bottlenecks
const identifyBottlenecks = (items: ScheduleItem[]): string[] => {
const bottlenecks: string[] = [];
// Check for equipment conflicts
const equipmentUsage: Record<string, number> = {};
items.forEach(item => {
item.assignedEquipment?.forEach(equipment => {
equipmentUsage[equipment] = (equipmentUsage[equipment] || 0) + 1;
});
});
Object.entries(equipmentUsage).forEach(([equipment, usage]) => {
if (usage > 1) {
bottlenecks.push(`Conflicto de equipamiento: ${equipment}`);
}
});
return bottlenecks;
};
// Generate unique ID
const generateScheduleItemId = (): string => {
return `schedule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
// Get schedule from API (placeholder)
const getScheduleFromAPI = async (date: string): Promise<DailySchedule | null> => {
// This would fetch from actual API
return null;
};
// Placeholder implementations for remaining functions
const updateSchedule = useCallback(async (schedule: Partial<DailySchedule>): Promise<boolean> => {
return true;
}, []);
const optimizeSchedule = useCallback(async (date: string): Promise<boolean> => {
return true;
}, []);
const calculateUtilization = useCallback(async (date: string): Promise<number> => {
return state.currentSchedule?.totalUtilization || 0;
}, [state.currentSchedule]);
const checkEquipmentAvailability = useCallback(async (date: string, equipment: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const checkStaffAvailability = useCallback(async (date: string, staff: string[], timeSlot: { start: Date; end: Date }): Promise<boolean> => {
return true;
}, []);
const getScheduleAnalytics = useCallback(async (startDate: string, endDate: string): Promise<any> => {
return {};
}, []);
const getBottleneckAnalysis = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const getEfficiencyReport = useCallback(async (period: string): Promise<any> => {
return {};
}, []);
const predictDelays = useCallback(async (date: string): Promise<any> => {
return {};
}, []);
const saveScheduleTemplate = useCallback(async (name: string, template: Omit<ScheduleItem, 'id'>[]): Promise<boolean> => {
return true;
}, []);
const loadScheduleTemplate = useCallback(async (templateId: string, date: string): Promise<boolean> => {
return true;
}, []);
const getScheduleTemplates = useCallback(async (): Promise<any[]> => {
return [];
}, []);
const clearError = useCallback(() => {
setState(prev => ({ ...prev, error: null }));
}, []);
const refresh = useCallback(async () => {
if (state.currentSchedule) {
await loadSchedule(state.currentSchedule.date);
}
}, [state.currentSchedule, loadSchedule]);
// Load today's schedule on mount
useEffect(() => {
const today = new Date().toISOString().split('T')[0];
loadSchedule(today);
}, [loadSchedule]);
return {
...state,
loadSchedule,
createSchedule,
updateSchedule,
addScheduleItem,
updateScheduleItem,
removeScheduleItem,
moveScheduleItem,
autoSchedule,
optimizeSchedule,
generateFromForecast,
checkCapacity,
getAvailableSlots,
calculateUtilization,
checkIngredientAvailability,
checkEquipmentAvailability,
checkStaffAvailability,
getScheduleAnalytics,
getBottleneckAnalysis,
getEfficiencyReport,
predictDelays,
saveScheduleTemplate,
loadScheduleTemplate,
getScheduleTemplates,
clearError,
refresh,
};
};