diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx index 48dd01c..0f3b1fc 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx @@ -1,25 +1,30 @@ -// frontend/src/pages/ShiftPlans/ShiftPlanView.tsx (updated) +// 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/types'; +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' } - ]; + { 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 }>(); @@ -80,6 +85,69 @@ const ShiftPlanView: React.FC = () => { } }; + // 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; @@ -150,7 +218,7 @@ const ShiftPlanView: React.FC = () => { console.log(`✅ Successfully updated shift ${scheduledShift.id}`); } catch (error) { console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error); - throw error; // Re-throw to stop the process + throw error; } }); @@ -168,13 +236,8 @@ const ShiftPlanView: React.FC = () => { message: 'Schichtplan wurde erfolgreich veröffentlicht!' }); - // Lade den Plan neu, um die aktuellen Daten zu erhalten - const updatedPlan = await shiftPlanService.getShiftPlan(shiftPlan.id); - setShiftPlan(updatedPlan); - - // Behalte die assignmentResult für die Anzeige bei - // Die Tabelle wird nun automatisch die tatsächlichen Mitarbeiternamen anzeigen - + // Reload the plan to reflect changes + loadShiftPlanData(); setShowAssignmentPreview(false); } catch (error) { @@ -197,18 +260,6 @@ const ShiftPlanView: React.FC = () => { } }; - 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 handleRevertToDraft = async () => { if (!shiftPlan || !id) return; @@ -229,9 +280,6 @@ const ShiftPlanView: React.FC = () => { message: 'Schichtplan wurde erfolgreich zurück in den Entwurfsstatus gesetzt.' }); - // Verfügbarkeiten neu laden - loadAvailabilities(); - } catch (error) { console.error('Error reverting plan to draft:', error); showNotification({ @@ -244,25 +292,16 @@ const ShiftPlanView: React.FC = () => { } }; - const loadAvailabilities = async () => { - if (!employees.length) return; + const canPublish = () => { + if (!shiftPlan || shiftPlan.status === 'published') return false; - try { - const availabilityPromises = employees - .filter(emp => emp.isActive) - .map(emp => employeeService.getAvailabilities(emp.id)); - - const allAvailabilities = await Promise.all(availabilityPromises); - const flattenedAvailabilities = allAvailabilities.flat(); - - const planAvailabilities = flattenedAvailabilities.filter( - availability => availability.planId === id - ); - - setAvailabilities(planAvailabilities); - } catch (error) { - console.error('Error loading availabilities:', error); - } + // 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 = () => { @@ -278,90 +317,188 @@ const ShiftPlanView: React.FC = () => { }; }; - // Simplified timetable data generation - const getTimetableData = () => { - if (!shiftPlan) return { shifts: [], weekdays: [] }; + // Render timetable using the same structure as AvailabilityManager + const renderTimetable = () => { + const { days, allTimeSlots, timeSlotsByDay } = getTimetableData(); - const hasAssignments = shiftPlan.status === 'published' || (assignmentResult && Object.keys(assignmentResult.assignments).length > 0); + if (days.length === 0 || allTimeSlots.length === 0) { + return ( +
+
📅
+

Keine Shifts im Plan definiert

+

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

+
+ ); + } - const timetableShifts = shiftPlan.timeSlots.map(timeSlot => { - const weekdayData: Record = {}; - - weekdays.forEach(weekday => { - const shiftsOnDay = shiftPlan.shifts.filter(shift => - shift.dayOfWeek === weekday.id && - shift.timeSlotId === timeSlot.id - ); + return ( +
+
+ Schichtplan +
+ {allTimeSlots.length} Schichttypen • {days.length} Tage • Nur tatsächlich im Plan verwendete Schichten +
+
- if (shiftsOnDay.length === 0) { - weekdayData[weekday.id] = { employees: [], display: '' }; - } else { - const totalRequired = shiftsOnDay.reduce((sum, shift) => - sum + shift.requiredEmployees, 0); - - let assignedEmployees: string[] = []; - - if (hasAssignments && shiftPlan.scheduledShifts) { - const scheduledShift = shiftPlan.scheduledShifts.find(scheduled => { - const scheduledDayOfWeek = getDayOfWeek(scheduled.date); - return scheduledDayOfWeek === weekday.id && - scheduled.timeSlotId === timeSlot.id; - }); - - if (scheduledShift) { - if (shiftPlan.status === 'published') { - // Verwende tatsächliche Zuweisungen aus der Datenbank - assignedEmployees = scheduledShift.assignedEmployees - .map(empId => { - const employee = employees.find(emp => emp.id === empId); - return employee ? employee.name : 'Unbekannt'; - }); - } else if (assignmentResult && assignmentResult.assignments[scheduledShift.id]) { - // Verwende Preview-Zuweisungen - assignedEmployees = assignmentResult.assignments[scheduledShift.id] - .map(empId => { - const employee = employees.find(emp => emp.id === empId); - return employee ? employee.name : 'Unbekannt'; - }); - } - } - } - - const displayText = hasAssignments - ? assignedEmployees.join(', ') || `0/${totalRequired}` - : `0/${totalRequired}`; - - weekdayData[weekday.id] = { - employees: assignedEmployees, - display: displayText - }; - } - }); +
+ + + + + {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 ( + + ); + } - return { - ...timeSlot, - displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}–${formatTime(timeSlot.endTime)})`, - weekdayData - }; - }); + // 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(', '); + } + } - return { shifts: timetableShifts, weekdays }; - }; + // 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}`; + } - const getDayOfWeek = (dateString: string): number => { - const date = new Date(dateString); - return date.getDay() === 0 ? 7 : date.getDay(); + 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 timetableData = getTimetableData(); const availabilityStatus = getAvailabilityStatus(); + const { days, allTimeSlots } = getTimetableData(); return (
- {/* Header mit Plan-Informationen und Aktionen */} + {/* Header with Plan Information and Actions */}
{
- {/* Availability Status - nur für Entwürfe anzeigen */} + {/* Availability Status - only show for drafts */} {shiftPlan.status === 'draft' && (
{
)} + + {/* Plan Structure Info */} +
+ Plan-Struktur: {allTimeSlots.length} Schichttypen an {days.length} Tagen +
)} @@ -600,95 +748,23 @@ const ShiftPlanView: React.FC = () => { )} + {/* Timetable */}
-
-

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

- - {timetableData.shifts.length === 0 ? ( -
- Keine Schichten für diesen Zeitraum konfiguriert -
- ) : ( -
- - - - - {timetableData.weekdays.map(weekday => ( - - ))} - - - - {timetableData.shifts.map((shift, index) => ( - - - {timetableData.weekdays.map(weekday => ( - - ))} - - ))} - -
- Schicht (Zeit) - - {weekday.name} -
- {shift.displayName} - 0 ? '14px' : 'inherit' - }}> - {shift.weekdayData[weekday.id].display || '–'} -
-
- )} -
+

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

+ + {renderTimetable()} {/* Summary */} - {timetableData.shifts.length > 0 && ( + {days.length > 0 && (