diff --git a/database.js b/database.js index 9bcb239..1812054 100644 --- a/database.js +++ b/database.js @@ -20,7 +20,7 @@ function initDatabase() { last_week_start TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP )`); - + // Migration: last_week_start Spalte hinzufügen falls sie nicht existiert db.run(`ALTER TABLE users ADD COLUMN last_week_start TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert @@ -51,7 +51,7 @@ function initDatabase() { updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) )`); - + // Migration: Tätigkeitsfelder hinzufügen falls sie nicht existieren const activityColumns = [ 'activity1_desc', 'activity1_hours', @@ -60,7 +60,7 @@ function initDatabase() { 'activity4_desc', 'activity4_hours', 'activity5_desc', 'activity5_hours' ]; - + activityColumns.forEach(col => { const colType = col.includes('_hours') ? 'REAL' : 'TEXT'; db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} ${colType}`, (err) => { @@ -85,18 +85,18 @@ function initDatabase() { FOREIGN KEY (reviewed_by) REFERENCES users(id), FOREIGN KEY (pdf_downloaded_by) REFERENCES users(id) )`); - + // Migration: version Spalte hinzufügen falls sie nicht existiert db.run(`ALTER TABLE weekly_timesheets ADD COLUMN version INTEGER DEFAULT 1`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert // Wenn Spalte neu erstellt wurde, bestehende Einträge haben automatisch version = 1 }); - + // Migration: pdf_downloaded_at Spalte hinzufügen falls sie nicht existiert db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_at DATETIME`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert }); - + // Migration: pdf_downloaded_by Spalte hinzufügen falls sie nicht existiert db.run(`ALTER TABLE weekly_timesheets ADD COLUMN pdf_downloaded_by INTEGER`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert @@ -124,7 +124,7 @@ function initDatabase() { 'activity3_project_number', 'activity4_project_number', 'activity5_project_number' ]; - + projectNumberColumns.forEach(col => { db.run(`ALTER TABLE timesheet_entries ADD COLUMN ${col} TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert @@ -141,7 +141,7 @@ function initDatabase() { console.warn('Warnung beim Hinzufügen der Spalte overtime_taken_hours:', err.message); } }); - + db.run(`ALTER TABLE timesheet_entries ADD COLUMN vacation_type TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert if (err && !err.message.includes('duplicate column')) { @@ -164,7 +164,7 @@ function initDatabase() { console.warn('Warnung beim Hinzufügen der Spalte pause_start_time:', err.message); } }); - + db.run(`ALTER TABLE timesheet_entries ADD COLUMN pause_end_time TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert if (err && !err.message.includes('duplicate column')) { @@ -176,15 +176,15 @@ function initDatabase() { db.run(`ALTER TABLE users ADD COLUMN personalnummer TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert }); - + db.run(`ALTER TABLE users ADD COLUMN wochenstunden REAL`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert }); - + db.run(`ALTER TABLE users ADD COLUMN urlaubstage REAL`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert }); - + // Migration: Überstunden-Offset (manuelle Korrektur durch Verwaltung) db.run(`ALTER TABLE users ADD COLUMN overtime_offset_hours REAL DEFAULT 0`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert @@ -249,6 +249,22 @@ function initDatabase() { name TEXT )`); + // System-Optionen-Tabelle für Wochenend-Prozentsätze + db.run(`CREATE TABLE IF NOT EXISTS system_options ( + id INTEGER PRIMARY KEY DEFAULT 1, + saturday_percentage REAL DEFAULT 100, + sunday_percentage REAL DEFAULT 100, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + CHECK (id = 1) + )`); + + // Standard-Eintrag für system_options erstellen falls nicht vorhanden + db.run(`INSERT OR IGNORE INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, 100, 100)`, (err) => { + if (err && !err.message.includes('UNIQUE constraint')) { + console.warn('Warnung beim Erstellen des Standard-Eintrags für system_options:', err.message); + } + }); + // Migration: Bestehende Rollen zu JSON-Arrays konvertieren // Prüfe ob Rollen noch als einfache Strings gespeichert sind (nicht als JSON-Array) db.all('SELECT id, role FROM users', (err, users) => { @@ -265,7 +281,7 @@ function initDatabase() { } catch (e) { // Nicht JSON, konvertiere zu JSON-Array } - + // Konvertiere zu JSON-Array const roleArray = JSON.stringify([roleValue]); db.run('UPDATE users SET role = ? WHERE id = ?', [roleArray, user.id], (err) => { @@ -280,14 +296,14 @@ function initDatabase() { // Standard Admin-Benutzer erstellen const adminPassword = bcrypt.hashSync('admin123', 10); db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role) - VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`, - [adminPassword, JSON.stringify(['admin'])]); + VALUES (1, 'admin', ?, 'System', 'Administrator', ?)`, + [adminPassword, JSON.stringify(['admin'])]); // Standard Verwaltungs-Benutzer erstellen const verwaltungPassword = bcrypt.hashSync('verwaltung123', 10); db.run(`INSERT OR IGNORE INTO users (id, username, password, firstname, lastname, role) - VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`, - [verwaltungPassword, JSON.stringify(['verwaltung'])]); + VALUES (2, 'verwaltung', ?, 'Verwaltung', 'User', ?)`, + [verwaltungPassword, JSON.stringify(['verwaltung'])]); }); } diff --git a/email-mitarbeiter-stundenerfassung.txt b/email-mitarbeiter-stundenerfassung.txt deleted file mode 100644 index 7df208f..0000000 --- a/email-mitarbeiter-stundenerfassung.txt +++ /dev/null @@ -1,21 +0,0 @@ -Test - Stundenerfassung - -Hallo zusammen, - -Mara ist auf mich mit einer Bitte herangetreten, ob ich die Stundenerfassung digitalisieren kann. -Das habe ich die letzten 2 Wochen am abend und am WE gemacht. - -Ich glaube, dass das System jetzt fest fertig ist und ihr es testen könnt -Der test soll Fehler finden und mir noch die möglichkeit geben diese dann zu beheben. - -Am Montag würde ich gerne eine kurze Einführung für die Leute im Büro geben. -Um ca. 11:00 Uhr für so 10-15 Minuten. - -Achtet bitte am Anfangauf die Überstundenerechnung, da könnte noch der ein oder andere Fehler drin sein. - -Die Seite ist im Browser zu finden unter http://stunden.sds-systemtechnik.de:3333 oder http://192.168.120.64:3333 - - -Viele Grüße -Carsten Graf - diff --git a/public/js/admin.js b/public/js/admin.js index db21727..734da52 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -54,6 +54,43 @@ document.addEventListener('DOMContentLoaded', function() { // LDAP-Konfiguration laden loadLDAPConfig(); + // Optionen laden + loadOptions(); + + // Optionen-Formular + const optionsForm = document.getElementById('optionsForm'); + if (optionsForm) { + optionsForm.addEventListener('submit', async function(e) { + e.preventDefault(); + + const formData = { + saturday_percentage: document.getElementById('saturdayPercentage').value, + sunday_percentage: document.getElementById('sundayPercentage').value + }; + + try { + const response = await fetch('/admin/options', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (result.success) { + alert('Optionen wurden erfolgreich gespeichert!'); + } else { + alert('Fehler: ' + (result.error || 'Optionen konnten nicht gespeichert werden')); + } + } catch (error) { + console.error('Fehler:', error); + alert('Fehler beim Speichern der Optionen'); + } + }); + } + // LDAP-Konfigurationsformular const ldapConfigForm = document.getElementById('ldapConfigForm'); if (ldapConfigForm) { @@ -161,6 +198,27 @@ document.addEventListener('DOMContentLoaded', function() { } }); +// Optionen laden und Formular ausfüllen +async function loadOptions() { + try { + const response = await fetch('/admin/options'); + const result = await response.json(); + + if (result.config) { + const config = result.config; + + if (document.getElementById('saturdayPercentage')) { + document.getElementById('saturdayPercentage').value = config.saturday_percentage || 0; + } + if (document.getElementById('sundayPercentage')) { + document.getElementById('sundayPercentage').value = config.sunday_percentage || 0; + } + } + } catch (error) { + console.error('Fehler beim Laden der Optionen:', error); + } +} + // LDAP-Konfiguration laden und Formular ausfüllen async function loadLDAPConfig() { try { diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 3104b96..bb4b842 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -4,6 +4,36 @@ let currentWeekStart = getMonday(new Date()); let currentEntries = {}; let currentHolidayDates = new Set(); // Feiertage der aktuellen Woche (YYYY-MM-DD) let userWochenstunden = 0; // Wochenstunden des Users +let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal) + +// Wochenend-Prozentsätze laden +async function loadWeekendPercentages() { + try { + const response = await fetch('/api/user/weekend-percentages'); + if (!response.ok) { + throw new Error('Fehler beim Laden der Wochenend-Prozentsätze'); + } + const data = await response.json(); + weekendPercentages.saturday = data.saturday_percentage || 100; + weekendPercentages.sunday = data.sunday_percentage || 100; + } catch (error) { + console.error('Fehler beim Laden der Wochenend-Prozentsätze:', error); + // Standardwerte verwenden + weekendPercentages.saturday = 100; + weekendPercentages.sunday = 100; + } +} + +// Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück +function getWeekendPercentage(date) { + const day = date.getDay(); + if (day === 6) { // Samstag + return weekendPercentages.saturday; + } else if (day === 0) { // Sonntag + return weekendPercentages.sunday; + } + return 100; // Kein Wochenende = 100% (normal) +} // Statistiken laden async function loadUserStats() { @@ -110,6 +140,9 @@ document.addEventListener('DOMContentLoaded', async function() { // Ping-IP laden loadPingIP(); + // Wochenend-Prozentsätze laden + loadWeekendPercentages(); + // Statistiken laden loadUserStats(); @@ -374,11 +407,19 @@ function renderWeek() { // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) // Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde) // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt + // Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage) + let hoursToAdd = 0; if (isHoliday) { - totalHours += 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden) + hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden) } else { - totalHours += hours; + hoursToAdd = hours || 0; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit) + const weekendPercentage = getWeekendPercentage(date); + if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { + hoursToAdd = hours * (weekendPercentage / 100); + } } + totalHours += hoursToAdd; // Bearbeitung ist immer möglich, auch nach Abschicken // Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden) @@ -662,10 +703,22 @@ function updateOvertimeDisplay() { const end = new Date(`2000-01-01T${endTime}`); const diffMs = end - start; const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); - totalHours += hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(date); + let adjustedHours = hours; + if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus && !isFullDayOvertime) { + adjustedHours = hours * (weekendPercentage / 100); + } + totalHours += adjustedHours; } else if (currentEntries[dateStr]?.total_hours && !isFullDayOvertime) { // Fallback auf gespeicherte Werte - totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0; + let hours = parseFloat(currentEntries[dateStr].total_hours) || 0; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(date); + if (weekendPercentage >= 100 && hours > 0 && vacationType !== 'full' && !sickStatus) { + hours = hours * (weekendPercentage / 100); + } + totalHours += hours; } } else if (sickStatus) { totalHours += 8; // Krank = 8 Stunden @@ -706,10 +759,22 @@ function updateOvertimeDisplay() { const end = new Date(`2000-01-01T${endTime}`); const diffMs = end - start; const hours = (diffMs / (1000 * 60 * 60)) - (breakMinutes / 60); - totalHours += hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(date); + let adjustedHours = hours; + if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) { + adjustedHours = hours * (weekendPercentage / 100); + } + totalHours += adjustedHours; } else if (currentEntries[dateStr]?.total_hours) { // Fallback auf gespeicherte Werte - totalHours += parseFloat(currentEntries[dateStr].total_hours) || 0; + let hours = parseFloat(currentEntries[dateStr].total_hours) || 0; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(date); + if (weekendPercentage >= 100 && hours > 0 && !isFullDayOvertime) { + hours = hours * (weekendPercentage / 100); + } + totalHours += hours; } } } diff --git a/routes/admin.js b/routes/admin.js index f8cbe54..02f0bb5 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -10,33 +10,36 @@ function registerAdminRoutes(app) { 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 + // LDAP-Konfiguration, Sync-Log und Optionen 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]; + db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => { + // 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']; } - } 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' - } + return { ...u, roles }; + }); + + res.render('admin', { + users: usersWithRoles, + ldapConfig: ldapConfig || null, + syncLogs: syncLogs || [], + options: options || { saturday_percentage: 100, sunday_percentage: 100 }, + user: { + firstname: req.session.firstname, + lastname: req.session.lastname, + roles: req.session.roles || [], + currentRole: req.session.currentRole || 'admin' + } + }); }); }); }); @@ -149,6 +152,71 @@ function registerAdminRoutes(app) { }); } }); + + // Optionen laden + app.get('/admin/options', requireAdmin, (req, res) => { + db.get('SELECT * FROM system_options WHERE id = 1', (err, options) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Laden der Optionen' }); + } + // Wenn keine Optionen vorhanden, Standardwerte zurückgeben + if (!options) { + return res.json({ + config: { + saturday_percentage: 100, + sunday_percentage: 100 + } + }); + } + res.json({ config: options }); + }); + }); + + // Optionen speichern + app.post('/admin/options', requireAdmin, (req, res) => { + const { saturday_percentage, sunday_percentage } = req.body; + + // Validierung + const satPercent = parseFloat(saturday_percentage); + const sunPercent = parseFloat(sunday_percentage); + + if (isNaN(satPercent) || isNaN(sunPercent)) { + return res.status(400).json({ error: 'Ungültige Prozentsätze' }); + } + + if (satPercent < 100 || satPercent > 200 || sunPercent < 100 || sunPercent > 200) { + return res.status(400).json({ error: 'Prozentsätze müssen zwischen 100 und 200 liegen' }); + } + + // Prüfe ob Eintrag existiert + db.get('SELECT id FROM system_options WHERE id = 1', (err, existing) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Prüfen der Optionen' }); + } + + if (existing) { + // Update + db.run('UPDATE system_options SET saturday_percentage = ?, sunday_percentage = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1', + [satPercent, sunPercent], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' }); + } + res.json({ success: true }); + }); + } else { + // Insert + db.run('INSERT INTO system_options (id, saturday_percentage, sunday_percentage) VALUES (1, ?, ?)', + [satPercent, sunPercent], + (err) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Speichern der Optionen' }); + } + res.json({ success: true }); + }); + } + }); + }); } module.exports = registerAdminRoutes; diff --git a/routes/timesheet.js b/routes/timesheet.js index 2050666..d4f819a 100644 --- a/routes/timesheet.js +++ b/routes/timesheet.js @@ -27,6 +27,28 @@ function registerTimesheetRoutes(app) { // Normalisiere sick_status: Boolean oder 1/0 zu Boolean const isSick = sick_status === true || sick_status === 1 || sick_status === 'true' || sick_status === '1'; + // Wochenend-Prozentsätze laden + db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { + if (err) { + console.error('Fehler beim Laden der Optionen:', err); + return res.status(500).json({ error: 'Fehler beim Laden der Optionen' }); + } + + const saturdayPercentage = options?.saturday_percentage || 100; + const sundayPercentage = options?.sunday_percentage || 100; + + // Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück + function getWeekendPercentage(dateStr) { + const date = new Date(dateStr); + const day = date.getDay(); + if (day === 6) { // Samstag + return saturdayPercentage; + } else if (day === 0) { // Sonntag + return sundayPercentage; + } + return 100; // Kein Wochenende = 100% (normal) + } + // User-Daten laden (für Überstunden-Berechnung) db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { if (err) { @@ -73,6 +95,11 @@ function registerTimesheetRoutes(app) { const end = new Date(`2000-01-01T${normalizedEndTime}`); const diffMs = end - start; total_hours = (diffMs / (1000 * 60 * 60)) - (break_minutes / 60); + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Urlaub/Krankheit) + const weekendPercentage = getWeekendPercentage(date); + if (weekendPercentage >= 100 && total_hours > 0 && !isSick && vacation_type !== 'full') { + total_hours = total_hours * (weekendPercentage / 100); + } // Bei halbem Tag Urlaub: total_hours bleibt die tatsächlich gearbeiteten Stunden // Die 4 Stunden Urlaub werden nur in der Überstunden-Berechnung hinzugezählt } @@ -146,6 +173,7 @@ function registerTimesheetRoutes(app) { } }); }); + }); }); // API: Feiertage für einen Zeitraum (Dashboard-Anzeige) diff --git a/routes/user.js b/routes/user.js index 5fef85d..c7a7ae4 100644 --- a/routes/user.js +++ b/routes/user.js @@ -10,12 +10,12 @@ 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 }); }); }); @@ -24,13 +24,13 @@ function registerUserRoutes(app) { 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], + + 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' }); @@ -39,15 +39,29 @@ function registerUserRoutes(app) { }); }); + // API: Wochenend-Prozentsätze abrufen + app.get('/api/user/weekend-percentages', requireAuth, (req, res) => { + db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochenend-Prozentsätze' }); + } + // Wenn keine Optionen vorhanden, Standardwerte zurückgeben + res.json({ + saturday_percentage: options?.saturday_percentage || 100, + sunday_percentage: options?.sunday_percentage || 100 + }); + }); + }); + // 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 }); }); }); @@ -55,28 +69,28 @@ function registerUserRoutes(app) { // API: Client-IP abrufen app.get('/api/user/client-ip', requireAuth, (req, res) => { // Versuche verschiedene Methoden, um die Client-IP zu erhalten - const clientIp = req.ip || - req.connection.remoteAddress || - req.socket.remoteAddress || - (req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) || - req.headers['x-real-ip'] || - 'unknown'; - + const clientIp = req.ip || + req.connection.remoteAddress || + req.socket.remoteAddress || + (req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : null) || + req.headers['x-real-ip'] || + 'unknown'; + // Entferne IPv6-Präfix falls vorhanden (::ffff:192.168.1.1 -> 192.168.1.1) const cleanIp = clientIp.replace(/^::ffff:/, ''); - + res.json({ client_ip: cleanIp }); }); // 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 }); }); }); @@ -85,21 +99,21 @@ function registerUserRoutes(app) { 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(); @@ -107,7 +121,7 @@ function registerUserRoutes(app) { // Fehler ignorieren }); } - + res.json({ success: true, ping_ip: normalizedPingIp }); }); }); @@ -115,25 +129,25 @@ function registerUserRoutes(app) { // 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 }); }); @@ -141,7 +155,7 @@ function registerUserRoutes(app) { app.get('/api/user/planned-vacation', requireAuth, (req, res) => { const userId = req.session.userId; const { getCalendarWeek } = require('../helpers/utils'); - + db.all(`SELECT date, vacation_type FROM timesheet_entries WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, [userId], @@ -149,33 +163,33 @@ function registerUserRoutes(app) { if (err) { return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); } - + let plannedDays = 0; const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } - + entries.forEach(entry => { const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; plannedDays += dayValue; - + // Berechne Kalenderwoche const date = new Date(entry.date); const year = date.getFullYear(); const week = getCalendarWeek(entry.date); const weekKey = `${year}-KW${week}`; - + if (!weeksMap[weekKey]) { weeksMap[weekKey] = { year, week, days: 0 }; } weeksMap[weekKey].days += dayValue; }); - + // Konvertiere zu sortiertem Array const weeks = Object.values(weeksMap).sort((a, b) => { if (a.year !== b.year) return a.year - b.year; return a.week - b.week; }); - - res.json({ + + res.json({ plannedVacationDays: plannedDays, weeks: weeks }); @@ -186,283 +200,317 @@ function registerUserRoutes(app) { // 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, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { - if (err || !user) { - return res.status(500).json({ error: 'Fehler beim Abrufen der User-Daten' }); + + // Wochenend-Prozentsätze laden + db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Laden der Optionen' }); } - - const wochenstunden = user.wochenstunden || 0; - const urlaubstage = user.urlaubstage || 0; - const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; - - // Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte) - const { getCalendarWeek } = require('../helpers/utils'); - db.all(`SELECT date, vacation_type FROM timesheet_entries + + const saturdayPercentage = options?.saturday_percentage || 100; + const sundayPercentage = options?.sunday_percentage || 100; + + // Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück + function getWeekendPercentage(dateStr) { + const date = new Date(dateStr); + const day = date.getDay(); + if (day === 6) { // Samstag + return saturdayPercentage; + } else if (day === 0) { // Sonntag + return sundayPercentage; + } + return 100; // Kein Wochenende = 100% (normal) + } + + // User-Daten abrufen + db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours 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; + const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; + + // Verplante Urlaubstage berechnen (alle Wochen, auch nicht-eingereichte) + const { getCalendarWeek } = require('../helpers/utils'); + db.all(`SELECT date, vacation_type FROM timesheet_entries WHERE user_id = ? AND vacation_type IS NOT NULL AND vacation_type != ''`, - [userId], - (err, allVacationEntries) => { - if (err) { - return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); - } - - let plannedVacationDays = 0; - const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } - - (allVacationEntries || []).forEach(entry => { - const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; - plannedVacationDays += dayValue; - - // Berechne Kalenderwoche - const date = new Date(entry.date); - const year = date.getFullYear(); - const week = getCalendarWeek(entry.date); - const weekKey = `${year}-KW${week}`; - - if (!weeksMap[weekKey]) { - weeksMap[weekKey] = { year, week, days: 0 }; + [userId], + (err, allVacationEntries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der verplanten Tage' }); } - weeksMap[weekKey].days += dayValue; - }); - - // Konvertiere zu sortiertem Array - const plannedWeeks = Object.values(weeksMap).sort((a, b) => { - if (a.year !== b.year) return a.year - b.year; - return a.week - b.week; - }); - - // Alle eingereichten Wochen abrufen - db.all(`SELECT DISTINCT week_start, week_end + + let plannedVacationDays = 0; + const weeksMap = {}; // { KW: { year: YYYY, week: KW, days: X } } + + (allVacationEntries || []).forEach(entry => { + const dayValue = entry.vacation_type === 'full' ? 1 : 0.5; + plannedVacationDays += dayValue; + + // Berechne Kalenderwoche + const date = new Date(entry.date); + const year = date.getFullYear(); + const week = getCalendarWeek(entry.date); + const weekKey = `${year}-KW${week}`; + + if (!weeksMap[weekKey]) { + weeksMap[weekKey] = { year, week, days: 0 }; + } + weeksMap[weekKey].days += dayValue; + }); + + // Konvertiere zu sortiertem Array + const plannedWeeks = Object.values(weeksMap).sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.week - b.week; + }); + + // 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: overtimeOffsetHours, - remainingVacation: urlaubstage, - totalOvertimeHours: 0, - totalOvertimeTaken: 0, - totalVacationDays: 0, - plannedVacationDays: plannedVacationDays, - plannedWeeks: plannedWeeks, - urlaubstage: urlaubstage, - overtimeOffsetHours: overtimeOffsetHours - }); - } - - 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 - + [userId], + (err, weeks) => { if (err) { - hasError = true; - return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); } - - // 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) - - // Feiertage für die Woche laden (Feiertag zählt als ausgefüllt) - getHolidaysForDateRange(week.week_start, week.week_end) - .catch(() => new Set()) - .then((holidaySet) => { - // 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]; - if (holidaySet.has(dateStr)) { - filledWorkdays++; - continue; - } - 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) + overtimeOffsetHours; - const remainingVacation = urlaubstage - totalVacationDays; - - res.json({ - currentOvertime: currentOvertime, - remainingVacation: remainingVacation, - totalOvertimeHours: totalOvertimeHours, - totalOvertimeTaken: totalOvertimeTaken, - totalVacationDays: totalVacationDays, - plannedVacationDays: plannedVacationDays, - plannedWeeks: plannedWeeks, - urlaubstage: urlaubstage, - overtimeOffsetHours: overtimeOffsetHours - }); - } - 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; - - const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; - let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden - - entries.forEach(entry => { - // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind - const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; - const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; - - if (entry.overtime_taken_hours) { - weekOvertimeTaken += entry.overtime_taken_hours; - } - - // Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden - // Diese Tage werden separat gezählt, um die Sollstunden anzupassen - if (isFullDayOvertime) { - fullDayOvertimeDays++; - } - - // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden - if (entry.vacation_type === 'full') { - weekVacationDays += 1; - weekVacationHours += 8; // Ganzer Tag = 8 Stunden - // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt - } else if (entry.vacation_type === 'half') { - weekVacationDays += 0.5; - weekVacationHours += 4; // Halber Tag = 4 Stunden - // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein - if (entry.total_hours && !isFullDayOvertime) { - weekTotalHours += entry.total_hours; - } - } else { - // Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden) - if (entry.total_hours && !isFullDayOvertime) { - weekTotalHours += entry.total_hours; - } - } - }); - - // Feiertagsstunden: 8h pro Werktag der ein Feiertag ist - let holidayHours = 0; - for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { - const day = d.getDay(); - if (day >= 1 && day <= 5) { - const dateStr = d.toISOString().split('T')[0]; - if (holidaySet.has(dateStr)) holidayHours += 8; - } - } - - // Sollstunden berechnen - const sollStunden = (wochenstunden / 5) * workdays; - - // Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden - const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; - const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); - // weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet) - const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; - - // Kumulativ addieren - // WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden) - // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) - // Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden - totalOvertimeHours += weekOvertimeHours; - totalOvertimeTaken += weekOvertimeTaken; - totalVacationDays += weekVacationDays; - - processedWeeks++; - - // Wenn alle Wochen verarbeitet wurden, Antwort senden - if (processedWeeks === weeks.length && !hasError) { - // Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset - // weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend) - // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) - const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; - const remainingVacation = urlaubstage - totalVacationDays; - - res.json({ - currentOvertime: currentOvertime, - remainingVacation: remainingVacation, - totalOvertimeHours: totalOvertimeHours, - totalOvertimeTaken: totalOvertimeTaken, - totalVacationDays: totalVacationDays, + + // Wenn keine Wochen vorhanden + if (!weeks || weeks.length === 0) { + return res.json({ + currentOvertime: overtimeOffsetHours, + remainingVacation: urlaubstage, + totalOvertimeHours: 0, + totalOvertimeTaken: 0, + totalVacationDays: 0, plannedVacationDays: plannedVacationDays, plannedWeeks: plannedWeeks, urlaubstage: urlaubstage, overtimeOffsetHours: overtimeOffsetHours }); } - }); - }); - }); - }); - }); - }); - }); + + 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) + + // Feiertage für die Woche laden (Feiertag zählt als ausgefüllt) + getHolidaysForDateRange(week.week_start, week.week_end) + .catch(() => new Set()) + .then((holidaySet) => { + // 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]; + if (holidaySet.has(dateStr)) { + filledWorkdays++; + continue; + } + 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) + overtimeOffsetHours; + const remainingVacation = urlaubstage - totalVacationDays; + + res.json({ + currentOvertime: currentOvertime, + remainingVacation: remainingVacation, + totalOvertimeHours: totalOvertimeHours, + totalOvertimeTaken: totalOvertimeTaken, + totalVacationDays: totalVacationDays, + plannedVacationDays: plannedVacationDays, + plannedWeeks: plannedWeeks, + urlaubstage: urlaubstage, + overtimeOffsetHours: overtimeOffsetHours + }); + } + 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; + + const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden + + entries.forEach(entry => { + // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind + const overtimeValue = entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; + const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; + + if (entry.overtime_taken_hours) { + weekOvertimeTaken += entry.overtime_taken_hours; + } + + // Wenn 8 Überstunden eingetragen sind, zählt der Tag als 0 Stunden + // Diese Tage werden separat gezählt, um die Sollstunden anzupassen + if (isFullDayOvertime) { + fullDayOvertimeDays++; + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + weekVacationDays += 1; + weekVacationHours += 8; // Ganzer Tag = 8 Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + weekVacationDays += 0.5; + weekVacationHours += 4; // Halber Tag = 4 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + if (entry.total_hours && !isFullDayOvertime) { + let hours = entry.total_hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(entry.date); + if (weekendPercentage >= 100 && hours > 0 && !entry.sick_status) { + hours = hours * (weekendPercentage / 100); + } + weekTotalHours += hours; + } + } else { + // Kein Urlaub - zähle nur Arbeitsstunden (wenn nicht 8 Überstunden) + if (entry.total_hours && !isFullDayOvertime) { + let hours = entry.total_hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Krankheit) + const weekendPercentage = getWeekendPercentage(entry.date); + if (weekendPercentage > 0 && hours > 0 && !entry.sick_status) { + hours = hours * (1 + weekendPercentage / 100); + } + weekTotalHours += hours; + } + } + }); + + // Feiertagsstunden: 8h pro Werktag der ein Feiertag ist + let holidayHours = 0; + for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) { + const day = d.getDay(); + if (day >= 1 && day <= 5) { + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += 8; + } + } + + // Sollstunden berechnen + const sollStunden = (wochenstunden / 5) * workdays; + + // Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden + const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; + const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); + // weekOvertimeHours = Überstunden diese Woche (wie im Frontend berechnet) + const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; + + // Kumulativ addieren + // WICHTIG: weekOvertimeHours enthält bereits die Überstunden dieser Woche (kann negativ sein bei 8 Überstunden) + // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) + // Die aktuellen Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + totalOvertimeHours += weekOvertimeHours; + totalOvertimeTaken += weekOvertimeTaken; + totalVacationDays += weekVacationDays; + + processedWeeks++; + + // Wenn alle Wochen verarbeitet wurden, Antwort senden + if (processedWeeks === weeks.length && !hasError) { + // Aktuelle Überstunden = Summe aller Wochen-Überstunden - verbrauchte Überstunden + Offset + // weekOvertimeHours enthält bereits die korrekte Berechnung pro Woche (wie im Frontend) + // weekOvertimeTaken enthält die verbrauchten Überstunden (8 Stunden pro Tag mit 8 Überstunden) + const currentOvertime = (totalOvertimeHours - totalOvertimeTaken) + overtimeOffsetHours; + const remainingVacation = urlaubstage - totalVacationDays; + + res.json({ + currentOvertime: currentOvertime, + remainingVacation: remainingVacation, + totalOvertimeHours: totalOvertimeHours, + totalOvertimeTaken: totalOvertimeTaken, + totalVacationDays: totalVacationDays, + plannedVacationDays: plannedVacationDays, + plannedWeeks: plannedWeeks, + urlaubstage: urlaubstage, + overtimeOffsetHours: overtimeOffsetHours + }); + } + }); // getHolidaysForDateRange.then + }); // db.all (allEntries) + }); // weeks.forEach + }); // db.all (weeks) + }); // db.all (allVacationEntries) + }); // db.get (user) + }); // db.get (options) + }); // app.get } module.exports = registerUserRoutes; diff --git a/routes/verwaltung.js b/routes/verwaltung.js index 9a0133c..d0003db 100644 --- a/routes/verwaltung.js +++ b/routes/verwaltung.js @@ -142,6 +142,27 @@ function registerVerwaltungRoutes(app) { const userId = req.params.id; const { week_start, week_end } = req.query; + // Wochenend-Prozentsätze laden + db.get('SELECT saturday_percentage, sunday_percentage FROM system_options WHERE id = 1', (err, options) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Laden der Optionen' }); + } + + const saturdayPercentage = options?.saturday_percentage || 100; + const sundayPercentage = options?.sunday_percentage || 100; + + // Hilfsfunktion: Prüft ob ein Datum ein Wochenendtag ist und gibt den Prozentsatz zurück + function getWeekendPercentage(dateStr) { + const date = new Date(dateStr); + const day = date.getDay(); + if (day === 6) { // Samstag + return saturdayPercentage; + } else if (day === 0) { // Sonntag + return sundayPercentage; + } + return 100; // Kein Wochenende = 100% (normal) + } + // User-Daten abrufen db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { @@ -190,12 +211,24 @@ function registerVerwaltungRoutes(app) { vacationHours += 4; // Halber Tag = 4 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein if (entry.total_hours) { - totalHours += entry.total_hours; + let hours = entry.total_hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden) + const weekendPercentage = getWeekendPercentage(entry.date); + if (weekendPercentage >= 100 && hours > 0 && !entry.sick_status) { + hours = hours * (weekendPercentage / 100); + } + totalHours += hours; } } else { // Kein Urlaub - zähle nur Arbeitsstunden if (entry.total_hours) { - totalHours += entry.total_hours; + let hours = entry.total_hours; + // Wochenend-Prozentsatz anwenden (nur auf tatsächlich gearbeitete Stunden, nicht auf Krankheit) + const weekendPercentage = getWeekendPercentage(entry.date); + if (weekendPercentage > 0 && hours > 0 && !entry.sick_status) { + hours = hours * (1 + weekendPercentage / 100); + } + totalHours += hours; } } }); @@ -248,6 +281,7 @@ function registerVerwaltungRoutes(app) { }); }); }); + }); }); // API: Admin-Kommentar speichern diff --git a/views/admin.ejs b/views/admin.ejs index 34231f8..5b8fb5f 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -203,6 +203,43 @@ +
+
+

Optionen

+ +
+ + +
+

LDAP-Synchronisation

@@ -375,6 +412,20 @@ } } + // Optionen-Sektion ein-/ausklappen + function toggleOptionsSection() { + const content = document.getElementById('optionsContent'); + const icon = document.getElementById('optionsToggleIcon'); + + if (content.style.display === 'none') { + content.style.display = 'block'; + icon.style.transform = 'rotate(180deg)'; + } else { + content.style.display = 'none'; + icon.style.transform = 'rotate(0deg)'; + } + } + // Rollenwechsel-Handler document.addEventListener('DOMContentLoaded', function() { const roleSwitcher = document.getElementById('roleSwitcher');