From 4e120c87893c1c30051cf24c991226e692cc0c87 Mon Sep 17 00:00:00 2001 From: donpat1to Date: Wed, 8 Oct 2025 15:25:03 +0200 Subject: [PATCH] added frontend user management --- frontend/src/components/Layout/Navigation.tsx | 14 +- .../pages/Employees/EmployeeManagement.tsx | 210 +++++++++- .../components/AvailabilityManager.tsx | 329 ++++++++++++++++ .../Employees/components/EmployeeForm.tsx | 370 ++++++++++++++++++ .../Employees/components/EmployeeList.tsx | 285 ++++++++++++++ .../src/pages/ShiftPlans/ShiftPlanCreate.tsx | 4 +- frontend/src/services/employeeService.ts | 124 ++++++ frontend/src/types/employee.ts | 38 ++ 8 files changed, 1351 insertions(+), 23 deletions(-) create mode 100644 frontend/src/pages/Employees/components/AvailabilityManager.tsx create mode 100644 frontend/src/pages/Employees/components/EmployeeForm.tsx create mode 100644 frontend/src/pages/Employees/components/EmployeeList.tsx create mode 100644 frontend/src/services/employeeService.ts create mode 100644 frontend/src/types/employee.ts diff --git a/frontend/src/components/Layout/Navigation.tsx b/frontend/src/components/Layout/Navigation.tsx index 83ebd7d..49837f9 100644 --- a/frontend/src/components/Layout/Navigation.tsx +++ b/frontend/src/components/Layout/Navigation.tsx @@ -10,13 +10,13 @@ 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'] }, - ]; +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'] }, +]; const filteredNavigation = navigationItems.filter(item => hasRole(item.roles) diff --git a/frontend/src/pages/Employees/EmployeeManagement.tsx b/frontend/src/pages/Employees/EmployeeManagement.tsx index 861a42b..9486ea9 100644 --- a/frontend/src/pages/Employees/EmployeeManagement.tsx +++ b/frontend/src/pages/Employees/EmployeeManagement.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [viewMode, setViewMode] = useState('list'); + const [selectedEmployee, setSelectedEmployee] = useState(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 ( +
+
⏳ Lade Mitarbeiter...
+
+ ); + } + return (
-

👥 Mitarbeiter Verwaltung

- + {/* Header mit Titel und Aktionen */}
-
👥
-

Mitarbeiter Übersicht

-

Hier können Sie Mitarbeiter verwalten und deren Verfügbarkeiten einsehen.

-

- Diese Seite wird demnächst mit Funktionen gefüllt. -

+
+

👥 Mitarbeiter Verwaltung

+

+ Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten +

+
+ + {viewMode === 'list' && hasRole(['admin']) && ( + + )} + + {viewMode !== 'list' && ( + + )}
+ + {/* Fehleranzeige */} + {error && ( +
+ Fehler: {error} + +
+ )} + + {/* Inhalt basierend auf View Mode */} + {viewMode === 'list' && ( + + )} + + {viewMode === 'create' && ( + + )} + + {viewMode === 'edit' && selectedEmployee && ( + + )} + + {viewMode === 'availability' && selectedEmployee && ( + + )}
); }; diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx new file mode 100644 index 0000000..c372517 --- /dev/null +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -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 = ({ + employee, + onSave, + onCancel +}) => { + const [availabilities, setAvailabilities] = useState([]); + 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 ( +
+
⏳ Lade Verfügbarkeiten...
+
+ ); + } + + return ( +
+

+ 📅 Verfügbarkeit verwalten +

+ +
+

+ {employee.name} +

+

+ Legen Sie fest, an welchen Tagen und Zeiten {employee.name} verfügbar ist. +

+
+ + {error && ( +
+ Fehler: {error} +
+ )} + + {/* Verfügbarkeiten Tabelle */} +
+ {daysOfWeek.map(day => { + const dayAvailabilities = getAvailabilitiesForDay(day.id); + + return ( +
+ {/* Tag Header */} +
+ {day.name} +
+ + {/* Zeit-Slots */} +
+ {dayAvailabilities.map(availability => ( +
+ {/* Verfügbarkeit Toggle */} +
+ handleAvailabilityChange(availability.id, e.target.checked)} + style={{ width: '18px', height: '18px' }} + /> + +
+ + {/* Startzeit */} +
+ + 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' + }} + /> +
+ + {/* Endzeit */} +
+ + 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' + }} + /> +
+ + {/* Status Badge */} +
+ + {availability.isAvailable ? 'Aktiv' : 'Inaktiv'} + +
+
+ ))} +
+
+ ); + })} +
+ + {/* Info Text */} +
+

