mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +01:00
updated every file for database changes; starting scheduling debugging
This commit is contained in:
@@ -54,15 +54,122 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
{ level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' }
|
||||
];
|
||||
|
||||
// Lade initial die Schichtpläne
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
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);
|
||||
setError('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(() => {
|
||||
if (selectedPlanId) {
|
||||
loadSelectedPlan();
|
||||
}
|
||||
}, [selectedPlanId]);
|
||||
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);
|
||||
invalidAvailabilities.forEach(invalid => {
|
||||
console.warn(' - Ungültiger Eintrag:', invalid);
|
||||
});
|
||||
}
|
||||
|
||||
// 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.forEach(avail => {
|
||||
const shift = plan.shifts?.find(s => s.id === avail.shiftId);
|
||||
console.log(` - Shift: ${avail.shiftId} (Day: ${shift?.dayOfWeek}), Level: ${avail.preferenceLevel}`);
|
||||
});
|
||||
}
|
||||
} catch (availError) {
|
||||
console.error('❌ FEHLER BEIM LADEN DER VERFÜGBARKEITEN:', availError);
|
||||
setAvailabilities([]);
|
||||
}
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
|
||||
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPlanData();
|
||||
}, [selectedPlanId, employee.id]);
|
||||
|
||||
const formatTime = (time: string): string => {
|
||||
if (!time) return '--:--';
|
||||
@@ -116,73 +223,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
return { days, shiftsByDay };
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🔄 LADE DATEN FÜR MITARBEITER:', employee.id);
|
||||
|
||||
// 1. Load availabilities
|
||||
let existingAvailabilities: Availability[] = [];
|
||||
try {
|
||||
const availabilitiesData = await employeeService.getAvailabilities(employee.id);
|
||||
existingAvailabilities = availabilitiesData.map(avail => ({
|
||||
...avail,
|
||||
isAvailable: avail.preferenceLevel !== 3
|
||||
}));
|
||||
console.log('✅ VERFÜGBARKEITEN GELADEN:', existingAvailabilities.length);
|
||||
} catch (err) {
|
||||
console.log('⚠️ KEINE VERFÜGBARKEITEN GEFUNDEN ODER FEHLER:', err);
|
||||
}
|
||||
|
||||
// 2. Load shift plans
|
||||
console.log('🔄 LADE SCHICHTPLÄNE...');
|
||||
const plans = await shiftPlanService.getShiftPlans();
|
||||
console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length);
|
||||
|
||||
setShiftPlans(plans);
|
||||
|
||||
// 3. Select first plan with actual shifts if available
|
||||
if (plans.length > 0) {
|
||||
const planWithShifts = plans.find(plan =>
|
||||
plan.shifts && plan.shifts.length > 0 &&
|
||||
plan.timeSlots && plan.timeSlots.length > 0
|
||||
) || plans[0];
|
||||
|
||||
setSelectedPlanId(planWithShifts.id);
|
||||
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
|
||||
|
||||
await loadSelectedPlan();
|
||||
}
|
||||
|
||||
// 4. Set existing availabilities
|
||||
setAvailabilities(existingAvailabilities);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('❌ FEHLER BEIM LADEN DER DATEN:', err);
|
||||
setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSelectedPlan = async () => {
|
||||
try {
|
||||
console.log('🔄 LADE AUSGEWÄHLTEN SCHICHTPLAN:', selectedPlanId);
|
||||
const plan = await shiftPlanService.getShiftPlan(selectedPlanId);
|
||||
setSelectedPlan(plan);
|
||||
console.log('✅ SCHICHTPLAN 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()
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
|
||||
setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler'));
|
||||
}
|
||||
};
|
||||
|
||||
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 => {
|
||||
@@ -238,24 +284,56 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Get all unique shifts across all days for row headers
|
||||
const allShifts: ExtendedShift[] = [];
|
||||
const shiftIds = new Set<string>();
|
||||
|
||||
// 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 => {
|
||||
if (!shiftIds.has(shift.id)) {
|
||||
shiftIds.add(shift.id);
|
||||
allShifts.push(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
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Sort shifts by time slot start time
|
||||
allShifts.sort((a, b) => {
|
||||
const timeA = a.startTime || '';
|
||||
const timeB = b.startTime || '';
|
||||
return timeA.localeCompare(timeB);
|
||||
// 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 || '');
|
||||
});
|
||||
|
||||
// Validation: Check if shifts are correctly placed
|
||||
const validationErrors: string[] = [];
|
||||
|
||||
// Check for missing time slots
|
||||
const usedTimeSlotIds = new Set(selectedPlan?.shifts?.map(s => s.timeSlotId) || []);
|
||||
const availableTimeSlotIds = new Set(selectedPlan?.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
|
||||
selectedPlan?.shifts?.forEach(shift => {
|
||||
if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) {
|
||||
validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -273,10 +351,39 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
}}>
|
||||
Verfügbarkeit definieren
|
||||
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
||||
{allShifts.length} Schichten • {days.length} Tage • Direkte Shift-ID Zuordnung
|
||||
{sortedTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Warnings */}
|
||||
{validationErrors.length > 0 && (
|
||||
<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' }}>
|
||||
{validationErrors.map((error, index) => (
|
||||
<li key={index}>{error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timetable Structure Info */}
|
||||
<div style={{
|
||||
backgroundColor: '#d1ecf1',
|
||||
border: '1px solid #bee5eb',
|
||||
padding: '10px 15px',
|
||||
margin: '10px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<strong>Struktur-Info:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} Zellen
|
||||
</div>
|
||||
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{
|
||||
width: '100%',
|
||||
@@ -290,9 +397,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
textAlign: 'left',
|
||||
border: '1px solid #dee2e6',
|
||||
fontWeight: 'bold',
|
||||
minWidth: '150px'
|
||||
minWidth: '200px'
|
||||
}}>
|
||||
Schicht (Zeit)
|
||||
Zeitslot
|
||||
</th>
|
||||
{days.map(weekday => (
|
||||
<th key={weekday.id} style={{
|
||||
@@ -300,7 +407,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
textAlign: 'center',
|
||||
border: '1px solid #dee2e6',
|
||||
fontWeight: 'bold',
|
||||
minWidth: '90px'
|
||||
minWidth: '120px'
|
||||
}}>
|
||||
{weekday.name}
|
||||
</th>
|
||||
@@ -308,26 +415,32 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{allShifts.map((shift, shiftIndex) => (
|
||||
<tr key={shift.id} style={{
|
||||
backgroundColor: shiftIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
||||
{sortedTimeSlots.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'
|
||||
backgroundColor: '#f8f9fa',
|
||||
position: 'sticky',
|
||||
left: 0
|
||||
}}>
|
||||
{shift.displayName}
|
||||
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
|
||||
Shift-ID: {shift.id.substring(0, 8)}...
|
||||
<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 => {
|
||||
// Check if this shift exists for this day
|
||||
const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id);
|
||||
const shift = timeSlot.shiftsByDay[weekday.id];
|
||||
|
||||
if (!shiftForDay) {
|
||||
if (!shift) {
|
||||
return (
|
||||
<td key={weekday.id} style={{
|
||||
padding: '12px 16px',
|
||||
@@ -337,11 +450,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
color: '#ccc',
|
||||
fontStyle: 'italic'
|
||||
}}>
|
||||
-
|
||||
Kein Shift
|
||||
</td>
|
||||
);
|
||||
}
|
||||
|
||||
// Validation: Check if shift has correct timeSlotId and dayOfWeek
|
||||
const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id;
|
||||
|
||||
const currentLevel = getAvailabilityForShift(shift.id);
|
||||
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
|
||||
|
||||
@@ -350,8 +466,31 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
padding: '12px 16px',
|
||||
border: '1px solid #dee2e6',
|
||||
textAlign: 'center',
|
||||
backgroundColor: levelConfig?.bgColor
|
||||
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Validation indicator */}
|
||||
{!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>
|
||||
)}
|
||||
|
||||
<select
|
||||
value={currentLevel}
|
||||
onChange={(e) => {
|
||||
@@ -360,10 +499,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: `2px solid ${levelConfig?.color || '#ddd'}`,
|
||||
border: `2px solid ${!isValidShift ? '#f39c12' : (levelConfig?.color || '#ddd')}`,
|
||||
borderRadius: '6px',
|
||||
backgroundColor: levelConfig?.bgColor || 'white',
|
||||
color: levelConfig?.color || '#333',
|
||||
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
|
||||
color: !isValidShift ? '#856404' : (levelConfig?.color || '#333'),
|
||||
fontWeight: 'bold',
|
||||
minWidth: '140px',
|
||||
cursor: 'pointer',
|
||||
@@ -384,6 +523,23 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{/* Shift debug info */}
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
@@ -392,6 +548,27 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Summary Statistics */}
|
||||
<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> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} mögliche Shifts
|
||||
</div>
|
||||
<div>
|
||||
<strong>Aktive Verfügbarkeiten:</strong> {availabilities.filter(a => a.preferenceLevel !== 3).length}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Validierungsfehler:</strong> {validationErrors.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -406,14 +583,24 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { days, shiftsByDay } = getTimetableData();
|
||||
|
||||
// Filter availabilities to only include those with actual shifts
|
||||
// Filter availabilities to only include those with actual shifts AND valid shiftIds
|
||||
const validAvailabilities = availabilities.filter(avail => {
|
||||
// Check if this shiftId exists and is valid
|
||||
if (!avail.shiftId) {
|
||||
console.warn('⚠️ Überspringe ungültige Verfügbarkeit ohne Shift-ID:', avail);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if this shiftId exists in the current plan
|
||||
return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId);
|
||||
});
|
||||
|
||||
console.log('💾 SPEICHERE VERFÜGBARKEITEN:', {
|
||||
total: availabilities.length,
|
||||
valid: validAvailabilities.length,
|
||||
invalid: availabilities.length - validAvailabilities.length
|
||||
});
|
||||
|
||||
if (validAvailabilities.length === 0) {
|
||||
setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden');
|
||||
return;
|
||||
@@ -485,25 +672,63 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
|
||||
{/* Debug-Info */}
|
||||
<div style={{
|
||||
backgroundColor: shiftsCount === 0 ? '#f8d7da' : '#d1ecf1',
|
||||
border: `1px solid ${shiftsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
||||
backgroundColor: !selectedPlan ? '#f8d7da' : (shiftsCount === 0 ? '#fff3cd' : '#d1ecf1'),
|
||||
border: `1px solid ${!selectedPlan ? '#f5c6cb' : (shiftsCount === 0 ? '#ffeaa7' : '#bee5eb')}`,
|
||||
borderRadius: '6px',
|
||||
padding: '15px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h4 style={{
|
||||
margin: '0 0 10px 0',
|
||||
color: shiftsCount === 0 ? '#721c24' : '#0c5460'
|
||||
color: !selectedPlan ? '#721c24' : (shiftsCount === 0 ? '#856404' : '#0c5460')
|
||||
}}>
|
||||
{shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'}
|
||||
{!selectedPlan ? '❌ KEIN PLAN AUSGEWÄHLT' :
|
||||
shiftsCount === 0 ? '⚠️ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
|
||||
</h4>
|
||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
||||
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
||||
<div><strong>Plan ID:</strong> {selectedPlanId || 'Nicht gesetzt'}</div>
|
||||
<div><strong>Geladene Pläne:</strong> {shiftPlans.length}</div>
|
||||
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
|
||||
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
||||
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
|
||||
<div><strong>Methode:</strong> Direkte Shift-ID Zuordnung</div>
|
||||
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
|
||||
{selectedPlan && (
|
||||
<>
|
||||
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
||||
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan.shifts?.length || 0}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Show existing preferences */}
|
||||
{availabilities.length > 0 && (
|
||||
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
|
||||
<strong>Vorhandene Präferenzen:</strong>
|
||||
{availabilities.slice(0, 5).map(avail => {
|
||||
// SICHERHEITSCHECK: Stelle sicher, dass shiftId existiert
|
||||
if (!avail.shiftId) {
|
||||
return (
|
||||
<div key={avail.id} style={{ fontSize: '11px', color: 'red' }}>
|
||||
• UNGÜLTIG: Keine Shift-ID
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const shift = selectedPlan?.shifts?.find(s => s.id === avail.shiftId);
|
||||
const shiftIdDisplay = avail.shiftId ? avail.shiftId.substring(0, 8) + '...' : 'KEINE ID';
|
||||
|
||||
return (
|
||||
<div key={avail.id} style={{ fontSize: '11px' }}>
|
||||
• Shift {shiftIdDisplay} (Day {shift?.dayOfWeek || '?'}): Level {avail.preferenceLevel}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{availabilities.length > 5 && (
|
||||
<div style={{ fontSize: '11px', fontStyle: 'italic' }}>
|
||||
... und {availabilities.length - 5} weitere
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Employee Info */}
|
||||
@@ -588,7 +813,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
</label>
|
||||
<select
|
||||
value={selectedPlanId}
|
||||
onChange={(e) => setSelectedPlanId(e.target.value)}
|
||||
onChange={(e) => {
|
||||
const newPlanId = e.target.value;
|
||||
console.log('🔄 PLAN WECHSELN ZU:', newPlanId);
|
||||
setSelectedPlanId(newPlanId);
|
||||
// Der useEffect wird automatisch ausgelöst
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #ddd',
|
||||
@@ -609,10 +839,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||
<div><strong>Plan:</strong> {selectedPlan.name}</div>
|
||||
<div><strong>Shifts:</strong> {selectedPlan.shifts?.length || 0}</div>
|
||||
<div><strong>Zeitslots:</strong> {selectedPlan.timeSlots?.length || 0}</div>
|
||||
<div><strong>Status:</strong> {selectedPlan.status}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Debug Info für Plan Loading */}
|
||||
{!selectedPlanId && shiftPlans.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '10px',
|
||||
padding: '10px',
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeaa7',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
⚠️ Bitte wählen Sie einen Schichtplan aus
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Availability Timetable */}
|
||||
|
||||
@@ -11,6 +11,9 @@ interface EmployeeFormProps {
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||
type ContractType = 'small' | 'large' | 'flexible';
|
||||
|
||||
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
mode,
|
||||
employee,
|
||||
@@ -20,13 +23,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
const [formData, setFormData] = useState({
|
||||
firstname: '',
|
||||
lastname: '',
|
||||
email: '', // Will be auto-generated and display only
|
||||
email: '',
|
||||
password: '',
|
||||
roles: ['user'] as string[], // Changed from single role to array
|
||||
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
|
||||
contractType: 'small' as 'small' | 'large',
|
||||
roles: ['user'] as string[],
|
||||
employeeType: 'personell' as EmployeeType,
|
||||
contractType: 'small' as ContractType | undefined,
|
||||
canWorkAlone: false,
|
||||
isActive: true
|
||||
isActive: true,
|
||||
isTrainee: false
|
||||
});
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
newPassword: '',
|
||||
@@ -62,12 +66,13 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
firstname: employee.firstname,
|
||||
lastname: employee.lastname,
|
||||
email: employee.email,
|
||||
password: '', // Password wird beim Bearbeiten nicht angezeigt
|
||||
roles: employee.roles || ['user'], // Use roles array
|
||||
password: '',
|
||||
roles: employee.roles || ['user'],
|
||||
employeeType: employee.employeeType,
|
||||
contractType: employee.contractType,
|
||||
canWorkAlone: employee.canWorkAlone,
|
||||
isActive: employee.isActive
|
||||
isActive: employee.isActive,
|
||||
isTrainee: employee.isTrainee || false
|
||||
});
|
||||
}
|
||||
}, [mode, employee]);
|
||||
@@ -92,13 +97,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
const handleRoleChange = (role: string, checked: boolean) => {
|
||||
setFormData(prev => {
|
||||
if (checked) {
|
||||
// Add role if checked
|
||||
return {
|
||||
...prev,
|
||||
roles: [...prev.roles, role]
|
||||
};
|
||||
} else {
|
||||
// Remove role if unchecked
|
||||
return {
|
||||
...prev,
|
||||
roles: prev.roles.filter(r => r !== role)
|
||||
@@ -107,18 +110,36 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
|
||||
// Manager and experienced can work alone, trainee cannot
|
||||
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
|
||||
const handleEmployeeTypeChange = (employeeType: EmployeeType) => {
|
||||
// Determine if contract type should be shown and set default
|
||||
const requiresContract = employeeType !== 'guest';
|
||||
const defaultContractType = requiresContract ? 'small' as ContractType : undefined;
|
||||
|
||||
// Determine if can work alone based on employee type
|
||||
const canWorkAlone = employeeType === 'manager' ||
|
||||
(employeeType === 'personell' && !formData.isTrainee);
|
||||
|
||||
// Reset isTrainee if not personell
|
||||
const isTrainee = employeeType === 'personell' ? formData.isTrainee : false;
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
employeeType,
|
||||
canWorkAlone
|
||||
contractType: defaultContractType,
|
||||
canWorkAlone,
|
||||
isTrainee
|
||||
}));
|
||||
};
|
||||
|
||||
const handleContractTypeChange = (contractType: 'small' | 'large') => {
|
||||
const handleTraineeChange = (isTrainee: boolean) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
isTrainee,
|
||||
canWorkAlone: prev.employeeType === 'personell' ? !isTrainee : prev.canWorkAlone
|
||||
}));
|
||||
};
|
||||
|
||||
const handleContractTypeChange = (contractType: ContractType) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
contractType
|
||||
@@ -136,25 +157,27 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
firstname: formData.firstname.trim(),
|
||||
lastname: formData.lastname.trim(),
|
||||
password: formData.password,
|
||||
roles: formData.roles, // Use roles array
|
||||
roles: formData.roles,
|
||||
employeeType: formData.employeeType,
|
||||
contractType: formData.contractType,
|
||||
canWorkAlone: formData.canWorkAlone
|
||||
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
|
||||
canWorkAlone: formData.canWorkAlone,
|
||||
isTrainee: formData.isTrainee
|
||||
};
|
||||
await employeeService.createEmployee(createData);
|
||||
} else if (employee) {
|
||||
const updateData: UpdateEmployeeRequest = {
|
||||
firstname: formData.firstname.trim(),
|
||||
lastname: formData.lastname.trim(),
|
||||
roles: formData.roles, // Use roles array
|
||||
roles: formData.roles,
|
||||
employeeType: formData.employeeType,
|
||||
contractType: formData.contractType,
|
||||
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
|
||||
canWorkAlone: formData.canWorkAlone,
|
||||
isActive: formData.isActive,
|
||||
isTrainee: formData.isTrainee
|
||||
};
|
||||
await employeeService.updateEmployee(employee.id, updateData);
|
||||
|
||||
// If password change is requested and user is admin
|
||||
// Password change logic remains the same
|
||||
if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) {
|
||||
if (passwordForm.newPassword.length < 6) {
|
||||
throw new Error('Das neue Passwort muss mindestens 6 Zeichen lang sein');
|
||||
@@ -163,9 +186,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
throw new Error('Die Passwörter stimmen nicht überein');
|
||||
}
|
||||
|
||||
// Use the password change endpoint
|
||||
await employeeService.changePassword(employee.id, {
|
||||
currentPassword: '', // Empty for admin reset - backend should handle this
|
||||
currentPassword: '',
|
||||
newPassword: passwordForm.newPassword
|
||||
});
|
||||
}
|
||||
@@ -189,9 +211,12 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
|
||||
const contractTypeOptions = [
|
||||
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
|
||||
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' }
|
||||
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' },
|
||||
{ value: 'flexible' as const, label: 'Flexibler Vertrag', description: 'Flexible Arbeitszeiten' }
|
||||
];
|
||||
|
||||
const showContractType = formData.employeeType !== 'guest';
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '700px',
|
||||
@@ -330,8 +355,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vertragstyp (nur für Admins) */}
|
||||
{hasRole(['admin']) && (
|
||||
{/* Vertragstyp (nur für Admins und interne Mitarbeiter) */}
|
||||
{hasRole(['admin']) && showContractType && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
@@ -470,6 +495,37 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* FIXED: Trainee checkbox for personell type */}
|
||||
{formData.employeeType === 'personell' && (
|
||||
<div style={{
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
padding: '15px',
|
||||
border: '1px solid #e0e0e0',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#fff'
|
||||
}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="isTrainee"
|
||||
id="isTrainee"
|
||||
checked={formData.isTrainee}
|
||||
onChange={(e) => handleTraineeChange(e.target.checked)}
|
||||
style={{ width: '18px', height: '18px' }}
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="isTrainee" style={{ fontWeight: 'bold', color: '#2c3e50', display: 'block' }}>
|
||||
Als Neuling markieren
|
||||
</label>
|
||||
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
|
||||
Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Eigenständigkeit */}
|
||||
@@ -496,11 +552,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
id="canWorkAlone"
|
||||
checked={formData.canWorkAlone}
|
||||
onChange={handleChange}
|
||||
disabled={formData.employeeType === 'manager'}
|
||||
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
|
||||
style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
opacity: formData.employeeType === 'manager' ? 0.5 : 1
|
||||
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
@@ -508,14 +564,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
fontWeight: 'bold',
|
||||
color: '#2c3e50',
|
||||
display: 'block',
|
||||
opacity: formData.employeeType === 'manager' ? 0.5 : 1
|
||||
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
|
||||
}}>
|
||||
Als ausreichend eigenständig markieren
|
||||
{formData.employeeType === 'manager' && ' (Automatisch für Chefs)'}
|
||||
{(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'}
|
||||
</label>
|
||||
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
||||
{formData.employeeType === 'manager'
|
||||
? 'Chefs sind automatisch als eigenständig markiert.'
|
||||
: formData.employeeType === 'personell' && formData.isTrainee
|
||||
? 'Auszubildende können nicht als eigenständig markiert werden.'
|
||||
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
|
||||
}
|
||||
</div>
|
||||
@@ -527,7 +585,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
borderRadius: '15px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
opacity: formData.employeeType === 'manager' ? 0.7 : 1
|
||||
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.7 : 1
|
||||
}}>
|
||||
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
|
||||
</div>
|
||||
@@ -632,7 +690,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Systemrollen (nur für Admins) */}
|
||||
{/* Systemrollen (nur für Admins) - AKTUALISIERT FÜR MEHRFACHE ROLLEN */}
|
||||
{hasRole(['admin']) && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
@@ -714,6 +772,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// EmployeeList.tsx
|
||||
import React, { useState } from 'react';
|
||||
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||
import { Employee } from '../../../models/Employee';
|
||||
@@ -13,6 +14,9 @@ interface EmployeeListProps {
|
||||
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
|
||||
type SortDirection = 'asc' | 'desc';
|
||||
|
||||
// FIXED: Use the actual employee types from the Employee interface
|
||||
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||
|
||||
const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
employees,
|
||||
onEdit,
|
||||
@@ -122,18 +126,18 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
// Using shared configuration for consistent styling
|
||||
type EmployeeType = 'manager' | 'trainee' | 'experienced';
|
||||
|
||||
const getEmployeeTypeBadge = (type: EmployeeType) => {
|
||||
const getEmployeeTypeBadge = (type: EmployeeType, isTrainee: boolean = false) => {
|
||||
const config = EMPLOYEE_TYPE_CONFIG[type];
|
||||
|
||||
// FIXED: Updated color mapping for actual employee types
|
||||
const bgColor =
|
||||
type === 'manager'
|
||||
? '#fadbd8'
|
||||
: type === 'trainee'
|
||||
? '#d5f4e6'
|
||||
: '#d6eaf8'; // experienced
|
||||
? '#fadbd8' // light red
|
||||
: type === 'personell'
|
||||
? isTrainee ? '#d5f4e6' : '#d6eaf8' // light green for trainee, light blue for experienced
|
||||
: type === 'apprentice'
|
||||
? '#e8d7f7' // light purple for apprentice
|
||||
: '#f8f9fa'; // light gray for guest
|
||||
|
||||
return { text: config.label, color: config.color, bgColor };
|
||||
};
|
||||
@@ -296,7 +300,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
</div>
|
||||
|
||||
{sortedEmployees.map(employee => {
|
||||
const employeeType = getEmployeeTypeBadge(employee.employeeType);
|
||||
// FIXED: Type assertion to ensure type safety
|
||||
const employeeType = getEmployeeTypeBadge(employee.employeeType as EmployeeType, employee.isTrainee);
|
||||
const independence = getIndependenceBadge(employee.canWorkAlone);
|
||||
const roleInfo = getRoleBadge(employee.roles);
|
||||
const status = getStatusBadge(employee.isActive);
|
||||
@@ -541,19 +546,30 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
}}>👴 ERFAHREN</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>Langjährige Erfahrung</span>
|
||||
}}>👨🏭 PERSONAL</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>Reguläre Mitarbeiter</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<span style={{
|
||||
backgroundColor: '#d5f4e6',
|
||||
color: '#27ae60',
|
||||
backgroundColor: '#e8d7f7',
|
||||
color: '#9b59b6',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
}}>👶 NEULING</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>Benötigt Einarbeitung</span>
|
||||
}}>👨🎓 AUSZUBILDENDER</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>Auszubildende</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<span style={{
|
||||
backgroundColor: '#f8f9fa',
|
||||
color: '#95a5a6',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '12px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 'bold'
|
||||
}}>👤 GAST</span>
|
||||
<span style={{ fontSize: '12px', color: '#666' }}>Externe Mitarbeiter</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user