import React, { useState, useCallback, useMemo } from 'react'; import { Card, Button, Badge, Select, DatePicker, Input, Modal, Table } from '../../ui'; import { productionService, type ProductionScheduleEntry, ProductionBatchStatus } from '../../../api/services/production.service'; import type { ProductionSchedule as ProductionScheduleType, ProductionBatch, EquipmentReservation, StaffAssignment } from '../../../types/production.types'; interface ProductionScheduleProps { className?: string; onBatchSelected?: (batch: ProductionBatch) => void; onScheduleUpdate?: (schedule: ProductionScheduleEntry) => void; } interface TimeSlot { time: string; capacity: number; utilized: number; available: number; batches: ProductionScheduleEntry[]; } interface CapacityResource { id: string; name: string; type: 'oven' | 'mixer' | 'staff'; capacity: number; slots: TimeSlot[]; } const PRODUCT_TYPE_COLORS = { pan: 'bg-[var(--color-warning)]/10 border-[var(--color-warning)]/20 text-[var(--color-warning)]', bolleria: 'bg-[var(--color-primary)]/10 border-[var(--color-primary)]/20 text-[var(--color-primary)]', reposteria: 'bg-[var(--color-error)]/10 border-[var(--color-error)]/20 text-[var(--color-error)]', especial: 'bg-[var(--color-info)]/10 border-[var(--color-info)]/20 text-[var(--color-info)]', }; const STATUS_COLORS = { [ProductionBatchStatus.PLANNED]: 'bg-[var(--color-info)]/10 text-[var(--color-info)]', [ProductionBatchStatus.IN_PROGRESS]: 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]', [ProductionBatchStatus.COMPLETED]: 'bg-[var(--color-success)]/10 text-[var(--color-success)]', [ProductionBatchStatus.CANCELLED]: 'bg-[var(--color-error)]/10 text-[var(--color-error)]', [ProductionBatchStatus.ON_HOLD]: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]', }; export const ProductionSchedule: React.FC = ({ className = '', onBatchSelected, onScheduleUpdate, }) => { const [scheduleEntries, setScheduleEntries] = useState([]); const [loading, setLoading] = useState(false); const [selectedDate, setSelectedDate] = useState(new Date()); const [viewMode, setViewMode] = useState<'calendar' | 'timeline' | 'capacity'>('timeline'); const [filterStatus, setFilterStatus] = useState('all'); const [filterProduct, setFilterProduct] = useState('all'); const [draggedBatch, setDraggedBatch] = useState(null); const [isScheduleModalOpen, setIsScheduleModalOpen] = useState(false); const [selectedBatchForScheduling, setSelectedBatchForScheduling] = useState(null); // Mock data for demonstration const mockCapacityResources: CapacityResource[] = [ { id: 'horno-1', name: 'Horno Principal', type: 'oven', capacity: 4, slots: generateTimeSlots('06:00', '20:00', 2), }, { id: 'horno-2', name: 'Horno Secundario', type: 'oven', capacity: 2, slots: generateTimeSlots('06:00', '18:00', 2), }, { id: 'amasadora-1', name: 'Amasadora Industrial', type: 'mixer', capacity: 6, slots: generateTimeSlots('05:00', '21:00', 1), }, { id: 'panadero-1', name: 'Equipo Panadería', type: 'staff', capacity: 8, slots: generateTimeSlots('06:00', '14:00', 1), }, ]; function generateTimeSlots(startTime: string, endTime: string, intervalHours: number): TimeSlot[] { const slots: TimeSlot[] = []; const start = new Date(`2024-01-01 ${startTime}`); const end = new Date(`2024-01-01 ${endTime}`); while (start < end) { const timeStr = start.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); slots.push({ time: timeStr, capacity: 100, utilized: Math.floor(Math.random() * 80), available: 100 - Math.floor(Math.random() * 80), batches: [], }); start.setHours(start.getHours() + intervalHours); } return slots; } const loadSchedule = useCallback(async () => { setLoading(true); try { const response = await productionService.getProductionSchedule({ start_date: selectedDate.toISOString().split('T')[0], end_date: selectedDate.toISOString().split('T')[0], }); if (response.success) { setScheduleEntries(response.data || []); } } catch (error) { console.error('Error loading schedule:', error); } finally { setLoading(false); } }, [selectedDate]); const filteredEntries = useMemo(() => { return scheduleEntries.filter(entry => { if (filterStatus !== 'all' && entry.batch?.status !== filterStatus) { return false; } if (filterProduct !== 'all' && entry.batch?.recipe?.category !== filterProduct) { return false; } return true; }); }, [scheduleEntries, filterStatus, filterProduct]); const handleDragStart = (e: React.DragEvent, entry: ProductionScheduleEntry) => { setDraggedBatch(entry); e.dataTransfer.effectAllowed = 'move'; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; const handleDrop = async (e: React.DragEvent, newTime: string) => { e.preventDefault(); if (!draggedBatch) return; try { const response = await productionService.updateScheduleEntry(draggedBatch.id, { scheduled_start_time: newTime, scheduled_end_time: calculateEndTime(newTime, draggedBatch.estimated_duration_minutes), }); if (response.success && onScheduleUpdate) { onScheduleUpdate(response.data); await loadSchedule(); } } catch (error) { console.error('Error updating schedule:', error); } finally { setDraggedBatch(null); } }; const calculateEndTime = (startTime: string, durationMinutes: number): string => { const start = new Date(`2024-01-01 ${startTime}`); start.setMinutes(start.getMinutes() + durationMinutes); return start.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' }); }; const handleScheduleBatch = async (scheduleData: { batch_id: string; scheduled_date: string; scheduled_start_time: string; scheduled_end_time: string; equipment_reservations?: string[]; staff_assignments?: string[]; }) => { try { const response = await productionService.scheduleProductionBatch(scheduleData); if (response.success && onScheduleUpdate) { onScheduleUpdate(response.data); await loadSchedule(); setIsScheduleModalOpen(false); setSelectedBatchForScheduling(null); } } catch (error) { console.error('Error scheduling batch:', error); } }; const getProductTypeColor = (category?: string) => { return PRODUCT_TYPE_COLORS[category as keyof typeof PRODUCT_TYPE_COLORS] || 'bg-[var(--bg-tertiary)] border-[var(--border-secondary)] text-[var(--text-secondary)]'; }; const getStatusColor = (status?: ProductionBatchStatus) => { return STATUS_COLORS[status as keyof typeof STATUS_COLORS] || 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)]'; }; const renderTimelineView = () => (
setSelectedDate(date || new Date())} dateFormat="dd/MM/yyyy" className="w-40" />
Producto
{Array.from({ length: 22 }, (_, i) => (
{String(i + 3).padStart(2, '0')}:00
))}
{filteredEntries.map((entry) => (
{entry.batch?.recipe?.name} {entry.batch?.status === ProductionBatchStatus.PLANNED && 'Planificado'} {entry.batch?.status === ProductionBatchStatus.IN_PROGRESS && 'En progreso'} {entry.batch?.status === ProductionBatchStatus.COMPLETED && 'Completado'}
{Array.from({ length: 22 }, (_, hourIndex) => { const hour = hourIndex + 3; const hourStr = `${String(hour).padStart(2, '0')}:00`; const startHour = parseInt(entry.scheduled_start_time.split(':')[0]); const endHour = parseInt(entry.scheduled_end_time.split(':')[0]); const isInRange = hour >= startHour && hour < endHour; const isStart = hour === startHour; return (
handleDrop(e, hourStr)} > {isStart && (
handleDragStart(e, entry)} onClick={() => onBatchSelected?.(entry.batch!)} >
{entry.batch?.recipe?.name}
{entry.batch?.planned_quantity} uds
)}
); })}
))}
); const renderCapacityView = () => (
setSelectedDate(date || new Date())} dateFormat="dd/MM/yyyy" className="w-40" />
{mockCapacityResources.map((resource) => (

{resource.name}

{resource.type === 'oven' && 'Horno'} {resource.type === 'mixer' && 'Amasadora'} {resource.type === 'staff' && 'Personal'} - Capacidad: {resource.capacity}

{Math.round(resource.slots.reduce((acc, slot) => acc + slot.utilized, 0) / resource.slots.length)}% utilizado
{resource.slots.slice(0, 8).map((slot, index) => (
{slot.time}
80 ? 'bg-[var(--color-error)]' : slot.utilized > 60 ? 'bg-yellow-500' : 'bg-[var(--color-success)]' }`} style={{ width: `${slot.utilized}%` }} />
{slot.utilized}%
))}
))}
); const renderCalendarView = () => (
{['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom'].map((day) => (
{day}
))}
{Array.from({ length: 35 }, (_, i) => { const date = new Date(selectedDate); date.setDate(date.getDate() - date.getDay() + i); const dayEntries = filteredEntries.filter(entry => new Date(entry.scheduled_date).toDateString() === date.toDateString() ); return (
{date.getDate()}
{dayEntries.slice(0, 3).map((entry) => (
onBatchSelected?.(entry.batch!)} > {entry.batch?.recipe?.name}
))} {dayEntries.length > 3 && (
+{dayEntries.length - 3} más
)}
); })}
); return (

Programación de Producción

Gestiona y programa la producción diaria de la panadería

{loading ? (
) : ( <> {viewMode === 'calendar' && renderCalendarView()} {viewMode === 'timeline' && renderTimelineView()} {viewMode === 'capacity' && renderCapacityView()} )} {/* Schedule Creation Modal */} { setIsScheduleModalOpen(false); setSelectedBatchForScheduling(null); }} title="Programar Lote de Producción" >
setSelectedDate(date || new Date())} dateFormat="dd/MM/yyyy" />
{/* Legend */}

Leyenda de productos

Pan
Bollería
Repostería
Especial
); }; export default ProductionSchedule;