💡 Information

+

+ 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. +

+
+ + {/* Buttons */} +
+ + + +
+
+ ); +}; + +export default AvailabilityManager; \ No newline at end of file diff --git a/frontend/src/pages/Employees/components/EmployeeForm.tsx b/frontend/src/pages/Employees/components/EmployeeForm.tsx new file mode 100644 index 0000000..e84318a --- /dev/null +++ b/frontend/src/pages/Employees/components/EmployeeForm.tsx @@ -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 = ({ + 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) => { + 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 ( +
+

+ {mode === 'create' ? '👤 Neuen Mitarbeiter erstellen' : '✏️ Mitarbeiter bearbeiten'} +

+ + {error && ( +
+ Fehler: {error} +
+ )} + +
+
+ {/* Name */} +
+ + +
+ + {/* E-Mail */} +
+ + +
+ + {/* Passwort (nur bei Erstellung) */} + {mode === 'create' && ( +
+ + +
+ Das Passwort muss mindestens 6 Zeichen lang sein. +
+
+ )} + + {/* Rolle */} +
+ + +
+ {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.'} +
+
+ + {/* Telefon */} +
+ + +
+ + {/* Abteilung */} +
+ + +
+ + {/* Aktiv Status (nur beim Bearbeiten) */} + {mode === 'edit' && ( +
+ + +
+ )} +
+ + {/* Buttons */} +
+ + + +
+
+
+ ); +}; + +export default EmployeeForm; \ No newline at end of file diff --git a/frontend/src/pages/Employees/components/EmployeeList.tsx b/frontend/src/pages/Employees/components/EmployeeList.tsx new file mode 100644 index 0000000..4867ce1 --- /dev/null +++ b/frontend/src/pages/Employees/components/EmployeeList.tsx @@ -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 = ({ + 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 ( +
+
👥
+

Noch keine Mitarbeiter

+

+ Erstellen Sie den ersten Mitarbeiter, um zu beginnen. +

+
+ ); + } + + return ( +
+ {/* Filter und Suche */} +
+
+ + +
+ +
+ + setSearchTerm(e.target.value)} + style={{ + padding: '8px 12px', + border: '1px solid #ddd', + borderRadius: '4px', + flex: 1, + maxWidth: '400px' + }} + /> +
+ +
+ {filteredEmployees.length} von {employees.length} Mitarbeitern +
+
+ + {/* Mitarbeiter Tabelle */} +
+
+
Name & E-Mail
+
Abteilung
+
Rolle
+
Status
+
Letzter Login
+
Aktionen
+
+ + {filteredEmployees.map(employee => { + const status = getStatusBadge(employee.isActive); + + return ( +
+ {/* Name & E-Mail */} +
+
+ {employee.name} +
+
+ {employee.email} +
+
+ + {/* Abteilung */} +
+ {employee.department || ( + Nicht zugewiesen + )} +
+ + {/* Rolle */} +
+ + {employee.role} + +
+ + {/* Status */} +
+ + {status.text} + +
+ + {/* Letzter Login */} +
+ {employee.lastLogin + ? new Date(employee.lastLogin).toLocaleDateString('de-DE') + : 'Noch nie' + } +
+ + {/* Aktionen */} +
+ + + {(currentUserRole === 'admin' || employee.role !== 'admin') && ( + + )} + + {currentUserRole === 'admin' && employee.role !== 'admin' && ( + + )} +
+
+ ); + })} +
+
+ ); +}; + +export default EmployeeList; \ No newline at end of file diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx index e9a7326..89d7063 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanCreate.tsx @@ -53,4 +53,6 @@ const ShiftPlanCreate: React.FC = () => { ); -}; \ No newline at end of file +}; + +export default ShiftPlanCreate; \ No newline at end of file diff --git a/frontend/src/services/employeeService.ts b/frontend/src/services/employeeService.ts new file mode 100644 index 0000000..bc1cfec --- /dev/null +++ b/frontend/src/services/employeeService.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(); + } +}; \ No newline at end of file diff --git a/frontend/src/types/employee.ts b/frontend/src/types/employee.ts new file mode 100644 index 0000000..fa2444f --- /dev/null +++ b/frontend/src/types/employee.ts @@ -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; +} \ No newline at end of file