From 6cc8c913174be0d918cee2847b7ba8d28ce98127 Mon Sep 17 00:00:00 2001 From: donpat1to Date: Fri, 31 Oct 2025 00:27:50 +0100 Subject: [PATCH] updated validation handling together with shiftplan --- .../components/AvailabilityManager.tsx | 256 +++++------------- .../Employees/components/EmployeeList.tsx | 29 +- .../src/pages/ShiftPlans/ShiftPlanCreate.tsx | 116 ++++---- .../src/pages/ShiftPlans/ShiftPlanEdit.tsx | 202 ++++++++------ .../src/pages/ShiftPlans/ShiftPlanList.tsx | 168 ++++++++---- 5 files changed, 410 insertions(+), 361 deletions(-) diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index c80faa3..ec215a5 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -3,6 +3,8 @@ 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; @@ -36,7 +38,8 @@ const AvailabilityManager: React.FC = ({ const [selectedPlan, setSelectedPlan] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); + const { showNotification } = useNotification(); + const { executeWithValidation, isSubmitting } = useBackendValidation(); const daysOfWeek = [ { id: 1, name: 'Montag' }, @@ -81,7 +84,11 @@ const AvailabilityManager: React.FC = ({ } catch (err: any) { console.error('❌ FEHLER BEIM LADEN DER INITIALDATEN:', err); - setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); + showNotification({ + type: 'error', + title: 'Fehler beim Laden', + message: 'Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler') + }); setLoading(false); } }; @@ -134,9 +141,6 @@ const AvailabilityManager: React.FC = ({ ); 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 @@ -149,20 +153,20 @@ const AvailabilityManager: React.FC = ({ // 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}`); - }); + console.log('🎯 VORHANDENE PRÄFERENZEN:', planAvailabilities.length); } - } catch (availError) { - console.error('❌ FEHLER BEIM LADEN DER VERFÜGBARKEITEN:', availError); - setAvailabilities([]); - } + } 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')); + showNotification({ + type: 'error', + title: 'Fehler beim Laden', + message: 'Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler') + }); } finally { setLoading(false); } @@ -316,26 +320,6 @@ const AvailabilityManager: React.FC = ({ 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 (
= ({
- {/* Validation Warnings */} - {validationErrors.length > 0 && ( -
-

⚠️ Validierungswarnungen:

-
    - {validationErrors.map((error, index) => ( -
  • {error}
  • - ))} -
-
- )} -
= ({
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
-
- ID: {timeSlot.id.substring(0, 8)}... -
{days.map(weekday => { const shift = timeSlot.shiftsByDay[weekday.id]; @@ -443,9 +407,6 @@ const AvailabilityManager: React.FC = ({ ); } - // 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); @@ -454,31 +415,8 @@ const AvailabilityManager: React.FC = ({ padding: '12px 16px', border: '1px solid #dee2e6', textAlign: 'center', - backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'), - position: 'relative' + backgroundColor: levelConfig?.bgColor || 'white' }}> - {/* Validation indicator */} - {!isValidShift && ( -
- ⚠️ -
- )} - - - {/* Shift debug info */} -
-
Shift: {shift.id.substring(0, 6)}...
-
Day: {shift.dayOfWeek}
- {!isValidShift && ( -
- VALIDATION ERROR -
- )} -
); })} @@ -556,63 +477,35 @@ const AvailabilityManager: React.FC = ({ }; const handleSave = async () => { - try { + 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); - setError(''); - if (!selectedPlanId) { - setError('Bitte wählen Sie einen Schichtplan aus'); - return; - } - - // 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; - } - - // Contract type validation - const availableShifts = validAvailabilities.filter(avail => - avail.preferenceLevel === 1 || avail.preferenceLevel === 2 - ).length; - - let contractRequirement = 0; - let contractTypeName = ''; - - if (employee.contractType === 'small') { - contractRequirement = 2; - contractTypeName = 'Kleiner Vertrag'; - } else if (employee.contractType === 'large') { - contractRequirement = 3; - contractTypeName = 'Großer Vertrag'; - } - - if (contractRequirement > 0 && availableShifts < contractRequirement) { - setError( - `${contractTypeName} erfordert mindestens ${contractRequirement} verfügbare Shifts. ` + - `Aktuell sind nur ${availableShifts} Shifts mit Verfügbarkeit "Bevorzugt" oder "Möglich" ausgewählt.` - ); - setSaving(false); - return; - } - // Convert to the format expected by the API - using shiftId directly const requestData = { planId: selectedPlanId, @@ -627,15 +520,16 @@ const AvailabilityManager: React.FC = ({ 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(); - } catch (err: any) { - console.error('❌ FEHLER BEIM SPEICHERN:', err); - setError(err.message || 'Fehler beim Speichern der Verfügbarkeiten'); - } finally { - setSaving(false); - } + }); }; if (loading) { @@ -658,11 +552,10 @@ const AvailabilityManager: React.FC = ({ // Get full name for display const employeeFullName = `${employee.firstname} ${employee.lastname}`; - // Mininmum amount of shifts per contract type + // Available shifts count for display only (not for validation) const availableShiftsCount = availabilities.filter(avail => avail.preferenceLevel === 1 || avail.preferenceLevel === 2 ).length; - return (
= ({ {employee.contractType && (

Vertrag: - {employee.contractType === 'small' ? ' Kleiner Vertrag (min. 2 verfügbare Shifts)' : - employee.contractType === 'large' ? ' Großer Vertrag (min. 3 verfügbare Shifts)' : + {employee.contractType === 'small' ? ' Kleiner Vertrag' : + employee.contractType === 'large' ? ' Großer Vertrag' : ' Flexibler Vertrag'} + {/* Note: Contract validation is now handled by backend */}

)}
- {error && ( -
- Fehler: {error} -
- )} - {/* Availability Legend */}
= ({ const newPlanId = e.target.value; console.log('🔄 PLAN WECHSELN ZU:', newPlanId); setSelectedPlanId(newPlanId); - // Der useEffect wird automatisch ausgelöst }} style={{ padding: '8px 12px', @@ -828,15 +708,15 @@ const AvailabilityManager: React.FC = ({ }}>
diff --git a/frontend/src/pages/Employees/components/EmployeeList.tsx b/frontend/src/pages/Employees/components/EmployeeList.tsx index 2471ef4..a09c3db 100644 --- a/frontend/src/pages/Employees/components/EmployeeList.tsx +++ b/frontend/src/pages/Employees/components/EmployeeList.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { Employee } from '../../../models/Employee'; import { useAuth } from '../../../contexts/AuthContext'; +import { useNotification } from '../../../contexts/NotificationContext'; interface EmployeeListProps { employees: Employee[]; @@ -28,6 +29,7 @@ const EmployeeList: React.FC = ({ const [sortField, setSortField] = useState('name'); const [sortDirection, setSortDirection] = useState('asc'); const { user: currentUser, hasRole } = useAuth(); + const { showNotification, confirmDialog } = useNotification(); // Filter employees based on active/inactive and search term const filteredEmployees = employees.filter(employee => { @@ -176,6 +178,31 @@ const EmployeeList: React.FC = ({ return 'MITARBEITER'; }; + const handleDeleteClick = async (employee: Employee) => { + const confirmed = await confirmDialog({ + title: 'Mitarbeiter löschen', + message: `Sind Sie sicher, dass Sie ${employee.firstname} ${employee.lastname} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.`, + confirmText: 'Löschen', + cancelText: 'Abbrechen', + type: 'warning' + }); + + if (confirmed) { + try { + onDelete(employee); + showNotification({ + type: 'success', + title: 'Erfolg', + message: `${employee.firstname} ${employee.lastname} wurde erfolgreich gelöscht.` + }); + } catch (error: any) { + // Error will be handled by parent component through useBackendValidation + // We just need to re-throw it so the parent can catch it + throw error; + } + } + }; + if (employees.length === 0) { return (
= ({ {/* Löschen Button */} {canDelete && (
- - {error && ( -
- {error} -
- )} - - {success && ( -
- {success} -
- )}
@@ -146,6 +164,7 @@ const ShiftPlanCreate: React.FC = () => { onChange={(e) => setPlanName(e.target.value)} placeholder="z.B. KW 42 2025" className={styles.input} + disabled={isSubmitting} />
@@ -157,6 +176,7 @@ const ShiftPlanCreate: React.FC = () => { value={startDate} onChange={(e) => setStartDate(e.target.value)} className={styles.input} + disabled={isSubmitting} />
@@ -167,6 +187,7 @@ const ShiftPlanCreate: React.FC = () => { value={endDate} onChange={(e) => setEndDate(e.target.value)} className={styles.input} + disabled={isSubmitting} /> @@ -177,6 +198,7 @@ const ShiftPlanCreate: React.FC = () => { value={selectedPreset} onChange={(e) => setSelectedPreset(e.target.value)} className={`${styles.select} ${presets.length === 0 ? styles.empty : ''}`} + disabled={isSubmitting} > {presets.map(preset => ( @@ -203,9 +225,9 @@ const ShiftPlanCreate: React.FC = () => { diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx index c71084c..1263eaa 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanEdit.tsx @@ -4,11 +4,14 @@ import { useParams, useNavigate } from 'react-router-dom'; import { shiftPlanService } from '../../services/shiftPlanService'; import { ShiftPlan, Shift, ScheduledShift } from '../../models/ShiftPlan'; import { useNotification } from '../../contexts/NotificationContext'; +import { useBackendValidation } from '../../hooks/useBackendValidation'; const ShiftPlanEdit: React.FC = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { showNotification } = useNotification(); + const { showNotification, confirmDialog } = useNotification(); + const { executeWithValidation, isSubmitting } = useBackendValidation(); + const [shiftPlan, setShiftPlan] = useState(null); const [loading, setLoading] = useState(true); const [editingShift, setEditingShift] = useState(null); @@ -24,122 +27,150 @@ const ShiftPlanEdit: React.FC = () => { const loadShiftPlan = async () => { if (!id) return; - try { - const plan = await shiftPlanService.getShiftPlan(id); - setShiftPlan(plan); - } catch (error) { - console.error('Error loading shift plan:', error); - showNotification({ - type: 'error', - title: 'Fehler', - message: 'Der Schichtplan konnte nicht geladen werden.' - }); - navigate('/shift-plans'); - } finally { - setLoading(false); - } + + await executeWithValidation(async () => { + try { + const plan = await shiftPlanService.getShiftPlan(id); + setShiftPlan(plan); + } catch (error) { + console.error('Error loading shift plan:', error); + navigate('/shift-plans'); + } finally { + setLoading(false); + } + }); }; const handleUpdateShift = async (shift: Shift) => { if (!shiftPlan || !id) return; - try { - // Update logic here + await executeWithValidation(async () => { + // Update logic here - will be implemented when backend API is available + // For now, just simulate success + console.log('Updating shift:', shift); + loadShiftPlan(); setEditingShift(null); - } catch (error) { - console.error('Error updating shift:', error); + showNotification({ - type: 'error', - title: 'Fehler', - message: 'Die Schicht konnte nicht aktualisiert werden.' + type: 'success', + title: 'Erfolg', + message: 'Schicht wurde erfolgreich aktualisiert.' }); - } + }); }; const handleAddShift = async () => { if (!shiftPlan || !id) return; - if (!newShift.timeSlotId || !newShift.requiredEmployees) { + // Basic frontend validation only + if (!newShift.timeSlotId) { showNotification({ type: 'error', - title: 'Fehler', - message: 'Bitte füllen Sie alle Pflichtfelder aus.' + title: 'Fehlende Angaben', + message: 'Bitte wählen Sie einen Zeit-Slot aus.' }); return; } - try { - // Add shift logic here + if (!newShift.requiredEmployees || newShift.requiredEmployees < 1) { + showNotification({ + type: 'error', + title: 'Fehlende Angaben', + message: 'Bitte geben Sie die Anzahl der benötigten Mitarbeiter an.' + }); + return; + } + + await executeWithValidation(async () => { + // Add shift logic here - will be implemented when backend API is available + // For now, just simulate success + console.log('Adding shift:', newShift); + showNotification({ type: 'success', title: 'Erfolg', message: 'Neue Schicht wurde hinzugefügt.' }); + setNewShift({ timeSlotId: '', dayOfWeek: 1, requiredEmployees: 1 }); loadShiftPlan(); - } catch (error) { - console.error('Error adding shift:', error); - showNotification({ - type: 'error', - title: 'Fehler', - message: 'Die Schicht konnte nicht hinzugefügt werden.' - }); - } + }); }; const handleDeleteShift = async (shiftId: string) => { - if (!window.confirm('Möchten Sie diese Schicht wirklich löschen?')) { - return; - } + const confirmed = await confirmDialog({ + title: 'Schicht löschen', + message: 'Möchten Sie diese Schicht wirklich löschen?', + confirmText: 'Löschen', + cancelText: 'Abbrechen', + type: 'warning' + }); - try { - // Delete logic here + if (!confirmed) return; + + await executeWithValidation(async () => { + // Delete logic here - will be implemented when backend API is available + // For now, just simulate success + console.log('Deleting shift:', shiftId); + loadShiftPlan(); - } catch (error) { - console.error('Error deleting shift:', error); + showNotification({ - type: 'error', - title: 'Fehler', - message: 'Die Schicht konnte nicht gelöscht werden.' + type: 'success', + title: 'Erfolg', + message: 'Schicht wurde erfolgreich gelöscht.' }); - } + }); }; const handlePublish = async () => { if (!shiftPlan || !id) return; - try { + await executeWithValidation(async () => { await shiftPlanService.updateShiftPlan(id, { ...shiftPlan, status: 'published' }); + showNotification({ type: 'success', title: 'Erfolg', message: 'Schichtplan wurde veröffentlicht.' }); + loadShiftPlan(); - } catch (error) { - console.error('Error publishing shift plan:', error); - showNotification({ - type: 'error', - title: 'Fehler', - message: 'Der Schichtplan konnte nicht veröffentlicht werden.' - }); - } + }); }; if (loading) { - return
Lade Schichtplan...
; + return ( +
+ Lade Schichtplan... +
+ ); } if (!shiftPlan) { - return
Schichtplan nicht gefunden
; + return ( +
+ Schichtplan nicht gefunden +
+ ); } // Group shifts by dayOfWeek @@ -174,28 +205,32 @@ const ShiftPlanEdit: React.FC = () => { {shiftPlan.status === 'draft' && ( )} @@ -300,6 +339,7 @@ const ShiftPlanEdit: React.FC = () => { value={editingShift.timeSlotId} onChange={(e) => setEditingShift({ ...editingShift, timeSlotId: e.target.value })} style={{ width: '100%', padding: '8px', borderRadius: '4px', border: '1px solid #ddd' }} + disabled={isSubmitting} > {shiftPlan.timeSlots.map(slot => (