diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts index a65c30a..0916b13 100644 --- a/backend/src/models/Employee.ts +++ b/backend/src/models/Employee.ts @@ -9,7 +9,7 @@ export interface Employee { phone?: string; department?: string; createdAt: string; - lastLogin?: string; + lastLogin?: string | null; } export interface Availability { diff --git a/backend/src/server.ts b/backend/src/server.ts index 01d1438..78f47ef 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,31 +1,49 @@ -// backend/src/server.ts - Erweitert -import express from 'express'; -import cors from 'cors'; -import { db } from './services/databaseService.js'; -import { seedData } from './scripts/seedData.js'; -import authRoutes from './routes/auth.js'; -import shiftTemplateRoutes from './routes/shiftTemplates.js'; -import shiftPlanRoutes from './routes/shiftPlans.js'; -import employeeRoutes from './routes/employees.js'; // NEU HINZUGEFÜGT +// backend/src/server.ts - Login für alle Benutzer +const express = require('express'); +const cors = require('cors'); +const { v4: uuidv4 } = require('uuid'); +const bcrypt = require('bcryptjs'); const app = express(); const PORT = 3002; -// Middleware +// IN-MEMORY STORE für Mitarbeiter (mit gehashten Passwörtern) +let employees = [ + { + id: '1', + email: 'admin@schichtplan.de', + password: '$2a$10$8K1p/a0dRTlB0ZQ1.5Q.2e5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q', // admin123 + name: 'Admin User', + role: 'admin', + isActive: true, + phone: '+49 123 456789', + department: 'IT', + createdAt: new Date().toISOString(), + lastLogin: new Date().toISOString() + }, + { + id: '2', + email: 'instandhalter@schichtplan.de', + password: '$2a$10$8K1p/a0dRTlB0ZQ1.5Q.2e5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q5Q', // instandhalter123 + name: 'Max Instandhalter', + role: 'instandhalter', + isActive: true, + phone: '+49 123 456790', + department: 'Produktion', + createdAt: new Date().toISOString(), + lastLogin: new Date().toISOString() + } +]; + +// CORS und Middleware app.use(cors({ origin: 'http://localhost:3000', credentials: true })); app.use(express.json()); -// Routes -app.use('/api/auth', authRoutes); -app.use('/api/shift-templates', shiftTemplateRoutes); -app.use('/api/shift-plans', shiftPlanRoutes); -app.use('/api/employees', employeeRoutes); // NEU HINZUGEFÜGT - -// Health check -app.get('/api/health', (req, res) => { +// Health route +app.get('/api/health', (req: any, res: any) => { res.json({ status: 'OK', message: 'Backend läuft!', @@ -33,12 +51,271 @@ app.get('/api/health', (req, res) => { }); }); +// Login Route für ALLE Benutzer +app.post('/api/auth/login', async (req: any, res: any) => { + try { + const { email, password } = req.body; + + console.log('🔐 Login attempt for:', email); + + if (!email || !password) { + return res.status(400).json({ error: 'Email and password are required' }); + } + + // Benutzer in der employees Liste suchen + const user = employees.find(emp => emp.email === email && emp.isActive); + + if (!user) { + console.log('❌ User not found or inactive:', email); + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Passwort vergleichen + // Für Test: Wenn Passwort nicht gehasht ist (neue Benutzer), direkt vergleichen + let isPasswordValid = false; + + if (user.password.startsWith('$2a$')) { + // Gehashtes Passwort (bcrypt) + isPasswordValid = await bcrypt.compare(password, user.password); + } else { + // Klartext-Passwort (für Test) + isPasswordValid = password === user.password; + } + + if (!isPasswordValid) { + console.log('❌ Invalid password for:', email); + return res.status(401).json({ error: 'Invalid credentials' }); + } + + // Last Login aktualisieren + user.lastLogin = new Date().toISOString(); + + console.log('✅ Login successful for:', email); + + // User ohne Passwort zurückgeben + const { password: _, ...userWithoutPassword } = user; + + res.json({ + user: userWithoutPassword, + token: 'jwt-token-' + Date.now() + '-' + user.id, + expiresIn: '7d' + }); + + } catch (error) { + console.error('Login error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// EMPLOYEE ROUTES mit In-Memory Store +app.get('/api/employees', async (req: any, res: any) => { + try { + console.log('📋 Fetching employees - Total:', employees.length); + + // Passwörter ausblenden + const employeesWithoutPasswords = employees.map(emp => { + const { password, ...empWithoutPassword } = emp; + return empWithoutPassword; + }); + + res.json(employeesWithoutPasswords); + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.post('/api/employees', async (req: any, res: any) => { + try { + const { email, password, name, role, phone, department } = req.body; + + console.log('👤 Creating employee:', { email, name, role }); + + // Validierung + if (!email || !password || !name || !role) { + return res.status(400).json({ error: 'Email, password, name and role are required' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: 'Password must be at least 6 characters long' }); + } + + // Check if email already exists + if (employees.find(emp => emp.email === email)) { + return res.status(409).json({ error: 'Email already exists' }); + } + + // Passwort hashen für neue Benutzer + const hashedPassword = await bcrypt.hash(password, 10); + + // Neuen Mitarbeiter erstellen + const newEmployee = { + id: uuidv4(), + email, + password: hashedPassword, // Gehashtes Passwort speichern + name, + role, + isActive: true, + phone: phone || '', + department: department || '', + createdAt: new Date().toISOString(), + lastLogin: '' + }; + + // Zum Store hinzufügen + employees.push(newEmployee); + + console.log('✅ Employee created. Total employees:', employees.length); + + // Response ohne Passwort + const { password: _, ...employeeWithoutPassword } = newEmployee; + res.status(201).json(employeeWithoutPassword); + + } catch (error) { + console.error('Error creating employee:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.put('/api/employees/:id', async (req: any, res: any) => { + try { + const { id } = req.params; + const { name, role, isActive, phone, department } = req.body; + + console.log('✏️ Updating employee:', id); + + // Mitarbeiter finden + const employeeIndex = employees.findIndex(emp => emp.id === id); + if (employeeIndex === -1) { + return res.status(404).json({ error: 'Employee not found' }); + } + + // Mitarbeiter aktualisieren (Passwort bleibt unverändert) + employees[employeeIndex] = { + ...employees[employeeIndex], + name: name || employees[employeeIndex].name, + role: role || employees[employeeIndex].role, + isActive: isActive !== undefined ? isActive : employees[employeeIndex].isActive, + phone: phone || employees[employeeIndex].phone, + department: department || employees[employeeIndex].department + }; + + console.log('✅ Employee updated:', employees[employeeIndex].name); + + // Response ohne Passwort + const { password, ...employeeWithoutPassword } = employees[employeeIndex]; + res.json(employeeWithoutPassword); + + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.delete('/api/employees/:id', async (req: any, res: any) => { + try { + const { id } = req.params; + console.log('🗑️ Deleting employee:', id); + + // Mitarbeiter finden + const employeeIndex = employees.findIndex(emp => emp.id === id); + if (employeeIndex === -1) { + return res.status(404).json({ error: 'Employee not found' }); + } + + // Soft delete - set isActive to false + employees[employeeIndex].isActive = false; + + console.log('✅ Employee deactivated:', employees[employeeIndex].name); + res.status(204).send(); + + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Get current user profile +app.get('/api/auth/me', async (req: any, res: any) => { + try { + // Einfache Mock-Implementation + // In einer echten App würde man den Token verifizieren + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ error: 'No token provided' }); + } + + // Einfache Token-"Validierung" für Demo + const tokenParts = token.split('-'); + const userId = tokenParts[tokenParts.length - 1]; + + const user = employees.find(emp => emp.id === userId && emp.isActive); + + if (!user) { + return res.status(401).json({ error: 'Invalid token' }); + } + + // User ohne Passwort zurückgeben + const { password, ...userWithoutPassword } = user; + res.json(userWithoutPassword); + + } catch (error) { + console.error('Error getting user profile:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Availability Routes (Mock) +app.get('/api/employees/:employeeId/availabilities', async (req: any, res: any) => { + try { + const { employeeId } = req.params; + console.log('📅 Fetching availabilities for:', employeeId); + + // Mock Verfügbarkeiten + const daysOfWeek = [0, 1, 2, 3, 4, 5, 6]; + const timeSlots = [ + { name: 'Vormittag', start: '08:00', end: '12:00' }, + { name: 'Nachmittag', start: '12:00', end: '16:00' }, + { name: 'Abend', start: '16:00', end: '20:00' } + ]; + + const mockAvailabilities = daysOfWeek.flatMap(day => + timeSlots.map((slot, index) => ({ + id: `avail-${employeeId}-${day}-${index}`, + employeeId, + dayOfWeek: day, + startTime: slot.start, + endTime: slot.end, + isAvailable: day >= 1 && day <= 5 // Nur Mo-Fr verfügbar + })) + ); + + res.json(mockAvailabilities); + + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + +app.put('/api/employees/:employeeId/availabilities', async (req: any, res: any) => { + try { + const { employeeId } = req.params; + const availabilities = req.body; + + console.log('💾 Saving availabilities for:', employeeId); + console.log('Data:', availabilities); + + // Mock erfolgreiches Speichern + res.json(availabilities); + + } catch (error) { + res.status(500).json({ error: 'Internal server error' }); + } +}); + // Start server -app.listen(PORT, async () => { +app.listen(PORT, () => { console.log('🎉 BACKEND STARTED SUCCESSFULLY!'); console.log(`📍 Port: ${PORT}`); console.log(`📍 Health: http://localhost:${PORT}/api/health`); - console.log('📊 Employee management ready!'); - - await seedData(); + console.log('🔐 Login system READY for ALL users!'); + console.log('👥 Employee management READY with proper authentication!'); }); \ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 858ec2c..72e3d36 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,11 +1,10 @@ // frontend/src/contexts/AuthContext.tsx import React, { createContext, useContext, useState, useEffect } from 'react'; -import { authService, User, LoginRequest, RegisterRequest } from '../services/authService'; +import { authService, User, LoginRequest } from '../services/authService'; interface AuthContextType { user: User | null; login: (credentials: LoginRequest) => Promise; - register: (userData: RegisterRequest) => Promise; logout: () => void; hasRole: (roles: string[]) => boolean; loading: boolean; @@ -19,43 +18,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children useEffect(() => { // User aus localStorage laden beim Start - const initAuth = async () => { - const savedUser = authService.getCurrentUser(); - if (savedUser) { - setUser(savedUser); - } - setLoading(false); - }; - - initAuth(); + const savedUser = authService.getCurrentUser(); + if (savedUser) { + setUser(savedUser); + } + setLoading(false); }, []); const login = async (credentials: LoginRequest) => { try { const response = await authService.login(credentials); - setUser(response.user); // ← WICHTIG: User State updaten! - console.log('AuthContext: User nach Login gesetzt', response.user); + setUser(response.user); } catch (error) { console.error('AuthContext: Login fehlgeschlagen', error); throw error; } }; - const register = async (userData: RegisterRequest) => { - try { - const response = await authService.register(userData); - setUser(response.user); - console.log('AuthContext: User nach Registrierung gesetzt', response.user); - } catch (error) { - console.error('AuthContext: Registrierung fehlgeschlagen', error); - throw error; - } - }; - const logout = () => { authService.logout(); setUser(null); - console.log('AuthContext: User nach Logout entfernt'); }; const hasRole = (roles: string[]) => { @@ -65,7 +47,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const value = { user, login, - register, logout, hasRole, loading diff --git a/frontend/src/pages/Employees/EmployeeManagement.tsx b/frontend/src/pages/Employees/EmployeeManagement.tsx index 9486ea9..f8a85d5 100644 --- a/frontend/src/pages/Employees/EmployeeManagement.tsx +++ b/frontend/src/pages/Employees/EmployeeManagement.tsx @@ -24,9 +24,13 @@ const EmployeeManagement: React.FC = () => { const loadEmployees = async () => { try { setLoading(true); + setError(''); + console.log('🔄 Loading employees...'); const data = await employeeService.getEmployees(); + console.log('✅ Employees loaded:', data); setEmployees(data); } catch (err: any) { + console.error('❌ Error loading employees:', err); setError(err.message || 'Fehler beim Laden der Mitarbeiter'); } finally { setLoading(false); @@ -51,15 +55,19 @@ const EmployeeManagement: React.FC = () => { const handleBackToList = () => { setViewMode('list'); setSelectedEmployee(null); - loadEmployees(); // Daten aktualisieren }; + // KORRIGIERT: Explizit Daten neu laden nach Create/Update const handleEmployeeCreated = () => { - handleBackToList(); + console.log('🔄 Reloading employees after creation...'); + loadEmployees(); // Daten neu laden + setViewMode('list'); // Zurück zur Liste }; const handleEmployeeUpdated = () => { - handleBackToList(); + console.log('🔄 Reloading employees after update...'); + loadEmployees(); // Daten neu laden + setViewMode('list'); // Zurück zur Liste }; const handleDeleteEmployee = async (employeeId: string) => { @@ -75,6 +83,13 @@ const EmployeeManagement: React.FC = () => { } }; + // Debug: Zeige aktuellen State + console.log('📊 Current state:', { + viewMode, + employeesCount: employees.length, + selectedEmployee: selectedEmployee?.name + }); + if (loading && viewMode === 'list') { return (
@@ -97,7 +112,7 @@ const EmployeeManagement: React.FC = () => {

👥 Mitarbeiter Verwaltung

- Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten + {employees.length} Mitarbeiter gefunden

@@ -197,7 +212,7 @@ const EmployeeManagement: React.FC = () => { {viewMode === 'availability' && selectedEmployee && ( )} diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index c372517..b741155 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -152,13 +152,13 @@ const AvailabilityManager: React.FC = ({ overflow: 'hidden', marginBottom: '30px' }}> - {daysOfWeek.map(day => { + {daysOfWeek.map((day, dayIndex) => { const dayAvailabilities = getAvailabilitiesForDay(day.id); + const isLastDay = dayIndex === daysOfWeek.length - 1; return (
{/* Tag Header */}
= ({ {/* Zeit-Slots */}
- {dayAvailabilities.map(availability => ( -
- {/* Verfügbarkeit Toggle */} -
- handleAvailabilityChange(availability.id, e.target.checked)} - style={{ width: '18px', height: '18px' }} - /> - -
+ {dayAvailabilities.map((availability, availabilityIndex) => { + const isLastAvailability = availabilityIndex === dayAvailabilities.length - 1; + + return ( +
+ {/* 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' - }} - /> -
+ {/* 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' - }} - /> -
+ {/* 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'} - + {/* Status Badge */} +
+ + {availability.isAvailable ? 'Aktiv' : 'Inaktiv'} + +
-
- ))} + ); + })}
); diff --git a/frontend/src/services/authService.ts b/frontend/src/services/authService.ts index 4aadcee..d588662 100644 --- a/frontend/src/services/authService.ts +++ b/frontend/src/services/authService.ts @@ -11,6 +11,8 @@ export interface RegisterRequest { password: string; name: string; role?: string; + phone?: string; + department?: string; } export interface AuthResponse { @@ -25,6 +27,10 @@ export interface User { name: string; role: 'admin' | 'instandhalter' | 'user'; createdAt: string; + lastLogin?: string; + phone?: string; + department?: string; + isActive?: boolean; } class AuthService { @@ -38,7 +44,8 @@ class AuthService { }); if (!response.ok) { - throw new Error('Login fehlgeschlagen'); + const errorData = await response.json(); + throw new Error(errorData.error || 'Login fehlgeschlagen'); } const data: AuthResponse = await response.json(); @@ -49,23 +56,56 @@ class AuthService { return data; } + // Register Methode hinzufügen async register(userData: RegisterRequest): Promise { - const response = await fetch(`${API_BASE}/auth/register`, { + const response = await fetch(`${API_BASE}/employees`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(userData) }); if (!response.ok) { - throw new Error('Registrierung fehlgeschlagen'); + const errorData = await response.json(); + throw new Error(errorData.error || 'Registrierung fehlgeschlagen'); } - const data: AuthResponse = await response.json(); - this.token = data.token; - localStorage.setItem('token', data.token); - localStorage.setItem('user', JSON.stringify(data.user)); - - return data; + // Nach der Erstellung automatisch einloggen + return this.login({ + email: userData.email, + password: userData.password + }); + } + + // getCurrentUser als SYNCHRON machen + getCurrentUser(): User | null { + const userStr = localStorage.getItem('user'); + return userStr ? JSON.parse(userStr) : null; + } + + // Asynchrone Methode für Server-Abfrage + async fetchCurrentUser(): Promise { + const token = this.getToken(); + if (!token) { + return null; + } + + try { + const response = await fetch(`${API_BASE}/auth/me`, { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const user = await response.json(); + localStorage.setItem('user', JSON.stringify(user)); + return user; + } + } catch (error) { + console.error('Error fetching current user:', error); + } + + return null; } logout(): void { @@ -81,11 +121,6 @@ class AuthService { return this.token; } - getCurrentUser(): User | null { - const userStr = localStorage.getItem('user'); - return userStr ? JSON.parse(userStr) : null; - } - isAuthenticated(): boolean { return this.getToken() !== null; } diff --git a/frontend/src/types/employee.ts b/frontend/src/types/employee.ts index fa2444f..26b8762 100644 --- a/frontend/src/types/employee.ts +++ b/frontend/src/types/employee.ts @@ -6,7 +6,7 @@ export interface Employee { role: 'admin' | 'instandhalter' | 'user'; isActive: boolean; createdAt: string; - lastLogin?: string; + lastLogin?: string | null; phone?: string; department?: string; }