mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
fixed role naming instandhalter -> maintenance
This commit is contained in:
@@ -20,9 +20,9 @@ router.use(authMiddleware);
|
||||
|
||||
// Employee CRUD Routes
|
||||
router.get('/', authMiddleware, getEmployees);
|
||||
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
|
||||
router.get('/:id', requireRole(['admin', 'maintenance']), getEmployee);
|
||||
router.post('/', requireRole(['admin']), createEmployee);
|
||||
router.put('/:id', requireRole(['admin']), updateEmployee);
|
||||
router.put('/:id', requireRole(['admin', 'maintenance']), updateEmployee);
|
||||
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
||||
router.put('/:id/password', authMiddleware, changePassword);
|
||||
router.put('/:id/last-login', authMiddleware, updateLastLogin);
|
||||
|
||||
@@ -14,9 +14,9 @@ const router = express.Router();
|
||||
router.use(authMiddleware);
|
||||
|
||||
|
||||
router.post('/:id/generate-shifts', requireRole(['admin', 'instandhalter']), generateScheduledShiftsForPlan);
|
||||
router.post('/:id/generate-shifts', requireRole(['admin', 'maintenance']), generateScheduledShiftsForPlan);
|
||||
|
||||
router.post('/:id/regenerate-shifts', requireRole(['admin', 'instandhalter']), regenerateScheduledShifts);
|
||||
router.post('/:id/regenerate-shifts', requireRole(['admin', 'maintenance']), regenerateScheduledShifts);
|
||||
|
||||
// GET all scheduled shifts for a plan
|
||||
router.get('/plan/:planId', authMiddleware, getScheduledShiftsFromPlan);
|
||||
|
||||
@@ -24,18 +24,18 @@ router.get('/' , authMiddleware, getShiftPlans);
|
||||
router.get('/:id', authMiddleware, getShiftPlan);
|
||||
|
||||
// POST create new shift plan
|
||||
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
|
||||
router.post('/', requireRole(['admin', 'maintenance']), createShiftPlan);
|
||||
|
||||
// POST create new plan from preset
|
||||
router.post('/from-preset', requireRole(['admin', 'instandhalter']), createFromPreset);
|
||||
router.post('/from-preset', requireRole(['admin', 'maintenance']), createFromPreset);
|
||||
|
||||
// PUT update shift plan or template
|
||||
router.put('/:id', requireRole(['admin', 'instandhalter']), updateShiftPlan);
|
||||
router.put('/:id', requireRole(['admin', 'maintenance']), updateShiftPlan);
|
||||
|
||||
// DELETE shift plan or template
|
||||
router.delete('/:id', requireRole(['admin', 'instandhalter']), deleteShiftPlan);
|
||||
router.delete('/:id', requireRole(['admin', 'maintenance']), deleteShiftPlan);
|
||||
|
||||
// POST clear assignments and reset to draft
|
||||
router.post('/:id/clear-assignments', requireRole(['admin', 'instandhalter']), clearAssignments);
|
||||
router.post('/:id/clear-assignments', requireRole(['admin', 'maintenance']), clearAssignments);
|
||||
|
||||
export default router;
|
||||
@@ -19,7 +19,7 @@ import Setup from './pages/Setup/Setup';
|
||||
// Protected Route Component
|
||||
const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
|
||||
children,
|
||||
roles = ['admin', 'instandhalter', 'user']
|
||||
roles = ['admin', 'maintenance', 'user']
|
||||
}) => {
|
||||
const { user, loading, hasRole } = useAuth();
|
||||
|
||||
@@ -91,12 +91,12 @@ const AppContent: React.FC = () => {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/shift-plans/new" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<ProtectedRoute roles={['admin', 'maintenance']}>
|
||||
<ShiftPlanCreate />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/shift-plans/:id/edit" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<ProtectedRoute roles={['admin', 'maintenance']}>
|
||||
<ShiftPlanEdit />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
@@ -106,7 +106,7 @@ const AppContent: React.FC = () => {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/employees" element={
|
||||
<ProtectedRoute roles={['admin', 'instandhalter']}>
|
||||
<ProtectedRoute roles={['admin', 'maintenance']}>
|
||||
<EmployeeManagement />
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
@@ -30,11 +30,11 @@ const Navigation: React.FC = () => {
|
||||
};
|
||||
|
||||
const navigationItems = [
|
||||
{ path: '/', label: 'Dashboard', roles: ['admin', 'instandhalter', 'user'] },
|
||||
{ path: '/shift-plans', label: 'Schichtpläne', roles: ['admin', 'instandhalter', 'user'] },
|
||||
{ path: '/employees', label: 'Mitarbeiter', roles: ['admin', 'instandhalter'] },
|
||||
{ path: '/help', label: 'Hilfe', roles: ['admin', 'instandhalter', 'user'] },
|
||||
{ path: '/settings', label: 'Einstellungen', roles: ['admin', 'instandhalter', 'user'] },
|
||||
{ path: '/', label: 'Dashboard', roles: ['admin', 'maintenance', 'user'] },
|
||||
{ path: '/shift-plans', label: 'Schichtpläne', roles: ['admin', 'maintenance', 'user'] },
|
||||
{ path: '/employees', label: 'Mitarbeiter', roles: ['admin', 'maintenance'] },
|
||||
{ path: '/help', label: 'Hilfe', roles: ['admin', 'maintenance', 'user'] },
|
||||
{ path: '/settings', label: 'Einstellungen', roles: ['admin', 'maintenance', 'user'] },
|
||||
];
|
||||
|
||||
const filteredNavigation = navigationItems.filter(item =>
|
||||
|
||||
@@ -393,7 +393,7 @@ const Dashboard: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Quick Actions - Nur für Admins/Instandhalter */}
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<div style={{ marginBottom: '30px' }}>
|
||||
<h2 style={{ marginBottom: '15px', color: '#2c3e50' }}>Schnellaktionen</h2>
|
||||
<div style={{
|
||||
@@ -535,7 +535,7 @@ const Dashboard: React.FC = () => {
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '10px' }}>📅</div>
|
||||
<div>Kein aktiver Schichtplan</div>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<Link to="/shift-plans/new">
|
||||
<button style={{
|
||||
marginTop: '10px',
|
||||
@@ -643,7 +643,7 @@ const Dashboard: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Letzte Schichtpläne (für Admins/Instandhalter) */}
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
padding: '20px',
|
||||
|
||||
@@ -588,6 +588,31 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Contract type validation
|
||||
const availableShifts = validAvailabilities.filter(avail =>
|
||||
avail.preferenceLevel === 1 || avail.preferenceLevel === 2
|
||||
).length;
|
||||
|
||||
let contractRequirement = 0;
|
||||
let contractTypeName = '';
|
||||
|
||||
if (employee.contractType === 'small') {
|
||||
contractRequirement = 2;
|
||||
contractTypeName = 'Kleiner Vertrag';
|
||||
} else if (employee.contractType === 'large') {
|
||||
contractRequirement = 3;
|
||||
contractTypeName = 'Großer Vertrag';
|
||||
}
|
||||
|
||||
if (contractRequirement > 0 && availableShifts < contractRequirement) {
|
||||
setError(
|
||||
`${contractTypeName} erfordert mindestens ${contractRequirement} verfügbare Shifts. ` +
|
||||
`Aktuell sind nur ${availableShifts} Shifts mit Verfügbarkeit "Bevorzugt" oder "Möglich" ausgewählt.`
|
||||
);
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert to the format expected by the API - using shiftId directly
|
||||
const requestData = {
|
||||
planId: selectedPlanId,
|
||||
@@ -633,6 +658,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
// Get full name for display
|
||||
const employeeFullName = `${employee.firstname} ${employee.lastname}`;
|
||||
|
||||
// Mininmum amount of shifts per contract type
|
||||
const availableShiftsCount = availabilities.filter(avail =>
|
||||
avail.preferenceLevel === 1 || avail.preferenceLevel === 2
|
||||
).length;
|
||||
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
maxWidth: '1900px',
|
||||
@@ -660,6 +691,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
<p style={{ margin: 0, color: '#7f8c8d' }}>
|
||||
<strong>Email:</strong> {employee.email}
|
||||
</p>
|
||||
{employee.contractType && (
|
||||
<p style={{ margin: '5px 0 0 0', color: employee.contractType === 'small' ? '#f39c12' : '#27ae60' }}>
|
||||
<strong>Vertrag:</strong>
|
||||
{employee.contractType === 'small' ? ' Kleiner Vertrag (min. 2 verfügbare Shifts)' :
|
||||
employee.contractType === 'large' ? ' Großer Vertrag (min. 3 verfügbare Shifts)' :
|
||||
' Flexibler Vertrag'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -816,7 +855,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{saving ? '⏳ Wird gespeichert...' : 'Verfügbarkeiten speichern'}
|
||||
{saving ? '⏳ Wird gespeichert...' : `Verfügbarkeiten speichern (${availableShiftsCount})`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,21 +99,26 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
if (checked) {
|
||||
return {
|
||||
...prev,
|
||||
roles: [...prev.roles, role]
|
||||
roles: [role]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
const newRoles = prev.roles.filter(r => r !== role);
|
||||
return{
|
||||
...prev,
|
||||
roles: prev.roles.filter(r => r !== role)
|
||||
roles: newRoles.length > 0 ? newRoles : ['user']
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleEmployeeTypeChange = (employeeType: EmployeeType) => {
|
||||
// Determine if contract type should be shown and set default
|
||||
const requiresContract = employeeType !== 'guest';
|
||||
const defaultContractType = requiresContract ? 'small' as ContractType : undefined;
|
||||
// Determine contract type based on employee type
|
||||
let contractType: ContractType | undefined;
|
||||
if (employeeType === 'manager' || employeeType === 'apprentice') {
|
||||
contractType = 'flexible';
|
||||
} else if (employeeType !== 'guest') {
|
||||
contractType = 'small';
|
||||
}
|
||||
|
||||
// Determine if can work alone based on employee type
|
||||
const canWorkAlone = employeeType === 'manager' ||
|
||||
@@ -125,7 +130,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
employeeType,
|
||||
contractType: defaultContractType,
|
||||
contractType,
|
||||
canWorkAlone,
|
||||
isTrainee
|
||||
}));
|
||||
@@ -273,7 +278,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '94%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -294,7 +299,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
onChange={handleChange}
|
||||
required
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '94%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -311,7 +316,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
E-Mail Adresse (automatisch generiert)
|
||||
</label>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
width: '97%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -340,7 +345,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
required
|
||||
minLength={6}
|
||||
style={{
|
||||
width: '100%',
|
||||
width: '97%',
|
||||
padding: '10px',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
@@ -355,78 +360,6 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vertragstyp (nur für Admins und interne Mitarbeiter) */}
|
||||
{hasRole(['admin']) && showContractType && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #b6d7e8'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 15px 0', color: '#0c5460' }}>📝 Vertragstyp</h3>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{contractTypeOptions.map(contract => (
|
||||
<div
|
||||
key={contract.value}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
padding: '15px',
|
||||
border: `2px solid ${formData.contractType === contract.value ? '#3498db' : '#e0e0e0'}`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: formData.contractType === contract.value ? '#f0f8ff' : 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
onClick={() => handleContractTypeChange(contract.value)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="contractType"
|
||||
value={contract.value}
|
||||
checked={formData.contractType === contract.value}
|
||||
onChange={() => handleContractTypeChange(contract.value)}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
marginTop: '2px',
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
color: '#2c3e50',
|
||||
marginBottom: '4px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{contract.label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#7f8c8d',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{contract.description}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: formData.contractType === contract.value ? '#3498db' : '#95a5a6',
|
||||
color: 'white',
|
||||
borderRadius: '15px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{contract.value.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mitarbeiter Kategorie */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
@@ -528,6 +461,107 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vertragstyp (nur für Admins und interne Mitarbeiter) */}
|
||||
{hasRole(['admin']) && showContractType && (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
backgroundColor: '#e8f4fd',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #b6d7e8'
|
||||
}}>
|
||||
<h3 style={{ margin: '0 0 15px 0', color: '#0c5460' }}>📝 Vertragstyp</h3>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{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 (
|
||||
<div
|
||||
key={contract.value}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
padding: '15px',
|
||||
border: `2px solid ${formData.contractType === contract.value ? '#3498db' : '#e0e0e0'}`,
|
||||
borderRadius: '8px',
|
||||
backgroundColor: formData.contractType === contract.value ? '#f0f8ff' : 'white',
|
||||
cursor: isDisabled ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
opacity: isDisabled ? 0.6 : 1
|
||||
}}
|
||||
onClick={isDisabled ? undefined : () => handleContractTypeChange(contract.value)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="contractType"
|
||||
value={contract.value}
|
||||
checked={formData.contractType === contract.value}
|
||||
onChange={isDisabled ? undefined : () => handleContractTypeChange(contract.value)}
|
||||
disabled={isDisabled}
|
||||
style={{
|
||||
marginRight: '12px',
|
||||
marginTop: '2px',
|
||||
width: '18px',
|
||||
height: '18px'
|
||||
}}
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{
|
||||
fontWeight: 'bold',
|
||||
color: '#2c3e50',
|
||||
marginBottom: '4px',
|
||||
fontSize: '16px'
|
||||
}}>
|
||||
{contract.label}
|
||||
{isFlexibleDisabled && (
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: '#e74c3c',
|
||||
marginLeft: '8px',
|
||||
fontWeight: 'normal'
|
||||
}}>
|
||||
(Nicht verfügbar für Personell)
|
||||
</span>
|
||||
)}
|
||||
{isSmallLargeDisabled && (
|
||||
<span style={{
|
||||
fontSize: '12px',
|
||||
color: '#e74c3c',
|
||||
marginLeft: '8px',
|
||||
fontWeight: 'normal'
|
||||
}}>
|
||||
(Nicht verfügbar für {formData.employeeType === 'manager' ? 'Manager' : 'Auszubildende'})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
color: '#7f8c8d',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
{contract.description}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '6px 12px',
|
||||
backgroundColor: isDisabled ? '#95a5a6' : (formData.contractType === contract.value ? '#3498db' : '#95a5a6'),
|
||||
color: 'white',
|
||||
borderRadius: '15px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{contract.value.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Eigenständigkeit */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
|
||||
@@ -48,6 +48,13 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
return true;
|
||||
});
|
||||
|
||||
// Helper to get highest role for sorting
|
||||
const getHighestRole = (roles: string[]): string => {
|
||||
if (roles.includes('admin')) return 'admin';
|
||||
if (roles.includes('maintenance')) return 'maintenance';
|
||||
return 'user';
|
||||
};
|
||||
|
||||
// Sort employees based on selected field and direction
|
||||
const sortedEmployees = [...filteredEmployees].sort((a, b) => {
|
||||
let aValue: any;
|
||||
@@ -67,7 +74,6 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
bValue = b.canWorkAlone;
|
||||
break;
|
||||
case 'role':
|
||||
// Use the highest role for sorting
|
||||
aValue = getHighestRole(a.roles || []);
|
||||
bValue = getHighestRole(b.roles || []);
|
||||
break;
|
||||
@@ -87,13 +93,6 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to get highest role for sorting
|
||||
const getHighestRole = (roles: string[]): string => {
|
||||
if (roles.includes('admin')) return 'admin';
|
||||
if (roles.includes('maintenance')) return 'maintenance';
|
||||
return 'user';
|
||||
};
|
||||
|
||||
const handleSort = (field: SortField) => {
|
||||
if (sortField === field) {
|
||||
// Toggle direction if same field
|
||||
|
||||
@@ -80,134 +80,6 @@ const Help: React.FC = () => {
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<h1>❓ Hilfe & Support - Scheduling Algorithmus</h1>
|
||||
|
||||
{/* Algorithm Visualization */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '12px',
|
||||
padding: '30px',
|
||||
marginTop: '20px',
|
||||
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||
border: '1px solid #e0e0e0'
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
|
||||
<h2 style={{ margin: 0, color: '#2c3e50' }}>🧠 Algorithmus Visualisierung</h2>
|
||||
<button
|
||||
onClick={toggleAnimation}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: isAnimating ? '#e74c3c' : '#2ecc71',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '20px',
|
||||
cursor: 'pointer',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{isAnimating ? '⏸️ Animation pausieren' : '▶️ Animation starten'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stage Indicators */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '30px',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{algorithmStages.map((stage, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '60px',
|
||||
height: '60px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: currentStage === index ? stage.color : '#ecf0f1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: currentStage === index ? 'white' : '#7f8c8d',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '18px',
|
||||
transition: 'all 0.5s ease',
|
||||
boxShadow: currentStage === index ? `0 0 20px ${stage.color}80` : 'none',
|
||||
border: `3px solid ${stage.color}`
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
marginTop: '10px',
|
||||
fontWeight: currentStage === index ? 'bold' : 'normal',
|
||||
color: currentStage === index ? stage.color : '#7f8c8d'
|
||||
}}>
|
||||
{stage.title.split(':')[0]}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{index < algorithmStages.length - 1 && (
|
||||
<div style={{
|
||||
flex: 1,
|
||||
height: '3px',
|
||||
backgroundColor: currentStage > index ? stage.color : '#ecf0f1',
|
||||
alignSelf: 'center',
|
||||
margin: '0 10px',
|
||||
transition: 'all 0.5s ease'
|
||||
}} />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Current Stage Details */}
|
||||
<div style={{
|
||||
backgroundColor: algorithmStages[currentStage].color + '15',
|
||||
border: `2px solid ${algorithmStages[currentStage].color}30`,
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
marginBottom: '20px',
|
||||
transition: 'all 0.5s ease'
|
||||
}}>
|
||||
<h3 style={{ color: algorithmStages[currentStage].color, marginTop: 0 }}>
|
||||
{algorithmStages[currentStage].title}
|
||||
</h3>
|
||||
<p style={{ color: '#2c3e50', fontSize: '16px', marginBottom: '15px' }}>
|
||||
{algorithmStages[currentStage].description}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gap: '8px' }}>
|
||||
{algorithmStages[currentStage].steps.map((step, stepIndex) => (
|
||||
<div
|
||||
key={stepIndex}
|
||||
style={{
|
||||
padding: '10px 15px',
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '6px',
|
||||
borderLeft: `4px solid ${algorithmStages[currentStage].color}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
animation: isAnimating ? 'pulse 2s infinite' : 'none'
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
marginRight: '10px',
|
||||
color: algorithmStages[currentStage].color,
|
||||
fontWeight: 'bold'
|
||||
}}>•</span>
|
||||
{step}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Business Rules */}
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
@@ -271,24 +143,7 @@ const Help: React.FC = () => {
|
||||
<h4 style={{ color: '#3498db' }}>🏗️ Phasen-basierter Ansatz</h4>
|
||||
<p>Der Algorithmus arbeitet in klar definierten Phasen, um komplexe Probleme schrittweise zu lösen und Stabilität zu gewährleisten.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ color: '#e74c3c' }}>⚖️ Wert-basierte Entscheidungen</h4>
|
||||
<p>Jede Zuweisung wird anhand eines Wertesystems bewertet, das Verfügbarkeit, Erfahrung und aktuelle Auslastung berücksichtigt.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ color: '#2ecc71' }}>🔧 Automatische Reparatur</h4>
|
||||
<p>Probleme werden automatisch erkannt und durch intelligente Tausch- und Bewegungsoperationen behoben.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style={{ color: '#f39c12' }}>📊 Transparente Berichterstattung</h4>
|
||||
<p>Detaillierte Berichte zeigen genau, welche Probleme behoben wurden und welche verbleiben.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
border: '1px solid rgba(255, 255, 255, 0.8)',
|
||||
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
minHeight: '200px',
|
||||
minHeight: '100px',
|
||||
},
|
||||
section: {
|
||||
marginBottom: '2rem',
|
||||
|
||||
@@ -70,7 +70,7 @@ const ShiftPlanList: React.FC = () => {
|
||||
marginBottom: '30px'
|
||||
}}>
|
||||
<h1>📅 Schichtpläne</h1>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<Link to="/shift-plans/new">
|
||||
<button style={{
|
||||
padding: '10px 20px',
|
||||
@@ -143,7 +143,7 @@ const ShiftPlanList: React.FC = () => {
|
||||
>
|
||||
Anzeigen
|
||||
</button>
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => navigate(`/shift-plans/${plan.id}/edit`)}
|
||||
|
||||
@@ -1005,7 +1005,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
|
||||
{shiftPlan.status === 'published' && hasRole(['admin', 'instandhalter']) && (
|
||||
{shiftPlan.status === 'published' && hasRole(['admin', 'maintenance']) && (
|
||||
<button
|
||||
onClick={handleRecreateAssignments}
|
||||
disabled={recreating}
|
||||
@@ -1076,7 +1076,7 @@ const ShiftPlanView: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasRole(['admin', 'instandhalter']) && (
|
||||
{hasRole(['admin', 'maintenance']) && (
|
||||
<div>
|
||||
<button
|
||||
onClick={handlePreviewAssignment}
|
||||
|
||||
Reference in New Issue
Block a user