import React, { useState, useEffect } from 'react'; import { employeeService } from '../../../services/employeeService'; import { shiftPlanService } from '../../../services/shiftPlanService'; import { Employee, EmployeeAvailability } from '../../../models/Employee'; import { ShiftPlan, TimeSlot, Shift } from '../../../models/ShiftPlan'; import { useNotification } from '../../../contexts/NotificationContext'; import { useBackendValidation } from '../../../hooks/useBackendValidation'; interface AvailabilityManagerProps { employee: Employee; onSave: () => void; onCancel: () => void; } // Local interface extensions interface ExtendedShift extends Shift { timeSlotName?: string; startTime?: string; endTime?: string; displayName?: string; } interface Availability extends EmployeeAvailability { isAvailable?: boolean; } // Verfügbarkeits-Level export type AvailabilityLevel = 1 | 2 | 3; const AvailabilityManager: React.FC = ({ employee, onSave, onCancel }) => { const [availabilities, setAvailabilities] = useState([]); const [shiftPlans, setShiftPlans] = useState([]); const [selectedPlanId, setSelectedPlanId] = useState(''); const [selectedPlan, setSelectedPlan] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const { showNotification } = useNotification(); const { executeWithValidation, isSubmitting } = useBackendValidation(); const daysOfWeek = [ { 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 availabilityLevels = [ { level: 1 as AvailabilityLevel, label: 'Bevorzugt', color: '#27ae60', bgColor: '#d5f4e6', description: 'Ideale Zeit' }, { level: 2 as AvailabilityLevel, label: 'Möglich', color: '#f39c12', bgColor: '#fef5e7', description: 'Akzeptable Zeit' }, { level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' } ]; // Lade initial die Schichtpläne useEffect(() => { const loadInitialData = async () => { try { setLoading(true); console.log('🔄 LADE INITIALDATEN FÜR MITARBEITER:', employee.id); // 1. Lade alle Schichtpläne const plans = await shiftPlanService.getShiftPlans(); console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length); setShiftPlans(plans); // 2. Wähle ersten verfügbaren Plan aus if (plans.length > 0) { const planWithShifts = plans.find(plan => plan.shifts && plan.shifts.length > 0 && plan.timeSlots && plan.timeSlots.length > 0 ) || plans[0]; console.log('✅ ERSTER PLAN AUSGEWÄHLT:', planWithShifts.name); setSelectedPlanId(planWithShifts.id); } else { setLoading(false); } } catch (err: any) { console.error('❌ FEHLER BEIM LADEN DER INITIALDATEN:', err); showNotification({ type: 'error', title: 'Fehler beim Laden', message: 'Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler') }); setLoading(false); } }; loadInitialData(); }, [employee.id]); // Lade Plan-Details und Verfügbarkeiten wenn selectedPlanId sich ändert useEffect(() => { const loadPlanData = async () => { if (!selectedPlanId) { setLoading(false); return; } try { setLoading(true); console.log('🔄 LADE PLAN-DATEN FÜR:', selectedPlanId); // 1. Lade Schichtplan Details const plan = await shiftPlanService.getShiftPlan(selectedPlanId); setSelectedPlan(plan); console.log('✅ SCHICHTPLAN DETAILS GELADEN:', { name: plan.name, timeSlotsCount: plan.timeSlots?.length || 0, shiftsCount: plan.shifts?.length || 0, usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort() }); // 2. Lade Verfügbarkeiten für DIESEN Mitarbeiter und DIESEN Plan console.log('🔄 LADE VERFÜGBARKEITEN FÜR:', { employeeId: employee.id, planId: selectedPlanId }); try { const allAvailabilities = await employeeService.getAvailabilities(employee.id); console.log('📋 ALLE VERFÜGBARKEITEN DES MITARBEITERS:', allAvailabilities.length); // Filtere nach dem aktuellen Plan UND stelle sicher, dass shiftId vorhanden ist const planAvailabilities = allAvailabilities.filter( avail => avail.planId === selectedPlanId && avail.shiftId ); console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN (MIT SHIFT-ID):', planAvailabilities.length); // Debug: Zeige auch ungültige Einträge const invalidAvailabilities = allAvailabilities.filter( avail => avail.planId === selectedPlanId && !avail.shiftId ); if (invalidAvailabilities.length > 0) { console.warn('⚠️ UNGÜLTIGE VERFÜGBARKEITEN (OHNE SHIFT-ID):', invalidAvailabilities.length); } // Transformiere die Daten const transformedAvailabilities: Availability[] = planAvailabilities.map(avail => ({ ...avail, isAvailable: avail.preferenceLevel !== 3 })); setAvailabilities(transformedAvailabilities); // Debug: Zeige vorhandene Präferenzen if (planAvailabilities.length > 0) { console.log('🎯 VORHANDENE PRÄFERENZEN:', planAvailabilities.length); } } catch (availError) { console.error('❌ FEHLER BEIM LADEN DER VERFÜGBARKEITEN:', availError); setAvailabilities([]); } } catch (err: any) { console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); showNotification({ type: 'error', title: 'Fehler beim Laden', message: 'Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler') }); } finally { setLoading(false); } }; loadPlanData(); }, [selectedPlanId, employee.id]); const formatTime = (time: string): string => { if (!time) return '--:--'; return time.substring(0, 5); }; // Create a data structure that maps days to their shifts with time slot info const getTimetableData = () => { if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) { return { days: [], shiftsByDay: {} }; } // Create a map for quick time slot lookups const timeSlotMap = new Map(selectedPlan.timeSlots.map(ts => [ts.id, ts])); // Group shifts by day and enhance with time slot info const shiftsByDay = selectedPlan.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); // Sort shifts within each day by start time 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 const days = Array.from(new Set(selectedPlan.shifts.map(shift => shift.dayOfWeek))) .sort() .map(dayId => { return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` }; }); return { days, shiftsByDay }; }; const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => { if (!shiftId) { console.error('❌ Versuch, Verfügbarkeit ohne Shift-ID zu ändern'); return; } console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`); setAvailabilities(prev => { const existingIndex = prev.findIndex(avail => avail.shiftId === shiftId); if (existingIndex >= 0) { // Update existing availability const updated = [...prev]; updated[existingIndex] = { ...updated[existingIndex], preferenceLevel: level, isAvailable: level !== 3 }; return updated; } else { // Create new availability using shiftId directly const newAvailability: Availability = { id: `temp-${shiftId}-${Date.now()}`, employeeId: employee.id, planId: selectedPlanId, shiftId: shiftId, preferenceLevel: level, isAvailable: level !== 3 }; return [...prev, newAvailability]; } }); }; const getAvailabilityForShift = (shiftId: string): AvailabilityLevel => { const availability = availabilities.find(avail => avail.shiftId === shiftId); return availability?.preferenceLevel || 3; }; // Update the timetable rendering to use shifts directly const renderTimetable = () => { const { days, shiftsByDay } = getTimetableData(); if (days.length === 0 || Object.keys(shiftsByDay).length === 0) { return (
📅

