// frontend/src/pages/ShiftPlans/ShiftPlanView.tsx - UPDATED import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { shiftPlanService } from '../../services/shiftPlanService'; import { employeeService } from '../../services/employeeService'; import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService'; import { AssignmentResult } from '../../services/scheduling'; import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan'; import { Employee, EmployeeAvailability } from '../../models/Employee'; import { useNotification } from '../../contexts/NotificationContext'; import { formatDate, formatTime } from '../../utils/foramatters'; // Local interface extensions (same as AvailabilityManager) interface ExtendedTimeSlot extends TimeSlot { displayName?: string; } const weekdays = [ { id: 1, name: 'Mo' }, { id: 2, name: 'Di' }, { id: 3, name: 'Mi' }, { id: 4, name: 'Do' }, { id: 5, name: 'Fr' }, { id: 6, name: 'Sa' }, { id: 7, name: 'So' } ]; const ShiftPlanView: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { hasRole, user } = useAuth(); const { showNotification } = useNotification(); const [shiftPlan, setShiftPlan] = useState(null); const [employees, setEmployees] = useState([]); const [availabilities, setAvailabilities] = useState([]); const [assignmentResult, setAssignmentResult] = useState(null); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [reverting, setReverting] = useState(false); const [showAssignmentPreview, setShowAssignmentPreview] = useState(false); useEffect(() => { loadShiftPlanData(); }, [id]); const loadShiftPlanData = async () => { if (!id) return; try { setLoading(true); const [plan, employeesData] = await Promise.all([ shiftPlanService.getShiftPlan(id), employeeService.getEmployees() ]); setShiftPlan(plan); setEmployees(employeesData.filter(emp => emp.isActive)); // Load availabilities for all employees const availabilityPromises = employeesData .filter(emp => emp.isActive) .map(emp => employeeService.getAvailabilities(emp.id)); const allAvailabilities = await Promise.all(availabilityPromises); const flattenedAvailabilities = allAvailabilities.flat(); // Filter availabilities to only include those for the current shift plan const planAvailabilities = flattenedAvailabilities.filter( availability => availability.planId === id ); setAvailabilities(planAvailabilities); } catch (error) { console.error('Error loading shift plan data:', error); showNotification({ type: 'error', title: 'Fehler', message: 'Daten konnten nicht geladen werden.' }); } finally { setLoading(false); } }; // Extract plan-specific shifts using the same logic as AvailabilityManager const getTimetableData = () => { if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { return { days: [], timeSlotsByDay: {}, allTimeSlots: [] }; } // Group shifts by day const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => { if (!acc[shift.dayOfWeek]) { acc[shift.dayOfWeek] = []; } acc[shift.dayOfWeek].push(shift); return acc; }, {} as Record); // Get unique days that have shifts const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek))) .sort() .map(dayId => { return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` }; }); // For each day, get the time slots that actually have shifts const timeSlotsByDay: Record = {}; days.forEach(day => { const shiftsForDay = shiftsByDay[day.id] || []; const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId)); timeSlotsByDay[day.id] = shiftPlan.timeSlots .filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id)) .map(timeSlot => ({ ...timeSlot, displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` })) .sort((a, b) => a.startTime.localeCompare(b.startTime)); }); // Get all unique time slots across all days for row headers const allTimeSlotIds = new Set(); days.forEach(day => { timeSlotsByDay[day.id]?.forEach(timeSlot => { allTimeSlotIds.add(timeSlot.id); }); }); const allTimeSlots = Array.from(allTimeSlotIds) .map(timeSlotId => shiftPlan.timeSlots.find(ts => ts.id === timeSlotId)) .filter(Boolean) .map(timeSlot => ({ ...timeSlot!, displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})` })) .sort((a, b) => a.startTime.localeCompare(b.startTime)); return { days, timeSlotsByDay, allTimeSlots }; }; const getDayOfWeek = (dateString: string): number => { const date = new Date(dateString); return date.getDay() === 0 ? 7 : date.getDay(); }; const handlePreviewAssignment = async () => { if (!shiftPlan) return; try { setPublishing(true); const result = await ShiftAssignmentService.assignShifts( shiftPlan, employees, availabilities, { enforceExperiencedWithChef: true, enforceNoTraineeAlone: true, maxRepairAttempts: 50 } ); setAssignmentResult(result); setShowAssignmentPreview(true); if (!result.success) { showNotification({ type: 'warning', title: 'Warnung', message: `Automatische Zuordnung hat ${result.violations.length} Probleme gefunden.` }); } } catch (error) { console.error('Error during assignment:', error); showNotification({ type: 'error', title: 'Fehler', message: 'Automatische Zuordnung fehlgeschlagen.' }); } finally { setPublishing(false); } }; const handlePublish = async () => { if (!shiftPlan || !assignmentResult) return; try { setPublishing(true); console.log('🔄 Starting to publish assignments...'); // Debug: Check if scheduled shifts exist if (!shiftPlan.scheduledShifts || shiftPlan.scheduledShifts.length === 0) { throw new Error('No scheduled shifts found in the plan'); } // Update scheduled shifts with assignments const updatePromises = shiftPlan.scheduledShifts.map(async (scheduledShift) => { const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || []; console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees.length, 'employees'); try { // First, verify the shift exists await shiftAssignmentService.getScheduledShift(scheduledShift.id); // Then update it await shiftAssignmentService.updateScheduledShift(scheduledShift.id, { assignedEmployees }); console.log(`✅ Successfully updated shift ${scheduledShift.id}`); } catch (error) { console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error); throw error; } }); await Promise.all(updatePromises); // Update plan status to published console.log('🔄 Updating plan status to published...'); await shiftPlanService.updateShiftPlan(shiftPlan.id, { status: 'published' }); showNotification({ type: 'success', title: 'Erfolg', message: 'Schichtplan wurde erfolgreich veröffentlicht!' }); // Reload the plan to reflect changes loadShiftPlanData(); setShowAssignmentPreview(false); } catch (error) { console.error('❌ Error publishing shift plan:', error); let message = 'Unbekannter Fehler'; if (error instanceof Error) { message = error.message; } else if (typeof error === 'string') { message = error; } showNotification({ type: 'error', title: 'Fehler', message: `Schichtplan konnte nicht veröffentlicht werden: ${message}` }); } finally { setPublishing(false); } }; const handleRevertToDraft = async () => { if (!shiftPlan || !id) return; if (!window.confirm('Möchten Sie diesen Schichtplan wirklich zurück in den Entwurfsstatus setzen? Alle Zuweisungen werden entfernt.')) { return; } try { setReverting(true); const updatedPlan = await shiftPlanService.revertToDraft(id); setShiftPlan(updatedPlan); setAssignmentResult(null); showNotification({ type: 'success', title: 'Erfolg', message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt.' }); } catch (error) { console.error('Error reverting plan to draft:', error); showNotification({ type: 'error', title: 'Fehler', message: 'Schichtplan konnte nicht zurückgesetzt werden.' }); } finally { setReverting(false); } }; const canPublish = () => { if (!shiftPlan || shiftPlan.status === 'published') return false; // Check if all active employees have set their availabilities const employeesWithoutAvailabilities = employees.filter(emp => { const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id); return empAvailabilities.length === 0; }); return employeesWithoutAvailabilities.length === 0; }; const getAvailabilityStatus = () => { const totalEmployees = employees.length; const employeesWithAvailabilities = new Set( availabilities.map(avail => avail.employeeId) ).size; return { completed: employeesWithAvailabilities, total: totalEmployees, percentage: Math.round((employeesWithAvailabilities / totalEmployees) * 100) }; }; // Render timetable using the same structure as AvailabilityManager const renderTimetable = () => { const { days, allTimeSlots, timeSlotsByDay } = getTimetableData(); if (days.length === 0 || allTimeSlots.length === 0) { return (
📅

