diff --git a/Stundenregeln.txt b/Stundenregeln.txt index 9b5709c..3fb515e 100644 --- a/Stundenregeln.txt +++ b/Stundenregeln.txt @@ -3,10 +3,14 @@ - Überstunden nehmen - Wenn Überstunden eingetragen > (Wochenarbeitszeit / Arbeitstage) -> Muss noch Start und Ende und Pause eingetragen werden - Wenn Überstunden = (Wochenarbeitszeit / Arbeitstage) -> Tag als ausgefüllt zu betrachten + - Wird nur für die anzeige benötigt + - Stunden werden (Ende - Start ) - Pause berechnet. - Urlaub - - Wird als ausgefüllt betrachtet - - Stunden werden als (Wochenarbeitszeit / Arbeitstage) gerechnet + - Wird als ausgefüllt betrachtet wenn ganzer Urlaubstag + - Wenn halber Urlaubstag muss (Ende - Start ) - Pause eingetragen werden. + - Stunden werden als (Wochenarbeitszeit / Arbeitstage) gerechnet wenn ganzer Tag + - Wenn halber tag Ulaub ((Wochenarbeitszeit / Arbeitstage) / 2) zur ((Ende - Start ) - Pause) addiert - Wird von Verbleibendem Urlaub abgezogen - Krank diff --git a/Stunderfassung todo.txt b/Stunderfassung todo.txt index a84957d..0d76971 100644 --- a/Stunderfassung todo.txt +++ b/Stunderfassung todo.txt @@ -1,7 +1,7 @@ - Mitarbeiter Name in den QR code Sheets -> DONE - Pause vorbelegen (einstellbar in der Admin) -> DONE Wird anhand der Gesetztlichen vorgaben berechnet - Offset für die Verwaltung für Urlaubstage -> DONE -- Stunden pro Tag und wie viele Tage arbeit +- Stunden pro Tag und wie viele Tage arbeit -> DONE - Reisen für Wochenende -> DONE - LDAP Prüfung - DSGVO Sicherheit diff --git a/database.js b/database.js index 117ca7a..01401a8 100644 --- a/database.js +++ b/database.js @@ -216,6 +216,14 @@ function initDatabase() { } }); + // Migration: Arbeitstage pro Woche hinzufügen + db.run(`ALTER TABLE users ADD COLUMN arbeitstage INTEGER DEFAULT 5`, (err) => { + // Fehler ignorieren wenn Spalte bereits existiert + if (err && !err.message.includes('duplicate column')) { + console.warn('Warnung beim Hinzufügen der Spalte arbeitstage:', err.message); + } + }); + // Migration: ping_ip Spalte hinzufügen db.run(`ALTER TABLE users ADD COLUMN ping_ip TEXT`, (err) => { // Fehler ignorieren wenn Spalte bereits existiert diff --git a/public/js/admin.js b/public/js/admin.js index 32abd31..f27c4d7 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -25,6 +25,7 @@ document.addEventListener('DOMContentLoaded', function() { roles: roles, personalnummer: document.getElementById('personalnummer').value, wochenstunden: document.getElementById('wochenstunden').value, + arbeitstage: document.getElementById('arbeitstage').value, urlaubstage: document.getElementById('urlaubstage').value }; @@ -315,6 +316,7 @@ async function saveUser(userId) { const personalnummer = row.querySelector('input[data-field="personalnummer"]').value; const wochenstunden = row.querySelector('input[data-field="wochenstunden"]').value; + const arbeitstage = row.querySelector('input[data-field="arbeitstage"]').value; const urlaubstage = row.querySelector('input[data-field="urlaubstage"]').value; // Rollen aus Checkboxen sammeln @@ -336,6 +338,7 @@ async function saveUser(userId) { body: JSON.stringify({ personalnummer: personalnummer || null, wochenstunden: wochenstunden || null, + arbeitstage: arbeitstage || 5, urlaubstage: urlaubstage || null, roles: roles }) diff --git a/public/js/dashboard.js b/public/js/dashboard.js index 7c6710c..4d95f6a 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -4,6 +4,7 @@ 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 userArbeitstage = 5; // Arbeitstage pro Woche des Users (Standard: 5) let weekendPercentages = { saturday: 100, sunday: 100 }; // Wochenend-Prozentsätze (100% = normal) let latestSubmittedTimesheetId = null; // ID der neuesten eingereichten Version @@ -303,17 +304,24 @@ function getWeekday(dateStr) { return days[d.getDay()]; } +// Hilfsfunktion: Berechnet die Stunden pro Tag (Wochenarbeitszeit / Arbeitstage) +function getFullDayHours() { + return userWochenstunden && userArbeitstage ? (userWochenstunden / userArbeitstage) : 8; +} + // Woche laden async function loadWeek() { try { - // User-Daten laden (Wochenstunden) + // User-Daten laden (Wochenstunden, Arbeitstage) try { const userResponse = await fetch('/api/user/data'); const userData = await userResponse.json(); userWochenstunden = userData.wochenstunden || 0; + userArbeitstage = userData.arbeitstage || 5; } catch (error) { console.warn('Konnte User-Daten nicht laden:', error); userWochenstunden = 0; + userArbeitstage = 5; } const parts = currentWeekStart.split('-'); @@ -420,7 +428,7 @@ function renderWeek() { // Bei Feiertag, ganztägigem Urlaub oder Krank gilt der Tag als ausgefüllt // Bei 8 Überstunden (ganzer Tag) gilt der Tag auch als ausgefüllt const overtimeValue = overtimeTaken ? parseFloat(overtimeTaken) : 0; - const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + const fullDayHours = getFullDayHours(); const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; if (i < 5 && !isHoliday && vacationType !== 'full' && !sickStatus && !isFullDayOvertime && (!startTime || !endTime || startTime.trim() === '' || endTime.trim() === '')) { @@ -434,7 +442,7 @@ function renderWeek() { // Wochenend-Prozentsätze: Nur auf tatsächlich gearbeitete Stunden anwenden (nicht auf Urlaub, Krankheit, Feiertage) let hoursToAdd = 0; if (isHoliday) { - hoursToAdd = 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden) + hoursToAdd = fullDayHours + (hours || 0); // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) } else { hoursToAdd = hours || 0; // Wochenend-Prozentsatz anwenden (nur wenn weekend_travel aktiviert UND es ist ein Wochenendtag) @@ -482,7 +490,7 @@ function renderWeek() { data-date="${dateStr}" data-field="break_minutes" ${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)"> - ${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : isHoliday && !hours ? '8.00 h (Feiertag)' : isHoliday && hours ? '8.00 + ' + hours.toFixed(2) + ' h (Überst.)' : hours.toFixed(2) + ' h'} + ${isFullDayVacation ? fullDayHours.toFixed(2) + ' h (Urlaub)' : isSick ? fullDayHours.toFixed(2) + ' h (Krank)' : isHoliday && !hours ? fullDayHours.toFixed(2) + ' h (Feiertag)' : isHoliday && hours ? fullDayHours.toFixed(2) + ' + ' + hours.toFixed(2) + ' h (Überst.)' : hours.toFixed(2) + ' h'} @@ -640,15 +648,16 @@ function renderWeek() { } // Sollstunden berechnen - const sollStunden = (userWochenstunden / 5) * workdays; + const sollStunden = (userWochenstunden / userArbeitstage) * workdays; // Urlaubsstunden berechnen (Urlaub zählt als normale Arbeitszeit) let vacationHours = 0; + const fullDayHours = getFullDayHours(); Object.values(currentEntries).forEach(e => { if (e.vacation_type === 'full') { - vacationHours += 8; // Ganzer Tag = 8 Stunden + vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden } else if (e.vacation_type === 'half') { - vacationHours += 4; // Halber Tag = 4 Stunden + vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden } }); @@ -684,7 +693,7 @@ function updateOvertimeDisplay() { } // Sollstunden berechnen - const sollStunden = (userWochenstunden / 5) * workdays; + const sollStunden = (userWochenstunden / userArbeitstage) * workdays; // Gesamtstunden berechnen - direkt aus DOM-Elementen lesen für Echtzeit-Aktualisierung let totalHours = 0; @@ -715,9 +724,9 @@ function updateOvertimeDisplay() { // Wenn Urlaub oder Krank, zähle nur diese Stunden (nicht zusätzlich Arbeitsstunden) if (vacationType === 'full') { - vacationHours += 8; // Ganzer Tag Urlaub = 8 Stunden + vacationHours += fullDayHours; // Ganzer Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) Stunden } else if (vacationType === 'half') { - vacationHours += 4; // Halber Tag Urlaub = 4 Stunden + vacationHours += fullDayHours / 2; // Halber Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); @@ -776,9 +785,9 @@ function updateOvertimeDisplay() { totalHours += hours; } } else if (sickStatus) { - totalHours += 8; // Krank = 8 Stunden + totalHours += fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden } else if (currentHolidayDates.has(dateStr)) { - // Feiertag: 8h Basis + gearbeitete Stunden (jede Stunde = Überstunde) + // Feiertag: (Wochenarbeitszeit / Arbeitstage) Basis + gearbeitete Stunden (jede Stunde = Überstunde) const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); const startTime = startInput ? startInput.value : ''; @@ -793,7 +802,7 @@ function updateOvertimeDisplay() { } else if (currentEntries[dateStr]?.total_hours) { worked = parseFloat(currentEntries[dateStr].total_hours) || 0; } - totalHours += 8 + worked; // 8h Feiertag + gearbeitete Stunden (= Überstunden) + totalHours += fullDayHours + worked; // (Wochenarbeitszeit / Arbeitstage) Feiertag + gearbeitete Stunden (= Überstunden) } else { // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden if (isFullDayOvertime) { @@ -902,7 +911,7 @@ function handleOvertimeChange(dateStr, overtimeHours) { return; } - const fullDayHours = userWochenstunden / 5; + const fullDayHours = getFullDayHours(); const overtimeValue = parseFloat(overtimeHours) || 0; // Prüfe ob ganzer Tag Überstunden @@ -1204,11 +1213,11 @@ function checkWeekComplete() { const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0); // Berechne fullDayHours (normalerweise 8 Stunden) - const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + const fullDayHours = getFullDayHours(); - // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt + // Wenn Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { - continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag) + continue; // Tag ist ausgefüllt (Überstunden = ganzer Tag) } // Prüfe IMMER direkt die Input-Felder im DOM (das ist die zuverlässigste Quelle) @@ -1231,8 +1240,17 @@ function checkWeekComplete() { fullDayHours: fullDayHours }); + // Wenn Überstunden > fullDayHours, dann müssen Start/Ende vorhanden sein + if (overtimeValue > fullDayHours) { + if (!startTime || !endTime || startTime === '' || endTime === '') { + allWeekdaysFilled = false; + missingFields.push(formatDateDE(dateStr) + ' (bei Überstunden > ' + fullDayHours.toFixed(2) + 'h müssen Start/Ende vorhanden sein)'); + continue; // Weiter zum nächsten Tag + } + } + // Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein - // (außer wenn 8 Überstunden eingetragen sind, dann sind Start/Ende nicht nötig) + // (außer wenn Überstunden = fullDayHours eingetragen sind, dann sind Start/Ende nicht nötig) if (!startTime || !endTime || startTime === '' || endTime === '') { allWeekdaysFilled = false; missingFields.push(formatDateDE(dateStr)); @@ -1338,7 +1356,7 @@ async function submitWeek() { // Prüfe ob 8 Überstunden eingetragen sind (dann ist der Tag auch ausgefüllt, Start/Ende nicht nötig) const overtimeInput = document.querySelector(`input[data-date="${dateStr}"][data-field="overtime_taken_hours"]`); const overtimeValue = overtimeInput ? parseFloat(overtimeInput.value) || 0 : (entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0); - const fullDayHours = userWochenstunden ? (userWochenstunden / 5) : 8; + const fullDayHours = getFullDayHours(); // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, ist der Tag ausgefüllt if (overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01) { diff --git a/routes/admin-routes.js b/routes/admin-routes.js index 49ffe49..347d2fb 100644 --- a/routes/admin-routes.js +++ b/routes/admin-routes.js @@ -8,7 +8,7 @@ const { requireAdmin } = require('../middleware/auth'); 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', + db.all('SELECT id, username, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage, created_at FROM users ORDER BY created_at DESC', (err, users) => { // LDAP-Konfiguration, Sync-Log und Optionen abrufen db.get('SELECT * FROM ldap_config WHERE id = 1', (err, ldapConfig) => { @@ -48,13 +48,14 @@ function registerAdminRoutes(app) { // Benutzer erstellen app.post('/admin/users', requireAdmin, (req, res) => { - const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage } = req.body; + const { username, password, firstname, lastname, roles, personalnummer, wochenstunden, urlaubstage, arbeitstage } = 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; + const normalizedArbeitstage = arbeitstage && arbeitstage !== '' ? parseInt(arbeitstage) : 5; // Rollen verarbeiten: Erwarte Array, konvertiere zu JSON-String let rolesArray = []; @@ -72,8 +73,8 @@ function registerAdminRoutes(app) { 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], + db.run('INSERT INTO users (username, password, firstname, lastname, role, personalnummer, wochenstunden, urlaubstage, arbeitstage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', + [username, hashedPassword, firstname, lastname, rolesJson, normalizedPersonalnummer, normalizedWochenstunden, normalizedUrlaubstage, normalizedArbeitstage], (err) => { if (err) { return res.status(400).json({ error: 'Benutzername existiert bereits' }); @@ -102,7 +103,7 @@ function registerAdminRoutes(app) { // 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; + const { personalnummer, wochenstunden, urlaubstage, arbeitstage, roles } = req.body; // Rollen verarbeiten falls vorhanden let rolesJson = null; @@ -121,11 +122,12 @@ function registerAdminRoutes(app) { // SQL-Query dynamisch zusammenstellen if (rolesJson !== null) { // Aktualisiere auch Rollen - db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, role = ? WHERE id = ?', + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ?, role = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, + arbeitstage ? parseInt(arbeitstage) : 5, rolesJson, userId ], @@ -137,11 +139,12 @@ function registerAdminRoutes(app) { }); } else { // Nur andere Felder aktualisieren - db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ? WHERE id = ?', + db.run('UPDATE users SET personalnummer = ?, wochenstunden = ?, urlaubstage = ?, arbeitstage = ? WHERE id = ?', [ personalnummer || null, wochenstunden ? parseFloat(wochenstunden) : null, urlaubstage ? parseFloat(urlaubstage) : null, + arbeitstage ? parseInt(arbeitstage) : 5, userId ], (err) => { diff --git a/routes/timesheet-routes.js b/routes/timesheet-routes.js index ad89899..490bc06 100644 --- a/routes/timesheet-routes.js +++ b/routes/timesheet-routes.js @@ -55,15 +55,16 @@ function registerTimesheetRoutes(app) { } // User-Daten laden (für Überstunden-Berechnung) - db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, arbeitstage 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 arbeitstage = user?.arbeitstage || 5; const overtimeValue = overtime_taken_hours ? parseFloat(overtime_taken_hours) : 0; - const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 0; + const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 0; // Überstunden-Logik: Prüfe ob ganzer Tag oder weniger let isFullDayOvertime = false; @@ -96,11 +97,11 @@ function registerTimesheetRoutes(app) { finalEndTime = null; // Keine Tätigkeit setzen - Überstunden werden über overtime_taken_hours in der PDF angezeigt } else if (vacation_type === 'full') { - total_hours = 8; // Ganzer Tag Urlaub = 8 Stunden normale Arbeitszeit + total_hours = fullDayHours; // Ganzer Tag Urlaub = (Wochenarbeitszeit / Arbeitstage) Stunden normale Arbeitszeit } else if (isSick) { - total_hours = 8; // Krank = 8 Stunden normale Arbeitszeit + total_hours = fullDayHours; // Krank = (Wochenarbeitszeit / Arbeitstage) Stunden normale Arbeitszeit finalActivity1Desc = 'Krank'; - finalActivity1Hours = 8; + finalActivity1Hours = fullDayHours; } else if (normalizedStartTime && normalizedEndTime) { const start = new Date(`2000-01-01T${normalizedStartTime}`); const end = new Date(`2000-01-01T${normalizedEndTime}`); @@ -314,13 +315,14 @@ function registerTimesheetRoutes(app) { const startDay = parseInt(startDateParts[2]); // User-Daten laden für Überstunden-Berechnung - db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, arbeitstage FROM users WHERE id = ?', [userId], (err, user) => { if (err) { return res.status(500).json({ error: 'Fehler beim Laden der User-Daten' }); } const wochenstunden = user?.wochenstunden || 0; - const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + const arbeitstage = user?.arbeitstage || 5; + const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; // Feiertage laden: Feiertag zählt als ausgefüllt (kein Start/Ende nötig) getHolidaysForDateRange(week_start, week_end) @@ -348,12 +350,23 @@ function registerTimesheetRoutes(app) { continue; // Tag ist ausgefüllt } - // Prüfe ob 8 Überstunden (ganzer Tag) eingetragen sind + // Prüfe ob Überstunden (ganzer Tag) eingetragen sind const overtimeValue = entry && entry.overtime_taken_hours ? parseFloat(entry.overtime_taken_hours) : 0; const isFullDayOvertime = overtimeValue > 0 && Math.abs(overtimeValue - fullDayHours) < 0.01; if (isFullDayOvertime) { - continue; // Tag ist ausgefüllt (8 Überstunden = ganzer Tag) + continue; // Tag ist ausgefüllt (Überstunden = ganzer Tag) + } + + // Wenn Überstunden > fullDayHours, dann müssen Start/Ende vorhanden sein + if (overtimeValue > fullDayHours) { + 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); + continue; // Weiter zum nächsten Tag + } } // Bei halbem Tag Urlaub oder keinem Urlaub müssen Start- und Endzeit vorhanden sein diff --git a/routes/user-routes.js b/routes/user-routes.js index e9b428f..7635fbd 100644 --- a/routes/user-routes.js +++ b/routes/user-routes.js @@ -57,12 +57,15 @@ function registerUserRoutes(app) { app.get('/api/user/data', requireAuth, (req, res) => { const userId = req.session.userId; - db.get('SELECT wochenstunden FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, arbeitstage 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 }); + res.json({ + wochenstunden: user?.wochenstunden || 0, + arbeitstage: user?.arbeitstage || 5 + }); }); }); @@ -223,12 +226,13 @@ function registerUserRoutes(app) { } // User-Daten abrufen - db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days, arbeitstage 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 arbeitstage = user.arbeitstage || 5; const urlaubstage = user.urlaubstage || 0; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0; @@ -407,7 +411,7 @@ function registerUserRoutes(app) { let weekVacationDays = 0; let weekVacationHours = 0; - const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; let fullDayOvertimeDays = 0; // Anzahl Tage mit 8 Überstunden entries.forEach(entry => { @@ -428,11 +432,11 @@ function registerUserRoutes(app) { // 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 + weekVacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) 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 + weekVacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) if (entry.total_hours && !isFullDayOvertime) { @@ -447,18 +451,18 @@ function registerUserRoutes(app) { } }); - // Feiertagsstunden: 8h pro Werktag der ein Feiertag ist + // Feiertagsstunden: (Wochenarbeitszeit / Arbeitstage) 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; + if (holidaySet.has(dateStr)) holidayHours += fullDayHours; } } // Sollstunden berechnen - const sollStunden = (wochenstunden / 5) * workdays; + const sollStunden = (wochenstunden / arbeitstage) * workdays; // Überstunden für diese Woche: (totalHours + vacationHours + holidayHours) - adjustedSollStunden const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; @@ -532,12 +536,13 @@ function registerUserRoutes(app) { } // User-Daten abrufen - db.get('SELECT wochenstunden, overtime_offset_hours FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, overtime_offset_hours, arbeitstage 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 arbeitstage = user.arbeitstage || 5; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; // Alle eingereichten Wochen abrufen @@ -645,7 +650,7 @@ function registerUserRoutes(app) { let weekVacationDays = 0; let weekVacationHours = 0; - const fullDayHours = wochenstunden > 0 ? wochenstunden / 5 : 8; + const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; let fullDayOvertimeDays = 0; entries.forEach(entry => { @@ -662,10 +667,10 @@ function registerUserRoutes(app) { if (entry.vacation_type === 'full') { weekVacationDays += 1; - weekVacationHours += 8; + weekVacationHours += fullDayHours; } else if (entry.vacation_type === 'half') { weekVacationDays += 0.5; - weekVacationHours += 4; + weekVacationHours += fullDayHours / 2; // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) if (entry.total_hours && !isFullDayOvertime) { weekTotalHours += parseFloat(entry.total_hours) || 0; @@ -678,18 +683,18 @@ function registerUserRoutes(app) { } }); - // Feiertagsstunden + // Feiertagsstunden: (Wochenarbeitszeit / Arbeitstage) 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; + if (holidaySet.has(dateStr)) holidayHours += fullDayHours; } } // Sollstunden berechnen - const sollStunden = (wochenstunden / 5) * workdays; + const sollStunden = (wochenstunden / arbeitstage) * workdays; const weekTotalHoursWithVacation = weekTotalHours + weekVacationHours + holidayHours; const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); const weekOvertimeHours = weekTotalHoursWithVacation - adjustedSollStunden; diff --git a/routes/verwaltung-routes.js b/routes/verwaltung-routes.js index 0cfdf3a..0a53a7d 100644 --- a/routes/verwaltung-routes.js +++ b/routes/verwaltung-routes.js @@ -12,7 +12,7 @@ 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, u.overtime_offset_hours, u.vacation_offset_days, + SELECT wt.*, u.firstname, u.lastname, u.username, u.personalnummer, u.wochenstunden, u.urlaubstage, u.overtime_offset_hours, u.vacation_offset_days, u.arbeitstage, dl.firstname as downloaded_by_firstname, dl.lastname as downloaded_by_lastname, (SELECT COUNT(*) FROM weekly_timesheets wt2 @@ -217,115 +217,191 @@ function registerVerwaltungRoutes(app) { } // User-Daten abrufen - db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days FROM users WHERE id = ?', [userId], (err, user) => { + db.get('SELECT wochenstunden, urlaubstage, overtime_offset_hours, vacation_offset_days, arbeitstage 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 arbeitstage = user.arbeitstage || 5; + const fullDayHours = wochenstunden > 0 && arbeitstage > 0 ? wochenstunden / arbeitstage : 8; const urlaubstage = user.urlaubstage || 0; const overtimeOffsetHours = user.overtime_offset_hours ? parseFloat(user.overtime_offset_hours) : 0; const vacationOffsetDays = user.vacation_offset_days ? parseFloat(user.vacation_offset_days) : 0; - // Einträge für die Woche abrufen - db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status - FROM timesheet_entries - WHERE user_id = ? AND date >= ? AND date <= ? - ORDER BY date`, - [userId, week_start, week_end], - (err, entries) => { + // Alle bereits genommenen Urlaubstage aus eingereichten Wochen berechnen + // Zuerst: 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 Einträge' }); + return res.status(500).json({ error: 'Fehler beim Abrufen der Wochen' }); } - // Berechnungen - let totalHours = 0; - let overtimeTaken = 0; - let vacationDays = 0; - let vacationHours = 0; - let sickDays = 0; + // Für jede Woche die neuesten Einträge abrufen + let processedWeeks = 0; + let totalVacationDays = 0; + const vacationByDate = {}; - entries.forEach(entry => { - if (entry.overtime_taken_hours) { - overtimeTaken += entry.overtime_taken_hours; - } - - // Krankheitstage zählen - if (entry.sick_status === 1 || entry.sick_status === true) { - sickDays += 1; - } - - // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden - if (entry.vacation_type === 'full') { - vacationDays += 1; - vacationHours += 8; // Ganzer Tag = 8 Stunden - // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt - } else if (entry.vacation_type === 'half') { - vacationDays += 0.5; - vacationHours += 4; // Halber Tag = 4 Stunden - // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein - // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) - if (entry.total_hours) { - totalHours += parseFloat(entry.total_hours) || 0; - } - } else { - // Kein Urlaub - zähle nur Arbeitsstunden - // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) - if (entry.total_hours) { - totalHours += parseFloat(entry.total_hours) || 0; - } - } - }); - - // Feiertage für die Woche laden (8h pro Feiertag; Arbeit an Feiertag = Überstunden) - getHolidaysForDateRange(week_start, week_end) - .catch(() => new Set()) - .then((holidaySet) => { - // Anzahl Werktage berechnen (Montag-Freitag) - const startDate = new Date(week_start); - const endDate = new Date(week_end); - let workdays = 0; - 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) { // Montag bis Freitag - workdays++; - const dateStr = d.toISOString().split('T')[0]; - if (holidaySet.has(dateStr)) holidayHours += 8; - } - } - - // Sollstunden berechnen - const sollStunden = (wochenstunden / 5) * workdays; - - // Überstunden: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden - const totalHoursWithVacation = totalHours + vacationHours + holidayHours; - const overtimeHours = totalHoursWithVacation - sollStunden; - const remainingOvertime = overtimeHours - overtimeTaken; - const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; - - // Verbleibende Urlaubstage - const remainingVacation = urlaubstage - vacationDays + vacationOffsetDays; - - res.json({ - wochenstunden, - urlaubstage, - totalHours, - sollStunden, - overtimeHours, - overtimeTaken, - remainingOvertime, - overtimeOffsetHours, - remainingOvertimeWithOffset, - vacationDays, - vacationOffsetDays, - remainingVacation, - sickDays, - workdays - }); + if (!weeks || weeks.length === 0) { + // Keine eingereichten Wochen - setze totalVacationDays auf 0 + totalVacationDays = 0; + // Weiter mit der normalen Verarbeitung der aktuellen Woche + processCurrentWeek(0); + } else { + weeks.forEach((week) => { + // Einträge für diese Woche abrufen (nur neueste pro Tag) + db.all(`SELECT date, vacation_type, updated_at, id + FROM timesheet_entries + WHERE user_id = ? AND date >= ? AND date <= ? + AND vacation_type IS NOT NULL + AND vacation_type != '' + ORDER BY date, updated_at DESC, id DESC`, + [userId, week.week_start, week.week_end], + (err, weekEntries) => { + if (err) { + return res.status(500).json({ error: 'Fehler beim Abrufen der Einträge' }); + } + + // Filtere auf neuesten Eintrag pro Tag + (weekEntries || []).forEach(entry => { + const existing = vacationByDate[entry.date]; + if (!existing) { + vacationByDate[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)) { + vacationByDate[entry.date] = entry; + } + } + }); + + processedWeeks++; + if (processedWeeks === weeks.length) { + // Alle Wochen verarbeitet - summiere Urlaubstage + Object.values(vacationByDate).forEach(entry => { + if (entry.vacation_type === 'full') { + totalVacationDays += 1; + } else if (entry.vacation_type === 'half') { + totalVacationDays += 0.5; + } + }); + + // Weiter mit der normalen Verarbeitung der aktuellen Woche + processCurrentWeek(totalVacationDays); + } + }); }); + } + + function processCurrentWeek(totalVacationDays) { + // Einträge für die Woche abrufen + db.all(`SELECT date, total_hours, overtime_taken_hours, vacation_type, sick_status + 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; + let sickDays = 0; + + entries.forEach(entry => { + if (entry.overtime_taken_hours) { + overtimeTaken += entry.overtime_taken_hours; + } + + // Krankheitstage zählen + if (entry.sick_status === 1 || entry.sick_status === true) { + sickDays += 1; + } + + // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + if (entry.vacation_type === 'full') { + vacationDays += 1; + vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden + // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + } else if (entry.vacation_type === 'half') { + vacationDays += 0.5; + vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden + // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) + if (entry.total_hours) { + totalHours += parseFloat(entry.total_hours) || 0; + } + } else { + // Kein Urlaub - zähle nur Arbeitsstunden + // WICHTIG: total_hours enthält bereits Wochenend-Prozentsätze (aus timesheet.js) + if (entry.total_hours) { + totalHours += parseFloat(entry.total_hours) || 0; + } + } + }); + + // Feiertage für die Woche laden ((Wochenarbeitszeit / Arbeitstage) pro Feiertag; Arbeit an Feiertag = Überstunden) + getHolidaysForDateRange(week_start, week_end) + .catch(() => new Set()) + .then((holidaySet) => { + // Anzahl Werktage berechnen (Montag-Freitag) + const startDate = new Date(week_start); + const endDate = new Date(week_end); + let workdays = 0; + 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) { // Montag bis Freitag + workdays++; + const dateStr = d.toISOString().split('T')[0]; + if (holidaySet.has(dateStr)) holidayHours += fullDayHours; + } + } + + // Sollstunden berechnen + const sollStunden = (wochenstunden / arbeitstage) * workdays; + + // Überstunden: (Tatsächliche Stunden + Urlaubsstunden + Feiertagsstunden) - Sollstunden + const totalHoursWithVacation = totalHours + vacationHours + holidayHours; + const overtimeHours = totalHoursWithVacation - sollStunden; + const remainingOvertime = overtimeHours - overtimeTaken; + const remainingOvertimeWithOffset = remainingOvertime + overtimeOffsetHours; + + // Verbleibende Urlaubstage (berücksichtigt alle eingereichten Wochen, nicht nur die aktuelle) + const remainingVacation = urlaubstage - totalVacationDays + vacationOffsetDays; + + res.json({ + wochenstunden, + urlaubstage, + totalHours, + sollStunden, + overtimeHours, + overtimeTaken, + remainingOvertime, + overtimeOffsetHours, + remainingOvertimeWithOffset, + vacationDays, + vacationOffsetDays, + remainingVacation, + totalVacationDays, + sickDays, + workdays + }); + }); + }); + } }); - }); + }); }); }); diff --git a/services/pdf-service.js b/services/pdf-service.js index 452dd13..202aca4 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -19,7 +19,7 @@ function getCalendarWeek(dateStr) { // PDF generieren function generatePDF(timesheetId, req, res) { - db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden + db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden, u.arbeitstage FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { @@ -73,11 +73,13 @@ function generatePDF(timesheetId, req, res) { let holidayHours = 0; const start = new Date(timesheet.week_start); const end = new Date(timesheet.week_end); + const arbeitstage = timesheet.arbeitstage || 5; + const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8; for (let d = new Date(start); d <= end; 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; + if (holidaySet.has(dateStr)) holidayHours += fullDayHours; } } return { holidaySet, holidayHours }; @@ -241,11 +243,13 @@ function generatePDF(timesheetId, req, res) { } // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + const arbeitstage = timesheet.arbeitstage || 5; + const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8; if (entry.vacation_type === 'full') { - vacationHours += 8; // Ganzer Tag = 8 Stunden + vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt } else if (entry.vacation_type === 'half') { - vacationHours += 4; // Halber Tag = 4 Stunden + vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein if (entry.total_hours) { totalHours += entry.total_hours; @@ -299,7 +303,7 @@ function generatePDF(timesheetId, req, res) { // PDF als Buffer generieren (für ZIP-Downloads) function generatePDFToBuffer(timesheetId, req) { return new Promise((resolve, reject) => { - db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden + db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden, u.arbeitstage FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { @@ -348,11 +352,13 @@ function generatePDFToBuffer(timesheetId, req) { let holidayHours = 0; const start = new Date(timesheet.week_start); const end = new Date(timesheet.week_end); + const arbeitstage = timesheet.arbeitstage || 5; + const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8; for (let d = new Date(start); d <= end; 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; + if (holidaySet.has(dateStr)) holidayHours += fullDayHours; } } return { holidaySet, holidayHours }; @@ -477,11 +483,13 @@ function generatePDFToBuffer(timesheetId, req) { } // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden + const arbeitstage = timesheet.arbeitstage || 5; + const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8; if (entry.vacation_type === 'full') { - vacationHours += 8; // Ganzer Tag = 8 Stunden + vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt } else if (entry.vacation_type === 'half') { - vacationHours += 4; // Halber Tag = 4 Stunden + vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein if (entry.total_hours) { totalHours += entry.total_hours; diff --git a/views/admin.ejs b/views/admin.ejs index 4a86d7c..fa00a63 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -98,6 +98,10 @@ +
+ + +
@@ -129,6 +133,7 @@ Rolle Personalnummer Wochenstunden + Arbeitstage Urlaubstage Erstellt am Aktionen @@ -180,6 +185,10 @@ <%= u.wochenstunden || '-' %> + + <%= u.arbeitstage || 5 %> + + <%= u.urlaubstage || '-' %>