diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..effa617 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +node_modules +npm-debug.log +.git +.gitignore +README.md +SCHNELLSTART.md +.env +.env.local +*.db +*.sqlite +*.sqlite3 +.vscode +.idea +*.swp +*.swo +.DS_Store +Thumbs.db +logs +*.log +dev diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..812cbb8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:18-alpine + +WORKDIR /app + +# Package-Dateien kopieren +COPY package*.json ./ + +# Dependencies installieren +RUN npm ci --only=production + +# Anwendungsdateien kopieren +COPY . . + +# Ports freigeben +EXPOSE 3333 3334 + +# Anwendung starten +CMD ["node", "server.js"] diff --git a/checkin-server.js b/checkin-server.js new file mode 100644 index 0000000..58cd9f9 --- /dev/null +++ b/checkin-server.js @@ -0,0 +1,127 @@ +// Check-in Server (separater Express-App auf Port 3334) + +const express = require('express'); +const { db } = require('./database'); +const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); + +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 + }); + }); + }); + }); +}); + +// Check-in-Server starten (auf Port 3334) +checkinApp.listen(CHECKIN_PORT, () => { + console.log(`Check-in Server läuft auf http://localhost:${CHECKIN_PORT}`); +}); + +module.exports = checkinApp; diff --git a/database.js b/database.js index 623e61e..6b50871 100644 --- a/database.js +++ b/database.js @@ -2,7 +2,8 @@ const sqlite3 = require('sqlite3').verbose(); const bcrypt = require('bcryptjs'); const path = require('path'); -const dbPath = path.join(__dirname, 'stundenerfassung.db'); +// Datenbank-Pfad: Umgebungsvariable oder Standard-Pfad +const dbPath = process.env.DB_PATH || path.join(__dirname, 'stundenerfassung.db'); const db = new sqlite3.Database(dbPath); // Datenbank initialisieren @@ -183,6 +184,27 @@ function initDatabase() { db.run(`ALTER TABLE users ADD COLUMN urlaubstage REAL`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert }); + + // Migration: ping_ip Spalte hinzufügen + db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + }); + + // Ping-Status-Tabelle für IP-basierte Zeiterfassung + db.run(`CREATE TABLE IF NOT EXISTS ping_status ( + user_id INTEGER NOT NULL, + date TEXT NOT NULL, + last_successful_ping DATETIME, + failed_ping_count INTEGER DEFAULT 0, + start_time_set INTEGER DEFAULT 0, + first_failed_ping_time DATETIME, + PRIMARY KEY (user_id, date), + FOREIGN KEY (user_id) REFERENCES users(id) + )`, (err) => { + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Erstellen der ping_status Tabelle:', err.message); + } + }); // LDAP-Konfiguration-Tabelle db.run(`CREATE TABLE IF NOT EXISTS ldap_config ( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9e311a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3.8' + +services: + stundenerfassung: + build: . + container_name: stundenerfassung + ports: + - "3333:3333" # Hauptserver + - "3334:3334" # Check-in Server + volumes: + # Datenbank: Host /var/stundenerfassung/ -> Container Projekt-Verzeichnis (/app) + - /var/stundenerfassung/stundenerfassung.db:/app/stundenerfassung.db + environment: + - NODE_ENV=production + restart: unless-stopped diff --git a/helpers/utils.js b/helpers/utils.js new file mode 100644 index 0000000..94efc81 --- /dev/null +++ b/helpers/utils.js @@ -0,0 +1,86 @@ +// Helper-Funktionen für das Stundenerfassungs-System + +// 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 +} + +// Helper: Formatiert Datum für Anzeige (DD.MM.YYYY) +function formatDate(dateStr) { + const date = new Date(dateStr); + return date.toLocaleDateString('de-DE'); +} + +// Helper: Formatiert Datum und Zeit für Anzeige +function formatDateTime(dateStr) { + const date = new Date(dateStr); + return date.toLocaleString('de-DE'); +} + +module.exports = { + hasRole, + getDefaultRole, + getCurrentDate, + getCurrentTime, + calculateBreakMinutes, + updateTotalHours, + formatDate, + formatDateTime +}; diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..cbda134 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,48 @@ +// Authentifizierungs-Middleware + +const { hasRole } = require('../helpers/utils'); + +// 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'); + } +} + +module.exports = { + requireAuth, + requireRole, + requireAdmin, + requireVerwaltung +}; diff --git a/package-lock.json b/package-lock.json index be5afbb..fe006e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "ldapjs": "^3.0.7", "node-cron": "^3.0.3", "pdfkit": "^0.13.0", + "ping": "^0.4.4", "sqlite3": "^5.1.6" }, "devDependencies": { @@ -2485,6 +2486,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/png-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", @@ -5286,6 +5295,11 @@ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-56ZMC0j7SCsMMLdOoUg12VZCfj/+ZO+yfOSjaNCRrmZZr6GLbN2X/Ui56T15dI8NhiHckaw5X2pvyfAomanwqQ==" + }, "png-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", diff --git a/package.json b/package.json index 5dacbe4..bd91740 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "ejs": "^3.1.9", "pdfkit": "^0.13.0", "ldapjs": "^3.0.7", - "node-cron": "^3.0.3" + "node-cron": "^3.0.3", + "ping": "^0.4.4" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 27c413b..05dd25e 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -64,6 +64,9 @@ document.addEventListener('DOMContentLoaded', async function() { // Statistiken laden loadUserStats(); + // Ping-IP laden + loadPingIP(); + loadWeek(); document.getElementById('prevWeek').addEventListener('click', function() { @@ -371,9 +374,9 @@ function renderWeek() { step="0.25" placeholder="0.00" ${disabled} - onblur="saveEntry(this)" + onblur="handleOvertimeChange('${dateStr}', this.value); saveEntry(this);" oninput="updateOvertimeDisplay();" - onchange="updateOvertimeDisplay();" + onchange="handleOvertimeChange('${dateStr}', this.value); updateOvertimeDisplay();" style="width: 80px; margin-left: 5px;" class="overtime-input"> h @@ -620,6 +623,85 @@ function updateOvertimeDisplay() { } } +// Überstunden-Änderung verarbeiten +function handleOvertimeChange(dateStr, overtimeHours) { + if (!userWochenstunden || userWochenstunden <= 0) { + console.warn('Wochenstunden nicht verfügbar, kann Überstunden-Logik nicht anwenden'); + return; + } + + const fullDayHours = userWochenstunden / 5; + const overtimeValue = parseFloat(overtimeHours) || 0; + + // Prüfe ob ganzer Tag Überstunden + if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { + // Ganzer Tag Überstunden + // Setze Activity1 auf "Überstunden" mit 0 Stunden + const activity1DescInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_desc"]`); + const activity1HoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity1_hours"]`); + + if (activity1DescInput) { + activity1DescInput.value = 'Überstunden'; + // Trigger saveEntry für dieses Feld + saveEntry(activity1DescInput); + } + + if (activity1HoursInput) { + activity1HoursInput.value = '0'; + // Trigger saveEntry für dieses Feld + saveEntry(activity1HoursInput); + } + + // Leere Start- und End-Zeit + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + + if (startInput) { + startInput.value = ''; + saveEntry(startInput); + } + + if (endInput) { + endInput.value = ''; + saveEntry(endInput); + } + + } else if (overtimeValue > 0 && overtimeValue < fullDayHours) { + // Weniger als ganzer Tag - füge "Überstunden" als Tätigkeit hinzu + // Finde erste freie Activity-Spalte oder prüfe ob bereits vorhanden + let foundOvertime = false; + let firstEmptySlot = null; + + for (let i = 1; i <= 5; i++) { + const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_desc"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${i}_hours"]`); + + if (descInput && descInput.value && descInput.value.trim().toLowerCase() === 'überstunden') { + foundOvertime = true; + break; // Bereits vorhanden + } + + if (!firstEmptySlot && descInput && (!descInput.value || descInput.value.trim() === '')) { + firstEmptySlot = i; + } + } + + // Wenn nicht gefunden und freier Slot vorhanden, füge hinzu + if (!foundOvertime && firstEmptySlot) { + const descInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${firstEmptySlot}_desc"]`); + const hoursInput = document.querySelector(`input[data-date="${dateStr}"][data-field="activity${firstEmptySlot}_hours"]`); + + if (descInput) { + descInput.value = 'Überstunden'; + saveEntry(descInput); + } + + // Stunden bleiben unverändert (werden vom User eingegeben oder bleiben leer) + // total_hours bleibt auch unverändert + } + } +} + // Eintrag speichern async function saveEntry(input) { const date = input.dataset.date; @@ -1200,3 +1282,67 @@ function toggleSickStatus(dateStr) { } } } + +// Ping-IP laden +async function loadPingIP() { + try { + const response = await fetch('/api/user/ping-ip'); + if (!response.ok) { + throw new Error('Fehler beim Laden der IP-Adresse'); + } + const data = await response.json(); + const pingIpInput = document.getElementById('pingIpInput'); + if (pingIpInput) { + pingIpInput.value = data.ping_ip || ''; + } + } catch (error) { + console.error('Fehler beim Laden der Ping-IP:', error); + } +} + +// Ping-IP speichern (global für onclick) +window.savePingIP = async function() { + const pingIpInput = document.getElementById('pingIpInput'); + if (!pingIpInput) { + return; + } + + const pingIp = pingIpInput.value.trim(); + + // Finde den Button (nächstes Geschwisterelement oder über Parent) + const button = pingIpInput.parentElement?.querySelector('button') || + document.querySelector('button[onclick*="savePingIP"]'); + + try { + const response = await fetch('/api/user/ping-ip', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ ping_ip: pingIp }) + }); + + const result = await response.json(); + + if (!response.ok) { + alert(result.error || 'Fehler beim Speichern der IP-Adresse'); + return; + } + + // Erfolgs-Feedback + if (button) { + const originalText = button.textContent; + button.textContent = 'Gespeichert!'; + button.style.backgroundColor = '#27ae60'; + setTimeout(() => { + button.textContent = originalText; + button.style.backgroundColor = ''; + }, 2000); + } + + console.log('Ping-IP gespeichert:', result.ping_ip); + } catch (error) { + console.error('Fehler beim Speichern der Ping-IP:', error); + alert('Fehler beim Speichern der IP-Adresse'); + } +}; diff --git a/routes/admin-ldap.js b/routes/admin-ldap.js new file mode 100644 index 0000000..4e84c2e --- /dev/null +++ b/routes/admin-ldap.js @@ -0,0 +1,167 @@ +// LDAP Admin Routes + +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); +const { requireAdmin } = require('../middleware/auth'); + +// Routes registrieren +function registerAdminLDAPRoutes(app) { + // 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 || [] }); + } + ); + }); +} + +module.exports = registerAdminLDAPRoutes; diff --git a/routes/admin.js b/routes/admin.js new file mode 100644 index 0000000..f8cbe54 --- /dev/null +++ b/routes/admin.js @@ -0,0 +1,154 @@ +// Admin Routes + +const bcrypt = require('bcryptjs'); +const { db } = require('../database'); +const { requireAdmin } = require('../middleware/auth'); + +// Routes registrieren +function registerAdminRoutes(app) { + // 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 }); + }); + } + }); +} + +module.exports = registerAdminRoutes; diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..2561cbb --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,121 @@ +// Authentifizierungs-Routes + +const bcrypt = require('bcryptjs'); +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); +const { getDefaultRole } = require('../helpers/utils'); + +// 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'); + } +} + +// Routes registrieren +function registerAuthRoutes(app) { + // 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' }); + } + }); + } + }); + }); + + // Logout + app.get('/logout', (req, res) => { + req.session.destroy(); + res.redirect('/login'); + }); +} + +module.exports = registerAuthRoutes; diff --git a/routes/dashboard.js b/routes/dashboard.js new file mode 100644 index 0000000..3f0e256 --- /dev/null +++ b/routes/dashboard.js @@ -0,0 +1,35 @@ +// Dashboard-Route + +const { hasRole } = require('../helpers/utils'); +const { requireAuth } = require('../middleware/auth'); + +// Routes registrieren +function registerDashboardRoutes(app) { + // 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' + } + }); + }); +} + +module.exports = registerDashboardRoutes; diff --git a/routes/timesheet.js b/routes/timesheet.js new file mode 100644 index 0000000..6d107be --- /dev/null +++ b/routes/timesheet.js @@ -0,0 +1,378 @@ +// Timesheet API Routes + +const { db } = require('../database'); +const { requireAuth, requireVerwaltung } = require('../middleware/auth'); +const { generatePDF } = require('../services/pdf-service'); + +// Routes registrieren +function registerTimesheetRoutes(app) { + // 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'; + + // User-Daten laden (für Überstunden-Berechnung) + db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + console.error('Fehler beim Laden der User-Daten:', err); + return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' }); + } + + const wochenstunden = user?.wochenstunden || 0; + const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0; + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 0; + + // Überstunden-Logik: Prüfe ob ganzer Tag oder weniger + let isFullDayOvertime = false; + if (overtimeValue > 0 && fullDayHours > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { + isFullDayOvertime = true; + } + + // 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; + let finalActivity2Desc = activity2_desc; + let finalActivity3Desc = activity3_desc; + let finalActivity4Desc = activity4_desc; + let finalActivity5Desc = activity5_desc; + let finalStartTime = normalizedStartTime; + let finalEndTime = normalizedEndTime; + + // Überstunden-Logik: Bei vollem Tag Überstunden + if (isFullDayOvertime) { + total_hours = 0; + finalStartTime = null; + finalEndTime = null; + finalActivity1Desc = 'Überstunden'; + finalActivity1Hours = 0; + } else 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 + } + + // Überstunden-Logik: Bei weniger als vollem Tag - füge "Überstunden" als Tätigkeit hinzu + if (overtimeValue > 0 && !isFullDayOvertime && fullDayHours > 0) { + // Prüfe ob "Überstunden" bereits in activities vorhanden + const activities = [ + { desc: finalActivity1Desc, hours: finalActivity1Hours }, + { desc: finalActivity2Desc, hours: activity2_hours }, + { desc: finalActivity3Desc, hours: activity3_hours }, + { desc: finalActivity4Desc, hours: activity4_hours }, + { desc: finalActivity5Desc, hours: activity5_hours } + ]; + + let foundOvertime = false; + for (let i = 0; i < activities.length; i++) { + if (activities[i].desc && activities[i].desc.trim().toLowerCase() === 'überstunden') { + foundOvertime = true; + break; + } + } + + // Wenn nicht gefunden, füge zur ersten freien Activity-Spalte hinzu + if (!foundOvertime) { + for (let i = 0; i < activities.length; i++) { + if (!activities[i].desc || activities[i].desc.trim() === '') { + // Setze diese Activity auf "Überstunden" + if (i === 0) { + finalActivity1Desc = 'Überstunden'; + // Stunden bleiben unverändert (werden vom User eingegeben) + } else if (i === 1) { + finalActivity2Desc = 'Überstunden'; + } else if (i === 2) { + finalActivity3Desc = 'Überstunden'; + } else if (i === 3) { + finalActivity4Desc = 'Überstunden'; + } else if (i === 4) { + finalActivity5Desc = 'Überstunden'; + } + break; + } + } + } + } + + // 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 = ?`, + [ + finalStartTime, finalEndTime, break_minutes, total_hours, notes, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, + finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, + finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, + finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, + finalActivity5Desc || 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, finalStartTime, finalEndTime, break_minutes, total_hours, notes, + finalActivity1Desc || null, finalActivity1Hours, activity1_project_number || null, + finalActivity2Desc || null, parseFloat(activity2_hours) || 0, activity2_project_number || null, + finalActivity3Desc || null, parseFloat(activity3_hours) || 0, activity3_project_number || null, + finalActivity4Desc || null, parseFloat(activity4_hours) || 0, activity4_project_number || null, + finalActivity5Desc || 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 }); + }); + } + }); + }); + }); + + // 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: 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; + generatePDF(timesheetId, req, res); + }); +} + +module.exports = registerTimesheetRoutes; diff --git a/routes/user.js b/routes/user.js new file mode 100644 index 0000000..516bb76 --- /dev/null +++ b/routes/user.js @@ -0,0 +1,310 @@ +// User API Routes + +const { db } = require('../database'); +const { hasRole, getCurrentDate } = require('../helpers/utils'); +const { requireAuth } = require('../middleware/auth'); + +// Routes registrieren +function registerUserRoutes(app) { + // 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: Ping-IP abrufen + app.get('/api/user/ping-ip', requireAuth, (req, res) => { + const userId = req.session.userId; + + db.get('SELECT ping_ip FROM users WHERE id = ?', [userId], (err, user) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der IP-Adresse' }); + } + + res.json({ ping_ip: user?.ping_ip || null }); + }); + }); + + // API: Ping-IP speichern + app.post('/api/user/ping-ip', requireAuth, (req, res) => { + const userId = req.session.userId; + const { ping_ip } = req.body; + + // Validierung: IPv4 Format (einfache Prüfung) + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; + if (ping_ip && ping_ip.trim() !== '' && !ipv4Regex.test(ping_ip.trim())) { + return res.status(400).json({ error: 'Ungültige IP-Adresse. Bitte geben Sie eine gültige IPv4-Adresse ein.' }); + } + + // Normalisiere: Leere Strings werden zu null + const normalizedPingIp = (ping_ip && ping_ip.trim() !== '') ? ping_ip.trim() : null; + + db.run('UPDATE users SET ping_ip = ? WHERE id = ?', [normalizedPingIp, userId], (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der IP-Adresse' }); + } + + // Wenn IP entfernt wurde, lösche auch den Ping-Status für heute + if (!normalizedPingIp) { + const currentDate = getCurrentDate(); + db.run('DELETE FROM ping_status WHERE user_id = ? AND date = ?', [userId, currentDate], (err) => { + // Fehler ignorieren + }); + } + + res.json({ success: true, ping_ip: normalizedPingIp }); + }); + }); + + // 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 (nur neueste pro Tag) + db.all(`SELECT id, date, total_hours, overtime_taken_hours, vacation_type, sick_status, start_time, end_time, updated_at + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + ORDER BY date, updated_at DESC, id DESC`, + [userId, week.week_start, week.week_end], + (err, allEntries) => { + 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' }); + } + + // Filtere auf neuesten Eintrag pro Tag + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + 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; + } + } + }); + + // Konvertiere zurück zu Array + const entries = Object.values(entriesByDate); + + // Prüfe ob Woche vollständig ausgefüllt ist (alle 5 Werktage) + + // Prüfe alle 5 Werktage (Montag-Freitag) + const startDate = new Date(week.week_start); + const endDate = new Date(week.week_end); + let workdays = 0; + let filledWorkdays = 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++; + const dateStr = d.toISOString().split('T')[0]; + const entry = entriesByDate[dateStr]; + + // Tag gilt als ausgefüllt wenn: + // - Ganzer Tag Urlaub (vacation_type = 'full') + // - Krank (sick_status = 1) + // - ODER Start- und End-Zeit vorhanden sind + if (entry) { + const isFullDayVacation = entry.vacation_type === 'full'; + const isSick = entry.sick_status === 1 || entry.sick_status === true; + const hasStartAndEnd = entry.start_time && entry.end_time && + entry.start_time.toString().trim() !== '' && + entry.end_time.toString().trim() !== ''; + + if (isFullDayVacation || isSick || hasStartAndEnd) { + filledWorkdays++; + } + } + } + } + + // Nur berechnen wenn alle Werktage ausgefüllt sind + if (filledWorkdays < workdays) { + // Woche nicht vollständig - überspringe diese Woche + processedWeeks++; + 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 + }); + } + return; // Überspringe diese Woche + } + + // Berechnungen für diese Woche (nur wenn vollständig ausgefüllt) + 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; + } + }); + + // 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 + }); + } + }); + }); + }); + }); + }); +} + +module.exports = registerUserRoutes; diff --git a/routes/verwaltung.js b/routes/verwaltung.js new file mode 100644 index 0000000..0caa718 --- /dev/null +++ b/routes/verwaltung.js @@ -0,0 +1,195 @@ +// Verwaltung Routes + +const { db } = require('../database'); +const { requireVerwaltung } = require('../middleware/auth'); + +// Routes registrieren +function registerVerwaltungRoutes(app) { + // 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: Ü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 }); + }); + }); +} + +module.exports = registerVerwaltungRoutes; diff --git a/server.js b/server.js index a40a82d..9f6cdc2 100644 --- a/server.js +++ b/server.js @@ -1,11 +1,9 @@ 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 { initDatabase } = require('./database'); +const { getDefaultRole } = require('./helpers/utils'); const app = express(); const PORT = 3333; @@ -28,1462 +26,27 @@ app.use(session({ // 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); -} +// Routes importieren und registrieren +const registerAuthRoutes = require('./routes/auth'); +const registerDashboardRoutes = require('./routes/dashboard'); +const registerUserRoutes = require('./routes/user'); +const registerAdminRoutes = require('./routes/admin'); +const registerAdminLDAPRoutes = require('./routes/admin-ldap'); +const registerVerwaltungRoutes = require('./routes/verwaltung'); +const registerTimesheetRoutes = require('./routes/timesheet'); -// 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 -} +// Services importieren +const { setupPingService } = require('./services/ping-service'); +const { setupLDAPScheduler } = require('./services/ldap-scheduler'); -// 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'); -} +// Routes registrieren +registerAuthRoutes(app); +registerDashboardRoutes(app); +registerUserRoutes(app); +registerAdminRoutes(app); +registerAdminLDAPRoutes(app); +registerVerwaltungRoutes(app); +registerTimesheetRoutes(app); // Start-Route app.get('/', (req, res) => { @@ -1507,150 +70,6 @@ app.get('/', (req, res) => { } }); -// 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}`); @@ -1660,9 +79,11 @@ app.listen(PORT, () => { // LDAP-Scheduler starten setupLDAPScheduler(); + + // Ping-Service starten + setupPingService(); + console.log('Ping-Service gestartet (prüft alle IPs jede Minute)'); }); -// Check-in-Server starten (auf Port 3334) -checkinApp.listen(CHECKIN_PORT, () => { - console.log(`Check-in Server läuft auf http://localhost:${CHECKIN_PORT}`); -}); +// Check-in-Server starten (separater Server auf Port 3334) +require('./checkin-server'); diff --git a/services/ldap-scheduler.js b/services/ldap-scheduler.js new file mode 100644 index 0000000..b6e74c4 --- /dev/null +++ b/services/ldap-scheduler.js @@ -0,0 +1,34 @@ +// LDAP-Scheduler Service + +const { db } = require('../database'); +const LDAPService = require('../ldap-service'); + +// 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 +} + +module.exports = { setupLDAPScheduler }; diff --git a/services/pdf-service.js b/services/pdf-service.js new file mode 100644 index 0000000..1dc60a7 --- /dev/null +++ b/services/pdf-service.js @@ -0,0 +1,271 @@ +// PDF-Generierung Service + +const PDFDocument = require('pdfkit'); +const { db } = require('../database'); +const { formatDate, formatDateTime } = require('../helpers/utils'); + +// Kalenderwoche berechnen +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; +} + +// PDF generieren +function generatePDF(timesheetId, req, res) { + 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'); + } + + // Hole Einträge die zum Zeitpunkt der Einreichung existierten + // Filtere nach submitted_at der Version, damit jede Version ihre eigenen Daten zeigt + // Logik: Wenn updated_at existiert, verwende das, sonst created_at, sonst zeige Eintrag (für alte Daten ohne Timestamps) + db.all(`SELECT * FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + AND ( + (updated_at IS NOT NULL AND updated_at <= ?) OR + (updated_at IS NULL AND created_at IS NOT NULL AND created_at <= ?) OR + (updated_at IS NULL AND created_at IS NULL) + ) + ORDER BY date, updated_at DESC, id DESC`, + [timesheet.user_id, timesheet.week_start, timesheet.week_end, + timesheet.submitted_at, timesheet.submitted_at], + (err, allEntries) => { + if (err) { + return res.status(500).send('Fehler beim Abrufen der Einträge'); + } + + // Filtere auf neuesten Eintrag pro Tag (basierend auf updated_at oder id) + const entriesByDate = {}; + (allEntries || []).forEach(entry => { + const existing = entriesByDate[entry.date]; + 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; + } + } + }); + + // Konvertiere zu Array und sortiere nach Datum + const entries = Object.values(entriesByDate).sort((a, b) => { + return new Date(a.date) - new Date(b.date); + }); + + const doc = new PDFDocument({ margin: 50 }); + + // Prüfe ob inline angezeigt werden soll (für Vorschau) + const inline = req.query.inline === 'true'; + + // 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(); + }); + }); +} + +module.exports = { generatePDF }; diff --git a/services/ping-service.js b/services/ping-service.js new file mode 100644 index 0000000..90f10b6 --- /dev/null +++ b/services/ping-service.js @@ -0,0 +1,182 @@ +// Ping-Service für IP-basierte automatische Zeiterfassung + +const ping = require('ping'); +const { db } = require('../database'); +const { getCurrentDate, getCurrentTime, updateTotalHours } = require('../helpers/utils'); + +// Ping-Funktion für einen User +async function pingUserIP(userId, ip, currentDate, currentTime) { + try { + const result = await ping.promise.probe(ip, { + timeout: 3, + min_reply: 1 + }); + + const isReachable = result.alive; + const now = new Date().toISOString(); + + // Hole oder erstelle Ping-Status für heute + db.get('SELECT * FROM ping_status WHERE user_id = ? AND date = ?', + [userId, currentDate], (err, pingStatus) => { + if (err) { + console.error(`Fehler beim Abrufen des Ping-Status für User ${userId}:`, err); + return; + } + + // Hole aktuellen Eintrag für heute + 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) { + console.error(`Fehler beim Abrufen des Eintrags für User ${userId}:`, err); + return; + } + + if (isReachable) { + // IP ist erreichbar + if (!pingStatus) { + // Erstelle neuen Ping-Status + db.run(`INSERT INTO ping_status (user_id, date, last_successful_ping, failed_ping_count, start_time_set) + VALUES (?, ?, ?, 0, 0)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } else { + // Update Ping-Status: Reset failed_ping_count, update last_successful_ping + db.run(`UPDATE ping_status + SET last_successful_ping = ?, failed_ping_count = 0, first_failed_ping_time = NULL + WHERE user_id = ? AND date = ?`, + [now, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + } + + // Start-Zeit setzen wenn noch nicht vorhanden + if (entry && !entry.start_time) { + db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [currentTime, entry.id], (err) => { + if (err) { + console.error(`Fehler beim Setzen der Start-Zeit für User ${userId}:`, err); + } else { + console.log(`Start-Zeit gesetzt für User ${userId} um ${currentTime}`); + // Markiere dass Start-Zeit gesetzt wurde + db.run('UPDATE ping_status SET start_time_set = 1 WHERE user_id = ? AND date = ?', + [userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren von start_time_set:`, err); + }); + } + }); + } else 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) { + console.error(`Fehler beim Erstellen des Eintrags für User ${userId}:`, err); + } else { + console.log(`Eintrag erstellt und Start-Zeit gesetzt für User ${userId} um ${currentTime}`); + // Markiere dass Start-Zeit gesetzt wurde + db.run('UPDATE ping_status SET start_time_set = 1 WHERE user_id = ? AND date = ?', + [userId, currentDate], (err) => { + if (err) { + // Falls kein Ping-Status existiert, erstelle einen + db.run(`INSERT INTO ping_status (user_id, date, last_successful_ping, failed_ping_count, start_time_set) + VALUES (?, ?, ?, 0, 1)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } + }); + } + }); + } + } else { + // IP ist nicht erreichbar + if (!pingStatus) { + // Erstelle neuen Ping-Status mit failed_ping_count = 1 + db.run(`INSERT INTO ping_status (user_id, date, failed_ping_count, first_failed_ping_time) + VALUES (?, ?, 1, ?)`, + [userId, currentDate, now], (err) => { + if (err) console.error(`Fehler beim Erstellen des Ping-Status:`, err); + }); + } else { + // Erhöhe failed_ping_count + const newFailedCount = (pingStatus.failed_ping_count || 0) + 1; + const firstFailedTime = pingStatus.first_failed_ping_time || now; + + db.run(`UPDATE ping_status + SET failed_ping_count = ?, first_failed_ping_time = ? + WHERE user_id = ? AND date = ?`, + [newFailedCount, firstFailedTime, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + + // Wenn 3 oder mehr fehlgeschlagene Pings UND Start-Zeit existiert UND keine End-Zeit + if (newFailedCount >= 3 && entry && entry.start_time && !entry.end_time) { + // Setze End-Zeit auf Zeit des ersten fehlgeschlagenen Pings + const firstFailedDate = new Date(firstFailedTime); + const endTime = `${String(firstFailedDate.getHours()).padStart(2, '0')}:${String(firstFailedDate.getMinutes()).padStart(2, '0')}`; + + // Berechne total_hours + const breakMinutes = entry.break_minutes || 0; + const totalHours = updateTotalHours(entry.start_time, endTime, breakMinutes); + + db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [endTime, totalHours, entry.id], (err) => { + if (err) { + console.error(`Fehler beim Setzen der End-Zeit für User ${userId}:`, err); + } else { + console.log(`End-Zeit gesetzt für User ${userId} um ${endTime} (nach ${newFailedCount} fehlgeschlagenen Pings)`); + } + }); + } + } + } + }); + }); + } catch (error) { + console.error(`Fehler beim Ping für User ${userId} (IP: ${ip}):`, error); + // Behandle als nicht erreichbar + const now = new Date().toISOString(); + db.get('SELECT * FROM ping_status WHERE user_id = ? AND date = ?', + [userId, currentDate], (err, pingStatus) => { + if (!err && pingStatus) { + const newFailedCount = (pingStatus.failed_ping_count || 0) + 1; + const firstFailedTime = pingStatus.first_failed_ping_time || now; + + db.run(`UPDATE ping_status + SET failed_ping_count = ?, first_failed_ping_time = ? + WHERE user_id = ? AND date = ?`, + [newFailedCount, firstFailedTime, userId, currentDate], (err) => { + if (err) console.error(`Fehler beim Aktualisieren des Ping-Status:`, err); + }); + } + }); + } +} + +// Ping-Service Setup +function setupPingService() { + setInterval(async () => { + const currentDate = getCurrentDate(); + const currentTime = getCurrentTime(); + + // Hole alle User mit IP-Adresse + db.all('SELECT id, ping_ip FROM users WHERE ping_ip IS NOT NULL AND ping_ip != ""', (err, users) => { + if (err) { + console.error('Fehler beim Abrufen der User mit IP-Adressen:', err); + return; + } + + if (!users || users.length === 0) { + return; // Keine User mit IP-Adressen + } + + // Ping alle User parallel + users.forEach(user => { + pingUserIP(user.id, user.ping_ip, currentDate, currentTime); + }); + }); + }, 60000); // Jede Minute +} + +module.exports = { setupPingService }; diff --git a/views/dashboard.ejs b/views/dashboard.ejs index d31a11b..0174632 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -68,7 +68,7 @@
+ Geben Sie Ihre IP-Adresse ein. Das System pingt diese jede Minute automatisch. Bei erster Erreichbarkeit wird die Start-Zeit gesetzt, bei 3+ fehlgeschlagenen Pings die End-Zeit. +
+