Keine Shifts im Plan definiert

Der Schichtplan hat keine Shifts definiert oder keine Zeit-Slots konfiguriert.

); } return (
Schichtplan
{allTimeSlots.length} Schichttypen • {days.length} Tage • Nur tatsächlich im Plan verwendete Schichten
{days.map(weekday => ( ))} {allTimeSlots.map((timeSlot, timeIndex) => ( {days.map(weekday => { // Check if this time slot exists for this day const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id); if (!timeSlotForDay) { return ( ); } // Get assigned employees for this shift let assignedEmployees: string[] = []; let displayText = ''; if (shiftPlan?.status === 'published' && shiftPlan.scheduledShifts) { // For published plans, use actual assignments from scheduled shifts const scheduledShift = shiftPlan.scheduledShifts.find(scheduled => { const scheduledDayOfWeek = getDayOfWeek(scheduled.date); return scheduledDayOfWeek === weekday.id && scheduled.timeSlotId === timeSlot.id; }); if (scheduledShift) { assignedEmployees = scheduledShift.assignedEmployees || []; displayText = assignedEmployees.map(empId => { const employee = employees.find(emp => emp.id === empId); return employee ? employee.name : 'Unbekannt'; }).join(', '); } } else if (assignmentResult) { // For draft with preview, use assignment result const scheduledShift = shiftPlan?.scheduledShifts?.find(scheduled => { const scheduledDayOfWeek = getDayOfWeek(scheduled.date); return scheduledDayOfWeek === weekday.id && scheduled.timeSlotId === timeSlot.id; }); if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) { assignedEmployees = assignmentResult.assignments[scheduledShift.id]; displayText = assignedEmployees.map(empId => { const employee = employees.find(emp => emp.id === empId); return employee ? employee.name : 'Unbekannt'; }).join(', '); } } // If no assignments yet, show required count if (!displayText) { const shiftsForSlot = shiftPlan?.shifts?.filter(shift => shift.dayOfWeek === weekday.id && shift.timeSlotId === timeSlot.id ) || []; const totalRequired = shiftsForSlot.reduce((sum, shift) => sum + shift.requiredEmployees, 0); displayText = `0/${totalRequired}`; } return ( ); })} ))}
Schicht (Zeit) {weekday.name}
{timeSlot.displayName} - 0 ? '#e8f5e8' : 'transparent', color: assignedEmployees.length > 0 ? '#2c3e50' : '#666', fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit' }}> {displayText}
); }; if (loading) return
Lade Schichtplan...
; if (!shiftPlan) return
Schichtplan nicht gefunden
; const availabilityStatus = getAvailabilityStatus(); const { days, allTimeSlots } = getTimetableData(); return (
{/* Header with Plan Information and Actions */}

{shiftPlan.name}

{shiftPlan.startDate && shiftPlan.endDate && `Zeitraum: ${formatDate(shiftPlan.startDate)} - ${formatDate(shiftPlan.endDate)}` }

{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && ( )}
{/* Availability Status - only show for drafts */} {shiftPlan.status === 'draft' && (

Veröffentlichungsvoraussetzungen

Verfügbarkeitseinträge:
{availabilityStatus.completed} / {availabilityStatus.total} Mitarbeiter
{hasRole(['admin', 'instandhalter']) && (
{!canPublish() && (
{availabilityStatus.percentage === 100 ? 'Bereit zur Veröffentlichung' : `${availabilityStatus.total - availabilityStatus.completed} Mitarbeiter müssen noch Verfügbarkeit eintragen`}
)}
)}
{/* Plan Structure Info */}
Plan-Struktur: {allTimeSlots.length} Schichttypen an {days.length} Tagen
)} {/* Assignment Preview Modal */} {showAssignmentPreview && assignmentResult && (

Wochenmuster-Zuordnung

{/* Show weekly pattern info */} {assignmentResult.pattern && (

Wochenmuster erstellt

Der Algorithmus hat ein Muster für {assignmentResult.pattern.weekShifts.length} Schichten in der ersten Woche erstellt und dieses für alle {Math.ceil(Object.keys(assignmentResult.assignments).length / assignmentResult.pattern.weekShifts.length)} Wochen im Plan wiederholt.

Wochenmuster-Statistik:
- Schichten pro Woche: {assignmentResult.pattern.weekShifts.length}
- Zuweisungen pro Woche: {Object.values(assignmentResult.pattern.assignments).flat().length}
- Gesamtzuweisungen: {Object.values(assignmentResult.assignments).flat().length}
)} {assignmentResult.violations.length > 0 && (

Warnungen:

    {assignmentResult.violations.map((violation, index) => (
  • {violation}
  • ))}
)}

Zusammenfassung:

{assignmentResult.success ? '✅ Alle Schichten können zugeordnet werden!' : '⚠️ Es gibt Probleme bei der Zuordnung die manuell behoben werden müssen.'}

)} {/* Timetable */}

Schichtplan {shiftPlan.status === 'published' && ' (Aktuelle Zuweisungen)'} {assignmentResult && shiftPlan.status === 'draft' && ' (Exemplarische Woche)'}

{renderTimetable()} {/* Summary */} {days.length > 0 && (
Legende: { shiftPlan.status === 'published' ? 'Angezeigt werden die aktuell zugewiesenen Mitarbeiter' : assignmentResult ? 'Angezeigt werden die vorgeschlagenen Mitarbeiter für eine exemplarische Woche' : 'Angezeigt wird "zugewiesene/benötigte Mitarbeiter" pro Schicht und Wochentag' }
)}
); }; export default ShiftPlanView;