const express = require('express'); const session = require('express-session'); const bodyParser = require('body-parser'); const bcrypt = require('bcryptjs'); const path = require('path'); const { db, initDatabase } = require('./database'); const LDAPService = require('./ldap-service'); const cron = require('node-cron'); const app = express(); const PORT = 3333; // Middleware app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(express.static('public')); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // Session-Konfiguration app.use(session({ secret: 'stundenerfassung-geheim-2024', resave: false, saveUninitialized: false, cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 Stunden })); // Datenbank initialisieren initDatabase(); // Helper: Prüft ob User eine bestimmte Rolle hat function hasRole(req, role) { if (!req.session.roles || !Array.isArray(req.session.roles)) { return false; } return req.session.roles.includes(role); } // Helper: Bestimmt die Standard-Rolle (höchste Priorität: admin > verwaltung > mitarbeiter) function getDefaultRole(roles) { if (!Array.isArray(roles) || roles.length === 0) { return 'mitarbeiter'; } if (roles.includes('admin')) return 'admin'; if (roles.includes('verwaltung')) return 'verwaltung'; return roles[0]; // Fallback auf erste Rolle } // Helper: Gibt aktuelles Datum als YYYY-MM-DD zurück function getCurrentDate() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; } // Helper: Gibt aktuelle Zeit als HH:MM zurück function getCurrentTime() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } // Helper: Berechnet Pausenzeit in Minuten zwischen zwei Zeiten function calculateBreakMinutes(pauseStart, pauseEnd) { if (!pauseStart || !pauseEnd) return 0; const [startHours, startMinutes] = pauseStart.split(':').map(Number); const [endHours, endMinutes] = pauseEnd.split(':').map(Number); const startTotalMinutes = startHours * 60 + startMinutes; const endTotalMinutes = endHours * 60 + endMinutes; return endTotalMinutes - startTotalMinutes; } // Helper: Berechnet total_hours basierend auf start_time, end_time und break_minutes function updateTotalHours(startTime, endTime, breakMinutes) { if (!startTime || !endTime) return 0; const [startHours, startMinutes] = startTime.split(':').map(Number); const [endHours, endMinutes] = endTime.split(':').map(Number); const startTotalMinutes = startHours * 60 + startMinutes; const endTotalMinutes = endHours * 60 + endMinutes; const totalMinutes = endTotalMinutes - startTotalMinutes - (breakMinutes || 0); return totalMinutes / 60; // Konvertiere zu Stunden } // Middleware: Authentifizierung prüfen function requireAuth(req, res, next) { if (req.session.userId) { next(); } else { res.redirect('/login'); } } // Middleware: Prüft ob User eine bestimmte Rolle hat function requireRole(role) { return (req, res, next) => { if (req.session.userId && hasRole(req, role)) { next(); } else { res.status(403).send('Zugriff verweigert'); } }; } // Middleware: Admin-Rolle prüfen function requireAdmin(req, res, next) { if (req.session.userId && hasRole(req, 'admin')) { next(); } else { res.status(403).send('Zugriff verweigert'); } } // Middleware: Verwaltung-Rolle prüfen (Verwaltung oder Admin) function requireVerwaltung(req, res, next) { if (req.session.userId && (hasRole(req, 'verwaltung') || hasRole(req, 'admin'))) { next(); } else { res.status(403).send('Zugriff verweigert'); } } // ROUTEN // Login-Seite app.get('/login', (req, res) => { res.render('login', { error: null }); }); // Login-Verarbeitung app.post('/login', (req, res) => { const { username, password } = req.body; // Prüfe ob LDAP aktiviert ist LDAPService.getConfig((err, ldapConfig) => { if (err) { console.error('Fehler beim Abrufen der LDAP-Konfiguration:', err); } const isLDAPEnabled = ldapConfig && ldapConfig.enabled === 1; // Wenn LDAP aktiviert ist, authentifiziere gegen LDAP if (isLDAPEnabled) { LDAPService.authenticate(username, password, (authErr, authSuccess) => { if (authErr || !authSuccess) { // LDAP-Authentifizierung fehlgeschlagen - prüfe lokale Datenbank als Fallback db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { if (err || !user) { return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); } // Versuche lokale Authentifizierung if (bcrypt.compareSync(password, user.password)) { handleSuccessfulLogin(req, res, user); } else { res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); } }); } else { // LDAP-Authentifizierung erfolgreich - hole Benutzer aus Datenbank db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { if (err || !user) { return res.render('login', { error: 'Benutzer nicht in der Datenbank gefunden. Bitte führen Sie eine LDAP-Synchronisation durch.' }); } handleSuccessfulLogin(req, res, user); }); } }); } else { // LDAP nicht aktiviert - verwende lokale Authentifizierung db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => { if (err || !user) { return res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); } if (bcrypt.compareSync(password, user.password)) { handleSuccessfulLogin(req, res, user); } else { res.render('login', { error: 'Ungültiger Benutzername oder Passwort' }); } }); } }); }); // Helper-Funktion für erfolgreiche Anmeldung function handleSuccessfulLogin(req, res, user) { // Rollen als JSON-Array parsen let roles = []; try { roles = JSON.parse(user.role); if (!Array.isArray(roles)) { // Fallback: Falls kein Array, erstelle Array mit vorhandener Rolle roles = [user.role]; } } catch (e) { // Fallback: Falls kein JSON, erstelle Array mit vorhandener Rolle roles = [user.role || 'mitarbeiter']; } // Standard-Rolle bestimmen: Immer "mitarbeiter" wenn vorhanden, sonst höchste Priorität let defaultRole; if (roles.includes('mitarbeiter')) { defaultRole = 'mitarbeiter'; } else { defaultRole = getDefaultRole(roles); } req.session.userId = user.id; req.session.username = user.username; req.session.roles = roles; req.session.currentRole = defaultRole; req.session.firstname = user.firstname; req.session.lastname = user.lastname; // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf Standard-Rolle if (roles.includes('mitarbeiter')) { res.redirect('/dashboard'); } else if (defaultRole === 'admin') { res.redirect('/admin'); } else if (defaultRole === 'verwaltung') { res.redirect('/verwaltung'); } else { res.redirect('/dashboard'); } } // Logout app.get('/logout', (req, res) => { req.session.destroy(); res.redirect('/login'); }); // Dashboard für Mitarbeiter app.get('/dashboard', requireAuth, (req, res) => { // Prüfe ob User Mitarbeiter-Rolle hat if (!hasRole(req, 'mitarbeiter')) { // Wenn User keine Mitarbeiter-Rolle hat, aber andere Rollen, redirecte entsprechend if (hasRole(req, 'admin')) { return res.redirect('/admin'); } if (hasRole(req, 'verwaltung')) { return res.redirect('/verwaltung'); } return res.status(403).send('Zugriff verweigert'); } res.render('dashboard', { user: { id: req.session.userId, firstname: req.session.firstname, lastname: req.session.lastname, username: req.session.username, roles: req.session.roles || [], currentRole: req.session.currentRole || 'mitarbeiter' } }); }); // API: Letzte bearbeitete Woche abrufen app.get('/api/user/last-week', requireAuth, (req, res) => { const userId = req.session.userId; db.get('SELECT last_week_start FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der letzten Woche' }); } res.json({ last_week_start: user?.last_week_start || null }); }); }); // API: Letzte bearbeitete Woche speichern app.post('/api/user/last-week', requireAuth, (req, res) => { const userId = req.session.userId; const { week_start } = req.body; if (!week_start) { return res.status(400).json({ error: 'week_start ist erforderlich' }); } db.run('UPDATE users SET last_week_start = ? WHERE id = ?', [week_start, userId], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Speichern der letzten Woche' }); } res.json({ success: true }); }); }); // API: User-Daten abrufen (Wochenstunden) app.get('/api/user/data', requireAuth, (req, res) => { const userId = req.session.userId; db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } res.json({ wochenstunden: user?.wochenstunden || 0 }); }); }); // API: Rollenwechsel app.post('/api/user/switch-role', requireAuth, (req, res) => { const { role } = req.body; if (!role) { return res.status(400).json({ error: 'Rolle ist erforderlich' }); } // Prüfe ob User diese Rolle hat if (!hasRole(req, role)) { return res.status(403).json({ error: 'Sie haben keine Berechtigung für diese Rolle' }); } // Validiere dass die Rolle eine gültige Rolle ist const validRoles = ['mitarbeiter', 'verwaltung', 'admin']; if (!validRoles.includes(role)) { return res.status(400).json({ error: 'Ungültige Rolle' }); } // Setze aktuelle Rolle req.session.currentRole = role; res.json({ success: true, currentRole: role }); }); // API: Gesamtstatistiken für Mitarbeiter (Überstunden und Urlaubstage) app.get('/api/user/stats', requireAuth, (req, res) => { const userId = req.session.userId; // User-Daten abrufen db.get('SELECT wochenstunden, urlaubstage FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } const wochenstunden = user.wochenstunden || 0; const urlaubstage = user.urlaubstage || 0; // Alle eingereichten Wochen abrufen db.all(`SELECT DISTINCT week_start, week_end FROM weekly_timesheets WHERE user_id = ? AND status = 'eingereicht' ORDER BY week_start`, [userId], (err, weeks) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); } // Wenn keine Wochen vorhanden if (!weeks || weeks.length === 0) { return res.json({ currentOvertime: 0, remainingVacation: urlaubstage, totalOvertimeHours: 0, totalOvertimeTaken: 0, totalVacationDays: 0, urlaubstage: urlaubstage }); } let totalOvertimeHours = 0; let totalOvertimeTaken = 0; let totalVacationDays = 0; let processedWeeks = 0; let hasError = false; // Für jede Woche die Statistiken berechnen weeks.forEach((week) => { // Einträge für diese Woche abrufen db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date`, [userId, week.week_start, week.week_end], (err, entries) => { if (hasError) return; // Wenn bereits ein Fehler aufgetreten ist, ignoriere weitere Ergebnisse if (err) { hasError = true; return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); } // Berechnungen für diese Woche let weekTotalHours = 0; let weekOvertimeTaken = 0; let weekVacationDays = 0; let weekVacationHours = 0; entries.forEach(entry => { if (entry.total_hours) { weekTotalHours += entry.total_hours; } if (entry.overtime_taken_hours) { weekOvertimeTaken += entry.overtime_taken_hours; } if (entry.vacation_type === 'full') { weekVacationDays += 1; weekVacationHours += 8; } else if (entry.vacation_type === 'half') { weekVacationDays += 0.5; weekVacationHours += 4; } }); // Anzahl Werktage berechnen (Montag-Freitag) const startDate = new Date(week.week_start); const endDate = new Date(week.week_end); let workdays = 0; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const day = d.getDay(); if (day >= 1 && day <= 5) { // Montag bis Freitag workdays++; } } // Sollstunden berechnen const sollStunden = (wochenstunden / 5) * workdays; // Überstunden für diese Woche: Urlaub zählt als normale Arbeitszeit const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours; const weekOvertimeHours = weekTotalHoursWithVacation - sollStunden; // Kumulativ addieren totalOvertimeHours += weekOvertimeHours; totalOvertimeTaken += weekOvertimeTaken; totalVacationDays += weekVacationDays; processedWeeks++; // Wenn alle Wochen verarbeitet wurden, Antwort senden if (processedWeeks === weeks.length && !hasError) { const currentOvertime = totalOvertimeHours - totalOvertimeTaken; const remainingVacation = urlaubstage - totalVacationDays; res.json({ currentOvertime: currentOvertime, remainingVacation: remainingVacation, totalOvertimeHours: totalOvertimeHours, totalOvertimeTaken: totalOvertimeTaken, totalVacationDays: totalVacationDays, urlaubstage: urlaubstage }); } }); }); }); }); }); // Admin-Bereich app.get('/admin', requireAdmin, (req, res) => { db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, created_at FROM users ORDER BY created_at DESC', (err, users) => { // LDAP-Konfiguration und Sync-Log abrufen db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => { db.all('SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT 10', (err, syncLogs) => { // Parse Rollen für jeden User const usersWithRoles = (users || []).map(u => { let roles = []; try { roles = JSON.parse(u.role); if (!Array.isArray(roles)) { roles = [u.role]; } } catch (e) { roles = [u.role || 'mitarbeiter']; } return { ...u, roles }; }); res.render('admin', { users: usersWithRoles, ldapConfig: ldapConfig || null, syncLogs: syncLogs || [], user: { firstname: req.session.firstname, lastname: req.session.lastname, roles: req.session.roles || [], currentRole: req.session.currentRole || 'admin' } }); }); }); }); }); // Benutzer erstellen app.post('/admin/users', requireAdmin, (req, res) => { const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage } = req.body; const hashedPassword = bcrypt.hashSync(password, 10); // Normalisiere die optionalen Felder const normalizedPersonalnummer = personalnummer && personalnummer.trim() !== '' ? personalnummer.trim() : null; const normalizedWochenstunden = wochenstunden && wochenstunden !== '' ? parseFloat(wochenstunden) : null; const normalizedUrlaubstage = urlaubstage && urlaubstage !== '' ? parseFloat(urlaubstage) : null; // Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String let rolesArray = []; if (Array.isArray(roles)) { rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r)); } else if (roles) { // Fallback: Einzelne Rolle als Array rolesArray = [roles]; } // Mindestens eine Rolle erforderlich if (rolesArray.length === 0) { rolesArray = ['mitarbeiter']; // Standard-Rolle } const rolesJson = JSON.stringify(rolesArray); db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage], (err) => { if (err) { return res.status(400).json({ error: 'Benutzername existiert bereits' }); } res.json({ success: true }); }); }); // Benutzer löschen app.delete('/admin/users/:id', requireAdmin, (req, res) => { const userId = req.params.id; // Admin darf sich nicht selbst löschen if (userId == req.session.userId) { return res.status(400).json({ error: 'Sie können sich nicht selbst löschen' }); } db.run('DELETE FROM users WHERE id = ?', [userId], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Löschen' }); } res.json({ success: true }); }); }); // Benutzer aktualisieren (Personalnummer, Wochenstunden, Urlaubstage, Rollen) app.put('/admin/users/:id', requireAdmin, (req, res) => { const userId = req.params.id; const { personalnummer, wochenstunden, urlaubstage, roles } = req.body; // Rollen verarbeiten falls vorhanden let rolesJson = null; if (roles !== undefined) { let rolesArray = []; if (Array.isArray(roles)) { rolesArray = roles.filter(r => r && ['mitarbeiter', 'verwaltung', 'admin'].includes(r)); } // Mindestens eine Rolle erforderlich if (rolesArray.length === 0) { return res.status(400).json({ error: 'Mindestens eine Rolle ist erforderlich' }); } rolesJson = JSON.stringify(rolesArray); } // SQL-Query dynamisch zusammenstellen if (rolesJson !== null) { // Aktualisiere auch Rollen db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, role = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, rolesJson, userId ], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Aktualisieren' }); } res.json({ success: true }); }); } else { // Nur andere Felder aktualisieren db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, userId ], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Aktualisieren' }); } res.json({ success: true }); }); } }); // LDAP-Konfiguration abrufen app.get('/admin/ldap/config', requireAdmin, (req, res) => { db.get('SELECT * FROM ldap_config WHERE id = 1', (err, config) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der Konfiguration' }); } // Passwort nicht zurückgeben if (config) { delete config.bind_password; } res.json({ config: config || null }); }); }); // LDAP-Konfiguration speichern app.post('/admin/ldap/config', requireAdmin, (req, res) => { const { enabled, url, bind_dn, bind_password, base_dn, user_search_filter, username_attribute, firstname_attribute, lastname_attribute, sync_interval } = req.body; // Validierung - nur wenn aktiviert if (enabled && (!url || !base_dn)) { return res.status(400).json({ error: 'URL und Base DN sind erforderlich wenn LDAP aktiviert ist' }); } // Prüfe ob Konfiguration bereits existiert db.get('SELECT id FROM ldap_config WHERE id = 1', (err, existing) => { if (err) { return res.status(500).json({ error: 'Fehler beim Prüfen der Konfiguration' }); } const configData = { enabled: enabled ? 1 : 0, url: url.trim(), bind_dn: bind_dn ? bind_dn.trim() : null, bind_password: bind_password ? bind_password.trim() : null, base_dn: base_dn.trim(), user_search_filter: user_search_filter ? user_search_filter.trim() : '(objectClass=person)', username_attribute: username_attribute ? username_attribute.trim() : 'cn', firstname_attribute: firstname_attribute ? firstname_attribute.trim() : 'givenName', lastname_attribute: lastname_attribute ? lastname_attribute.trim() : 'sn', sync_interval: parseInt(sync_interval) || 0, updated_at: new Date().toISOString() }; if (existing) { // Update - Passwort nur aktualisieren wenn angegeben if (configData.bind_password) { db.run( `UPDATE ldap_config SET enabled = ?, url = ?, bind_dn = ?, bind_password = ?, base_dn = ?, user_search_filter = ?, username_attribute = ?, firstname_attribute = ?, lastname_attribute = ?, sync_interval = ?, updated_at = ? WHERE id = 1`, [ configData.enabled, configData.url, configData.bind_dn, configData.bind_password, configData.base_dn, configData.user_search_filter, configData.username_attribute, configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, configData.updated_at ], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' }); } res.json({ success: true }); } ); } else { // Passwort nicht ändern db.run( `UPDATE ldap_config SET enabled = ?, url = ?, bind_dn = ?, base_dn = ?, user_search_filter = ?, username_attribute = ?, firstname_attribute = ?, lastname_attribute = ?, sync_interval = ?, updated_at = ? WHERE id = 1`, [ configData.enabled, configData.url, configData.bind_dn, configData.base_dn, configData.user_search_filter, configData.username_attribute, configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, configData.updated_at ], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Speichern der Konfiguration' }); } res.json({ success: true }); } ); } } else { // Insert db.run( `INSERT INTO ldap_config ( enabled, url, bind_dn, bind_password, base_dn, user_search_filter, username_attribute, firstname_attribute, lastname_attribute, sync_interval, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ configData.enabled, configData.url, configData.bind_dn, configData.bind_password, configData.base_dn, configData.user_search_filter, configData.username_attribute, configData.firstname_attribute, configData.lastname_attribute, configData.sync_interval, configData.updated_at ], (err) => { if (err) { return res.status(500).json({ error: 'Fehler beim Erstellen der Konfiguration' }); } res.json({ success: true }); } ); } }); }); // Manuelle LDAP-Synchronisation starten app.post('/admin/ldap/sync', requireAdmin, (req, res) => { LDAPService.performSync('manual', (err, result) => { if (err) { return res.status(500).json({ error: err.message || 'Fehler bei der Synchronisation', synced: result ? result.synced : 0, errors: result ? result.errors : [] }); } res.json({ success: true, synced: result.synced, errors: result.errors || [] }); }); }); // Sync-Log abrufen app.get('/admin/ldap/sync/log', requireAdmin, (req, res) => { const limit = parseInt(req.query.limit) || 10; db.all( 'SELECT * FROM ldap_sync_log ORDER BY sync_started_at DESC LIMIT ?', [limit], (err, logs) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen des Logs' }); } res.json({ logs: logs || [] }); } ); }); // Verwaltungs-Bereich app.get('/verwaltung', requireVerwaltung, (req, res) => { db.all(` SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, dl.firstname as downloaded_by_firstname, dl.lastname as downloaded_by_lastname, (SELECT COUNT(*) FROM weekly_timesheets wt2 WHERE wt2.user_id = wt.user_id AND wt2.week_start = wt.week_start AND wt2.week_end = wt.week_end) as total_versions FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id WHERE wt.status = 'eingereicht' ORDER BY wt.week_start DESC, wt.user_id, wt.version DESC `, (err, timesheets) => { // Gruppiere nach Mitarbeiter, dann nach Kalenderwoche // Struktur: { [user_id]: { user: {...}, weeks: { [week_key]: {...} } } } const groupedByEmployee = {}; (timesheets || []).forEach(ts => { const userId = ts.user_id; const weekKey = `${ts.week_start}_${ts.week_end}`; // Level 1: Mitarbeiter if (!groupedByEmployee[userId]) { groupedByEmployee[userId] = { user: { id: ts.user_id, firstname: ts.firstname, lastname: ts.lastname, username: ts.username, personalnummer: ts.personalnummer, wochenstunden: ts.wochenstunden, urlaubstage: ts.urlaubstage }, weeks: {} }; } // Level 2: Kalenderwoche if (!groupedByEmployee[userId].weeks[weekKey]) { groupedByEmployee[userId].weeks[weekKey] = { week_start: ts.week_start, week_end: ts.week_end, total_versions: ts.total_versions, versions: [] }; } // Level 3: Versionen groupedByEmployee[userId].weeks[weekKey].versions.push(ts); }); // Sortierung: Mitarbeiter nach Name, Wochen nach Datum (neueste zuerst) const sortedEmployees = Object.values(groupedByEmployee).map(employee => { // Wochen innerhalb jedes Mitarbeiters sortieren const sortedWeeks = Object.values(employee.weeks).sort((a, b) => { return new Date(b.week_start) - new Date(a.week_start); }); return { ...employee, weeks: sortedWeeks }; }).sort((a, b) => { // Mitarbeiter nach Nachname, dann Vorname sortieren const nameA = `${a.user.lastname} ${a.user.firstname}`.toLowerCase(); const nameB = `${b.user.lastname} ${b.user.firstname}`.toLowerCase(); return nameA.localeCompare(nameB); }); res.render('verwaltung', { groupedByEmployee: sortedEmployees, user: { firstname: req.session.firstname, lastname: req.session.lastname, roles: req.session.roles || [], currentRole: req.session.currentRole || 'verwaltung' } }); }); }); // API: Stundenerfassung speichern app.post('/api/timesheet/save', requireAuth, (req, res) => { const { date, start_time, end_time, break_minutes, notes, activity1_desc, activity1_hours, activity1_project_number, activity2_desc, activity2_hours, activity2_project_number, activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_project_number, activity5_desc, activity5_hours, activity5_project_number, overtime_taken_hours, vacation_type, sick_status } = req.body; const userId = req.session.userId; // Normalisiere end_time: Leere Strings werden zu null const normalizedEndTime = (end_time && typeof end_time === 'string' && end_time.trim() !== '') ? end_time.trim() : (end_time || null); const normalizedStartTime = (start_time && typeof start_time === 'string' && start_time.trim() !== '') ? start_time.trim() : (start_time || null); // Normalisiere sick_status: Boolean oder 1/0 zu Boolean const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1'; // Gesamtstunden berechnen (aus Start- und Endzeit, nicht aus Tätigkeiten) // Wenn ganzer Tag Urlaub oder Krank, dann zählt dieser als 8 Stunden normale Arbeitszeit let total_hours = 0; let finalActivity1Desc = activity1_desc; let finalActivity1Hours = parseFloat(activity1_hours) || 0; if (vacation_type === 'full') { total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit } else if (isSick) { total_hours = 8; // Krank = 8 Stunden normale Arbeitszeit finalActivity1Desc = 'Krank'; finalActivity1Hours = 8; } else if (normalizedStartTime && normalizedEndTime) { const start = new Date(`2000-01-01T${normalizedStartTime}`); const end = new Date(`2000-01-01T${normalizedEndTime}`); const diffMs = end - start; total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60); // Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden // Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt } // Prüfen ob Eintrag existiert - verwende den neuesten Eintrag falls mehrere existieren db.get('SELECT id FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, date], (err, row) => { if (row) { // Update db.run(`UPDATE timesheet_entries SET start_time = ?, end_time = ?, break_minutes = ?, total_hours = ?, notes = ?, activity1_desc = ?, activity1_hours = ?, activity1_project_number = ?, activity2_desc = ?, activity2_hours = ?, activity2_project_number = ?, activity3_desc = ?, activity3_hours = ?, activity3_project_number = ?, activity4_desc = ?, activity4_hours = ?, activity4_project_number = ?, activity5_desc = ?, activity5_hours = ?, activity5_project_number = ?, overtime_taken_hours = ?, vacation_type = ?, sick_status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [ normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes, finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, vacation_type || null, isSick ? 1 : 0, row.id ], (err) => { if (err) { console.error('Fehler beim Update:', err); return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message }); } res.json({ success: true, total_hours }); }); } else { // Insert db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, end_time, break_minutes, total_hours, notes, activity1_desc, activity1_hours, activity1_project_number, activity2_desc, activity2_hours, activity2_project_number, activity3_desc, activity3_hours, activity3_project_number, activity4_desc, activity4_hours, activity4_project_number, activity5_desc, activity5_hours, activity5_project_number, overtime_taken_hours, vacation_type, sick_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ userId, date, normalizedStartTime, normalizedEndTime, break_minutes, total_hours, notes, finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, activity2_desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, activity3_desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, activity4_desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, activity5_desc || null, parseFloat(activity5_hours) || 0, activity5_project_number || null, overtime_taken_hours ? parseFloat(overtime_taken_hours) : null, vacation_type || null, isSick ? 1 : 0 ], (err) => { if (err) { console.error('Fehler beim Insert:', err); return res.status(500).json({ error: 'Fehler beim Speichern: ' + err.message }); } res.json({ success: true, total_hours }); }); } }); }); // Check-in und Check-out Endpoints wurden auf Port 3334 ausgelagert (checkin-server.js) // Diese Endpoints sind nicht mehr auf diesem Server verfügbar // API: Stundenerfassung für Woche laden app.get('/api/timesheet/week/:weekStart', requireAuth, (req, res) => { const userId = req.session.userId; const weekStart = req.params.weekStart; // Berechne Wochenende const startDate = new Date(weekStart); const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 6); const weekEnd = endDate.toISOString().split('T')[0]; // Prüfe ob die Woche bereits eingereicht wurde (aber ermögliche Bearbeitung) db.get(`SELECT id, version FROM weekly_timesheets WHERE user_id = ? AND week_start = ? AND week_end = ? ORDER BY version DESC LIMIT 1`, [userId, weekStart, weekEnd], (err, weeklySheet) => { const hasSubmittedVersion = !!weeklySheet; const latestVersion = weeklySheet ? weeklySheet.version : 0; // Lade alle Einträge für die Woche db.all(`SELECT * FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date`, [userId, weekStart, weekEnd], (err, entries) => { // Füge Status-Info hinzu (Bearbeitung ist immer möglich) const entriesWithStatus = (entries || []).map(entry => ({ ...entry, week_submitted: false, // Immer false, damit Bearbeitung möglich ist latest_version: latestVersion, has_existing_version: latestVersion > 0 })); res.json(entriesWithStatus); }); }); }); // API: Woche abschicken app.post('/api/timesheet/submit', requireAuth, (req, res) => { const { week_start, week_end, version_reason } = req.body; const userId = req.session.userId; // Validierung: Prüfen ob alle 7 Tage der Woche ausgefüllt sind db.all(`SELECT id, date, start_time, end_time, vacation_type, sick_status, updated_at FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date, updated_at DESC, id DESC`, [userId, week_start, week_end], (err, entries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Prüfen der Daten' }); } // Erstelle Set mit vorhandenen Daten // WICHTIG: Wenn mehrere Einträge für denselben Tag existieren, nimm den neuesten const entriesByDate = {}; entries.forEach(entry => { const existing = entriesByDate[entry.date]; // Wenn noch kein Eintrag existiert oder dieser neuer ist, verwende ihn if (!existing) { entriesByDate[entry.date] = entry; } else { // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { entriesByDate[entry.date] = entry; } } }); // Prüfe nur Werktage (Montag-Freitag, erste 5 Tage) // Samstag und Sonntag sind optional // Bei ganztägigem Urlaub (vacation_type = 'full') ist der Tag als ausgefüllt zu betrachten // week_start ist bereits im Format YYYY-MM-DD const startDateParts = week_start.split('-'); const startYear = parseInt(startDateParts[0]); const startMonth = parseInt(startDateParts[1]) - 1; // Monat ist 0-basiert const startDay = parseInt(startDateParts[2]); let missingDays = []; for (let i = 0; i < 5; i++) { // Datum direkt berechnen ohne Zeitzonenprobleme const date = new Date(startYear, startMonth, startDay + i); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const dateStr = `${year}-${month}-${day}`; const entry = entriesByDate[dateStr]; // Wenn ganztägiger Urlaub oder Krank, dann ist der Tag als ausgefüllt zu betrachten const isSick = entry && (entry.sick_status === 1 || entry.sick_status === true); if (entry && (entry.vacation_type === 'full' || isSick)) { continue; // Tag ist ausgefüllt } // Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein // start_time und end_time könnten null, undefined oder leer strings sein const hasStartTime = entry && entry.start_time && entry.start_time.toString().trim() !== ''; const hasEndTime = entry && entry.end_time && entry.end_time.toString().trim() !== ''; if (!entry || !hasStartTime || !hasEndTime) { missingDays.push(dateStr); } } if (missingDays.length > 0) { return res.status(400).json({ error: `Nicht alle Werktage (Montag bis Freitag) sind ausgefüllt. Fehlende Tage: ${missingDays.join(', ')}. Bitte füllen Sie alle Werktage mit Start- und Endzeit aus. Wochenende ist optional.` }); } // Alle Tage ausgefüllt - Woche abschicken (immer neue Version erstellen) // Prüfe welche Version die letzte ist db.get(`SELECT MAX(version) as max_version FROM weekly_timesheets WHERE user_id = ? AND week_start = ? AND week_end = ?`, [userId, week_start, week_end], (err, result) => { if (err) return res.status(500).json({ error: 'Fehler beim Prüfen der Version' }); const maxVersion = result && result.max_version ? result.max_version : 0; const newVersion = maxVersion + 1; // Wenn bereits eine Version existiert, ist version_reason erforderlich if (maxVersion > 0 && (!version_reason || version_reason.trim() === '')) { return res.status(400).json({ error: 'Bitte geben Sie einen Grund für die neue Version an.' }); } // Neue Version erstellen (nicht überschreiben) db.run(`INSERT INTO weekly_timesheets (user_id, week_start, week_end, version, status, version_reason) VALUES (?, ?, ?, ?, 'eingereicht', ?)`, [userId, week_start, week_end, newVersion, version_reason ? version_reason.trim() : null], (err) => { if (err) return res.status(500).json({ error: 'Fehler beim Abschicken' }); // Status der Einträge aktualisieren (optional - für Nachverfolgung) db.run(`UPDATE timesheet_entries SET status = 'eingereicht' WHERE user_id = ? AND date >= ? AND date <= ?`, [userId, week_start, week_end], (err) => { if (err) return res.status(500).json({ error: 'Fehler beim Aktualisieren des Status' }); res.json({ success: true, version: newVersion }); }); }); }); }); }); // API: Überstunden- und Urlaubsstatistiken für einen User abrufen app.get('/api/verwaltung/user/:id/stats', requireVerwaltung, (req, res) => { const userId = req.params.id; const { week_start, week_end } = req.query; // User-Daten abrufen db.get('SELECT wochenstunden, urlaubstage FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); } const wochenstunden = user.wochenstunden || 0; const urlaubstage = user.urlaubstage || 0; // Einträge für die Woche abrufen db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date`, [userId, week_start, week_end], (err, entries) => { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); } // Berechnungen let totalHours = 0; let overtimeTaken = 0; let vacationDays = 0; let vacationHours = 0; entries.forEach(entry => { if (entry.total_hours) { totalHours += entry.total_hours; } if (entry.overtime_taken_hours) { overtimeTaken += entry.overtime_taken_hours; } if (entry.vacation_type === 'full') { vacationDays += 1; vacationHours += 8; // Ganzer Tag = 8 Stunden } else if (entry.vacation_type === 'half') { vacationDays += 0.5; vacationHours += 4; // Halber Tag = 4 Stunden } }); // Anzahl Werktage berechnen (Montag-Freitag) const startDate = new Date(week_start); const endDate = new Date(week_end); let workdays = 0; for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { const day = d.getDay(); if (day >= 1 && day <= 5) { // Montag bis Freitag workdays++; } } // Sollstunden berechnen const sollStunden = (wochenstunden / 5) * workdays; // Überstunden berechnen: Urlaub zählt als normale Arbeitszeit // Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Sollstunden const totalHoursWithVacation = totalHours + vacationHours; const overtimeHours = totalHoursWithVacation - sollStunden; const remainingOvertime = overtimeHours - overtimeTaken; // Verbleibende Urlaubstage const remainingVacation = urlaubstage - vacationDays; res.json({ wochenstunden, urlaubstage, totalHours, sollStunden, overtimeHours, overtimeTaken, remainingOvertime, vacationDays, remainingVacation, workdays }); }); }); }); // API: Admin-Kommentar speichern app.put('/api/verwaltung/timesheet/:id/comment', requireVerwaltung, (req, res) => { const timesheetId = req.params.id; const { comment } = req.body; db.run('UPDATE weekly_timesheets SET admin_comment = ? WHERE id = ?', [comment ? comment.trim() : null, timesheetId], (err) => { if (err) { console.error('Fehler beim Speichern des Kommentars:', err); return res.status(500).json({ error: 'Fehler beim Speichern des Kommentars' }); } res.json({ success: true }); }); }); // API: PDF Download-Info abrufen app.get('/api/timesheet/download-info/:id', requireVerwaltung, (req, res) => { const timesheetId = req.params.id; db.get(`SELECT wt.pdf_downloaded_at, dl.firstname as downloaded_by_firstname, dl.lastname as downloaded_by_lastname FROM weekly_timesheets wt LEFT JOIN users dl ON wt.pdf_downloaded_by = dl.id WHERE wt.id = ?`, [timesheetId], (err, result) => { if (err) { console.error('Fehler beim Abrufen der Download-Info:', err); return res.status(500).json({ error: 'Fehler beim Abrufen der Informationen' }); } if (!result) { return res.status(404).json({ error: 'Stundenzettel nicht gefunden' }); } res.json({ downloaded: !!result.pdf_downloaded_at, downloaded_at: result.pdf_downloaded_at, downloaded_by_firstname: result.downloaded_by_firstname, downloaded_by_lastname: result.downloaded_by_lastname }); }); }); // API: PDF generieren app.get('/api/timesheet/pdf/:id', requireVerwaltung, (req, res) => { const timesheetId = req.params.id; db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { if (err || !timesheet) { return res.status(404).send('Stundenzettel nicht gefunden'); } db.all(`SELECT * FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? ORDER BY date`, [timesheet.user_id, timesheet.week_start, timesheet.week_end], (err, entries) => { const PDFDocument = require('pdfkit'); const doc = new PDFDocument({ margin: 50 }); // Prüfe ob inline angezeigt werden soll (für Vorschau) const inline = req.query.inline === 'true'; // Kalenderwoche berechnen (Funktion wird später definiert) function getCalendarWeek(dateStr) { const date = new Date(dateStr); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); return weekNo; } // Dateinamen generieren: Stundenzettel_KWxxx_NameMitarbeiter_heutigesDatum.pdf const calendarWeek = getCalendarWeek(timesheet.week_start); const today = new Date(); const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); const employeeName = `${timesheet.firstname}${timesheet.lastname}`.replace(/\s+/g, ''); const filename = `Stundenzettel_KW${String(calendarWeek).padStart(2, '0')}_${employeeName}_${todayStr}.pdf`; res.setHeader('Content-Type', 'application/pdf'); res.setHeader('X-Content-Type-Options', 'nosniff'); if (inline) { res.setHeader('Content-Disposition', `inline; filename="${filename}"`); // Zusätzliche Header für iframe-Unterstützung res.setHeader('X-Frame-Options', 'SAMEORIGIN'); } else { res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); // Marker setzen, dass PDF heruntergeladen wurde (nur bei Download, nicht bei Vorschau) const downloadedBy = req.session.userId; // User der die PDF herunterlädt console.log('PDF Download - User ID:', downloadedBy, 'Timesheet ID:', timesheetId); if (downloadedBy) { db.run(`UPDATE weekly_timesheets SET pdf_downloaded_at = CURRENT_TIMESTAMP, pdf_downloaded_by = ? WHERE id = ?`, [downloadedBy, timesheetId], (err) => { if (err) { console.error('Fehler beim Setzen des Download-Markers:', err); } else { console.log('Download-Marker erfolgreich gesetzt für User:', downloadedBy); } // Fehler wird ignoriert, damit PDF trotzdem generiert wird }); } else { console.warn('PDF Download - Keine User ID in Session gefunden!'); } } doc.pipe(res); // Header (Kalenderwoche wurde bereits oben berechnet) doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' }); doc.moveDown(); // Mitarbeiter-Info doc.fontSize(12); doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`); doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`); doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`); doc.moveDown(); // Tabelle - Basis-Informationen const tableTop = doc.y; const colWidths = [80, 80, 80, 60, 80]; const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden']; // Tabellen-Header doc.fontSize(10).font('Helvetica-Bold'); let x = 50; headers.forEach((header, i) => { doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); doc.moveDown(); let y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.5); // Tabellen-Daten doc.font('Helvetica'); let totalHours = 0; let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung entries.forEach((entry) => { y = doc.y; x = 50; // Basis-Zeile const rowData = [ formatDate(entry.date), entry.start_time || '-', entry.end_time || '-', entry.break_minutes ? `${entry.break_minutes} min` : '-', entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' ]; rowData.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); // Tätigkeiten sammeln const activities = []; for (let i = 1; i <= 5; i++) { const desc = entry[`activity${i}_desc`]; const hours = entry[`activity${i}_hours`]; const projectNumber = entry[`activity${i}_project_number`]; if (desc && desc.trim() && hours > 0) { activities.push({ desc: desc.trim(), hours: parseFloat(hours), projectNumber: projectNumber ? projectNumber.trim() : null }); } } // Tätigkeiten anzeigen if (activities.length > 0) { doc.moveDown(0.3); doc.fontSize(9).font('Helvetica-Oblique'); doc.text('Tätigkeiten:', 60, doc.y, { width: 380 }); doc.moveDown(0.2); activities.forEach((activity, idx) => { let activityText = `${idx + 1}. ${activity.desc}`; if (activity.projectNumber) { activityText += ` (Projekt: ${activity.projectNumber})`; } activityText += ` - ${activity.hours.toFixed(2)} h`; doc.fontSize(9).font('Helvetica'); doc.text(activityText, 70, doc.y, { width: 360 }); doc.moveDown(0.2); }); doc.fontSize(10); } // Überstunden und Urlaub anzeigen const overtimeInfo = []; if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); } if (entry.vacation_type) { const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; overtimeInfo.push(`Urlaub: ${vacationText}`); } if (overtimeInfo.length > 0) { doc.moveDown(0.2); doc.fontSize(9).font('Helvetica-Oblique'); overtimeInfo.forEach((info, idx) => { doc.text(info, 70, doc.y, { width: 360 }); doc.moveDown(0.15); }); doc.fontSize(10); } if (entry.total_hours) { totalHours += entry.total_hours; } // Urlaubsstunden für Überstunden-Berechnung sammeln if (entry.vacation_type === 'full') { vacationHours += 8; // Ganzer Tag = 8 Stunden } else if (entry.vacation_type === 'half') { vacationHours += 4; // Halber Tag = 4 Stunden } doc.moveDown(0.5); // Trennlinie zwischen Einträgen y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.3); }); // Summe y = doc.y; doc.moveTo(50, y).lineTo(550, y).stroke(); doc.moveDown(0.5); doc.font('Helvetica-Bold'); doc.text(`Gesamtstunden: ${totalHours.toFixed(2)} h`, 50, doc.y); // Überstunden berechnen und anzeigen const wochenstunden = timesheet.wochenstunden || 0; // Überstunden = Gesamtstunden - Wochenstunden // Urlaub zählt als normale Arbeitszeit, daher sind Urlaubsstunden bereits in totalHours enthalten const overtimeHours = totalHours - wochenstunden; doc.moveDown(0.3); doc.font('Helvetica-Bold'); if (overtimeHours > 0) { doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); } else if (overtimeHours < 0) { doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); } else { doc.text(`Überstunden: 0.00 h`, 50, doc.y); } doc.end(); }); }); }); // Hilfsfunktionen function formatDate(dateStr) { const date = new Date(dateStr); return date.toLocaleDateString('de-DE'); } function formatDateTime(dateStr) { const date = new Date(dateStr); return date.toLocaleString('de-DE'); } // Start-Route app.get('/', (req, res) => { if (req.session.userId) { // Redirect: Immer zu Dashboard wenn Mitarbeiter-Rolle vorhanden, sonst basierend auf currentRole const roles = req.session.roles || []; if (roles.includes('mitarbeiter')) { res.redirect('/dashboard'); } else { const currentRole = req.session.currentRole || getDefaultRole(roles); if (currentRole === 'admin') { res.redirect('/admin'); } else if (currentRole === 'verwaltung') { res.redirect('/verwaltung'); } else { res.redirect('/dashboard'); } } } else { res.redirect('/login'); } }); // Automatische LDAP-Synchronisation einrichten function setupLDAPScheduler() { // Prüfe alle 5 Minuten, ob eine Synchronisation notwendig ist setInterval(() => { db.get('SELECT * FROM ldap_config WHERE id = 1 AND enabled = 1 AND sync_interval > 0', (err, config) => { if (err || !config) { return; // Keine aktive Konfiguration } const now = new Date(); const lastSync = config.last_sync ? new Date(config.last_sync) : null; const syncIntervalMs = config.sync_interval * 60 * 1000; // Minuten in Millisekunden // Prüfe ob Synchronisation fällig ist if (!lastSync || (now - lastSync) >= syncIntervalMs) { console.log('Starte automatische LDAP-Synchronisation...'); LDAPService.performSync('scheduled', (err, result) => { if (err) { console.error('Fehler bei automatischer LDAP-Synchronisation:', err.message); } else { console.log(`Automatische LDAP-Synchronisation abgeschlossen: ${result.synced} Benutzer synchronisiert`); } }); } }); }, 5 * 60 * 1000); // Alle 5 Minuten prüfen } // Check-in-Server (separater Express-App auf Port 3334) const checkinApp = express(); const CHECKIN_PORT = 3334; // Middleware für Check-in-Server checkinApp.use(express.json()); // API: Check-in (Kommen) checkinApp.get('/api/checkin/:userId', (req, res) => { const userId = parseInt(req.params.userId); const currentDate = getCurrentDate(); const currentTime = getCurrentTime(); // Prüfe ob User existiert db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); } // Prüfe ob bereits ein Eintrag für heute existiert db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, currentDate], (err, entry) => { if (err) { return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); } if (!entry) { // Kein Eintrag existiert → Erstelle neuen mit start_time db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, [userId, currentDate, currentTime], (err) => { if (err) { return res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Eintrags' }); } res.json({ success: true, message: `Start-Zeit erfasst: ${currentTime}`, start_time: currentTime, date: currentDate }); }); } else if (!entry.start_time) { // Eintrag existiert, aber keine Start-Zeit → Setze start_time db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [currentTime, entry.id], (err) => { if (err) { return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); } res.json({ success: true, message: `Start-Zeit erfasst: ${currentTime}`, start_time: currentTime, date: currentDate }); }); } else { // Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in res.json({ success: true, message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`, start_time: entry.start_time, date: currentDate }); } }); }); }); // API: Check-out (Gehen) checkinApp.get('/api/checkout/:userId', (req, res) => { const userId = parseInt(req.params.userId); const currentDate = getCurrentDate(); const currentTime = getCurrentTime(); // Prüfe ob User existiert db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); } // Prüfe ob bereits ein Eintrag für heute existiert db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, currentDate], (err, entry) => { if (err) { return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); } if (!entry || !entry.start_time) { // Kein Eintrag oder keine Start-Zeit → Fehler return res.status(400).json({ success: false, error: 'Bitte zuerst einchecken (Kommen).' }); } // Berechne total_hours basierend auf start_time, end_time und break_minutes const breakMinutes = entry.break_minutes || 0; const totalHours = updateTotalHours(entry.start_time, currentTime, breakMinutes); // Setze end_time (überschreibt vorherige End-Zeit falls vorhanden) db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [currentTime, totalHours, entry.id], (err) => { if (err) { return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); } res.json({ success: true, message: `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`, end_time: currentTime, total_hours: totalHours, date: currentDate }); }); }); }); }); // Server starten app.listen(PORT, () => { console.log(`Server läuft auf http://localhost:${PORT}`); console.log('Standard-Zugangsdaten:'); console.log('Admin: admin / admin123'); console.log('Verwaltung: verwaltung / verwaltung123'); // LDAP-Scheduler starten setupLDAPScheduler(); }); // Check-in-Server starten (auf Port 3334) checkinApp.listen(CHECKIN_PORT, () => { console.log(`Check-in Server läuft auf http://localhost:${CHECKIN_PORT}`); });