mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
added hashed passwords
This commit is contained in:
@@ -9,7 +9,7 @@ export interface Employee {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
department?: string;
|
department?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastLogin?: string;
|
lastLogin?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Availability {
|
export interface Availability {
|
||||||
|
|||||||
@@ -1,31 +1,49 @@
|
|||||||
// backend/src/server.ts - Erweitert
|
// backend/src/server.ts - Login für alle Benutzer
|
||||||
import express from 'express';
|
const express = require('express');
|
||||||
import cors from 'cors';
|
const cors = require('cors');
|
||||||
import { db } from './services/databaseService.js';
|
const { v4: uuidv4 } = require('uuid');
|
||||||
import { seedData } from './scripts/seedData.js';
|
const bcrypt = require('bcryptjs');
|
||||||
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
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
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({
|
app.use(cors({
|
||||||
origin: 'http://localhost:3000',
|
origin: 'http://localhost:3000',
|
||||||
credentials: true
|
credentials: true
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Routes
|
// Health route
|
||||||
app.use('/api/auth', authRoutes);
|
app.get('/api/health', (req: any, res: any) => {
|
||||||
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) => {
|
|
||||||
res.json({
|
res.json({
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
message: 'Backend läuft!',
|
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
|
// Start server
|
||||||
app.listen(PORT, async () => {
|
app.listen(PORT, () => {
|
||||||
console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
|
console.log('🎉 BACKEND STARTED SUCCESSFULLY!');
|
||||||
console.log(`📍 Port: ${PORT}`);
|
console.log(`📍 Port: ${PORT}`);
|
||||||
console.log(`📍 Health: http://localhost:${PORT}/api/health`);
|
console.log(`📍 Health: http://localhost:${PORT}/api/health`);
|
||||||
console.log('📊 Employee management ready!');
|
console.log('🔐 Login system READY for ALL users!');
|
||||||
|
console.log('👥 Employee management READY with proper authentication!');
|
||||||
await seedData();
|
|
||||||
});
|
});
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
// frontend/src/contexts/AuthContext.tsx
|
// frontend/src/contexts/AuthContext.tsx
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
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 {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
login: (credentials: LoginRequest) => Promise<void>;
|
login: (credentials: LoginRequest) => Promise<void>;
|
||||||
register: (userData: RegisterRequest) => Promise<void>;
|
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
hasRole: (roles: string[]) => boolean;
|
hasRole: (roles: string[]) => boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -19,43 +18,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// User aus localStorage laden beim Start
|
// User aus localStorage laden beim Start
|
||||||
const initAuth = async () => {
|
const savedUser = authService.getCurrentUser();
|
||||||
const savedUser = authService.getCurrentUser();
|
if (savedUser) {
|
||||||
if (savedUser) {
|
setUser(savedUser);
|
||||||
setUser(savedUser);
|
}
|
||||||
}
|
setLoading(false);
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
initAuth();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (credentials: LoginRequest) => {
|
const login = async (credentials: LoginRequest) => {
|
||||||
try {
|
try {
|
||||||
const response = await authService.login(credentials);
|
const response = await authService.login(credentials);
|
||||||
setUser(response.user); // ← WICHTIG: User State updaten!
|
setUser(response.user);
|
||||||
console.log('AuthContext: User nach Login gesetzt', response.user);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('AuthContext: Login fehlgeschlagen', error);
|
console.error('AuthContext: Login fehlgeschlagen', error);
|
||||||
throw 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 = () => {
|
const logout = () => {
|
||||||
authService.logout();
|
authService.logout();
|
||||||
setUser(null);
|
setUser(null);
|
||||||
console.log('AuthContext: User nach Logout entfernt');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRole = (roles: string[]) => {
|
const hasRole = (roles: string[]) => {
|
||||||
@@ -65,7 +47,6 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
const value = {
|
const value = {
|
||||||
user,
|
user,
|
||||||
login,
|
login,
|
||||||
register,
|
|
||||||
logout,
|
logout,
|
||||||
hasRole,
|
hasRole,
|
||||||
loading
|
loading
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
const loadEmployees = async () => {
|
const loadEmployees = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError('');
|
||||||
|
console.log('🔄 Loading employees...');
|
||||||
const data = await employeeService.getEmployees();
|
const data = await employeeService.getEmployees();
|
||||||
|
console.log('✅ Employees loaded:', data);
|
||||||
setEmployees(data);
|
setEmployees(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
console.error('❌ Error loading employees:', err);
|
||||||
setError(err.message || 'Fehler beim Laden der Mitarbeiter');
|
setError(err.message || 'Fehler beim Laden der Mitarbeiter');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -51,15 +55,19 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
const handleBackToList = () => {
|
const handleBackToList = () => {
|
||||||
setViewMode('list');
|
setViewMode('list');
|
||||||
setSelectedEmployee(null);
|
setSelectedEmployee(null);
|
||||||
loadEmployees(); // Daten aktualisieren
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// KORRIGIERT: Explizit Daten neu laden nach Create/Update
|
||||||
const handleEmployeeCreated = () => {
|
const handleEmployeeCreated = () => {
|
||||||
handleBackToList();
|
console.log('🔄 Reloading employees after creation...');
|
||||||
|
loadEmployees(); // Daten neu laden
|
||||||
|
setViewMode('list'); // Zurück zur Liste
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmployeeUpdated = () => {
|
const handleEmployeeUpdated = () => {
|
||||||
handleBackToList();
|
console.log('🔄 Reloading employees after update...');
|
||||||
|
loadEmployees(); // Daten neu laden
|
||||||
|
setViewMode('list'); // Zurück zur Liste
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteEmployee = async (employeeId: string) => {
|
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') {
|
if (loading && viewMode === 'list') {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
@@ -97,7 +112,7 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1>
|
<h1 style={{ margin: 0, color: '#2c3e50' }}>👥 Mitarbeiter Verwaltung</h1>
|
||||||
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
|
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
|
||||||
Verwalten Sie Mitarbeiterkonten und Verfügbarkeiten
|
{employees.length} Mitarbeiter gefunden
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,7 +212,7 @@ const EmployeeManagement: React.FC = () => {
|
|||||||
{viewMode === 'availability' && selectedEmployee && (
|
{viewMode === 'availability' && selectedEmployee && (
|
||||||
<AvailabilityManager
|
<AvailabilityManager
|
||||||
employee={selectedEmployee}
|
employee={selectedEmployee}
|
||||||
onSave={handleBackToList}
|
onSave={handleEmployeeUpdated}
|
||||||
onCancel={handleBackToList}
|
onCancel={handleBackToList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -152,13 +152,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginBottom: '30px'
|
marginBottom: '30px'
|
||||||
}}>
|
}}>
|
||||||
{daysOfWeek.map(day => {
|
{daysOfWeek.map((day, dayIndex) => {
|
||||||
const dayAvailabilities = getAvailabilitiesForDay(day.id);
|
const dayAvailabilities = getAvailabilitiesForDay(day.id);
|
||||||
|
const isLastDay = dayIndex === daysOfWeek.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={day.id} style={{
|
<div key={day.id} style={{
|
||||||
borderBottom: '1px solid #f0f0f0',
|
borderBottom: isLastDay ? 'none' : '1px solid #f0f0f0'
|
||||||
':last-child': { borderBottom: 'none' }
|
|
||||||
}}>
|
}}>
|
||||||
{/* Tag Header */}
|
{/* Tag Header */}
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -173,96 +173,99 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
|
|
||||||
{/* Zeit-Slots */}
|
{/* Zeit-Slots */}
|
||||||
<div style={{ padding: '15px 20px' }}>
|
<div style={{ padding: '15px 20px' }}>
|
||||||
{dayAvailabilities.map(availability => (
|
{dayAvailabilities.map((availability, availabilityIndex) => {
|
||||||
<div
|
const isLastAvailability = availabilityIndex === dayAvailabilities.length - 1;
|
||||||
key={availability.id}
|
|
||||||
style={{
|
return (
|
||||||
display: 'grid',
|
<div
|
||||||
gridTemplateColumns: '1fr auto auto auto',
|
key={availability.id}
|
||||||
gap: '15px',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'grid',
|
||||||
padding: '10px 0',
|
gridTemplateColumns: '1fr auto auto auto',
|
||||||
borderBottom: '1px solid #f8f9fa',
|
gap: '15px',
|
||||||
':last-child': { borderBottom: 'none' }
|
alignItems: 'center',
|
||||||
}}
|
padding: '10px 0',
|
||||||
>
|
borderBottom: isLastAvailability ? 'none' : '1px solid #f8f9fa'
|
||||||
{/* Verfügbarkeit Toggle */}
|
}}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
>
|
||||||
<input
|
{/* Verfügbarkeit Toggle */}
|
||||||
type="checkbox"
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||||
id={`avail-${availability.id}`}
|
<input
|
||||||
checked={availability.isAvailable}
|
type="checkbox"
|
||||||
onChange={(e) => handleAvailabilityChange(availability.id, e.target.checked)}
|
id={`avail-${availability.id}`}
|
||||||
style={{ width: '18px', height: '18px' }}
|
checked={availability.isAvailable}
|
||||||
/>
|
onChange={(e) => handleAvailabilityChange(availability.id, e.target.checked)}
|
||||||
<label
|
style={{ width: '18px', height: '18px' }}
|
||||||
htmlFor={`avail-${availability.id}`}
|
/>
|
||||||
style={{
|
<label
|
||||||
fontWeight: 'bold',
|
htmlFor={`avail-${availability.id}`}
|
||||||
color: availability.isAvailable ? '#27ae60' : '#95a5a6'
|
style={{
|
||||||
}}
|
fontWeight: 'bold',
|
||||||
>
|
color: availability.isAvailable ? '#27ae60' : '#95a5a6'
|
||||||
{availability.isAvailable ? 'Verfügbar' : 'Nicht verfügbar'}
|
}}
|
||||||
</label>
|
>
|
||||||
</div>
|
{availability.isAvailable ? 'Verfügbar' : 'Nicht verfügbar'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Startzeit */}
|
{/* Startzeit */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
|
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
|
||||||
Von
|
Von
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={availability.startTime}
|
value={availability.startTime}
|
||||||
onChange={(e) => handleTimeChange(availability.id, 'startTime', e.target.value)}
|
onChange={(e) => handleTimeChange(availability.id, 'startTime', e.target.value)}
|
||||||
disabled={!availability.isAvailable}
|
disabled={!availability.isAvailable}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
|
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
|
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
|
||||||
color: availability.isAvailable ? '#333' : '#999'
|
color: availability.isAvailable ? '#333' : '#999'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Endzeit */}
|
{/* Endzeit */}
|
||||||
<div>
|
<div>
|
||||||
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
|
<label style={{ fontSize: '12px', color: '#7f8c8d', display: 'block', marginBottom: '4px' }}>
|
||||||
Bis
|
Bis
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={availability.endTime}
|
value={availability.endTime}
|
||||||
onChange={(e) => handleTimeChange(availability.id, 'endTime', e.target.value)}
|
onChange={(e) => handleTimeChange(availability.id, 'endTime', e.target.value)}
|
||||||
disabled={!availability.isAvailable}
|
disabled={!availability.isAvailable}
|
||||||
style={{
|
style={{
|
||||||
padding: '6px 8px',
|
padding: '6px 8px',
|
||||||
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
|
border: `1px solid ${availability.isAvailable ? '#ddd' : '#f0f0f0'}`,
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
|
backgroundColor: availability.isAvailable ? 'white' : '#f8f9fa',
|
||||||
color: availability.isavailable ? '#333' : '#999'
|
color: availability.isAvailable ? '#333' : '#999'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Badge */}
|
{/* Status Badge */}
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: availability.isAvailable ? '#d5f4e6' : '#fadbd8',
|
backgroundColor: availability.isAvailable ? '#d5f4e6' : '#fadbd8',
|
||||||
color: availability.isAvailable ? '#27ae60' : '#e74c3c',
|
color: availability.isAvailable ? '#27ae60' : '#e74c3c',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{availability.isAvailable ? 'Aktiv' : 'Inaktiv'}
|
{availability.isAvailable ? 'Aktiv' : 'Inaktiv'}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export interface RegisterRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
name: string;
|
name: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
|
phone?: string;
|
||||||
|
department?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
@@ -25,6 +27,10 @@ export interface User {
|
|||||||
name: string;
|
name: string;
|
||||||
role: 'admin' | 'instandhalter' | 'user';
|
role: 'admin' | 'instandhalter' | 'user';
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
lastLogin?: string;
|
||||||
|
phone?: string;
|
||||||
|
department?: string;
|
||||||
|
isActive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthService {
|
class AuthService {
|
||||||
@@ -38,7 +44,8 @@ class AuthService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const data: AuthResponse = await response.json();
|
||||||
@@ -49,23 +56,56 @@ class AuthService {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register Methode hinzufügen
|
||||||
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
async register(userData: RegisterRequest): Promise<AuthResponse> {
|
||||||
const response = await fetch(`${API_BASE}/auth/register`, {
|
const response = await fetch(`${API_BASE}/employees`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(userData)
|
body: JSON.stringify(userData)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
// Nach der Erstellung automatisch einloggen
|
||||||
this.token = data.token;
|
return this.login({
|
||||||
localStorage.setItem('token', data.token);
|
email: userData.email,
|
||||||
localStorage.setItem('user', JSON.stringify(data.user));
|
password: userData.password
|
||||||
|
});
|
||||||
return data;
|
}
|
||||||
|
|
||||||
|
// 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<User | null> {
|
||||||
|
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 {
|
logout(): void {
|
||||||
@@ -81,11 +121,6 @@ class AuthService {
|
|||||||
return this.token;
|
return this.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUser(): User | null {
|
|
||||||
const userStr = localStorage.getItem('user');
|
|
||||||
return userStr ? JSON.parse(userStr) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isAuthenticated(): boolean {
|
isAuthenticated(): boolean {
|
||||||
return this.getToken() !== null;
|
return this.getToken() !== null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface Employee {
|
|||||||
role: 'admin' | 'instandhalter' | 'user';
|
role: 'admin' | 'instandhalter' | 'user';
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastLogin?: string;
|
lastLogin?: string | null;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
department?: string;
|
department?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user