added Validation rules

This commit is contained in:
2025-10-30 18:10:44 +01:00
parent 5809bb8b09
commit 0623957993
8 changed files with 1380 additions and 680 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import StepSetup, { Step } from '../../components/StepSetup/StepSetup';
const API_BASE_URL = '/api'; const API_BASE_URL = '/api';
@@ -32,16 +31,16 @@ const useSetup = () => {
const { checkSetupStatus } = useAuth(); const { checkSetupStatus } = useAuth();
const steps: SetupStep[] = [ const steps: SetupStep[] = [
{
id: 'password-setup',
title: 'Passwort erstellen',
subtitle: 'Legen Sie ein sicheres Passwort fest'
},
{ {
id: 'profile-setup', id: 'profile-setup',
title: 'Profilinformationen', title: 'Profilinformationen',
subtitle: 'Geben Sie Ihre persönlichen Daten ein' subtitle: 'Geben Sie Ihre persönlichen Daten ein'
}, },
{
id: 'password-setup',
title: 'Passwort erstellen',
subtitle: 'Legen Sie ein sicheres Passwort fest'
},
{ {
id: 'confirmation', id: 'confirmation',
title: 'Bestätigung', title: 'Bestätigung',
@@ -51,18 +50,6 @@ const useSetup = () => {
// ===== VALIDIERUNGS-FUNKTIONEN ===== // ===== VALIDIERUNGS-FUNKTIONEN =====
const validateStep1 = (): boolean => { 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()) { if (!formData.firstname.trim()) {
setError('Bitte geben Sie einen Vornamen ein.'); setError('Bitte geben Sie einen Vornamen ein.');
return false; return false;
@@ -74,6 +61,18 @@ const useSetup = () => {
return true; 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 => { const validateCurrentStep = (stepIndex: number): boolean => {
switch (stepIndex) { switch (stepIndex) {
case 0: case 0:
@@ -225,69 +224,6 @@ interface StepContentProps {
} }
const Step1Content: React.FC<StepContentProps> = ({ const Step1Content: React.FC<StepContentProps> = ({
formData,
onInputChange
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Mindestens 6 Zeichen"
required
autoComplete="new-password"
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort bestätigen
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Passwort wiederholen"
required
autoComplete="new-password"
/>
</div>
</div>
);
const Step2Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange, onInputChange,
getEmailPreview getEmailPreview
@@ -378,6 +314,70 @@ const Step2Content: React.FC<StepContentProps> = ({
</div> </div>
); );
const Step2Content: React.FC<StepContentProps> = ({
formData,
onInputChange
}) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Mindestens 6 Zeichen"
required
autoComplete="new-password"
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Passwort bestätigen
</label>
<input
type="password"
name="confirmPassword"
value={formData.confirmPassword}
onChange={onInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem',
transition: 'border-color 0.3s ease'
}}
placeholder="Passwort wiederholen"
required
autoComplete="new-password"
/>
</div>
</div>
);
const Step3Content: React.FC<StepContentProps> = ({ const Step3Content: React.FC<StepContentProps> = ({
formData, formData,
getEmailPreview getEmailPreview
@@ -477,6 +477,83 @@ const Setup: React.FC = () => {
} }
}; };
// Inline Step Indicator Komponente
const StepIndicator: React.FC = () => (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '2.5rem',
position: 'relative',
width: '100%'
}}>
{/* Verbindungslinien */}
<div style={{
position: 'absolute',
top: '12px',
left: '0',
right: '0',
height: '2px',
backgroundColor: '#e9ecef',
zIndex: 1
}} />
{steps.map((step, index) => {
const isCompleted = index < currentStep;
const isCurrent = index === currentStep;
const isClickable = index <= currentStep + 1;
return (
<div
key={step.id}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
zIndex: 2,
position: 'relative',
flex: 1
}}
>
<button
onClick={() => isClickable && handleStepChange(index)}
disabled={!isClickable}
style={{
width: '28px',
height: '28px',
borderRadius: '50%',
border: '2px solid',
borderColor: isCompleted || isCurrent ? '#51258f' : '#e9ecef',
backgroundColor: isCompleted ? '#51258f' : 'white',
color: isCompleted ? 'white' : (isCurrent ? '#51258f' : '#6c757d'),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
fontWeight: 'bold',
cursor: isClickable ? 'pointer' : 'not-allowed',
transition: 'all 0.3s ease',
marginBottom: '8px'
}}
>
{index + 1}
</button>
<div style={{ textAlign: 'center' }}>
<div style={{
fontSize: '14px',
fontWeight: isCurrent ? '600' : '400',
color: isCurrent ? '#51258f' : '#6c757d'
}}>
{step.title}
</div>
</div>
</div>
);
})}
</div>
);
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100vh',
@@ -533,18 +610,8 @@ const Setup: React.FC = () => {
)} )}
</div> </div>
{/* StepSetup Komponente - nur die Steps ohne Beschreibung */} {/* Inline Step Indicator */}
<div style={{ marginBottom: '2.5rem' }}> <StepIndicator />
<StepSetup
steps={steps.map(step => ({ ...step, subtitle: undefined }))}
current={currentStep}
onChange={handleStepChange}
orientation="horizontal"
clickable={true}
size="md"
animated={true}
/>
</div>
{/* Fehleranzeige */} {/* Fehleranzeige */}
{error && ( {error && (