diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts new file mode 100644 index 0000000..8c8625b --- /dev/null +++ b/backend/src/controllers/employeeController.ts @@ -0,0 +1,255 @@ +// backend/src/controllers/employeeController.ts +import { Request, Response } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import bcrypt from 'bcryptjs'; +import { db } from '../services/databaseService.js'; +import { AuthRequest } from '../middleware/auth.js'; + +export const getEmployees = async (req: AuthRequest, res: Response): Promise => { + try { + const employees = await db.all(` + SELECT + id, email, name, role, is_active as isActive, + phone, department, created_at as createdAt, + last_login as lastLogin + FROM users + ORDER BY name + `); + + res.json(employees); + } catch (error) { + console.error('Error fetching employees:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const getEmployee = async (req: AuthRequest, res: Response): Promise => { + try { + const { id } = req.params; + + const employee = await db.get(` + SELECT + id, email, name, role, is_active as isActive, + phone, department, created_at as createdAt, + last_login as lastLogin + FROM users + WHERE id = ? + `, [id]); + + if (!employee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + res.json(employee); + } catch (error) { + console.error('Error fetching employee:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const createEmployee = async (req: AuthRequest, res: Response): Promise => { + try { + const { email, password, name, role, phone, department } = req.body; + + // Validierung + if (!email || !password || !name || !role) { + res.status(400).json({ error: 'Email, password, name and role are required' }); + return; + } + + if (password.length < 6) { + res.status(400).json({ error: 'Password must be at least 6 characters long' }); + return; + } + + // Check if email already exists + const existingUser = await db.get('SELECT id FROM users WHERE email = ?', [email]); + if (existingUser) { + res.status(409).json({ error: 'Email already exists' }); + return; + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + const employeeId = uuidv4(); + + await db.run( + `INSERT INTO users (id, email, password, name, role, phone, department, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [employeeId, email, hashedPassword, name, role, phone, department, 1] + ); + + // Return employee without password + const newEmployee = await db.get(` + SELECT + id, email, name, role, is_active as isActive, + phone, department, created_at as createdAt, + last_login as lastLogin + FROM users + WHERE id = ? + `, [employeeId]); + + res.status(201).json(newEmployee); + } catch (error) { + console.error('Error creating employee:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const updateEmployee = async (req: AuthRequest, res: Response): Promise => { + try { + const { id } = req.params; + const { name, role, isActive, phone, department } = req.body; + + // Check if employee exists + const existingEmployee = await db.get('SELECT * FROM users WHERE id = ?', [id]); + if (!existingEmployee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + // Update employee + await db.run( + `UPDATE users + SET name = COALESCE(?, name), + role = COALESCE(?, role), + is_active = COALESCE(?, is_active), + phone = COALESCE(?, phone), + department = COALESCE(?, department) + WHERE id = ?`, + [name, role, isActive, phone, department, id] + ); + + // Return updated employee + const updatedEmployee = await db.get(` + SELECT + id, email, name, role, is_active as isActive, + phone, department, created_at as createdAt, + last_login as lastLogin + FROM users + WHERE id = ? + `, [id]); + + res.json(updatedEmployee); + } catch (error) { + console.error('Error updating employee:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const deleteEmployee = async (req: AuthRequest, res: Response): Promise => { + try { + const { id } = req.params; + + // Check if employee exists + const existingEmployee = await db.get('SELECT * FROM users WHERE id = ?', [id]); + if (!existingEmployee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + // Soft delete - set is_active to false + await db.run('UPDATE users SET is_active = 0 WHERE id = ?', [id]); + + res.status(204).send(); + } catch (error) { + console.error('Error deleting employee:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const getAvailabilities = async (req: AuthRequest, res: Response): Promise => { + try { + const { employeeId } = req.params; + + // Check if employee exists + const existingEmployee = await db.get('SELECT id FROM users WHERE id = ?', [employeeId]); + if (!existingEmployee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + const availabilities = await db.all(` + SELECT * FROM employee_availabilities + WHERE employee_id = ? + ORDER BY day_of_week, start_time + `, [employeeId]); + + res.json(availabilities.map(avail => ({ + id: avail.id, + employeeId: avail.employee_id, + dayOfWeek: avail.day_of_week, + startTime: avail.start_time, + endTime: avail.end_time, + isAvailable: avail.is_available === 1 + }))); + } catch (error) { + console.error('Error fetching availabilities:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; + +export const updateAvailabilities = async (req: AuthRequest, res: Response): Promise => { + try { + const { employeeId } = req.params; + const availabilities = req.body; + + // Check if employee exists + const existingEmployee = await db.get('SELECT id FROM users WHERE id = ?', [employeeId]); + if (!existingEmployee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + await db.run('BEGIN TRANSACTION'); + + try { + // Delete existing availabilities + await db.run('DELETE FROM employee_availabilities WHERE employee_id = ?', [employeeId]); + + // Insert new availabilities + for (const availability of availabilities) { + const availabilityId = uuidv4(); + await db.run( + `INSERT INTO employee_availabilities (id, employee_id, day_of_week, start_time, end_time, is_available) + VALUES (?, ?, ?, ?, ?, ?)`, + [ + availabilityId, + employeeId, + availability.dayOfWeek, + availability.startTime, + availability.endTime, + availability.isAvailable ? 1 : 0 + ] + ); + } + + await db.run('COMMIT'); + + // Return updated availabilities + const updatedAvailabilities = await db.all(` + SELECT * FROM employee_availabilities + WHERE employee_id = ? + ORDER BY day_of_week, start_time + `, [employeeId]); + + res.json(updatedAvailabilities.map(avail => ({ + id: avail.id, + employeeId: avail.employee_id, + dayOfWeek: avail.day_of_week, + startTime: avail.start_time, + endTime: avail.end_time, + isAvailable: avail.is_available === 1 + }))); + + } catch (error) { + await db.run('ROLLBACK'); + throw error; + } + + } catch (error) { + console.error('Error updating availabilities:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}; \ No newline at end of file diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index e463d21..7ca459e 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -21,4 +21,20 @@ CREATE TABLE IF NOT EXISTS assigned_shifts ( required_employees INTEGER DEFAULT 1, assigned_employees TEXT DEFAULT '[]', -- JSON array of user IDs FOREIGN KEY (shift_plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE -); \ No newline at end of file +); + +-- Zusätzliche Tabelle für Mitarbeiter-Verfügbarkeiten +CREATE TABLE IF NOT EXISTS employee_availabilities ( + id TEXT PRIMARY KEY, + employee_id TEXT NOT NULL, + day_of_week INTEGER NOT NULL CHECK (day_of_week >= 0 AND day_of_week <= 6), + start_time TEXT NOT NULL, + end_time TEXT NOT NULL, + is_available BOOLEAN DEFAULT FALSE, + FOREIGN KEY (employee_id) REFERENCES users(id) ON DELETE CASCADE +); + +-- Users Tabelle erweitern um zusätzliche Felder +ALTER TABLE users ADD COLUMN phone TEXT; +ALTER TABLE users ADD COLUMN department TEXT; +ALTER TABLE users ADD COLUMN is_active BOOLEAN DEFAULT TRUE; \ No newline at end of file diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts new file mode 100644 index 0000000..a65c30a --- /dev/null +++ b/backend/src/models/Employee.ts @@ -0,0 +1,39 @@ +// backend/src/models/Employee.ts +export interface Employee { + id: string; + email: string; + password: string; + name: string; + role: 'admin' | 'instandhalter' | 'user'; + isActive: boolean; + phone?: string; + department?: string; + createdAt: string; + lastLogin?: string; +} + +export interface Availability { + id: string; + employeeId: string; + dayOfWeek: number; + startTime: string; + endTime: string; + 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 diff --git a/backend/src/routes/employees.ts b/backend/src/routes/employees.ts new file mode 100644 index 0000000..dc6492e --- /dev/null +++ b/backend/src/routes/employees.ts @@ -0,0 +1,30 @@ +// backend/src/routes/employees.ts +import express from 'express'; +import { authMiddleware, requireRole } from '../middleware/auth.js'; +import { + getEmployees, + getEmployee, + createEmployee, + updateEmployee, + deleteEmployee, + getAvailabilities, + updateAvailabilities +} from '../controllers/employeeController.js'; + +const router = express.Router(); + +// Alle Routes benötigen Authentication +router.use(authMiddleware); + +// Employee CRUD Routes +router.get('/', requireRole(['admin', 'instandhalter']), getEmployees); +router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee); +router.post('/', requireRole(['admin']), createEmployee); +router.put('/:id', requireRole(['admin']), updateEmployee); +router.delete('/:id', requireRole(['admin']), deleteEmployee); + +// Availability Routes +router.get('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), getAvailabilities); +router.put('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), updateAvailabilities); + +export default router; \ No newline at end of file diff --git a/backend/src/scripts/seedData.ts b/backend/src/scripts/seedData.ts index 6e7e652..c02bb28 100644 --- a/backend/src/scripts/seedData.ts +++ b/backend/src/scripts/seedData.ts @@ -1,24 +1,66 @@ -// backend/src/scripts/seedData.ts -import { db } from '../services/databaseService'; +// backend/src/scripts/seedData.ts - Erweitert +import { db } from '../services/databaseService.js'; import { v4 as uuidv4 } from 'uuid'; import bcrypt from 'bcryptjs'; export const seedData = async () => { try { + console.log('Starting database seeding...'); + // Admin User erstellen const adminId = uuidv4(); const hashedPassword = await bcrypt.hash('admin123', 10); await db.run( - `INSERT OR IGNORE INTO users (id, email, password, name, role) VALUES (?, ?, ?, ?, ?)`, - [adminId, 'admin@schichtplan.de', hashedPassword, 'System Administrator', 'admin'] + `INSERT OR IGNORE INTO users (id, email, password, name, role, phone, department, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [adminId, 'admin@schichtplan.de', hashedPassword, 'System Administrator', 'admin', '+49 123 456789', 'IT', 1] ); + // Test-Mitarbeiter erstellen + const testUsers = [ + { + id: uuidv4(), + email: 'instandhalter@schichtplan.de', + password: await bcrypt.hash('instandhalter123', 10), + name: 'Max Instandhalter', + role: 'instandhalter', + phone: '+49 123 456790', + department: 'Produktion' + }, + { + id: uuidv4(), + email: 'mitarbeiter1@schichtplan.de', + password: await bcrypt.hash('user123', 10), + name: 'Anna Müller', + role: 'user', + phone: '+49 123 456791', + department: 'Logistik' + }, + { + id: uuidv4(), + email: 'mitarbeiter2@schichtplan.de', + password: await bcrypt.hash('user123', 10), + name: 'Tom Schmidt', + role: 'user', + phone: '+49 123 456792', + department: 'Produktion' + } + ]; + + for (const user of testUsers) { + await db.run( + `INSERT OR IGNORE INTO users (id, email, password, name, role, phone, department, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + [user.id, user.email, user.password, user.name, user.role, user.phone, user.department, 1] + ); + } + // Standard Vorlage erstellen const templateId = uuidv4(); - await db.run( - `INSERT OR IGNORE INTO shift_templates (id, name, description, is_default, created_by) VALUES (?, ?, ?, ?, ?)`, + `INSERT OR IGNORE INTO shift_templates (id, name, description, is_default, created_by) + VALUES (?, ?, ?, ?, ?)`, [templateId, 'Standard Woche', 'Standard Schichtplan für Montag bis Freitag', 1, adminId] ); @@ -37,16 +79,14 @@ export const seedData = async () => { for (const shift of shifts) { await db.run( - `INSERT OR IGNORE INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees) VALUES (?, ?, ?, ?, ?, ?, ?)`, + `INSERT OR IGNORE INTO template_shifts (id, template_id, day_of_week, name, start_time, end_time, required_employees) + VALUES (?, ?, ?, ?, ?, ?, ?)`, [uuidv4(), templateId, shift.day, shift.name, shift.start, shift.end, shift.employees] ); } - console.log('Test data seeded successfully'); + console.log('✅ Test data seeded successfully'); } catch (error) { - console.error('Error seeding test data:', error); + console.error('❌ Error seeding test data:', error); } -}; - -// Beim Start ausführen -seedData(); \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 34753bb..01d1438 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -1,18 +1,31 @@ -const express = require('express'); -const cors = require('cors'); -const sqlite3 = require('sqlite3').verbose(); -const path = require('path'); +// 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 const app = express(); -const PORT = process.env.PORT || 3002; +const PORT = 3002; // Middleware -app.use(cors()); +app.use(cors({ + origin: 'http://localhost:3000', + credentials: true +})); app.use(express.json()); -// Health route -app.get('/api/health', (req: any, res: any) => { - console.log('✅ Health check called'); +// 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) => { res.json({ status: 'OK', message: 'Backend läuft!', @@ -20,55 +33,12 @@ app.get('/api/health', (req: any, res: any) => { }); }); -// Simple login without bcrypt -app.post('/api/auth/login', (req: any, res: any) => { - console.log('🔐 Login attempt:', req.body.email); - - // Einfache Hardcoded Auth (OHNE Passwort-Hashing für Test) - if (req.body.email === 'admin@schichtplan.de' && req.body.password === 'admin123') { - console.log('✅ Login successful'); - res.json({ - user: { - id: '1', - email: 'admin@schichtplan.de', - name: 'Admin User', - role: 'admin', - createdAt: new Date().toISOString() - }, - token: 'simple-jwt-token-' + Date.now(), - expiresIn: '7d' - }); - } else { - console.log('❌ Login failed'); - res.status(401).json({ error: 'Invalid credentials' }); - } -}); - -// Get shift templates -app.get('/api/shift-templates', (req: any, res: any) => { - console.log('📋 Fetching shift templates'); - res.json([ - { - id: '1', - name: 'Standard Woche', - description: 'Standard Schichtplan', - isDefault: true, - createdBy: '1', - createdAt: new Date().toISOString(), - shifts: [ - { id: '1', dayOfWeek: 1, name: 'Vormittag', startTime: '08:00', endTime: '12:00', requiredEmployees: 2 }, - { id: '2', dayOfWeek: 1, name: 'Nachmittag', startTime: '11:30', endTime: '15:30', requiredEmployees: 2 } - ] - } - ]); -}); - // Start server -app.listen(PORT, () => { +app.listen(PORT, async () => { console.log('🎉 BACKEND STARTED SUCCESSFULLY!'); console.log(`📍 Port: ${PORT}`); console.log(`📍 Health: http://localhost:${PORT}/api/health`); - console.log(`📍 Ready for login!`); -}); - -console.log('🚀 Server starting...'); \ No newline at end of file + console.log('📊 Employee management ready!'); + + await seedData(); +}); \ No newline at end of file