fixed role naming instandhalter -> maintenance

This commit is contained in:
2025-10-21 20:23:12 +02:00
parent 2200ef6937
commit 5809e6c44c
13 changed files with 193 additions and 266 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -322,7 +322,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
// Check for missing time slots
const usedTimeSlotIds = new Set(selectedPlan?.shifts?.map(s => s.timeSlotId) || []);
const availableTimeSlotIds = new Set(selectedPlan?.timeSlots?.map(ts => ts.id) || []);
usedTimeSlotIds.forEach(timeSlotId => {
if (!availableTimeSlotIds.has(timeSlotId)) {
validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`);
@@ -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>

View File

@@ -99,22 +99,27 @@ 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' ||
(employeeType === 'personell' && !formData.isTrainee);
@@ -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',

View File

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

View File

@@ -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>
<div style={{

View File

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

View File

@@ -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`)}

View File

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