added frontend user management

This commit is contained in:
2025-10-08 15:25:03 +02:00
parent 54eaa86924
commit 4e120c8789
8 changed files with 1351 additions and 23 deletions

View File

@@ -11,11 +11,11 @@ const Navigation: React.FC = () => {
const isActive = (path: string) => location.pathname === path; const isActive = (path: string) => location.pathname === path;
const navigationItems = [ const navigationItems = [
{ path: '/', label: '🏠 Dashboard', icon: '🏠', roles: ['admin', 'instandhalter', 'user'] }, { path: '/', label: 'Dashboard', icon: '🏠', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/shift-plans', label: '📅 Schichtpläne', icon: '📅', roles: ['admin', 'instandhalter', 'user'] }, { path: '/shift-plans', label: 'Schichtpläne', icon: '📅', roles: ['admin', 'instandhalter', 'user'] },
{ path: '/employees', label: '👥 Mitarbeiter', icon: '👥', roles: ['admin', 'instandhalter'] }, { path: '/employees', label: 'Mitarbeiter', icon: '👥', roles: ['admin', 'instandhalter'] },
{ path: '/settings', label: '⚙️ Einstellungen', icon: '⚙️', roles: ['admin'] }, { path: '/settings', label: 'Einstellungen', icon: '⚙️', roles: ['admin'] },
{ path: '/help', label: 'Hilfe', icon: '❓', roles: ['admin', 'instandhalter', 'user'] }, { path: '/help', label: 'Hilfe', icon: '❓', roles: ['admin', 'instandhalter', 'user'] },
]; ];
const filteredNavigation = navigationItems.filter(item => const filteredNavigation = navigationItems.filter(item =>

View File

@@ -1,26 +1,206 @@
// frontend/src/pages/Employees/EmployeeManagement.tsx // frontend/src/pages/Employees/EmployeeManagement.tsx
import React from 'react'; import React, { useState, useEffect } from 'react';
import { Employee } from '../../types/employee';
import { employeeService } from '../../services/employeeService';
import EmployeeList from './components/EmployeeList';
import EmployeeForm from './components/EmployeeForm';
import AvailabilityManager from './components/AvailabilityManager';
import { useAuth } from '../../contexts/AuthContext';
type ViewMode = 'list' | 'create' | 'edit' | 'availability';
const EmployeeManagement: React.FC = () => { const EmployeeManagement: React.FC = () => {
const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
const { hasRole } = useAuth();
useEffect(() => {
loadEmployees();
}, []);
const loadEmployees = async () => {
try {
setLoading(true);
const data = await employeeService.getEmployees();
setEmployees(data);
} catch (err: any) {
setError(err.message || 'Fehler beim Laden der Mitarbeiter');
} finally {
setLoading(false);
}
};
const handleCreateEmployee = () => {
setSelectedEmployee(null);
setViewMode('create');
};
const handleEditEmployee = (employee: Employee) => {
setSelectedEmployee(employee);
setViewMode('edit');
};
const handleManageAvailability = (employee: Employee) => {
setSelectedEmployee(employee);
setViewMode('availability');
};
const handleBackToList = () => {
setViewMode('list');
setSelectedEmployee(null);
loadEmployees(); // Daten aktualisieren
};
const handleEmployeeCreated = () => {
handleBackToList();
};
const handleEmployeeUpdated = () => {
handleBackToList();
};
const handleDeleteEmployee = async (employeeId: string) => {
if (!window.confirm('Mitarbeiter wirklich löschen?\nDer Mitarbeiter wird deaktiviert und kann keine Schichten mehr zugewiesen bekommen.')) {
return;
}
try {
await employeeService.deleteEmployee(employeeId);
await loadEmployees(); // Liste aktualisieren
} catch (err: any) {
setError(err.message || 'Fehler beim Löschen des Mitarbeiters');
}
};
if (loading && viewMode === 'list') {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div> Lade Mitarbeiter...</div>
</div>
);
}
return ( return (
<div> <div>
<h1>👥 Mitarbeiter Verwaltung</h1> {/* Header mit Titel und Aktionen */}
<div style={{ <div style={{
padding: '40px', display: 'flex',
textAlign: 'center', justifyContent: 'space-between',
backgroundColor: '#f8f9fa', alignItems: 'center',
borderRadius: '8px', marginBottom: '30px',
border: '2px dashed #dee2e6', flexWrap: 'wrap',
marginTop: '20px' gap: '15px'
}}> }}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>👥</div> <div>
<h3>Mitarbeiter Übersicht</h3> <h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1>
<p>Hier können Sie Mitarbeiter verwalten und deren Verfügbarkeiten einsehen.</p> <p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
<p style={{ fontSize: '14px', color: '#6c757d' }}> Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten
Diese Seite wird demnächst mit Funktionen gefüllt.
</p> </p>
</div> </div>
{viewMode === 'list' && hasRole(['admin']) && (
<button
onClick={handleCreateEmployee}
style={{
padding: '12px 24px',
backgroundColor: '#27ae60',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
<span>+</span>
Neuer Mitarbeiter
</button>
)}
{viewMode !== 'list' && (
<button
onClick={handleBackToList}
style={{
padding: '10px 20px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Zurück zur Liste
</button>
)}
</div>
{/* Fehleranzeige */}
{error && (
<div style={{
backgroundColor: '#fee',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '15px',
borderRadius: '6px',
marginBottom: '20px'
}}>
<strong>Fehler:</strong> {error}
<button
onClick={() => setError('')}
style={{
float: 'right',
background: 'none',
border: 'none',
color: '#721c24',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
×
</button>
</div>
)}
{/* Inhalt basierend auf View Mode */}
{viewMode === 'list' && (
<EmployeeList
employees={employees}
onEdit={handleEditEmployee}
onDelete={handleDeleteEmployee}
onManageAvailability={handleManageAvailability}
currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'}
/>
)}
{viewMode === 'create' && (
<EmployeeForm
mode="create"
onSuccess={handleEmployeeCreated}
onCancel={handleBackToList}
/>
)}
{viewMode === 'edit' && selectedEmployee && (
<EmployeeForm
mode="edit"
employee={selectedEmployee}
onSuccess={handleEmployeeUpdated}
onCancel={handleBackToList}
/>
)}
{viewMode === 'availability' && selectedEmployee && (
<AvailabilityManager
employee={selectedEmployee}
onSave={handleBackToList}
onCancel={handleBackToList}
/>
)}
</div> </div>
); );
}; };

View File

@@ -0,0 +1,329 @@
// frontend/src/pages/Employees/components/AvailabilityManager.tsx
import React, { useState, useEffect } from 'react';
import { Employee, Availability } from '../../../types/employee';
import { employeeService } from '../../../services/employeeService';
interface AvailabilityManagerProps {
employee: Employee;
onSave: () => void;
onCancel: () => void;
}
const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
employee,
onSave,
onCancel
}) => {
const [availabilities, setAvailabilities] = useState<Availability[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const daysOfWeek = [
{ id: 1, name: 'Montag' },
{ id: 2, name: 'Dienstag' },
{ id: 3, name: 'Mittwoch' },
{ id: 4, name: 'Donnerstag' },
{ id: 5, name: 'Freitag' },
{ id: 6, name: 'Samstag' },
{ id: 0, name: 'Sonntag' }
];
const defaultTimeSlots = [
{ name: 'Vormittag', start: '08:00', end: '12:00' },
{ name: 'Nachmittag', start: '12:00', end: '16:00' },
{ name: 'Abend', start: '16:00', end: '20:00' }
];
useEffect(() => {
loadAvailabilities();
}, [employee.id]);
const loadAvailabilities = async () => {
try {
setLoading(true);
const data = await employeeService.getAvailabilities(employee.id);
setAvailabilities(data);
} catch (err: any) {
// Falls keine Verfügbarkeiten existieren, erstelle Standard-Einträge
const defaultAvailabilities = daysOfWeek.flatMap(day =>
defaultTimeSlots.map(slot => ({
id: `temp-${day.id}-${slot.name}`,
employeeId: employee.id,
dayOfWeek: day.id,
startTime: slot.start,
endTime: slot.end,
isAvailable: false
}))
);
setAvailabilities(defaultAvailabilities);
} finally {
setLoading(false);
}
};
const handleAvailabilityChange = (id: string, isAvailable: boolean) => {
setAvailabilities(prev =>
prev.map(avail =>
avail.id === id ? { ...avail, isAvailable } : avail
)
);
};
const handleTimeChange = (id: string, field: 'startTime' | 'endTime', value: string) => {
setAvailabilities(prev =>
prev.map(avail =>
avail.id === id ? { ...avail, [field]: value } : avail
)
);
};
const handleSave = async () => {
try {
setSaving(true);
setError('');
await employeeService.updateAvailabilities(employee.id, availabilities);
onSave();
} catch (err: any) {
setError(err.message || 'Fehler beim Speichern der Verfügbarkeiten');
} finally {
setSaving(false);
}
};
const getAvailabilitiesForDay = (dayId: number) => {
return availabilities.filter(avail => avail.dayOfWeek === dayId);
};
if (loading) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<div> Lade Verfügbarkeiten...</div>
</div>
);
}
return (
<div style={{
maxWidth: '800px',
margin: '0 auto',
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}>
<h2 style={{
margin: '0 0 25px 0',
color: '#2c3e50',
borderBottom: '2px solid #f0f0f0',
paddingBottom: '15px'
}}>
📅 Verfügbarkeit verwalten
</h2>
<div style={{ marginBottom: '20px' }}>
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
{employee.name}
</h3>
<p style={{ margin: 0, color: '#7f8c8d' }}>
Legen Sie fest, an welchen Tagen und Zeiten {employee.name} verfügbar ist.
</p>
</div>
{error && (
<div style={{
backgroundColor: '#fee',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '12px',
borderRadius: '6px',
marginBottom: '20px'
}}>
<strong>Fehler:</strong> {error}
</div>
)}
{/* Verfügbarkeiten Tabelle */}
<div style={{
border: '1px solid #e0e0e0',
borderRadius: '8px',
overflow: 'hidden',
marginBottom: '30px'
}}>
{daysOfWeek.map(day => {
const dayAvailabilities = getAvailabilitiesForDay(day.id);
return (
<div key={day.id} style={{
borderBottom: '1px solid #f0f0f0',
':last-child': { borderBottom: 'none' }
}}>
{/* Tag Header */}
<div style={{
backgroundColor: '#f8f9fa',
padding: '15px 20px',
fontWeight: 'bold',
color: '#2c3e50',
borderBottom: '1px solid #e0e0e0'
}}>
{day.name}
</div>
{/* Zeit-Slots */}
<div style={{ padding: '15px 20px' }}>
{dayAvailabilities.map(availability => (
<div
key={availability.id}
style={{
display: 'grid',
gridTemplateColumns: '1fr auto auto auto',
gap: '15px',
alignItems: 'center',
padding: '10px 0',
borderBottom: '1px solid #f8f9fa',
':last-child': { borderBottom: 'none' }
}}
>
{/* Verfügbarkeit Toggle */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="checkbox"
id={`avail-${availability.id}`}
checked={availability.isAvailable}
onChange={(e) => handleAvailabilityChange(availability.id, e.target.checked)}
style={{ width: '18px', height: '18px' }}
/>
<label
htmlFor={`avail-${availability.id}`}
style={{
fontWeight: 'bold',
color: availability.isAvailable ? '#27ae60' : '#95a5a6'
}}
>
{availability.isAvailable ? 'Verfügbar' : 'Nicht verfügbar'}
</label>
</div>
{/* Startzeit */}
<div>
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
Von
</label>
<input
type="time"
value={availability.startTime}
onChange={(e) => handleTimeChange(availability.id, 'startTime', e.target.value)}
disabled={!availability.isAvailable}
style={{
padding: '6px 8px',
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
borderRadius: '4px',
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
color: availability.isAvailable ? '#333' : '#999'
}}
/>
</div>
{/* Endzeit */}
<div>
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
Bis
</label>
<input
type="time"
value={availability.endTime}
onChange={(e) => handleTimeChange(availability.id, 'endTime', e.target.value)}
disabled={!availability.isAvailable}
style={{
padding: '6px 8px',
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
borderRadius: '4px',
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
color: availability.isavailable ? '#333' : '#999'
}}
/>
</div>
{/* Status Badge */}
<div>
<span
style={{
backgroundColor: availability.isAvailable ? '#d5f4e6' : '#fadbd8',
color: availability.isAvailable ? '#27ae60' : '#e74c3c',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold'
}}
>
{availability.isAvailable ? 'Aktiv' : 'Inaktiv'}
</span>
</div>
</div>
))}
</div>
</div>
);
})}
</div>
{/* Info Text */}
<div style={{
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{ margin: '0 0 8px 0', color: '#2c3e50' }}>💡 Information</h4>
<p style={{ margin: 0, color: '#546e7a', fontSize: '14px' }}>
Verfügbarkeiten bestimmen, wann dieser Mitarbeiter für Schichten eingeplant werden kann.
Nur als "verfügbar" markierte Zeitfenster werden bei der automatischen Schichtplanung berücksichtigt.
</p>
</div>
{/* Buttons */}
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end'
}}>
<button
onClick={onCancel}
disabled={saving}
style={{
padding: '12px 24px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: saving ? 'not-allowed' : 'pointer',
opacity: saving ? 0.6 : 1
}}
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={saving}
style={{
padding: '12px 24px',
backgroundColor: saving ? '#bdc3c7' : '#3498db',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: saving ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{saving ? '⏳ Wird gespeichert...' : 'Verfügbarkeiten speichern'}
</button>
</div>
</div>
);
};
export default AvailabilityManager;

View File

@@ -0,0 +1,370 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx
import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../types/employee';
import { employeeService } from '../../../services/employeeService';
interface EmployeeFormProps {
mode: 'create' | 'edit';
employee?: Employee;
onSuccess: () => void;
onCancel: () => void;
}
const EmployeeForm: React.FC<EmployeeFormProps> = ({
mode,
employee,
onSuccess,
onCancel
}) => {
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
role: 'user' as 'admin' | 'instandhalter' | 'user',
phone: '',
department: '',
isActive: true
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
if (mode === 'edit' && employee) {
setFormData({
name: employee.name,
email: employee.email,
password: '', // Passwort wird beim Editieren nicht angezeigt
role: employee.role,
phone: employee.phone || '',
department: employee.department || '',
isActive: employee.isActive
});
}
}, [mode, employee]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
if (mode === 'create') {
const createData: CreateEmployeeRequest = {
name: formData.name,
email: formData.email,
password: formData.password,
role: formData.role,
phone: formData.phone || undefined,
department: formData.department || undefined
};
await employeeService.createEmployee(createData);
} else if (employee) {
const updateData: UpdateEmployeeRequest = {
name: formData.name,
role: formData.role,
isActive: formData.isActive,
phone: formData.phone || undefined,
department: formData.department || undefined
};
await employeeService.updateEmployee(employee.id, updateData);
}
onSuccess();
} catch (err: any) {
setError(err.message || `Fehler beim ${mode === 'create' ? 'Erstellen' : 'Aktualisieren'} des Mitarbeiters`);
} finally {
setLoading(false);
}
};
const isFormValid = () => {
if (mode === 'create') {
return formData.name.trim() &&
formData.email.trim() &&
formData.password.length >= 6;
}
return formData.name.trim() && formData.email.trim();
};
return (
<div style={{
maxWidth: '600px',
margin: '0 auto',
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}>
<h2 style={{
margin: '0 0 25px 0',
color: '#2c3e50',
borderBottom: '2px solid #f0f0f0',
paddingBottom: '15px'
}}>
{mode === 'create' ? '👤 Neuen Mitarbeiter erstellen' : '✏️ Mitarbeiter bearbeiten'}
</h2>
{error && (
<div style={{
backgroundColor: '#fee',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '12px',
borderRadius: '6px',
marginBottom: '20px'
}}>
<strong>Fehler:</strong> {error}
</div>
)}
<form onSubmit={handleSubmit}>
<div style={{ display: 'grid', gap: '20px' }}>
{/* Name */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Vollständiger Name *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Max Mustermann"
/>
</div>
{/* E-Mail */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
E-Mail Adresse *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
required
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="max.mustermann@example.com"
/>
</div>
{/* Passwort (nur bei Erstellung) */}
{mode === 'create' && (
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Passwort *
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
minLength={6}
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Mindestens 6 Zeichen"
/>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
Das Passwort muss mindestens 6 Zeichen lang sein.
</div>
</div>
)}
{/* Rolle */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Rolle *
</label>
<select
name="role"
value={formData.role}
onChange={handleChange}
required
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
backgroundColor: 'white'
}}
>
<option value="user">Mitarbeiter (User)</option>
<option value="instandhalter">Instandhalter</option>
<option value="admin">Administrator</option>
</select>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
{formData.role === 'admin' && 'Administratoren haben vollen Zugriff auf alle Funktionen.'}
{formData.role === 'instandhalter' && 'Instandhalter können Schichtpläne erstellen und Mitarbeiter verwalten.'}
{formData.role === 'user' && 'Mitarbeiter können ihre eigenen Schichten und Verfügbarkeiten einsehen.'}
</div>
</div>
{/* Telefon */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Telefonnummer
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="+49 123 456789"
/>
</div>
{/* Abteilung */}
<div>
<label style={{
display: 'block',
marginBottom: '8px',
fontWeight: 'bold',
color: '#2c3e50'
}}>
Abteilung
</label>
<input
type="text"
name="department"
value={formData.department}
onChange={handleChange}
style={{
width: '100%',
padding: '10px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="z.B. Produktion, Logistik, Verwaltung"
/>
</div>
{/* Aktiv Status (nur beim Bearbeiten) */}
{mode === 'edit' && (
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<input
type="checkbox"
name="isActive"
id="isActive"
checked={formData.isActive}
onChange={handleChange}
style={{ width: '18px', height: '18px' }}
/>
<label htmlFor="isActive" style={{ fontWeight: 'bold', color: '#2c3e50' }}>
Mitarbeiter ist aktiv
</label>
</div>
)}
</div>
{/* Buttons */}
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button
type="button"
onClick={onCancel}
disabled={loading}
style={{
padding: '12px 24px',
backgroundColor: '#95a5a6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
>
Abbrechen
</button>
<button
type="submit"
disabled={loading || !isFormValid()}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (isFormValid() ? '#27ae60' : '#95a5a6'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !isFormValid()) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Wird gespeichert...' : (mode === 'create' ? 'Mitarbeiter erstellen' : 'Änderungen speichern')}
</button>
</div>
</form>
</div>
);
};
export default EmployeeForm;

View File

@@ -0,0 +1,285 @@
// frontend/src/pages/Employees/components/EmployeeList.tsx
import React, { useState } from 'react';
import { Employee } from '../../../types/employee';
interface EmployeeListProps {
employees: Employee[];
onEdit: (employee: Employee) => void;
onDelete: (employeeId: string) => void;
onManageAvailability: (employee: Employee) => void;
currentUserRole: 'admin' | 'instandhalter';
}
const EmployeeList: React.FC<EmployeeListProps> = ({
employees,
onEdit,
onDelete,
onManageAvailability,
currentUserRole
}) => {
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [searchTerm, setSearchTerm] = useState('');
const filteredEmployees = employees.filter(employee => {
// Status-Filter
if (filter === 'active' && !employee.isActive) return false;
if (filter === 'inactive' && employee.isActive) return false;
// Suchfilter
if (searchTerm) {
const term = searchTerm.toLowerCase();
return (
employee.name.toLowerCase().includes(term) ||
employee.email.toLowerCase().includes(term) ||
employee.department?.toLowerCase().includes(term) ||
employee.role.toLowerCase().includes(term)
);
}
return true;
});
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'admin': return '#e74c3c';
case 'instandhalter': return '#3498db';
case 'user': return '#27ae60';
default: return '#95a5a6';
}
};
const getStatusBadge = (isActive: boolean) => {
return isActive
? { text: 'Aktiv', color: '#27ae60', bgColor: '#d5f4e6' }
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
};
if (employees.length === 0) {
return (
<div style={{
textAlign: 'center',
padding: '60px 20px',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '2px dashed #dee2e6'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>👥</div>
<h3 style={{ color: '#6c757d' }}>Noch keine Mitarbeiter</h3>
<p style={{ color: '#6c757d', marginBottom: '20px' }}>
Erstellen Sie den ersten Mitarbeiter, um zu beginnen.
</p>
</div>
);
}
return (
<div>
{/* Filter und Suche */}
<div style={{
display: 'flex',
gap: '15px',
marginBottom: '20px',
flexWrap: 'wrap',
alignItems: 'center'
}}>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<label style={{ fontWeight: 'bold', color: '#2c3e50' }}>Filter:</label>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as any)}
style={{
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: 'white'
}}
>
<option value="all">Alle Mitarbeiter</option>
<option value="active">Nur Aktive</option>
<option value="inactive">Nur Inaktive</option>
</select>
</div>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center', flex: 1 }}>
<label style={{ fontWeight: 'bold', color: '#2c3e50' }}>Suchen:</label>
<input
type="text"
placeholder="Nach Name, E-Mail oder Abteilung suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
flex: 1,
maxWidth: '400px'
}}
/>
</div>
<div style={{ color: '#7f8c8d', fontSize: '14px' }}>
{filteredEmployees.length} von {employees.length} Mitarbeitern
</div>
</div>
{/* Mitarbeiter Tabelle */}
<div style={{
backgroundColor: 'white',
borderRadius: '8px',
border: '1px solid #e0e0e0',
overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr auto',
gap: '15px',
padding: '15px 20px',
backgroundColor: '#f8f9fa',
borderBottom: '1px solid #dee2e6',
fontWeight: 'bold',
color: '#2c3e50'
}}>
<div>Name & E-Mail</div>
<div>Abteilung</div>
<div>Rolle</div>
<div>Status</div>
<div>Letzter Login</div>
<div>Aktionen</div>
</div>
{filteredEmployees.map(employee => {
const status = getStatusBadge(employee.isActive);
return (
<div
key={employee.id}
style={{
display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr auto',
gap: '15px',
padding: '15px 20px',
borderBottom: '1px solid #f0f0f0',
alignItems: 'center'
}}
>
{/* Name & E-Mail */}
<div>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{employee.name}
</div>
<div style={{ color: '#666', fontSize: '14px' }}>
{employee.email}
</div>
</div>
{/* Abteilung */}
<div>
{employee.department || (
<span style={{ color: '#999', fontStyle: 'italic' }}>Nicht zugewiesen</span>
)}
</div>
{/* Rolle */}
<div>
<span
style={{
backgroundColor: getRoleBadgeColor(employee.role),
color: 'white',
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold'
}}
>
{employee.role}
</span>
</div>
{/* Status */}
<div>
<span
style={{
backgroundColor: status.bgColor,
color: status.color,
padding: '4px 8px',
borderRadius: '12px',
fontSize: '12px',
fontWeight: 'bold'
}}
>
{status.text}
</span>
</div>
{/* Letzter Login */}
<div style={{ fontSize: '14px', color: '#666' }}>
{employee.lastLogin
? new Date(employee.lastLogin).toLocaleDateString('de-DE')
: 'Noch nie'
}
</div>
{/* Aktionen */}
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
onClick={() => onManageAvailability(employee)}
style={{
padding: '6px 12px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
title="Verfügbarkeit verwalten"
>
📅
</button>
{(currentUserRole === 'admin' || employee.role !== 'admin') && (
<button
onClick={() => onEdit(employee)}
style={{
padding: '6px 12px',
backgroundColor: '#f39c12',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
title="Mitarbeiter bearbeiten"
>
</button>
)}
{currentUserRole === 'admin' && employee.role !== 'admin' && (
<button
onClick={() => onDelete(employee.id)}
style={{
padding: '6px 12px',
backgroundColor: '#e74c3c',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px'
}}
title="Mitarbeiter löschen"
>
🗑
</button>
)}
</div>
</div>
);
})}
</div>
</div>
);
};
export default EmployeeList;

View File

@@ -54,3 +54,5 @@ const ShiftPlanCreate: React.FC = () => {
</div> </div>
); );
}; };
export default ShiftPlanCreate;

View File

@@ -0,0 +1,124 @@
// frontend/src/services/employeeService.ts
import { Employee, Availability, CreateEmployeeRequest, UpdateEmployeeRequest } from '../types/employee';
import { authService } from './authService';
const API_BASE = 'http://localhost:3002/api/employees';
export const employeeService = {
// Alle Mitarbeiter abrufen
async getEmployees(): Promise<Employee[]> {
const response = await fetch(API_BASE, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
throw new Error('Fehler beim Laden der Mitarbeiter');
}
return response.json();
},
// Einzelnen Mitarbeiter abrufen
async getEmployee(id: string): Promise<Employee> {
const response = await fetch(`${API_BASE}/${id}`, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
throw new Error('Mitarbeiter nicht gefunden');
}
return response.json();
},
// Neuen Mitarbeiter erstellen
async createEmployee(employeeData: CreateEmployeeRequest): Promise<Employee> {
const response = await fetch(API_BASE, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(employeeData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Fehler beim Erstellen des Mitarbeiters');
}
return response.json();
},
// Mitarbeiter aktualisieren
async updateEmployee(id: string, updates: UpdateEmployeeRequest): Promise<Employee> {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(updates)
});
if (!response.ok) {
throw new Error('Fehler beim Aktualisieren des Mitarbeiters');
}
return response.json();
},
// Mitarbeiter löschen (deaktivieren)
async deleteEmployee(id: string): Promise<void> {
const response = await fetch(`${API_BASE}/${id}`, {
method: 'DELETE',
headers: {
...authService.getAuthHeaders()
}
});
if (!response.ok) {
throw new Error('Fehler beim Löschen des Mitarbeiters');
}
},
// Verfügbarkeiten abrufen
async getAvailabilities(employeeId: string): Promise<Availability[]> {
const response = await fetch(`${API_BASE}/${employeeId}/availabilities`, {
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
throw new Error('Fehler beim Laden der Verfügbarkeiten');
}
return response.json();
},
// Verfügbarkeiten aktualisieren
async updateAvailabilities(employeeId: string, availabilities: Availability[]): Promise<Availability[]> {
const response = await fetch(`${API_BASE}/${employeeId}/availabilities`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
},
body: JSON.stringify(availabilities)
});
if (!response.ok) {
throw new Error('Fehler beim Aktualisieren der Verfügbarkeiten');
}
return response.json();
}
};

View File

@@ -0,0 +1,38 @@
// frontend/src/types/employee.ts
export interface Employee {
id: string;
email: string;
name: string;
role: 'admin' | 'instandhalter' | 'user';
isActive: boolean;
createdAt: string;
lastLogin?: string;
phone?: string;
department?: string;
}
export interface Availability {
id: string;
employeeId: string;
dayOfWeek: number; // 0-6 (Sonntag-Samstag)
startTime: string; // "08:00"
endTime: string; // "16:00"
isAvailable: boolean;
}
export interface CreateEmployeeRequest {
email: string;
password: string;
name: string;
role: 'admin' | 'instandhalter' | 'user';
phone?: string;
department?: string;
}
export interface UpdateEmployeeRequest {
name?: string;
role?: 'admin' | 'instandhalter' | 'user';
isActive?: boolean;
phone?: string;
department?: string;
}