mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
updated validation handling together with employeeform
This commit is contained in:
89
frontend/src/hooks/useBackendValidation.ts
Normal file
89
frontend/src/hooks/useBackendValidation.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user