mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added frontend user management
This commit is contained in:
@@ -11,11 +11,11 @@ const Navigation: React.FC = () => {
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
const navigationItems = [
|
||||
{ path: '/', label: '🏠 Dashboard', 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: '/settings', label: '⚙️ Einstellungen', icon: '⚙️', roles: ['admin'] },
|
||||
{ path: '/help', label: '❓ Hilfe', 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: '/employees', label: 'Mitarbeiter', icon: '👥', roles: ['admin', 'instandhalter'] },
|
||||
{ path: '/settings', label: 'Einstellungen', icon: '⚙️', roles: ['admin'] },
|
||||
{ path: '/help', label: 'Hilfe', icon: '❓', roles: ['admin', 'instandhalter', 'user'] },
|
||||
];
|
||||
|
||||
const filteredNavigation = navigationItems.filter(item =>
|
||||
|
||||
@@ -1,26 +1,206 @@
|
||||
// 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 [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 (
|
||||
<div>
|
||||
<h1>👥 Mitarbeiter Verwaltung</h1>
|
||||
|
||||
{/* Header mit Titel und Aktionen */}
|
||||
<div style={{
|
||||
padding: '40px',
|
||||
textAlign: 'center',
|
||||
backgroundColor: '#f8f9fa',
|
||||
borderRadius: '8px',
|
||||
border: '2px dashed #dee2e6',
|
||||
marginTop: '20px'
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '30px',
|
||||
flexWrap: 'wrap',
|
||||
gap: '15px'
|
||||
}}>
|
||||
<div style={{ fontSize: '48px', marginBottom: '20px' }}>👥</div>
|
||||
<h3>Mitarbeiter Übersicht</h3>
|
||||
<p>Hier können Sie Mitarbeiter verwalten und deren Verfügbarkeiten einsehen.</p>
|
||||
<p style={{ fontSize: '14px', color: '#6c757d' }}>
|
||||
Diese Seite wird demnächst mit Funktionen gefüllt.
|
||||
<div>
|
||||
<h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1>
|
||||
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
|
||||
Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
329
frontend/src/pages/Employees/components/AvailabilityManager.tsx
Normal file
329
frontend/src/pages/Employees/components/AvailabilityManager.tsx
Normal 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;
|
||||
370
frontend/src/pages/Employees/components/EmployeeForm.tsx
Normal file
370
frontend/src/pages/Employees/components/EmployeeForm.tsx
Normal 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;
|
||||
285
frontend/src/pages/Employees/components/EmployeeList.tsx
Normal file
285
frontend/src/pages/Employees/components/EmployeeList.tsx
Normal 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;
|
||||
@@ -54,3 +54,5 @@ const ShiftPlanCreate: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShiftPlanCreate;
|
||||
124
frontend/src/services/employeeService.ts
Normal file
124
frontend/src/services/employeeService.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
38
frontend/src/types/employee.ts
Normal file
38
frontend/src/types/employee.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user