From 0b35bb6dc60b87a08633cbc2f26138ae700839be Mon Sep 17 00:00:00 2001 From: donpat1to Date: Thu, 30 Oct 2025 23:39:21 +0100 Subject: [PATCH] updated validation handling together with employeeform --- frontend/src/hooks/useBackendValidation.ts | 89 +++ .../Employees/components/EmployeeForm.tsx | 639 +++++++++++------- frontend/src/services/employeeService.ts | 32 +- 3 files changed, 497 insertions(+), 263 deletions(-) create mode 100644 frontend/src/hooks/useBackendValidation.ts diff --git a/frontend/src/hooks/useBackendValidation.ts b/frontend/src/hooks/useBackendValidation.ts new file mode 100644 index 0000000..eaa3c83 --- /dev/null +++ b/frontend/src/hooks/useBackendValidation.ts @@ -0,0 +1,89 @@ +// frontend/src/hooks/useBackendValidation.ts +import { useState, useCallback } from 'react'; +import { ValidationError } from '../services/errorService'; +import { useNotification } from '../contexts/NotificationContext'; + +export const useBackendValidation = () => { + const [validationErrors, setValidationErrors] = useState([]); + const [isSubmitting, setIsSubmitting] = useState(false); + const { showNotification } = useNotification(); + + const clearErrors = useCallback(() => { + setValidationErrors([]); + }, []); + + const getFieldError = useCallback((fieldName: string): string | null => { + const error = validationErrors.find(error => error.field === fieldName); + return error ? error.message : null; + }, [validationErrors]); + + const hasErrors = useCallback((fieldName?: string): boolean => { + if (fieldName) { + return validationErrors.some(error => error.field === fieldName); + } + return validationErrors.length > 0; + }, [validationErrors]); + + const executeWithValidation = useCallback( + async (apiCall: () => Promise): Promise => { + setIsSubmitting(true); + clearErrors(); + + try { + const result = await apiCall(); + return result; + } catch (error: any) { + if (error.validationErrors) { + setValidationErrors(error.validationErrors); + + // Show specific validation error messages + if (error.validationErrors.length > 0) { + // Show the first validation error as the main notification + const firstError = error.validationErrors[0]; + showNotification({ + type: 'error', + title: 'Validierungsfehler', + message: firstError.message + }); + + // If there are multiple errors, show additional notifications for each + if (error.validationErrors.length > 1) { + // Wait a bit before showing additional notifications to avoid overlap + setTimeout(() => { + error.validationErrors.slice(1).forEach((validationError: ValidationError, index: number) => { + setTimeout(() => { + showNotification({ + type: 'error', + title: 'Weiterer Fehler', + message: validationError.message + }); + }, index * 300); // Stagger the notifications + }); + }, 500); + } + } + } else { + // Show notification for other errors + showNotification({ + type: 'error', + title: 'Fehler', + message: error.message || 'Ein unerwarteter Fehler ist aufgetreten' + }); + } + throw error; + } finally { + setIsSubmitting(false); + } + }, + [clearErrors, showNotification] + ); + + return { + validationErrors, + isSubmitting, + clearErrors, + getFieldError, + hasErrors, + executeWithValidation, + }; +}; \ No newline at end of file diff --git a/frontend/src/pages/Employees/components/EmployeeForm.tsx b/frontend/src/pages/Employees/components/EmployeeForm.tsx index 9f420c7..7204204 100644 --- a/frontend/src/pages/Employees/components/EmployeeForm.tsx +++ b/frontend/src/pages/Employees/components/EmployeeForm.tsx @@ -3,6 +3,8 @@ import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../.. import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { employeeService } from '../../../services/employeeService'; import { useAuth } from '../../../contexts/AuthContext'; +import { useBackendValidation } from '../../../hooks/useBackendValidation'; +import { useNotification } from '../../../contexts/NotificationContext'; interface EmployeeFormProps { mode: 'create' | 'edit'; @@ -40,6 +42,15 @@ interface PasswordFormData { // ===== HOOK FÜR FORMULAR-LOGIK ===== const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { + const { + validationErrors, + getFieldError, + hasErrors, + executeWithValidation, + isSubmitting, + clearErrors + } = useBackendValidation(); + const [currentStep, setCurrentStep] = useState(0); const [formData, setFormData] = useState({ firstname: '', @@ -63,6 +74,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); + // Steps definition const steps = [ { @@ -128,8 +140,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { } }, [mode, employee]); - // ===== VALIDIERUNGS-FUNKTIONEN ===== + // ===== SIMPLE FRONTEND VALIDATION (ONLY FOR REQUIRED FIELDS) ===== const validateStep1 = (): boolean => { + // Only check for empty required fields - let backend handle everything else if (!formData.firstname.trim()) { setError('Bitte geben Sie einen Vornamen ein.'); return false; @@ -138,10 +151,6 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { setError('Bitte geben Sie einen Nachnamen ein.'); return false; } - if (mode === 'create' && formData.password.length < 6) { - setError('Das Passwort muss mindestens 6 Zeichen lang sein.'); - return false; - } return true; }; @@ -167,6 +176,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { // ===== NAVIGATIONS-FUNKTIONEN ===== const goToNextStep = (): void => { setError(''); + clearErrors(); // Clear previous validation errors if (!validateCurrentStep(currentStep)) { return; @@ -179,6 +189,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { const goToPrevStep = (): void => { setError(''); + clearErrors(); // Clear validation errors when going back if (currentStep > 0) { setCurrentStep(prev => prev - 1); } @@ -186,6 +197,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { const handleStepChange = (stepIndex: number): void => { setError(''); + clearErrors(); // Clear validation errors when changing steps // Nur erlauben, zu bereits validierten Schritten zu springen if (stepIndex <= currentStep + 1) { @@ -205,6 +217,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { ...prev, [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value })); + + // Clear field-specific error when user starts typing + if (validationErrors.length > 0) { + clearErrors(); + } }; const handlePasswordChange = (e: React.ChangeEvent) => { @@ -213,6 +230,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { ...prev, [name]: value })); + + // Clear password errors when user starts typing + if (validationErrors.length > 0) { + clearErrors(); + } }; const handleRoleChange = (role: string, checked: boolean) => { @@ -275,6 +297,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { const handleSubmit = async (): Promise => { setLoading(true); setError(''); + clearErrors(); try { if (mode === 'create') { @@ -288,7 +311,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { canWorkAlone: formData.canWorkAlone, isTrainee: formData.isTrainee }; - await employeeService.createEmployee(createData); + + // Use executeWithValidation ONLY for the API call + await executeWithValidation(() => + employeeService.createEmployee(createData) + ); } else if (employee) { const updateData: UpdateEmployeeRequest = { firstname: formData.firstname.trim(), @@ -300,27 +327,34 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { isActive: formData.isActive, isTrainee: formData.isTrainee }; - await employeeService.updateEmployee(employee.id, updateData); + + // Use executeWithValidation for the update call + await executeWithValidation(() => + employeeService.updateEmployee(employee.id, updateData) + ); - // Password change logic + // Password change logic - backend will validate password requirements if (showPasswordSection && passwordForm.newPassword) { - if (passwordForm.newPassword.length < 6) { - throw new Error('Das Passwort muss mindestens 6 Zeichen lang sein'); - } if (passwordForm.newPassword !== passwordForm.confirmPassword) { throw new Error('Die Passwörter stimmen nicht überein'); } - await employeeService.changePassword(employee.id, { - currentPassword: '', - newPassword: passwordForm.newPassword - }); + // Use executeWithValidation for password change too + await executeWithValidation(() => + employeeService.changePassword(employee.id, { + currentPassword: '', + newPassword: passwordForm.newPassword + }) + ); } } return Promise.resolve(); } catch (err: any) { - setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`); + // Only set error if it's not a validation error (validation errors are handled by the hook) + if (!err.validationErrors) { + setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`); + } return Promise.reject(err); } finally { setLoading(false); @@ -331,8 +365,8 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { switch (stepIndex) { case 0: return !!formData.firstname.trim() && - !!formData.lastname.trim() && - (mode === 'edit' || formData.password.length >= 6); + !!formData.lastname.trim(); + // REMOVE: (mode === 'edit' || formData.password.length >= 6) case 1: return !!formData.employeeType; case 2: @@ -349,11 +383,14 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { currentStep, formData, passwordForm, - loading, + loading: loading || isSubmitting, error, steps, emailPreview, showPasswordSection, + validationErrors, + getFieldError, + hasErrors, // Actions goToNextStep, @@ -367,6 +404,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { handleContractTypeChange, handleSubmit, setShowPasswordSection, + clearErrors, // Helpers isStepCompleted @@ -388,6 +426,8 @@ interface StepContentProps { showPasswordSection: boolean; onShowPasswordSection: (show: boolean) => void; hasRole: (roles: string[]) => boolean; + getFieldError: (fieldName: string) => string | null; + hasErrors: (fieldName?: string) => boolean; } const Step1Content: React.FC = ({ @@ -497,7 +537,6 @@ const Step1Content: React.FC = ({ value={formData.password} onChange={onInputChange} required - minLength={6} style={{ width: '100%', padding: '0.75rem', @@ -505,14 +544,14 @@ const Step1Content: React.FC = ({ borderRadius: '6px', fontSize: '1rem' }} - placeholder="Mindestens 6 Zeichen" + placeholder="Passwort eingeben" />
- Das Passwort muss mindestens 6 Zeichen lang sein. + Das Passwort muss mindestens 8 Zeichen lang sein und Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen enthalten.
)} @@ -524,7 +563,8 @@ const Step2Content: React.FC = ({ onEmployeeTypeChange, onTraineeChange, onContractTypeChange, - hasRole + hasRole, + getFieldError }) => { const contractTypeOptions = [ { value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' }, @@ -533,6 +573,8 @@ const Step2Content: React.FC = ({ ]; const showContractType = formData.employeeType !== 'guest'; + const employeeTypeError = getFieldError('employeeType'); + const contractTypeError = getFieldError('contractType'); return (
@@ -540,6 +582,20 @@ const Step2Content: React.FC = ({

👥 Mitarbeiter Kategorie

+ {employeeTypeError && ( +
+ {employeeTypeError} +
+ )} +
{Object.values(EMPLOYEE_TYPE_CONFIG).map(type => (
= ({

📝 Vertragstyp

+ {contractTypeError && ( +
+ {contractTypeError} +
+ )} +
{contractTypeOptions.map(contract => { const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell'; @@ -735,117 +805,151 @@ const Step3Content: React.FC = ({ formData, onInputChange, onRoleChange, - hasRole -}) => ( -
- {/* Eigenständigkeit */} -
-

🎯 Eigenständigkeit

- -
- -
-