Keine Shifts im ausgewählten Plan

Der ausgewählte Schichtplan hat keine Shifts definiert.

); } // Create a map for quick time slot lookups const timeSlotMap = new Map(selectedPlan?.timeSlots?.map(ts => [ts.id, ts]) || []); // Get all unique time slots (rows) by collecting from all shifts const allTimeSlots = new Map(); days.forEach(day => { shiftsByDay[day.id]?.forEach(shift => { const timeSlot = timeSlotMap.get(shift.timeSlotId); if (timeSlot && !allTimeSlots.has(timeSlot.id)) { allTimeSlots.set(timeSlot.id, { ...timeSlot, shiftsByDay: {} // Initialize empty object to store shifts by day }); } }); }); // Populate shifts for each time slot by day days.forEach(day => { shiftsByDay[day.id]?.forEach(shift => { const timeSlot = allTimeSlots.get(shift.timeSlotId); if (timeSlot) { timeSlot.shiftsByDay[day.id] = shift; } }); }); // Convert to array and sort by start time const sortedTimeSlots = Array.from(allTimeSlots.values()).sort((a, b) => { return (a.startTime || '').localeCompare(b.startTime || ''); }); return (
Verfügbarkeit definieren
{sortedTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
{days.map(weekday => ( ))} {sortedTimeSlots.map((timeSlot, timeSlotIndex) => ( {days.map(weekday => { const shift = timeSlot.shiftsByDay[weekday.id]; if (!shift) { return ( ); } const currentLevel = getAvailabilityForShift(shift.id); const levelConfig = availabilityLevels.find(l => l.level === currentLevel); return ( ); })} ))}
Zeitslot {weekday.name}
{timeSlot.name}
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
Kein Shift
{/* Summary Statistics */}
Aktive Verfügbarkeiten: {availabilities.filter(a => a.preferenceLevel !== 3).length}
); }; const handleSave = async () => { if (!selectedPlanId) { showNotification({ type: 'error', title: 'Fehler', message: 'Bitte wählen Sie einen Schichtplan aus' }); return; } // Basic frontend validation: Check if we have any availabilities to save const validAvailabilities = availabilities.filter(avail => { return avail.shiftId && selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId); }); if (validAvailabilities.length === 0) { showNotification({ type: 'error', title: 'Fehler', message: 'Keine gültigen Verfügbarkeiten zum Speichern gefunden' }); return; } // Complex validation (contract type rules) is now handled by backend // We only do basic required field validation in frontend await executeWithValidation(async () => { setSaving(true); // Convert to the format expected by the API - using shiftId directly const requestData = { planId: selectedPlanId, availabilities: validAvailabilities.map(avail => ({ planId: selectedPlanId, shiftId: avail.shiftId, preferenceLevel: avail.preferenceLevel, notes: avail.notes })) }; await employeeService.updateAvailabilities(employee.id, requestData); console.log('✅ VERFÜGBARKEITEN ERFOLGREICH GESPEICHERT'); showNotification({ type: 'success', title: 'Erfolg', message: 'Verfügbarkeiten wurden erfolgreich gespeichert' }); window.dispatchEvent(new CustomEvent('availabilitiesChanged')); onSave(); }); }; if (loading) { return (
⏳ Lade Verfügbarkeiten...
); } const { days, shiftsByDay } = getTimetableData(); const allShiftIds = new Set(); days.forEach(day => { shiftsByDay[day.id]?.forEach(shift => { allShiftIds.add(shift.id); }); }); const shiftsCount = allShiftIds.size; // Get full name for display const employeeFullName = `${employee.firstname} ${employee.lastname}`; // Available shifts count for display only (not for validation) const availableShiftsCount = availabilities.filter(avail => avail.preferenceLevel === 1 || avail.preferenceLevel === 2 ).length; return (

📅 Verfügbarkeit verwalten

{/* Employee Info */}

{employeeFullName}

Email: {employee.email}

{employee.contractType && (

Vertrag: {employee.contractType === 'small' ? ' Kleiner Vertrag' : employee.contractType === 'large' ? ' Großer Vertrag' : ' Flexibler Vertrag'} {/* Note: Contract validation is now handled by backend */}

)}
{/* Availability Legend */}

Verfügbarkeits-Level

{availabilityLevels.map(level => (
{level.level}: {level.label}
{level.description}
))}
{/* Shift Plan Selection */}

Verfügbarkeit für Schichtplan

{selectedPlan && (
Plan: {selectedPlan.name}
Shifts: {selectedPlan.shifts?.length || 0}
Zeitslots: {selectedPlan.timeSlots?.length || 0}
Status: {selectedPlan.status}
)}
{/* Debug Info für Plan Loading */} {!selectedPlanId && shiftPlans.length > 0 && (
⚠️ Bitte wählen Sie einen Schichtplan aus
)}
{/* Availability Timetable */} {renderTimetable()} {/* Buttons */}
); }; export default AvailabilityManager;