updated validation handling together with employeeform

This commit is contained in:
2025-10-30 23:39:21 +01:00
parent 4ef8e7b1f3
commit 0b35bb6dc6
3 changed files with 497 additions and 263 deletions

View File

@@ -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<ValidationError[]>([]);
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 <T>(apiCall: () => Promise<T>): Promise<T> => {
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,
};
};

View File

@@ -3,6 +3,8 @@ import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../..
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
import { employeeService } from '../../../services/employeeService'; import { employeeService } from '../../../services/employeeService';
import { useAuth } from '../../../contexts/AuthContext'; import { useAuth } from '../../../contexts/AuthContext';
import { useBackendValidation } from '../../../hooks/useBackendValidation';
import { useNotification } from '../../../contexts/NotificationContext';
interface EmployeeFormProps { interface EmployeeFormProps {
mode: 'create' | 'edit'; mode: 'create' | 'edit';
@@ -40,6 +42,15 @@ interface PasswordFormData {
// ===== HOOK FÜR FORMULAR-LOGIK ===== // ===== HOOK FÜR FORMULAR-LOGIK =====
const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const {
validationErrors,
getFieldError,
hasErrors,
executeWithValidation,
isSubmitting,
clearErrors
} = useBackendValidation();
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
const [formData, setFormData] = useState<EmployeeFormData>({ const [formData, setFormData] = useState<EmployeeFormData>({
firstname: '', firstname: '',
@@ -63,6 +74,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
// Steps definition // Steps definition
const steps = [ const steps = [
{ {
@@ -128,8 +140,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
} }
}, [mode, employee]); }, [mode, employee]);
// ===== VALIDIERUNGS-FUNKTIONEN ===== // ===== SIMPLE FRONTEND VALIDATION (ONLY FOR REQUIRED FIELDS) =====
const validateStep1 = (): boolean => { const validateStep1 = (): boolean => {
// Only check for empty required fields - let backend handle everything else
if (!formData.firstname.trim()) { if (!formData.firstname.trim()) {
setError('Bitte geben Sie einen Vornamen ein.'); setError('Bitte geben Sie einen Vornamen ein.');
return false; return false;
@@ -138,10 +151,6 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
setError('Bitte geben Sie einen Nachnamen ein.'); setError('Bitte geben Sie einen Nachnamen ein.');
return false; return false;
} }
if (mode === 'create' && formData.password.length < 6) {
setError('Das Passwort muss mindestens 6 Zeichen lang sein.');
return false;
}
return true; return true;
}; };
@@ -167,6 +176,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
// ===== NAVIGATIONS-FUNKTIONEN ===== // ===== NAVIGATIONS-FUNKTIONEN =====
const goToNextStep = (): void => { const goToNextStep = (): void => {
setError(''); setError('');
clearErrors(); // Clear previous validation errors
if (!validateCurrentStep(currentStep)) { if (!validateCurrentStep(currentStep)) {
return; return;
@@ -179,6 +189,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const goToPrevStep = (): void => { const goToPrevStep = (): void => {
setError(''); setError('');
clearErrors(); // Clear validation errors when going back
if (currentStep > 0) { if (currentStep > 0) {
setCurrentStep(prev => prev - 1); setCurrentStep(prev => prev - 1);
} }
@@ -186,6 +197,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const handleStepChange = (stepIndex: number): void => { const handleStepChange = (stepIndex: number): void => {
setError(''); setError('');
clearErrors(); // Clear validation errors when changing steps
// Nur erlauben, zu bereits validierten Schritten zu springen // Nur erlauben, zu bereits validierten Schritten zu springen
if (stepIndex <= currentStep + 1) { if (stepIndex <= currentStep + 1) {
@@ -205,6 +217,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
...prev, ...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value [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<HTMLInputElement>) => { const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -213,6 +230,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
...prev, ...prev,
[name]: value [name]: value
})); }));
// Clear password errors when user starts typing
if (validationErrors.length > 0) {
clearErrors();
}
}; };
const handleRoleChange = (role: string, checked: boolean) => { const handleRoleChange = (role: string, checked: boolean) => {
@@ -275,6 +297,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const handleSubmit = async (): Promise<void> => { const handleSubmit = async (): Promise<void> => {
setLoading(true); setLoading(true);
setError(''); setError('');
clearErrors();
try { try {
if (mode === 'create') { if (mode === 'create') {
@@ -288,7 +311,11 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
canWorkAlone: formData.canWorkAlone, canWorkAlone: formData.canWorkAlone,
isTrainee: formData.isTrainee isTrainee: formData.isTrainee
}; };
await employeeService.createEmployee(createData);
// Use executeWithValidation ONLY for the API call
await executeWithValidation(() =>
employeeService.createEmployee(createData)
);
} else if (employee) { } else if (employee) {
const updateData: UpdateEmployeeRequest = { const updateData: UpdateEmployeeRequest = {
firstname: formData.firstname.trim(), firstname: formData.firstname.trim(),
@@ -300,27 +327,34 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
isActive: formData.isActive, isActive: formData.isActive,
isTrainee: formData.isTrainee 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 (showPasswordSection && passwordForm.newPassword) {
if (passwordForm.newPassword.length < 6) {
throw new Error('Das Passwort muss mindestens 6 Zeichen lang sein');
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) { if (passwordForm.newPassword !== passwordForm.confirmPassword) {
throw new Error('Die Passwörter stimmen nicht überein'); throw new Error('Die Passwörter stimmen nicht überein');
} }
await employeeService.changePassword(employee.id, { // Use executeWithValidation for password change too
currentPassword: '', await executeWithValidation(() =>
newPassword: passwordForm.newPassword employeeService.changePassword(employee.id, {
}); currentPassword: '',
newPassword: passwordForm.newPassword
})
);
} }
} }
return Promise.resolve(); return Promise.resolve();
} catch (err: any) { } 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); return Promise.reject(err);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -331,8 +365,8 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
switch (stepIndex) { switch (stepIndex) {
case 0: case 0:
return !!formData.firstname.trim() && return !!formData.firstname.trim() &&
!!formData.lastname.trim() && !!formData.lastname.trim();
(mode === 'edit' || formData.password.length >= 6); // REMOVE: (mode === 'edit' || formData.password.length >= 6)
case 1: case 1:
return !!formData.employeeType; return !!formData.employeeType;
case 2: case 2:
@@ -349,11 +383,14 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
currentStep, currentStep,
formData, formData,
passwordForm, passwordForm,
loading, loading: loading || isSubmitting,
error, error,
steps, steps,
emailPreview, emailPreview,
showPasswordSection, showPasswordSection,
validationErrors,
getFieldError,
hasErrors,
// Actions // Actions
goToNextStep, goToNextStep,
@@ -367,6 +404,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
handleContractTypeChange, handleContractTypeChange,
handleSubmit, handleSubmit,
setShowPasswordSection, setShowPasswordSection,
clearErrors,
// Helpers // Helpers
isStepCompleted isStepCompleted
@@ -388,6 +426,8 @@ interface StepContentProps {
showPasswordSection: boolean; showPasswordSection: boolean;
onShowPasswordSection: (show: boolean) => void; onShowPasswordSection: (show: boolean) => void;
hasRole: (roles: string[]) => boolean; hasRole: (roles: string[]) => boolean;
getFieldError: (fieldName: string) => string | null;
hasErrors: (fieldName?: string) => boolean;
} }
const Step1Content: React.FC<StepContentProps> = ({ const Step1Content: React.FC<StepContentProps> = ({
@@ -497,7 +537,6 @@ const Step1Content: React.FC<StepContentProps> = ({
value={formData.password} value={formData.password}
onChange={onInputChange} onChange={onInputChange}
required required
minLength={6}
style={{ style={{
width: '100%', width: '100%',
padding: '0.75rem', padding: '0.75rem',
@@ -505,14 +544,14 @@ const Step1Content: React.FC<StepContentProps> = ({
borderRadius: '6px', borderRadius: '6px',
fontSize: '1rem' fontSize: '1rem'
}} }}
placeholder="Mindestens 6 Zeichen" placeholder="Passwort eingeben"
/> />
<div style={{ <div style={{
fontSize: '0.875rem', fontSize: '0.875rem',
color: '#6c757d', color: '#6c757d',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
Das Passwort muss mindestens 6 Zeichen lang sein. Das Passwort muss mindestens 8 Zeichen lang sein und Groß-/Kleinbuchstaben, Zahlen und Sonderzeichen enthalten.
</div> </div>
</div> </div>
)} )}
@@ -524,7 +563,8 @@ const Step2Content: React.FC<StepContentProps> = ({
onEmployeeTypeChange, onEmployeeTypeChange,
onTraineeChange, onTraineeChange,
onContractTypeChange, onContractTypeChange,
hasRole hasRole,
getFieldError
}) => { }) => {
const contractTypeOptions = [ const contractTypeOptions = [
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' }, { value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
@@ -533,6 +573,8 @@ const Step2Content: React.FC<StepContentProps> = ({
]; ];
const showContractType = formData.employeeType !== 'guest'; const showContractType = formData.employeeType !== 'guest';
const employeeTypeError = getFieldError('employeeType');
const contractTypeError = getFieldError('contractType');
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
@@ -540,6 +582,20 @@ const Step2Content: React.FC<StepContentProps> = ({
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
{employeeTypeError && (
<div style={{
color: '#dc3545',
fontSize: '0.875rem',
marginBottom: '1rem',
padding: '0.5rem',
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: '4px'
}}>
{employeeTypeError}
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{Object.values(EMPLOYEE_TYPE_CONFIG).map(type => ( {Object.values(EMPLOYEE_TYPE_CONFIG).map(type => (
<div <div
@@ -637,6 +693,20 @@ const Step2Content: React.FC<StepContentProps> = ({
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#0c5460' }}>📝 Vertragstyp</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#0c5460' }}>📝 Vertragstyp</h3>
{contractTypeError && (
<div style={{
color: '#dc3545',
fontSize: '0.875rem',
marginBottom: '1rem',
padding: '0.5rem',
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: '4px'
}}>
{contractTypeError}
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{contractTypeOptions.map(contract => { {contractTypeOptions.map(contract => {
const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell'; const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell';
@@ -735,117 +805,151 @@ const Step3Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange, onInputChange,
onRoleChange, onRoleChange,
hasRole hasRole,
}) => ( getFieldError
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> }) => {
{/* Eigenständigkeit */} const rolesError = getFieldError('roles');
<div> const canWorkAloneError = getFieldError('canWorkAlone');
<h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>🎯 Eigenständigkeit</h3>
return (
<div style={{ <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
display: 'flex', {/* Eigenständigkeit */}
alignItems: 'center', <div>
gap: '15px', <h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>🎯 Eigenständigkeit</h3>
padding: '1rem',
border: '1px solid #e0e0e0', {canWorkAloneError && (
borderRadius: '6px', <div style={{
backgroundColor: '#fff' color: '#dc3545',
}}> fontSize: '0.875rem',
<input marginBottom: '1rem',
type="checkbox" padding: '0.5rem',
name="canWorkAlone" backgroundColor: '#f8d7da',
id="canWorkAlone" border: '1px solid #f5c6cb',
checked={formData.canWorkAlone} borderRadius: '4px'
onChange={onInputChange}
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
style={{
width: '20px',
height: '20px',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}
/>
<div style={{ flex: 1 }}>
<label htmlFor="canWorkAlone" style={{
fontWeight: 'bold',
color: '#2c3e50',
display: 'block',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}> }}>
Als ausreichend eigenständig markieren {canWorkAloneError}
{(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'} </div>
</label> )}
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{formData.employeeType === 'manager' <div style={{
? 'Chefs sind automatisch als eigenständig markiert.' display: 'flex',
: formData.employeeType === 'personell' && formData.isTrainee alignItems: 'center',
? 'Auszubildende können nicht als eigenständig markiert werden.' gap: '15px',
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.' padding: '1rem',
} border: '1px solid #e0e0e0',
borderRadius: '6px',
backgroundColor: '#fff'
}}>
<input
type="checkbox"
name="canWorkAlone"
id="canWorkAlone"
checked={formData.canWorkAlone}
onChange={onInputChange}
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
style={{
width: '20px',
height: '20px',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}
/>
<div style={{ flex: 1 }}>
<label htmlFor="canWorkAlone" style={{
fontWeight: 'bold',
color: '#2c3e50',
display: 'block',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}>
Als ausreichend eigenständig markieren
{(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>
</div>
<div style={{
padding: '6px 12px',
backgroundColor: formData.canWorkAlone ? '#27ae60' : '#e74c3c',
color: 'white',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.7 : 1
}}>
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
</div> </div>
</div> </div>
<div style={{
padding: '6px 12px',
backgroundColor: formData.canWorkAlone ? '#27ae60' : '#e74c3c',
color: 'white',
borderRadius: '15px',
fontSize: '12px',
fontWeight: 'bold',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.7 : 1
}}>
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
</div>
</div> </div>
</div>
{/* Systemrollen (nur für Admins) */} {/* Systemrollen (nur für Admins) */}
{hasRole(['admin']) && ( {hasRole(['admin']) && (
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}> Systemrollen</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}> Systemrollen</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> {rolesError && (
{ROLE_CONFIG.map(role => ( <div style={{
<div color: '#dc3545',
key={role.value} fontSize: '0.875rem',
style={{ marginBottom: '1rem',
display: 'flex', padding: '0.5rem',
alignItems: 'flex-start', backgroundColor: '#f8d7da',
padding: '0.75rem', border: '1px solid #f5c6cb',
border: `2px solid ${formData.roles.includes(role.value) ? '#f39c12' : '#e0e0e0'}`, borderRadius: '4px'
borderRadius: '6px', }}>
backgroundColor: formData.roles.includes(role.value) ? '#fef9e7' : 'white', {rolesError}
cursor: 'pointer' </div>
}} )}
onClick={() => onRoleChange(role.value, !formData.roles.includes(role.value))}
> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<input {ROLE_CONFIG.map(role => (
type="checkbox" <div
name="roles" key={role.value}
value={role.value}
checked={formData.roles.includes(role.value)}
onChange={(e) => onRoleChange(role.value, e.target.checked)}
style={{ style={{
marginRight: '10px', display: 'flex',
marginTop: '2px' alignItems: 'flex-start',
padding: '0.75rem',
border: `2px solid ${formData.roles.includes(role.value) ? '#f39c12' : '#e0e0e0'}`,
borderRadius: '6px',
backgroundColor: formData.roles.includes(role.value) ? '#fef9e7' : 'white',
cursor: 'pointer'
}} }}
/> onClick={() => onRoleChange(role.value, !formData.roles.includes(role.value))}
<div style={{ flex: 1 }}> >
<div style={{ fontWeight: 'bold', color: '#2c3e50' }}> <input
{role.label} type="checkbox"
</div> name="roles"
<div style={{ fontSize: '14px', color: '#7f8c8d' }}> value={role.value}
{role.description} checked={formData.roles.includes(role.value)}
onChange={(e) => onRoleChange(role.value, e.target.checked)}
style={{
marginRight: '10px',
marginTop: '2px'
}}
/>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 'bold', color: '#2c3e50' }}>
{role.label}
</div>
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
{role.description}
</div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '0.5rem' }}>
<strong>Hinweis:</strong> Ein Mitarbeiter kann mehrere Rollen haben.
</div>
</div> </div>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '0.5rem' }}> )}
<strong>Hinweis:</strong> Ein Mitarbeiter kann mehrere Rollen haben. </div>
</div> );
</div> };
)}
</div>
);
const Step4Content: React.FC<StepContentProps> = ({ const Step4Content: React.FC<StepContentProps> = ({
formData, formData,
@@ -854,128 +958,161 @@ const Step4Content: React.FC<StepContentProps> = ({
onPasswordChange, onPasswordChange,
showPasswordSection, showPasswordSection,
onShowPasswordSection, onShowPasswordSection,
mode mode,
}) => ( getFieldError
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> }) => {
{/* Passwort ändern */} const newPasswordError = getFieldError('newPassword');
<div> const confirmPasswordError = getFieldError('confirmPassword');
<h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}>🔒 Passwort zurücksetzen</h3> const isActiveError = getFieldError('isActive');
{!showPasswordSection ? (
<button
type="button"
onClick={() => onShowPasswordSection(true)}
style={{
padding: '0.75rem 1.5rem',
backgroundColor: '#f39c12',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔑 Passwort zurücksetzen
</button>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={onPasswordChange}
required
minLength={6}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Mindestens 6 Zeichen"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold', color: '#2c3e50' }}>
Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={onPasswordChange}
required
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Passwort wiederholen"
/>
</div>
<div style={{ fontSize: '0.875rem', color: '#6c757d' }}>
<strong>Hinweis:</strong> Als Administrator können Sie das Passwort des Benutzers ohne Kenntnis des aktuellen Passworts zurücksetzen.
</div>
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{/* Passwort ändern */}
<div>
<h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}>🔒 Passwort zurücksetzen</h3>
{!showPasswordSection ? (
<button <button
type="button" type="button"
onClick={() => onShowPasswordSection(false)} onClick={() => onShowPasswordSection(true)}
style={{ style={{
padding: '0.5rem 1rem', padding: '0.75rem 1.5rem',
backgroundColor: '#95a5a6', backgroundColor: '#f39c12',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '6px',
cursor: 'pointer', cursor: 'pointer',
alignSelf: 'flex-start' fontWeight: 'bold'
}} }}
> >
Abbrechen 🔑 Passwort zurücksetzen
</button> </button>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={onPasswordChange}
required
style={{
width: '100%',
padding: '0.75rem',
border: `1px solid ${newPasswordError ? '#dc3545' : '#ced4da'}`,
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Mindestens 6 Zeichen"
/>
{newPasswordError && (
<div style={{
color: '#dc3545',
fontSize: '0.875rem',
marginTop: '0.25rem'
}}>
{newPasswordError}
</div>
)}
</div>
<div>
<label style={{ display: 'block', marginBottom: '0.5rem', fontWeight: 'bold', color: '#2c3e50' }}>
Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={onPasswordChange}
required
style={{
width: '100%',
padding: '0.75rem',
border: `1px solid ${confirmPasswordError ? '#dc3545' : '#ced4da'}`,
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Passwort wiederholen"
/>
{confirmPasswordError && (
<div style={{
color: '#dc3545',
fontSize: '0.875rem',
marginTop: '0.25rem'
}}>
{confirmPasswordError}
</div>
)}
</div>
<div style={{ fontSize: '0.875rem', color: '#6c757d' }}>
<strong>Hinweis:</strong> Als Administrator können Sie das Passwort des Benutzers ohne Kenntnis des aktuellen Passworts zurücksetzen.
</div>
<button
type="button"
onClick={() => onShowPasswordSection(false)}
style={{
padding: '0.5rem 1rem',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
alignSelf: 'flex-start'
}}
>
Abbrechen
</button>
</div>
)}
</div>
{/* Aktiv Status */}
{mode === 'edit' && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '1rem',
border: `1px solid ${isActiveError ? '#dc3545' : '#e0e0e0'}`,
borderRadius: '6px',
backgroundColor: '#f8f9fa'
}}>
<input
type="checkbox"
name="isActive"
id="isActive"
checked={formData.isActive}
onChange={onInputChange}
style={{ width: '18px', height: '18px' }}
/>
<div>
<label htmlFor="isActive" style={{ fontWeight: 'bold', color: '#2c3e50', display: 'block' }}>
Mitarbeiter ist aktiv
</label>
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant.
</div>
{isActiveError && (
<div style={{
color: '#dc3545',
fontSize: '0.875rem',
marginTop: '0.25rem'
}}>
{isActiveError}
</div>
)}
</div>
</div> </div>
)} )}
</div> </div>
);
{/* Aktiv Status */} };
{mode === 'edit' && (
<div style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
padding: '1rem',
border: '1px solid #e0e0e0',
borderRadius: '6px',
backgroundColor: '#f8f9fa'
}}>
<input
type="checkbox"
name="isActive"
id="isActive"
checked={formData.isActive}
onChange={onInputChange}
style={{ width: '18px', height: '18px' }}
/>
<div>
<label htmlFor="isActive" style={{ fontWeight: 'bold', color: '#2c3e50', display: 'block' }}>
Mitarbeiter ist aktiv
</label>
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant.
</div>
</div>
</div>
)}
</div>
);
// ===== HAUPTKOMPONENTE ===== // ===== HAUPTKOMPONENTE =====
const EmployeeForm: React.FC<EmployeeFormProps> = ({ const EmployeeForm: React.FC<EmployeeFormProps> = ({
@@ -985,6 +1122,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
onCancel onCancel
}) => { }) => {
const { hasRole } = useAuth(); const { hasRole } = useAuth();
const { showNotification } = useNotification();
const { const {
currentStep, currentStep,
formData, formData,
@@ -994,6 +1132,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
steps, steps,
emailPreview, emailPreview,
showPasswordSection, showPasswordSection,
validationErrors,
getFieldError,
hasErrors,
goToNextStep, goToNextStep,
goToPrevStep, goToPrevStep,
handleStepChange, handleStepChange,
@@ -1005,7 +1146,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
handleContractTypeChange, handleContractTypeChange,
handleSubmit, handleSubmit,
setShowPasswordSection, setShowPasswordSection,
isStepCompleted clearErrors
} = useEmployeeForm(mode, employee); } = useEmployeeForm(mode, employee);
// Inline Step Indicator Komponente (wie in Setup.tsx) // Inline Step Indicator Komponente (wie in Setup.tsx)
@@ -1108,7 +1249,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode, mode,
showPasswordSection, showPasswordSection,
onShowPasswordSection: setShowPasswordSection, onShowPasswordSection: setShowPasswordSection,
hasRole hasRole,
getFieldError,
hasErrors
}; };
switch (currentStep) { switch (currentStep) {
@@ -1128,9 +1271,17 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const handleFinalSubmit = async (): Promise<void> => { const handleFinalSubmit = async (): Promise<void> => {
try { try {
await handleSubmit(); await handleSubmit();
// Show success notification
showNotification({ // Changed from addNotification to showNotification
type: 'success',
title: 'Erfolg',
message: mode === 'create'
? 'Mitarbeiter wurde erfolgreich erstellt'
: 'Mitarbeiter wurde erfolgreich aktualisiert'
});
onSuccess(); onSuccess();
} catch (err) { } catch (err) {
// Error is already handled in handleSubmit // Errors are already handled by the hook and shown as notifications
} }
}; };
@@ -1189,20 +1340,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
)} )}
</div> </div>
{/* Fehleranzeige */}
{error && (
<div style={{
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '1rem',
borderRadius: '6px',
marginBottom: '1.5rem'
}}>
<strong>Fehler:</strong> {error}
</div>
)}
{/* Schritt-Inhalt */} {/* Schritt-Inhalt */}
<div style={{ minHeight: '300px' }}> <div style={{ minHeight: '300px' }}>
{renderStepContent()} {renderStepContent()}
@@ -1237,7 +1374,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
disabled={loading} disabled={loading}
style={{ style={{
padding: '0.75rem 2rem', padding: '0.75rem 2rem',
backgroundColor: loading ? '#6c757d' : (isLastStep ? '#27ae60' : '#51258f'), backgroundColor: loading ? '#6c757d' : (isLastStep ? '#51258f' : '#51258f'),
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '6px', borderRadius: '6px',

View File

@@ -1,5 +1,6 @@
// frontend/src/services/employeeService.ts // frontend/src/services/employeeService.ts
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest, EmployeeAvailability } from '../models/Employee';
import { ErrorService, ValidationError } from './errorService';
const API_BASE_URL = '/api'; const API_BASE_URL = '/api';
@@ -12,6 +13,23 @@ const getAuthHeaders = () => {
}; };
export class EmployeeService { export class EmployeeService {
private async handleApiResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) {
const error = new Error('Validation failed');
(error as any).validationErrors = validationErrors;
throw error;
}
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
return response.json();
}
async getEmployees(includeInactive: boolean = false): Promise<Employee[]> { async getEmployees(includeInactive: boolean = false): Promise<Employee[]> {
console.log('🔄 Fetching employees from API...'); console.log('🔄 Fetching employees from API...');
@@ -55,12 +73,7 @@ export class EmployeeService {
body: JSON.stringify(employee), body: JSON.stringify(employee),
}); });
if (!response.ok) { return this.handleApiResponse<Employee>(response);
const error = await response.json();
throw new Error(error.error || 'Failed to create employee');
}
return response.json();
} }
async updateEmployee(id: string, employee: UpdateEmployeeRequest): Promise<Employee> { async updateEmployee(id: string, employee: UpdateEmployeeRequest): Promise<Employee> {
@@ -70,12 +83,7 @@ export class EmployeeService {
body: JSON.stringify(employee), body: JSON.stringify(employee),
}); });
if (!response.ok) { return this.handleApiResponse<Employee>(response);
const error = await response.json();
throw new Error(error.error || 'Failed to update employee');
}
return response.json();
} }
async deleteEmployee(id: string): Promise<void> { async deleteEmployee(id: string): Promise<void> {