diff --git a/backend/src/middleware/Validation/Access.md b/backend/src/middleware/Validation/Access.md new file mode 100644 index 0000000..278a646 --- /dev/null +++ b/backend/src/middleware/Validation/Access.md @@ -0,0 +1,25 @@ +## User Settings + +### \[UPDATE\] Personal availability +* Only the employee themselves can manage their availability +* Must select a valid shift plan with defined shifts +* All changes require explicit save action + +### \[VIEW\] ShiftPlan assignments +* Published plans show actual assignments +* Draft plans show preview assignments (if calculated) +* Regular users can only view, not modify assignments + +## System-wide + +### \[ACCESS\] Role-based restrictions +* `admin`: Full access to all features +* `maintenance`: Access to shift plans and employee management (except admin users) +* `user`: Read-only access to shift plans, can manage own availability and profile + +### \[DATA\] Validation rules +* Email addresses are automatically generated from firstname/lastname +* Employee status (`isActive`) controls login and planning eligibility +* Trainee status affects independence (`canWorkAlone`) automatically +* Date ranges must be valid (start before end) +* All required fields must be filled before form submission \ No newline at end of file diff --git a/backend/src/middleware/Validation/Assignment.md b/backend/src/middleware/Validation/Assignment.md new file mode 100644 index 0000000..26265fc --- /dev/null +++ b/backend/src/middleware/Validation/Assignment.md @@ -0,0 +1,20 @@ +## Shift Assignment + +### \[ACTION: update scheduled shift\] +* Requires valid scheduled shift ID +* Only updates assignedEmployees array +* Requires authentication with valid token +* Handles both JSON and non-JSON responses + +### \[ACTION: assign shifts automatically\] +* Requires shift plan, employees, and availabilities +* Availability preferenceLevel must be 1, 2, or 3 +* Constraints must be an array (converts non-array to empty array) +* All employees must have valid availability data + +### \[ACTION: get scheduled shifts\] +* Requires valid plan ID +* Automatically fixes data structure inconsistencies: + - timeSlotId mapping (handles both naming conventions) + - requiredEmployees fallback to 2 if missing + - assignedEmployees fallback to empty array if missing \ No newline at end of file diff --git a/backend/src/middleware/Validation/Authentication.md b/backend/src/middleware/Validation/Authentication.md new file mode 100644 index 0000000..8b13e9e --- /dev/null +++ b/backend/src/middleware/Validation/Authentication.md @@ -0,0 +1,16 @@ +## Authentication + +### \[ACTION: login\] +* Requires valid email and password format +* Server validates credentials before issuing token +* Token and employee data stored in localStorage upon success + +### \[ACTION: register\] +* Requires email, password, and name +* Role is optional during registration +* Automatically logs in user after successful registration + +### \[ACTION: access protected resources\] +* Requires valid JWT token in Authorization header +* Token is automatically retrieved from localStorage +* Unauthorized requests (401) trigger automatic logout \ No newline at end of file diff --git a/backend/src/middleware/Validation/DataIntegrity b/backend/src/middleware/Validation/DataIntegrity new file mode 100644 index 0000000..54d138a --- /dev/null +++ b/backend/src/middleware/Validation/DataIntegrity @@ -0,0 +1,23 @@ +## Data Integrity + +### \[GENERAL\] API communication +* All fetch requests include error handling +* Failed responses throw descriptive errors +* Token validation before protected operations +* Automatic localStorage cleanup on logout + +### \[GENERAL\] data persistence +* Employee data cached in localStorage after login +* Token automatically retrieved from localStorage +* Data structure normalization for scheduled shifts + +### \[GENERAL\] error handling +* Network errors are caught and logged +* HTTP errors include status codes and messages +* Failed authentication triggers cleanup and logout + +## Role & Permission Notes +* The frontend services don't explicitly restrict actions by role +* Role-based restrictions are likely handled by the backend +* Frontend assumes user has permissions for requested operations +* 401 responses indicate insufficient permissions at backend level \ No newline at end of file diff --git a/backend/src/middleware/Validation/Employee.md b/backend/src/middleware/Validation/Employee.md new file mode 100644 index 0000000..eb3a8ea --- /dev/null +++ b/backend/src/middleware/Validation/Employee.md @@ -0,0 +1,67 @@ +## Employee Management + +### \[CREATE/UPDATE\] employee +* All employee operations require authentication +* Password changes require current password + new password +* Only authenticated users can create/update employees + +### \[ACTION: delete employee\] +* Requires authentication +* Server validates permissions before deletion + +### \[ACTION: update availability\] +* Requires employee ID and plan ID +* Availability updates must include valid preference levels +* Only authenticated users can update availabilities + +### \[ACTION: update last login\] +* Requires employee ID +* Fails silently if update fails (logs error but doesn't block user) + +## Employee + +### \[CREATE\] Employee +* `firstname` must not be empty +* `lastname` must not be empty +* `password` must be at least 6 characters (in create mode) +* `employeeType` must be selected +* Contract type validation: + * `manager`, `apprentice` => `contractType` = flexible + * `guest` => `contractType` = undefined/NONE + * `personell` => `contractType` = small || large + +### \[UPDATE\] Employee profile +* `firstname` must not be empty +* `lastname` must not be empty +* Only the employee themselves or admins can update + +### \[UPDATE\] Employee password +* `newPassword` must be at least 6 characters +* `newPassword` must match `confirmPassword` +* For admin password reset: no `currentPassword` required +* For self-password change: `currentPassword` required + +### \[UPDATE\] Employee roles +* Only users with role 'admin' can modify roles +* At least one employee must maintain 'admin' role +* Users cannot remove their own admin role + +### \[UPDATE\] Employee availability +* Only active employees can set availability +* Contract type requirements: + * `small` contract: minimum 2 available shifts (preference level 1 or 2) + * `large` contract: minimum 3 available shifts (preference level 1 or 2) + * `flexible` contract: no minimum requirement +* Availability can only be set for valid shift patterns in selected plan +* `shiftId` must be valid and exist in the current plan + +### \[ACTION: delete\] Employee +* Only users with role 'admin' can delete employees +* Cannot delete yourself +* Cannot delete the last admin user +* User confirmation required before deletion + +### \[ACTION: edit\] Employee +* Admins can edit all employees +* Maintenance users can edit non-admin employees or themselves +* Regular users can only edit themselves diff --git a/backend/src/middleware/Validation/Shiftplan.md b/backend/src/middleware/Validation/Shiftplan.md new file mode 100644 index 0000000..a72628c --- /dev/null +++ b/backend/src/middleware/Validation/Shiftplan.md @@ -0,0 +1,66 @@ +## Shift Plan Management + +### \[CREATE\] shift plan +* All operations require authentication +* 401 responses trigger automatic logout +* Scheduled shifts array is guaranteed to exist (empty array if none) + +### \[CREATE\] shift plan from preset +* presetName must match existing TEMPLATE_PRESETS +* Requires name, startDate, and endDate +* isTemplate is optional (defaults to false) + +### \[UPDATE\] shift plan +* Requires valid shift plan ID +* Partial updates allowed +* Authentication required + +### \[ACTION: delete shift plan\] +* Requires authentication +* 401 responses trigger automatic logout + +### \[ACTION: regenerate scheduled shifts\] +* Requires valid plan ID +* Authentication required +* Fails silently if regeneration fails (logs error but continues) + +### \[ACTION: clear assignments\] +* Requires valid plan ID +* Authentication required +* Clears all employee assignments from scheduled shifts + +## ShiftPlan + +### \[CREATE\] ShiftPlan from template +* `planName` must not be empty +* `startDate` must be set +* `endDate` must be set +* `endDate` must be after `startDate` +* `selectedPreset` must be chosen (template must be selected) +* Only available template presets can be used + +### \[ACTION: publish\] ShiftPlan +* Plan must be in 'draft' status +* All active employees must have set their availabilities for the plan +* Only users with roles \['admin', 'maintenance'\] can publish +* Assignment algorithm must not have critical violations (ERROR or ❌ KRITISCH) +* employee && employee.contract_type === small => mind. 1 mal availability === 1 || availability === 2 +* employee && employee.contract_type === large => mind. 3 mal availability === 1 || availability === 2 + +### \[ACTION: recreate assignments\] +* Plan must be in 'published' status +* Only users with roles \['admin', 'maintenance'\] can recreate +* User confirmation required before clearing all assignments + +### \[ACTION: delete\] ShiftPlan +* Only users with roles \['admin', 'maintenance'\] can delete +* User confirmation required before deletion + +### \[ACTION: edit\] ShiftPlan +* Only users with roles \['admin', 'maintenance'\] can edit +* Can only edit plans in 'draft' status + +### \[UPDATE\] ShiftPlan shifts +* `timeSlotId` must be selected from available time slots +* `requiredEmployees` must be at least 1 +* `dayOfWeek` must be between 1-7 \ 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 b7e8204..9f420c7 100644 --- a/frontend/src/pages/Employees/components/EmployeeForm.tsx +++ b/frontend/src/pages/Employees/components/EmployeeForm.tsx @@ -14,32 +14,82 @@ interface EmployeeFormProps { type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest'; type ContractType = 'small' | 'large' | 'flexible'; -const EmployeeForm: React.FC = ({ - mode, - employee, - onSuccess, - onCancel -}) => { - const [formData, setFormData] = useState({ +// ===== TYP-DEFINITIONEN ===== +interface EmployeeFormData { + // Step 1: Grundinformationen + firstname: string; + lastname: string; + email: string; + password: string; + + // Step 2: Mitarbeiterkategorie + employeeType: EmployeeType; + contractType: ContractType | undefined; + isTrainee: boolean; + + // Step 3: Berechtigungen & Status + roles: string[]; + canWorkAlone: boolean; + isActive: boolean; +} + +interface PasswordFormData { + newPassword: string; + confirmPassword: string; +} + +// ===== HOOK FÜR FORMULAR-LOGIK ===== +const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => { + const [currentStep, setCurrentStep] = useState(0); + const [formData, setFormData] = useState({ firstname: '', lastname: '', email: '', password: '', - roles: ['user'] as string[], - employeeType: 'personell' as EmployeeType, - contractType: 'small' as ContractType | undefined, + employeeType: 'personell', + contractType: 'small', + isTrainee: false, + roles: ['user'], canWorkAlone: false, - isActive: true, - isTrainee: false + isActive: true }); - const [passwordForm, setPasswordForm] = useState({ + + const [passwordForm, setPasswordForm] = useState({ newPassword: '', confirmPassword: '' }); + const [showPasswordSection, setShowPasswordSection] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const { hasRole } = useAuth(); + + // Steps definition + const steps = [ + { + id: 'basic-info', + title: 'Grundinformationen', + subtitle: 'Name und Kontaktdaten' + }, + { + id: 'employee-category', + title: 'Mitarbeiterkategorie', + subtitle: 'Typ und Vertrag' + }, + { + id: 'permissions', + title: 'Berechtigungen', + subtitle: 'Rollen und Eigenständigkeit' + } + ]; + + // Add password step for edit mode + if (mode === 'edit') { + steps.push({ + id: 'security', + title: 'Sicherheit', + subtitle: 'Passwort und Status' + }); + } // Generate email preview const generateEmailPreview = (firstname: string, lastname: string): string => { @@ -60,6 +110,7 @@ const EmployeeForm: React.FC = ({ const emailPreview = generateEmailPreview(formData.firstname, formData.lastname); + // Initialize form data when employee is provided useEffect(() => { if (mode === 'edit' && employee) { setFormData({ @@ -67,17 +118,87 @@ const EmployeeForm: React.FC = ({ lastname: employee.lastname, email: employee.email, password: '', - roles: employee.roles || ['user'], employeeType: employee.employeeType, contractType: employee.contractType, + isTrainee: employee.isTrainee || false, + roles: employee.roles || ['user'], canWorkAlone: employee.canWorkAlone, - isActive: employee.isActive, - isTrainee: employee.isTrainee || false + isActive: employee.isActive }); } }, [mode, employee]); - const handleChange = (e: React.ChangeEvent) => { + // ===== VALIDIERUNGS-FUNKTIONEN ===== + const validateStep1 = (): boolean => { + if (!formData.firstname.trim()) { + setError('Bitte geben Sie einen Vornamen ein.'); + return false; + } + if (!formData.lastname.trim()) { + 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; + }; + + const validateStep2 = (): boolean => { + if (!formData.employeeType) { + setError('Bitte wählen Sie eine Mitarbeiterkategorie aus.'); + return false; + } + return true; + }; + + const validateCurrentStep = (stepIndex: number): boolean => { + switch (stepIndex) { + case 0: + return validateStep1(); + case 1: + return validateStep2(); + default: + return true; + } + }; + + // ===== NAVIGATIONS-FUNKTIONEN ===== + const goToNextStep = (): void => { + setError(''); + + if (!validateCurrentStep(currentStep)) { + return; + } + + if (currentStep < steps.length - 1) { + setCurrentStep(prev => prev + 1); + } + }; + + const goToPrevStep = (): void => { + setError(''); + if (currentStep > 0) { + setCurrentStep(prev => prev - 1); + } + }; + + const handleStepChange = (stepIndex: number): void => { + setError(''); + + // Nur erlauben, zu bereits validierten Schritten zu springen + if (stepIndex <= currentStep + 1) { + // Vor dem Wechsel validieren + if (stepIndex > currentStep && !validateCurrentStep(currentStep)) { + return; + } + setCurrentStep(stepIndex); + } + }; + + // ===== FORM HANDLER ===== + const handleInputChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; setFormData(prev => ({ @@ -103,7 +224,7 @@ const EmployeeForm: React.FC = ({ }; } else { const newRoles = prev.roles.filter(r => r !== role); - return{ + return { ...prev, roles: newRoles.length > 0 ? newRoles : ['user'] }; @@ -151,8 +272,7 @@ const EmployeeForm: React.FC = ({ })); }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (): Promise => { setLoading(true); setError(''); @@ -182,10 +302,10 @@ const EmployeeForm: React.FC = ({ }; await employeeService.updateEmployee(employee.id, updateData); - // Password change logic remains the same - if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) { + // Password change logic + if (showPasswordSection && passwordForm.newPassword) { if (passwordForm.newPassword.length < 6) { - throw new Error('Das Passwort muss mindestens 6 Zeichen lang sein, Zahlen und Groß- / Kleinbuchstaben enthalten'); + 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'); @@ -198,22 +318,214 @@ const EmployeeForm: React.FC = ({ } } - onSuccess(); + return Promise.resolve(); } catch (err: any) { setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`); + return Promise.reject(err); } finally { setLoading(false); } }; - const isFormValid = mode === 'create' - ? formData.firstname.trim() && formData.lastname.trim() && formData.password.length >= 6 - : formData.firstname.trim() && formData.lastname.trim(); + const isStepCompleted = (stepIndex: number): boolean => { + switch (stepIndex) { + case 0: + return !!formData.firstname.trim() && + !!formData.lastname.trim() && + (mode === 'edit' || formData.password.length >= 6); + case 1: + return !!formData.employeeType; + case 2: + return true; // Permissions step is always valid + case 3: + return true; // Security step is always valid + default: + return false; + } + }; - const availableRoles = hasRole(['admin']) - ? ROLE_CONFIG - : ROLE_CONFIG.filter(role => role.value !== 'admin'); + return { + // State + currentStep, + formData, + passwordForm, + loading, + error, + steps, + emailPreview, + showPasswordSection, + + // Actions + goToNextStep, + goToPrevStep, + handleStepChange, + handleInputChange, + handlePasswordChange, + handleRoleChange, + handleEmployeeTypeChange, + handleTraineeChange, + handleContractTypeChange, + handleSubmit, + setShowPasswordSection, + + // Helpers + isStepCompleted + }; +}; +// ===== STEP-INHALTS-KOMPONENTEN ===== +interface StepContentProps { + formData: EmployeeFormData; + passwordForm: PasswordFormData; + onInputChange: (e: React.ChangeEvent) => void; + onPasswordChange: (e: React.ChangeEvent) => void; + onRoleChange: (role: string, checked: boolean) => void; + onEmployeeTypeChange: (employeeType: EmployeeType) => void; + onTraineeChange: (isTrainee: boolean) => void; + onContractTypeChange: (contractType: ContractType) => void; + emailPreview: string; + mode: 'create' | 'edit'; + showPasswordSection: boolean; + onShowPasswordSection: (show: boolean) => void; + hasRole: (roles: string[]) => boolean; +} + +const Step1Content: React.FC = ({ + formData, + onInputChange, + emailPreview, + mode +}) => ( +
+
+
+ + +
+ +
+ + +
+
+ + {/* Email Preview */} +
+ +
+ {emailPreview || 'max.mustermann@sp.de'} +
+
+ Die E-Mail Adresse wird automatisch aus Vorname und Nachname generiert. +
+
+ + {mode === 'create' && ( +
+ + +
+ Das Passwort muss mindestens 6 Zeichen lang sein. +
+
+ )} +
+); + +const Step2Content: React.FC = ({ + formData, + onEmployeeTypeChange, + onTraineeChange, + onContractTypeChange, + hasRole +}) => { const contractTypeOptions = [ { value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' }, { value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' }, @@ -223,174 +535,138 @@ const EmployeeForm: React.FC = ({ const showContractType = formData.employeeType !== 'guest'; return ( -
-

- {mode === 'create' ? '👤 Neuen Mitarbeiter erstellen' : '✏️ Mitarbeiter bearbeiten'} -

- - {error && ( -
- Fehler: {error} -
- )} - -
-
- - {/* Grundinformationen */} -
-

📋 Grundinformationen

- -
-
- - -
- -
- - -
-
- - {/* Email Preview */} -
- -
- {emailPreview || 'max.mustermann@sp.de'} -
-
- Die E-Mail Adresse wird automatisch aus Vorname und Nachname generiert. - {formData.firstname && formData.lastname && ` Beispiel: ${emailPreview}`} -
-
- - {mode === 'create' && ( -
- - -
- Das Passwort muss mindestens 6 Zeichen lang sein, Zahlen und Groß- / Kleinbuchstaben enthalten. +
+ {/* Mitarbeiter Kategorie */} +
+

👥 Mitarbeiter Kategorie

+ +
+ {Object.values(EMPLOYEE_TYPE_CONFIG).map(type => ( +
onEmployeeTypeChange(type.value)} + > + onEmployeeTypeChange(type.value)} + style={{ + marginRight: '12px', + marginTop: '2px', + width: '18px', + height: '18px' + }} + /> +
+
+ {type.label} +
+
+ {type.description}
- )} -
+
+ {type.value.toUpperCase()} +
+
+ ))} +
- {/* Mitarbeiter Kategorie */} -
-

👥 Mitarbeiter Kategorie

- -
- {Object.values(EMPLOYEE_TYPE_CONFIG).map(type => ( + onTraineeChange(e.target.checked)} + style={{ width: '18px', height: '18px' }} + /> +
+ +
+ Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten. +
+
+
+ )} +
+ + {/* Vertragstyp (nur für Admins und interne Mitarbeiter) */} + {hasRole(['admin']) && showContractType && ( +
+

📝 Vertragstyp

+ +
+ {contractTypeOptions.map(contract => { + const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell'; + const isSmallLargeDisabled = (contract.value === 'small' || contract.value === 'large') && + (formData.employeeType === 'manager' || formData.employeeType === 'apprentice'); + const isDisabled = isFlexibleDisabled || isSmallLargeDisabled; + + return (
handleEmployeeTypeChange(type.value)} + onClick={isDisabled ? undefined : () => onContractTypeChange(contract.value)} > handleEmployeeTypeChange(type.value)} + name="contractType" + value={contract.value} + checked={formData.contractType === contract.value} + onChange={isDisabled ? undefined : () => onContractTypeChange(contract.value)} + disabled={isDisabled} style={{ marginRight: '12px', marginTop: '2px', @@ -405,453 +681,593 @@ const EmployeeForm: React.FC = ({ marginBottom: '4px', fontSize: '16px' }}> - {type.label} + {contract.label} + {isFlexibleDisabled && ( + + (Nicht verfügbar für Personell) + + )} + {isSmallLargeDisabled && ( + + (Nicht verfügbar für {formData.employeeType === 'manager' ? 'Manager' : 'Auszubildende'}) + + )}
- {type.description} + {contract.description}
- {type.value.toUpperCase()} + {contract.value.toUpperCase()}
- ))} -
- - {/* FIXED: Trainee checkbox for personell type */} - {formData.employeeType === 'personell' && ( -
- handleTraineeChange(e.target.checked)} - style={{ width: '18px', height: '18px' }} - /> -
- -
- Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten. -
-
-
- )} + ); + })}
+
+ )} +
+ ); +}; - {/* Vertragstyp (nur für Admins und interne Mitarbeiter) */} - {hasRole(['admin']) && showContractType && ( -
-

📝 Vertragstyp

- -
- {contractTypeOptions.map(contract => { - const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell'; - const isSmallLargeDisabled = (contract.value === 'small' || contract.value === 'large') && - (formData.employeeType === 'manager' || formData.employeeType === 'apprentice'); - const isDisabled = isFlexibleDisabled || isSmallLargeDisabled; - - return ( -
handleContractTypeChange(contract.value)} - > - handleContractTypeChange(contract.value)} - disabled={isDisabled} - style={{ - marginRight: '12px', - marginTop: '2px', - width: '18px', - height: '18px' - }} - /> -
-
- {contract.label} - {isFlexibleDisabled && ( - - (Nicht verfügbar für Personell) - - )} - {isSmallLargeDisabled && ( - - (Nicht verfügbar für {formData.employeeType === 'manager' ? 'Manager' : 'Auszubildende'}) - - )} -
-
- {contract.description} -
-
-
- {contract.value.toUpperCase()} -
-
- ); - })} -
-
- )} - - {/* Eigenständigkeit */} -
= ({ + formData, + onInputChange, + onRoleChange, + hasRole +}) => ( +
+ {/* Eigenständigkeit */} +
+

🎯 Eigenständigkeit

+ +
+ +
+
+
+ + {/* Systemrollen (nur für Admins) */} + {hasRole(['admin']) && ( +
+

⚙️ Systemrollen

+ +
+ {ROLE_CONFIG.map(role => ( +
onRoleChange(role.value, !formData.roles.includes(role.value))} + > onRoleChange(role.value, e.target.checked)} + style={{ + marginRight: '10px', + marginTop: '2px' }} />
- +
+ {role.label} +
- {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.' - } + {role.description}
-
- {formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'} -
+ ))} +
+
+ Hinweis: Ein Mitarbeiter kann mehrere Rollen haben. +
+
+ )} +
+); + +const Step4Content: React.FC = ({ + formData, + passwordForm, + onInputChange, + onPasswordChange, + showPasswordSection, + onShowPasswordSection, + mode +}) => ( +
+ {/* Passwort ändern */} +
+

🔒 Passwort zurücksetzen

+ + {!showPasswordSection ? ( + + ) : ( +
+
+ +
- {/* Passwort ändern (nur für Admins im Edit-Modus) */} - {mode === 'edit' && hasRole(['admin']) && ( -
-

🔒 Passwort zurücksetzen

- - {!showPasswordSection ? ( - - ) : ( -
-
- - -
+
+ + +
-
- - -
+
+ Hinweis: Als Administrator können Sie das Passwort des Benutzers ohne Kenntnis des aktuellen Passworts zurücksetzen. +
-
- Hinweis: Als Administrator können Sie das Passwort des Benutzers ohne Kenntnis des aktuellen Passworts zurücksetzen. -
- - -
- )} -
- )} - - {/* Systemrollen (nur für Admins) - AKTUALISIERT FÜR MEHRFACHE ROLLEN */} - {hasRole(['admin']) && ( -
-

⚙️ Systemrollen

- -
- {availableRoles.map(role => ( -
handleRoleChange(role.value, !formData.roles.includes(role.value))} - > - handleRoleChange(role.value, e.target.checked)} - style={{ - marginRight: '10px', - marginTop: '2px' - }} - /> -
-
- {role.label} -
-
- {role.description} -
-
-
- ))} -
-
- Hinweis: Ein Mitarbeiter kann mehrere Rollen haben. -
-
- )} - - {/* Aktiv Status (nur beim Bearbeiten) */} - {mode === 'edit' && ( -
- -
- -
- Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant. -
-
-
- )} - -
- - {/* Buttons */} -
- -
+ + {/* Aktiv Status */} + {mode === 'edit' && ( +
+ +
+ +
+ Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant. +
+
+
+ )} +
+); + +// ===== HAUPTKOMPONENTE ===== +const EmployeeForm: React.FC = ({ + mode, + employee, + onSuccess, + onCancel +}) => { + const { hasRole } = useAuth(); + const { + currentStep, + formData, + passwordForm, + loading, + error, + steps, + emailPreview, + showPasswordSection, + goToNextStep, + goToPrevStep, + handleStepChange, + handleInputChange, + handlePasswordChange, + handleRoleChange, + handleEmployeeTypeChange, + handleTraineeChange, + handleContractTypeChange, + handleSubmit, + setShowPasswordSection, + isStepCompleted + } = useEmployeeForm(mode, employee); + + // Inline Step Indicator Komponente (wie in Setup.tsx) + const StepIndicator: React.FC = () => ( +
+ {/* Verbindungslinien */} +
+ + {steps.map((step, index) => { + const isCompleted = index < currentStep; + const isCurrent = index === currentStep; + const isClickable = index <= currentStep + 1; + + return ( +
- {loading ? '⏳ Wird gespeichert...' : (mode === 'create' ? 'Mitarbeiter erstellen' : 'Änderungen speichern')} - + + +
+
+ {step.title} +
+ {step.subtitle && ( +
+ {step.subtitle} +
+ )} +
+
+ ); + })} +
+ ); + + const renderStepContent = (): React.ReactNode => { + const stepProps = { + formData, + passwordForm, + onInputChange: handleInputChange, + onPasswordChange: handlePasswordChange, + onRoleChange: handleRoleChange, + onEmployeeTypeChange: handleEmployeeTypeChange, + onTraineeChange: handleTraineeChange, + onContractTypeChange: handleContractTypeChange, + emailPreview, + mode, + showPasswordSection, + onShowPasswordSection: setShowPasswordSection, + hasRole + }; + + switch (currentStep) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + default: + return null; + } + }; + + const handleFinalSubmit = async (): Promise => { + try { + await handleSubmit(); + onSuccess(); + } catch (err) { + // Error is already handled in handleSubmit + } + }; + + const getNextButtonText = (): string => { + if (loading) return '⏳ Wird gespeichert...'; + + if (currentStep === steps.length - 1) { + return mode === 'create' ? 'Mitarbeiter erstellen' : 'Änderungen speichern'; + } + + return 'Weiter →'; + }; + + const isLastStep = currentStep === steps.length - 1; + + return ( +
+

+ {mode === 'create' ? '👤 Neuen Mitarbeiter erstellen' : '✏️ Mitarbeiter bearbeiten'} +

+ + {/* Inline Step Indicator */} + + + {/* Aktueller Schritt Titel und Beschreibung */} +
+

+ {steps[currentStep].title} +

+ {steps[currentStep].subtitle && ( +

+ {steps[currentStep].subtitle} +

+ )} +
+ + {/* Fehleranzeige */} + {error && ( +
+ Fehler: {error}
- + )} + + {/* Schritt-Inhalt */} +
+ {renderStepContent()} +
+ + {/* Navigations-Buttons */} +
+ + + +
+ + {/* Zusätzliche Informationen */} + {isLastStep && !loading && ( +
+ {mode === 'create' + ? 'Überprüfen Sie alle Daten, bevor Sie den Mitarbeiter erstellen' + : 'Überprüfen Sie alle Änderungen, bevor Sie sie speichern' + } +
+ )}
); }; diff --git a/frontend/src/pages/Setup/Setup.tsx b/frontend/src/pages/Setup/Setup.tsx index 3c5a2d7..1834636 100644 --- a/frontend/src/pages/Setup/Setup.tsx +++ b/frontend/src/pages/Setup/Setup.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { useAuth } from '../../contexts/AuthContext'; -import StepSetup, { Step } from '../../components/StepSetup/StepSetup'; const API_BASE_URL = '/api'; @@ -32,16 +31,16 @@ const useSetup = () => { const { checkSetupStatus } = useAuth(); const steps: SetupStep[] = [ - { - id: 'password-setup', - title: 'Passwort erstellen', - subtitle: 'Legen Sie ein sicheres Passwort fest' - }, { id: 'profile-setup', title: 'Profilinformationen', subtitle: 'Geben Sie Ihre persönlichen Daten ein' }, + { + id: 'password-setup', + title: 'Passwort erstellen', + subtitle: 'Legen Sie ein sicheres Passwort fest' + }, { id: 'confirmation', title: 'Bestätigung', @@ -51,18 +50,6 @@ const useSetup = () => { // ===== VALIDIERUNGS-FUNKTIONEN ===== const validateStep1 = (): boolean => { - if (formData.password.length < 6) { - setError('Das Passwort muss mindestens 6 Zeichen lang sein.'); - return false; - } - if (formData.password !== formData.confirmPassword) { - setError('Die Passwörter stimmen nicht überein.'); - return false; - } - return true; - }; - - const validateStep2 = (): boolean => { if (!formData.firstname.trim()) { setError('Bitte geben Sie einen Vornamen ein.'); return false; @@ -74,6 +61,18 @@ const useSetup = () => { return true; }; + const validateStep2 = (): boolean => { + if (formData.password.length < 6) { + setError('Das Passwort muss mindestens 6 Zeichen lang sein.'); + return false; + } + if (formData.password !== formData.confirmPassword) { + setError('Die Passwörter stimmen nicht überein.'); + return false; + } + return true; + }; + const validateCurrentStep = (stepIndex: number): boolean => { switch (stepIndex) { case 0: @@ -225,69 +224,6 @@ interface StepContentProps { } const Step1Content: React.FC = ({ - formData, - onInputChange -}) => ( -
-
- - -
- -
- - -
-
-); - -const Step2Content: React.FC = ({ formData, onInputChange, getEmailPreview @@ -378,6 +314,70 @@ const Step2Content: React.FC = ({
); + +const Step2Content: React.FC = ({ + formData, + onInputChange +}) => ( +
+
+ + +
+ +
+ + +
+
+); + const Step3Content: React.FC = ({ formData, getEmailPreview @@ -477,6 +477,83 @@ const Setup: React.FC = () => { } }; + // Inline Step Indicator Komponente + const StepIndicator: React.FC = () => ( +
+ {/* Verbindungslinien */} +
+ + {steps.map((step, index) => { + const isCompleted = index < currentStep; + const isCurrent = index === currentStep; + const isClickable = index <= currentStep + 1; + + return ( +
+ + +
+
+ {step.title} +
+
+
+ ); + })} +
+ ); + return (
{ )}
- {/* StepSetup Komponente - nur die Steps ohne Beschreibung */} -
- ({ ...step, subtitle: undefined }))} - current={currentStep} - onChange={handleStepChange} - orientation="horizontal" - clickable={true} - size="md" - animated={true} - /> -
+ {/* Inline Step Indicator */} + {/* Fehleranzeige */} {error && (