Files
Schichtenplaner/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx

1416 lines
55 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 } from '../../services/shiftAssignmentService';
import { AssignmentResult } from '../../models/scheduling';
import { ShiftPlan, TimeSlot, ScheduledShift } 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;
}
interface ExtendedShift {
id: string;
planId: string;
timeSlotId: string;
dayOfWeek: number;
requiredEmployees: number;
color?: string;
timeSlotName?: string;
startTime?: string;
endTime?: string;
displayName?: string;
}
const weekdays = [
{ id: 1, name: 'Montag' },
{ id: 2, name: 'Dienstag' },
{ id: 3, name: 'Mittwoch' },
{ id: 4, name: 'Donnerstag' },
{ id: 5, name: 'Freitag' },
{ id: 6, name: 'Samstag' },
{ id: 7, name: 'Sonntag' }
];
const ShiftPlanView: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { hasRole, user } = useAuth();
const { showNotification } = useNotification();
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
const [employees, setEmployees] = useState<Employee[]>([]);
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
const [loading, setLoading] = useState(true);
const [publishing, setPublishing] = useState(false);
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
const [recreating, setRecreating] = useState(false);
useEffect(() => {
loadShiftPlanData();
// Event Listener für Verfügbarkeits-Änderungen
const handleAvailabilityChange = () => {
console.log('📢 Verfügbarkeiten wurden geändert - lade Daten neu...');
reloadAvailabilities();
};
// Globales Event für Verfügbarkeits-Änderungen
window.addEventListener('availabilitiesChanged', handleAvailabilityChange);
return () => {
window.removeEventListener('availabilitiesChanged', handleAvailabilityChange);
};
}, [id]);
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
// Seite ist wieder sichtbar - Daten neu laden
console.log('🔄 Seite ist wieder sichtbar - lade Daten neu...');
reloadAvailabilities();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, []);
// Add this useEffect to debug state changes
useEffect(() => {
console.log('🔍 STATE DEBUG - showAssignmentPreview:', showAssignmentPreview);
console.log('🔍 STATE DEBUG - assignmentResult:', assignmentResult ? 'EXISTS' : 'NULL');
console.log('🔍 STATE DEBUG - publishing:', publishing);
}, [showAssignmentPreview, assignmentResult, publishing]);
// Create a data structure that maps days to their shifts with time slot info - SAME AS AVAILABILITYMANAGER
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { days: [], shiftsByDay: {}, allTimeSlots: [] };
}
// Create a map for quick time slot lookups
const timeSlotMap = new Map(shiftPlan.timeSlots.map(ts => [ts.id, ts]));
// Group shifts by day and enhance with time slot info - SAME LOGIC AS AVAILABILITYMANAGER
const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => {
if (!acc[shift.dayOfWeek]) {
acc[shift.dayOfWeek] = [];
}
const timeSlot = timeSlotMap.get(shift.timeSlotId);
const enhancedShift: ExtendedShift = {
...shift,
timeSlotName: timeSlot?.name,
startTime: timeSlot?.startTime,
endTime: timeSlot?.endTime,
displayName: timeSlot ? `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` : shift.id
};
acc[shift.dayOfWeek].push(enhancedShift);
return acc;
}, {} as Record<number, ExtendedShift[]>);
// Sort shifts within each day by start time - SAME LOGIC AS AVAILABILITYMANAGER
Object.keys(shiftsByDay).forEach(day => {
shiftsByDay[parseInt(day)].sort((a, b) => {
const timeA = a.startTime || '';
const timeB = b.startTime || '';
return timeA.localeCompare(timeB);
});
});
// Get unique days that have shifts - SAME LOGIC AS AVAILABILITYMANAGER
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}` };
});
// Get all unique time slots (rows) by collecting from all shifts - SAME LOGIC AS AVAILABILITYMANAGER
const allTimeSlotsMap = new Map();
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
const timeSlot = timeSlotMap.get(shift.timeSlotId);
if (timeSlot && !allTimeSlotsMap.has(timeSlot.id)) {
allTimeSlotsMap.set(timeSlot.id, {
...timeSlot,
shiftsByDay: {} // Initialize empty object to store shifts by day
});
}
});
});
// Populate shifts for each time slot by day - SAME LOGIC AS AVAILABILITYMANAGER
days.forEach(day => {
shiftsByDay[day.id]?.forEach(shift => {
const timeSlot = allTimeSlotsMap.get(shift.timeSlotId);
if (timeSlot) {
timeSlot.shiftsByDay[day.id] = shift;
}
});
});
// Convert to array and sort by start time - SAME LOGIC AS AVAILABILITYMANAGER
const allTimeSlots = Array.from(allTimeSlotsMap.values()).sort((a, b) => {
return (a.startTime || '').localeCompare(b.startTime || '');
});
return { days, shiftsByDay, allTimeSlots };
};
// VALIDATION FUNCTION - Check if shifts are correctly placed (like in AvailabilityManager)
const validateTimetableStructure = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
return { isValid: false, errors: ['No shift plan data available'] };
}
const validationErrors: string[] = [];
// Check for missing time slots - SAME VALIDATION AS AVAILABILITYMANAGER
const usedTimeSlotIds = new Set(shiftPlan.shifts.map(s => s.timeSlotId));
const availableTimeSlotIds = new Set(shiftPlan.timeSlots.map(ts => ts.id));
usedTimeSlotIds.forEach(timeSlotId => {
if (!availableTimeSlotIds.has(timeSlotId)) {
validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`);
}
});
// Check for shifts with invalid day numbers - SAME VALIDATION AS AVAILABILITYMANAGER
shiftPlan.shifts.forEach(shift => {
if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) {
validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`);
}
// Check if shift timeSlotId exists in timeSlots
const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === shift.timeSlotId);
if (!timeSlotExists) {
validationErrors.push(`Shift ${shift.id} verweist auf nicht existierenden Zeitslot: ${shift.timeSlotId}`);
}
});
// Check for scheduled shifts consistency
scheduledShifts.forEach(scheduledShift => {
const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === scheduledShift.timeSlotId);
if (!timeSlotExists) {
validationErrors.push(`Scheduled Shift ${scheduledShift.id} verweist auf nicht existierenden Zeitslot: ${scheduledShift.timeSlotId}`);
}
});
return {
isValid: validationErrors.length === 0,
errors: validationErrors
};
};
const loadShiftPlanData = async () => {
if (!id) return;
try {
setLoading(true);
// Load plan and employees first
const [plan, employeesData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
// CRITICAL: Load scheduled shifts and verify they exist
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
if (shiftsData.length === 0) {
console.warn('⚠️ No scheduled shifts found for plan:', id);
showNotification({
type: 'warning',
title: 'Keine Schichten gefunden',
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
});
}
setScheduledShifts(shiftsData);
// Load availabilities - USING THE SAME LOGIC AS AVAILABILITYMANAGER
console.log('🔄 LADE VERFÜGBARKEITEN FÜR PLAN:', id);
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// Filter to only include availabilities for the current plan - SAME LOGIC AS AVAILABILITYMANAGER
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN:', planAvailabilities.length);
setAvailabilities(planAvailabilities);
// Run validation
const validation = validateTimetableStructure();
if (!validation.isValid) {
console.warn('⚠️ TIMETABLE VALIDATION ERRORS:', validation.errors);
}
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Daten konnten nicht geladen werden.'
});
} finally {
setLoading(false);
}
};
const handleRecreateAssignments = async () => {
if (!shiftPlan) return;
try {
setRecreating(true);
if (!window.confirm('Möchten Sie die aktuellen Zuweisungen wirklich zurücksetzen? Alle vorhandenen Zuweisungen werden gelöscht.')) {
return;
}
console.log('🔄 STARTING COMPLETE ASSIGNMENT CLEARING PROCESS');
// STEP 1: Get current scheduled shifts
const currentScheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log(`📋 Found ${currentScheduledShifts.length} shifts to clear`);
// STEP 2: Clear ALL assignments by setting empty arrays
const clearPromises = currentScheduledShifts.map(async (scheduledShift) => {
console.log(`🗑️ Clearing assignments for shift: ${scheduledShift.id}`);
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees: [] // EMPTY ARRAY - this clears the assignments
});
});
await Promise.all(clearPromises);
console.log('✅ All assignments cleared from database');
// STEP 3: Update plan status to draft
await shiftPlanService.updateShiftPlan(shiftPlan.id, {
status: 'draft'
});
console.log('📝 Plan status set to draft');
// STEP 4: CRITICAL - Force reload of scheduled shifts to get EMPTY assignments
const refreshedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
setScheduledShifts(refreshedShifts); // Update state with EMPTY assignments
// STEP 5: Clear any previous assignment results
setAssignmentResult(null);
setShowAssignmentPreview(false);
// STEP 6: Force complete data refresh
await loadShiftPlanData();
console.log('🎯 ASSIGNMENT CLEARING COMPLETE - Table should now be empty');
showNotification({
type: 'success',
title: 'Zuweisungen gelöscht',
message: 'Alle Zuweisungen wurden erfolgreich gelöscht. Die Tabelle sollte jetzt leer sein.'
});
} catch (error) {
console.error('❌ Error clearing assignments:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: `Löschen der Zuweisungen fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`
});
} finally {
setRecreating(false);
}
};
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);
setAssignmentResult(null); // Reset previous results
setShowAssignmentPreview(false); // Reset preview
console.log('🔄 STARTING ASSIGNMENT PREVIEW...');
// FORCE COMPLETE REFRESH - don't rely on cached state
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
refreshAllAvailabilities()
]);
console.log('🔄 USING FRESH DATA:');
console.log('- Employees:', refreshedEmployees.length);
console.log('- Availabilities:', refreshedAvailabilities.length);
console.log('- Shift Patterns:', shiftPlan.shifts?.length || 0);
console.log('- Scheduled Shifts:', scheduledShifts.length);
// DEBUG: Show shift pattern IDs
if (shiftPlan.shifts) {
console.log('📋 SHIFT PATTERN IDs:');
shiftPlan.shifts.forEach((shift, index) => {
console.log(` ${index + 1}. ${shift.id} (Day ${shift.dayOfWeek}, TimeSlot ${shift.timeSlotId})`);
});
}
const constraints = {
enforceNoTraineeAlone: true,
enforceExperiencedWithChef: true,
maxRepairAttempts: 50,
targetEmployeesPerShift: 2
};
console.log('🧠 Calling shift assignment service...');
// Use the freshly loaded data, not the state
const result = await shiftAssignmentService.assignShifts(
shiftPlan,
refreshedEmployees,
refreshedAvailabilities,
constraints
);
console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", {
success: result.success,
assignmentsCount: Object.keys(result.assignments).length,
assignmentKeys: Object.keys(result.assignments),
violations: result.violations.length,
resolutionReport: result.resolutionReport?.length || 0
});
// Log assignments with shift pattern context
console.log('🔍 ASSIGNMENTS BY SHIFT PATTERN:');
Object.entries(result.assignments).forEach(([shiftId, empIds]) => {
const shiftPattern = shiftPlan.shifts?.find(s => s.id === shiftId);
if (shiftPattern) {
console.log(` ✅ Shift Pattern: ${shiftId}`);
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
console.log(` - Employees: ${empIds.join(', ')}`);
} else {
console.log(` ❌ UNKNOWN ID: ${shiftId}`);
console.log(` - Employees: ${empIds.join(', ')}`);
console.log(` - This ID does not match any shift pattern!`);
}
});
// CRITICAL: Update state and show preview
console.log('🔄 Setting assignment result and showing preview...');
setAssignmentResult(result);
setShowAssignmentPreview(true);
console.log('✅ Assignment preview ready, modal should be visible');
} 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...');
// Get fresh scheduled shifts
const updatedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
if (!updatedShifts || updatedShifts.length === 0) {
throw new Error('No scheduled shifts found in the plan');
}
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
console.log('🎯 Assignment keys from algorithm:', Object.keys(assignmentResult.assignments));
const updatePromises = updatedShifts.map(async (scheduledShift) => {
const dayOfWeek = getDayOfWeek(scheduledShift.date);
// Find the corresponding shift pattern for this day and time slot
const shiftPattern = shiftPlan?.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
let assignedEmployees: string[] = [];
if (shiftPattern) {
assignedEmployees = assignmentResult.assignments[shiftPattern.id] || [];
console.log(`📝 Updating scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId}) with`, assignedEmployees, 'employees');
if (assignedEmployees.length === 0) {
console.warn(`⚠️ No assignments found for shift pattern ${shiftPattern.id}`);
console.log('🔍 Available assignment keys:', Object.keys(assignmentResult.assignments));
}
} else {
console.warn(`⚠️ No shift pattern found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
}
try {
// Update the scheduled shift with assigned employees
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees
});
console.log(`✅ Successfully updated scheduled 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'
});
// Reload all data to reflect changes
const [reloadedPlan, reloadedShifts] = await Promise.all([
shiftPlanService.getShiftPlan(shiftPlan.id),
shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id)
]);
setShiftPlan(reloadedPlan);
setScheduledShifts(reloadedShifts);
setShowAssignmentPreview(false);
setAssignmentResult(null);
console.log('✅ Publishing completed, modal closed');
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
});
} catch (error) {
console.error('❌ Error publishing shift plan:', error);
let message = 'Unbekannter Fehler';
if (error instanceof Error) {
message = error.message;
}
showNotification({
type: 'error',
title: 'Fehler',
message: `Schichtplan konnte nicht veröffentlicht werden: ${message}`
});
} finally {
setPublishing(false);
}
};
const refreshAllAvailabilities = async (): Promise<EmployeeAvailability[]> => {
try {
console.log('🔄 Force refreshing ALL availabilities with error handling...');
if (!id) {
console.error('❌ No plan ID available');
return [];
}
const availabilityPromises = employees
.filter(emp => emp.isActive)
.map(async (emp) => {
try {
return await employeeService.getAvailabilities(emp.id);
} catch (error) {
console.error(`❌ Failed to load availabilities for ${emp.email}:`, error);
return []; // Return empty array instead of failing entire operation
}
});
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
// More robust filtering
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability && availability.planId === id
);
console.log(`✅ Successfully refreshed ${planAvailabilities.length} availabilities for plan ${id}`);
// IMMEDIATELY update state
setAvailabilities(planAvailabilities);
return planAvailabilities;
} catch (error) {
console.error('❌ Critical error refreshing availabilities:', error);
// DON'T return old data - throw error or return empty array
throw new Error('Failed to refresh availabilities: ' + error);
}
};
const debugShiftMatching = () => {
if (!shiftPlan || !scheduledShifts.length) return;
console.log('🔍 DEBUG: Shift Pattern to Scheduled Shift Matching');
console.log('==================================================');
shiftPlan.shifts?.forEach(shiftPattern => {
const matchingScheduledShifts = scheduledShifts.filter(scheduled => {
const dayOfWeek = getDayOfWeek(scheduled.date);
return dayOfWeek === shiftPattern.dayOfWeek &&
scheduled.timeSlotId === shiftPattern.timeSlotId;
});
console.log(`📅 Shift Pattern: ${shiftPattern.id}`);
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
console.log(` - Matching scheduled shifts: ${matchingScheduledShifts.length}`);
if (assignmentResult) {
const assignments = assignmentResult.assignments[shiftPattern.id] || [];
console.log(` - Assignments: ${assignments.length} employees`);
}
});
};
// Rufe die Debug-Funktion auf, wenn Assignment-Ergebnisse geladen werden
useEffect(() => {
if (assignmentResult && shiftPlan) {
debugShiftMatching();
}
}, [assignmentResult, shiftPlan]);
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)
};
};
const reloadAvailabilities = async () => {
try {
console.log('🔄 Lade Verfügbarkeiten neu...');
// Load availabilities for all employees
const availabilityPromises = employees
.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);
console.log('✅ Verfügbarkeiten neu geladen:', planAvailabilities.length);
} catch (error) {
console.error('❌ Fehler beim Neuladen der Verfügbarkeiten:', error);
}
};
const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
if (!assignmentResult) return [];
const dayOfWeek = getDayOfWeek(scheduledShift.date);
// Find the corresponding shift pattern for this day and time slot
const shiftPattern = shiftPlan?.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) {
console.log(`✅ Found assignments for shift pattern ${shiftPattern.id}:`, assignmentResult.assignments[shiftPattern.id]);
return assignmentResult.assignments[shiftPattern.id];
}
// Fallback: Check if there's a direct match with scheduled shift ID (unlikely)
if (assignmentResult.assignments[scheduledShift.id]) {
console.log(`⚠️ Using direct scheduled shift assignment for ${scheduledShift.id}`);
return assignmentResult.assignments[scheduledShift.id];
}
console.warn(`❌ No assignments found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
return [];
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => {
const { days, allTimeSlots } = getTimetableData();
const validation = validateTimetableStructure();
if (days.length === 0 || allTimeSlots.length === 0) {
return (
<div style={{
padding: '40px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
color: '#6c757d',
borderRadius: '8px',
border: '1px solid #e9ecef'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
<h4>Keine Shifts im Plan definiert</h4>
<p>Der Schichtplan hat keine Shifts definiert oder keine Zeit-Slots konfiguriert.</p>
</div>
);
}
return (
<div style={{
marginBottom: '30px',
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden'
}}>
<div style={{
backgroundColor: '#2c3e50',
color: 'white',
padding: '15px 20px',
fontWeight: 'bold'
}}>
Schichtplan
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{allTimeSlots.length} Zeitslots {days.length} Tage Zeitbasierte Darstellung
</div>
</div>
{/* Validation Warnings - SAME AS AVAILABILITYMANAGER */}
{!validation.isValid && (
<div style={{
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
padding: '15px',
margin: '10px'
}}>
<h4 style={{ margin: '0 0 10px 0', color: '#856404' }}> Validierungswarnungen:</h4>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px' }}>
{validation.errors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
{/* Timetable Structure Info - SAME AS AVAILABILITYMANAGER */}
<div style={{
backgroundColor: '#d1ecf1',
border: '1px solid #bee5eb',
padding: '10px 15px',
margin: '10px',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong>Struktur-Info:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} Zellen
</div>
<div style={{ overflowX: 'auto' }}>
<table style={{
width: '100%',
borderCollapse: 'collapse',
backgroundColor: 'white'
}}>
<thead>
<tr style={{ backgroundColor: '#f8f9fa' }}>
<th style={{
padding: '12px 16px',
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '200px'
}}>
Schicht (Zeit)
</th>
{days.map(weekday => (
<th key={weekday.id} style={{
padding: '12px 16px',
textAlign: 'center',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '120px'
}}>
{weekday.name}
</th>
))}
</tr>
</thead>
<tbody>
{allTimeSlots.map((timeSlot, timeSlotIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeSlotIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
fontWeight: '500',
backgroundColor: '#f8f9fa',
position: 'sticky',
left: 0
}}>
<div style={{ fontWeight: 'bold' }}>
{timeSlot.name}
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
</div>
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
ID: {timeSlot.id.substring(0, 8)}...
</div>
</td>
{days.map(weekday => {
const shift = timeSlot.shiftsByDay[weekday.id];
if (!shift) {
return (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: '#f8f9fa',
color: '#ccc',
fontStyle: 'italic'
}}>
Kein Shift
</td>
);
}
// Validation: Check if shift has correct timeSlotId and dayOfWeek - SAME AS AVAILABILITYMANAGER
const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id;
let assignedEmployees: string[] = [];
let displayText = '';
if (shiftPlan?.status === 'published') {
// For published plans, use actual assignments from scheduled shifts
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift) {
assignedEmployees = scheduledShift.assignedEmployees || [];
// DEBUG: Log if we're still seeing old data
if (assignedEmployees.length > 0) {
console.warn(`⚠️ Found non-empty assignments for ${weekday.name} ${timeSlot.name}:`, assignedEmployees);
}
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
}).join(', ');
}
} else if (assignmentResult) {
// For draft with preview, use assignment result
const scheduledShift = scheduledShifts.find(scheduled => {
const scheduledDayOfWeek = getDayOfWeek(scheduled.date);
return scheduledDayOfWeek === weekday.id &&
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift) {
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
}).join(', ');
}
}
// If no assignments yet, show empty or required count
if (!displayText) {
const shiftsForSlot = shiftPlan?.shifts?.filter(s =>
s.dayOfWeek === weekday.id &&
s.timeSlotId === timeSlot.id
) || [];
const totalRequired = shiftsForSlot.reduce((sum, s) =>
sum + s.requiredEmployees, 0);
// Show "0/2" instead of just "0" to indicate it's empty
displayText = `0/${totalRequired}`;
// Optional: Show empty state more clearly
if (totalRequired === 0) {
displayText = '-';
}
}
return (
<td key={weekday.id} style={{
padding: '12px 16px',
border: '1px solid #dee2e6',
textAlign: 'center',
backgroundColor: !isValidShift ? '#fff3cd' : (assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent'),
color: assignedEmployees.length > 0 ? '#2c3e50' : '#666',
fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit',
position: 'relative'
}}>
{/* Validation indicator - SAME AS AVAILABILITYMANAGER */}
{!isValidShift && (
<div style={{
position: 'absolute',
top: '2px',
right: '2px',
backgroundColor: '#f39c12',
color: 'white',
borderRadius: '50%',
width: '16px',
height: '16px',
fontSize: '10px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
title={`Shift Validierung: timeSlotId=${shift.timeSlotId}, dayOfWeek=${shift.dayOfWeek}`}
>
</div>
)}
{displayText}
{/* Shift debug info - SAME AS AVAILABILITYMANAGER */}
<div style={{
fontSize: '10px',
color: '#666',
marginTop: '4px',
textAlign: 'left',
fontFamily: 'monospace'
}}>
<div>Shift: {shift.id.substring(0, 6)}...</div>
<div>Day: {shift.dayOfWeek}</div>
{!isValidShift && (
<div style={{ color: '#e74c3c', fontWeight: 'bold' }}>
VALIDATION ERROR
</div>
)}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{/* Summary Statistics - SAME AS AVAILABILITYMANAGER */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px',
borderTop: '1px solid #dee2e6',
fontSize: '12px',
color: '#666'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<strong>Zusammenfassung:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} mögliche Shifts
</div>
<div>
<strong>Validierungsfehler:</strong> {validation.errors.length}
</div>
</div>
</div>
</div>
);
};
if (loading) return <div>Lade Schichtplan...</div>;
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
const { days, allTimeSlots } = getTimetableData();
const availabilityStatus = getAvailabilityStatus();
const validation = validateTimetableStructure();
return (
<div style={{ padding: '20px' }}>
{/* Header with Plan Information and Actions */}
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: '20px'
}}>
<div>
<h1>{shiftPlan.name}</h1>
<p style={{ color: '#666', margin: 0 }}>
{shiftPlan.startDate && shiftPlan.endDate &&
`Zeitraum: ${formatDate(shiftPlan.startDate)} - ${formatDate(shiftPlan.endDate)}`
}
</p>
<div style={{
display: 'inline-block',
padding: '4px 12px',
backgroundColor: shiftPlan.status === 'published' ? '#2ecc71' : '#f1c40f',
color: 'white',
borderRadius: '20px',
fontSize: '14px',
fontWeight: 'bold',
marginTop: '5px'
}}>
{shiftPlan.status === 'published' ? 'Veröffentlicht' : 'Entwurf'}
</div>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
<button
onClick={handleRecreateAssignments}
disabled={recreating}
style={{
padding: '10px 20px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: recreating ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{recreating ? 'Lösche Zuweisungen...' : 'Zuweisungen neu berechnen'}
</button>
)}
<button
onClick={() => navigate('/shift-plans')}
style={{
padding: '10px 20px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Zurück zur Übersicht
</button>
</div>
</div>
{/* Debug Info - Enhanced */}
<div style={{
backgroundColor: validation.errors.length > 0 ? '#fff3cd' : (allTimeSlots.length === 0 ? '#f8d7da' : '#d1ecf1'),
border: `1px solid ${validation.errors.length > 0 ? '#ffeaa7' : (allTimeSlots.length === 0 ? '#f5c6cb' : '#bee5eb')}`,
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{
margin: '0 0 10px 0',
color: validation.errors.length > 0 ? '#856404' : (allTimeSlots.length === 0 ? '#721c24' : '#0c5460')
}}>
{validation.errors.length > 0 ? '⚠️ VALIDIERUNGSPROBLEME' :
allTimeSlots.length === 0 ? '❌ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
</h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Ausgewählter Plan:</strong> {shiftPlan.name}</div>
<div><strong>Plan ID:</strong> {shiftPlan.id}</div>
<div><strong>Einzigartige Zeitslots:</strong> {allTimeSlots.length}</div>
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Shift Patterns:</strong> {shiftPlan.shifts?.length || 0}</div>
<div><strong>Scheduled Shifts:</strong> {scheduledShifts.length}</div>
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
<div><strong>Aktive Mitarbeiter:</strong> {employees.length}</div>
{assignmentResult && (
<div><strong>Assignment Keys:</strong> {Object.keys(assignmentResult.assignments).length}</div>
)}
</div>
{/* Show shift pattern vs scheduled shift matching */}
{shiftPlan.shifts && scheduledShifts.length > 0 && (
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
<strong>Shift Matching:</strong>
<div style={{ fontSize: '11px' }}>
{shiftPlan.shifts.length} Patterns {scheduledShifts.length} Scheduled Shifts
{assignmentResult && (
<div> {Object.keys(assignmentResult.assignments).length} Assignment Keys</div>
)}
</div>
</div>
)}
</div>
{/* Rest of the component remains the same... */}
{/* Availability Status - only show for drafts */}
{shiftPlan.status === 'draft' && (
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
marginBottom: '20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3>Veröffentlichungsvoraussetzungen</h3>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '15px' }}>
<div>
<div style={{ fontSize: '14px', color: '#666', marginBottom: '5px' }}>
Verfügbarkeitseinträge:
</div>
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>
{availabilityStatus.completed} / {availabilityStatus.total} Mitarbeiter
</div>
<div style={{
width: '200px',
height: '8px',
backgroundColor: '#e0e0e0',
borderRadius: '4px',
marginTop: '5px',
overflow: 'hidden'
}}>
<div
style={{
width: `${availabilityStatus.percentage}%`,
height: '100%',
backgroundColor: availabilityStatus.percentage === 100 ? '#2ecc71' : '#f1c40f',
transition: 'all 0.3s ease'
}}
/>
</div>
</div>
{hasRole(['admin', 'instandhalter']) && (
<div>
<button
onClick={handlePreviewAssignment}
disabled={!canPublish() || publishing}
style={{
padding: '10px 20px',
backgroundColor: canPublish() ? '#3498db' : '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: canPublish() ? 'pointer' : 'not-allowed',
fontWeight: 'bold'
}}
>
{publishing ? 'Berechne...' : 'Automatisch zuweisen'}
</button>
{!canPublish() && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
{availabilityStatus.percentage === 100
? 'Bereit zur Berechnung'
: `${availabilityStatus.total - availabilityStatus.completed} Mitarbeiter müssen noch Verfügbarkeit eintragen`}
</div>
)}
</div>
)}
</div>
{/* Plan Structure Info */}
<div style={{
backgroundColor: '#e8f4fd',
border: '1px solid #b8d4f0',
borderRadius: '4px',
padding: '12px 16px',
fontSize: '14px'
}}>
<strong>Plan-Struktur:</strong> {allTimeSlots.length} Schichttypen an {days.length} Tagen
</div>
</div>
)}
{/* Assignment Preview Modal - FIXED CONDITION */}
{(showAssignmentPreview || assignmentResult) && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '30px',
maxWidth: '800px',
maxHeight: '80vh',
overflow: 'auto',
width: '90%'
}}>
<h2>Wochenmuster-Zuordnung</h2>
{/* Detaillierter Reparatur-Bericht anzeigen */}
{assignmentResult?.resolutionReport && (
<div style={{
backgroundColor: '#f8f9fa',
border: '1px solid #e9ecef',
borderRadius: '4px',
padding: '15px',
marginBottom: '20px',
fontSize: '14px',
maxHeight: '400px',
overflow: 'auto'
}}>
<h4 style={{ color: '#2c3e50', marginTop: 0, display: 'flex', alignItems: 'center', gap: '10px' }}>
<span>📋</span> Detaillierter Reparatur-Bericht
</h4>
<div style={{
fontFamily: 'monospace',
fontSize: '12px',
lineHeight: '1.4'
}}>
{assignmentResult.resolutionReport.map((line, index) => {
let color = '#2c3e50';
let fontWeight = 'normal';
if (line.includes('✅') || line.includes('ALLES KRITISCHEN PROBLEME BEHOBEN')) {
color = '#2ecc71';
fontWeight = 'bold';
} else if (line.includes('❌') || line.includes('KRITISCHEN PROBLEME')) {
color = '#e74c3c';
fontWeight = 'bold';
} else if (line.includes('⚠️')) {
color = '#f39c12';
} else if (line.includes('📊') || line.includes('🔧') || line.includes('📅') || line.includes('🚨') || line.includes('🛠️') || line.includes('💡') || line.includes('🎯')) {
color = '#3498db';
fontWeight = 'bold';
} else if (line.startsWith(' •') || line.startsWith(' -')) {
color = '#7f8c8d';
}
return (
<div key={index} style={{
color,
fontWeight,
marginBottom: line === '' ? '5px' : '2px',
paddingLeft: line.startsWith(' ') ? '20px' : '0px'
}}>
{line}
</div>
);
})}
</div>
</div>
)}
{/* KORRIGIERTE ZUSAMMENFASSUNG */}
{assignmentResult && (
<div style={{ marginBottom: '20px' }}>
<h4>Zusammenfassung:</h4>
{/* Entscheidung basierend auf tatsächlichen kritischen Problemen */}
{assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0 ? (
<div style={{
padding: '15px',
backgroundColor: '#d4edda',
border: '1px solid #c3e6cb',
borderRadius: '4px',
color: '#155724',
marginBottom: '15px'
}}>
<h5 style={{ margin: '0 0 10px 0', color: '#155724' }}> Bereit zur Veröffentlichung</h5>
<p style={{ margin: 0 }}>
Alle kritischen Probleme wurden behoben. Der Schichtplan kann veröffentlicht werden.
</p>
</div>
) : (
<div style={{
padding: '15px',
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: '4px',
color: '#721c24',
marginBottom: '15px'
}}>
<h5 style={{ margin: '0 0 10px 0', color: '#721c24' }}> Kritische Probleme</h5>
<p style={{ margin: '0 0 10px 0' }}>
Folgende kritische Probleme müssen behoben werden, bevor der Plan veröffentlicht werden kann:
</p>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{assignmentResult.violations
.filter(v => v.includes('ERROR:') || v.includes('❌ KRITISCH:'))
.map((violation, index) => (
<li key={index} style={{ fontSize: '14px' }}>
{violation.replace('ERROR: ', '').replace('❌ KRITISCH: ', '')}
</li>
))}
</ul>
</div>
)}
{/* Warnungen separat anzeigen - NUR wenn welche vorhanden sind */}
{assignmentResult.violations.some(v => v.includes('WARNING:') || v.includes('⚠️')) && (
<div style={{
padding: '10px',
backgroundColor: '#fff3cd',
border: '1px solid #ffeaa7',
borderRadius: '4px',
color: '#856404'
}}>
<h6 style={{ margin: '0 0 5px 0', color: '#856404' }}>
Hinweise & Warnungen
</h6>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
{assignmentResult.violations
.filter(v => v.includes('WARNING:') || v.includes('⚠️'))
.map((warning, index) => (
<li key={index} style={{ fontSize: '13px' }}>
{warning.replace('WARNING: ', '').replace('⚠️ WARNHINWEIS: ', '')}
</li>
))}
</ul>
</div>
)}
</div>
)}
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
<button
onClick={() => {
setShowAssignmentPreview(false);
setAssignmentResult(null);
}}
style={{
padding: '8px 16px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Abbrechen
</button>
{/* KORRIGIERTER BUTTON MIT TYPESCRIPT-FIX */}
<button
onClick={handlePublish}
disabled={publishing || (assignmentResult ? assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length > 0 : true)}
style={{
padding: '10px 20px',
backgroundColor: assignmentResult ? (assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0 ? '#2ecc71' : '#95a5a6') : '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: assignmentResult ? (assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0 ? 'pointer' : 'not-allowed') : 'not-allowed',
fontWeight: 'bold',
fontSize: '16px'
}}
>
{publishing ? 'Veröffentliche...' : (
assignmentResult ? (
assignmentResult.violations.filter(v =>
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
).length === 0
? 'Schichtplan veröffentlichen'
: 'Kritische Probleme müssen behoben werden'
) : 'Lade Zuordnungen...'
)}
</button>
</div>
</div>
</div>
)}
{/* Timetable */}
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h3>
Schichtplan
{shiftPlan.status === 'published' && ' (Aktuelle Zuweisungen)'}
{assignmentResult && shiftPlan.status === 'draft' && ' (Exemplarische Woche)'}
</h3>
{renderTimetable()}
{/* Summary */}
{days.length > 0 && (
<div style={{
marginTop: '20px',
padding: '12px 16px',
backgroundColor: shiftPlan.status === 'published' ? '#d4edda' : '#e8f4fd',
borderRadius: '4px',
border: shiftPlan.status === 'published' ? '1px solid #c3e6cb' : '1px solid #b8d4f0',
fontSize: '14px'
}}>
<strong>Legende:</strong> {
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'
}
</div>
)}
</div>
</div>
);
};
export default ShiftPlanView;