From 76f63ed4ec7ad4e7d41d7ca7987611ac4b0edd23 Mon Sep 17 00:00:00 2001 From: Carsten Graf Date: Wed, 4 Feb 2026 16:08:40 +0100 Subject: [PATCH] DSGVO und LDAP fix --- DSGVO-Dokumentation.md | 15 +-- DSGVO-Dokumentation.pdf | Bin 0 -> 24084 bytes Stunderfassung todo.txt | 10 +- generate-dsgvo-pdf.js | 177 ++++++++++++++++++++++++++++++++++ package.json | 3 +- services/feiertage-service.js | 27 ++++++ services/ldap-service.js | 37 ++++++- services/pdf-service.js | 146 +++++++++++++++++++--------- 8 files changed, 355 insertions(+), 60 deletions(-) create mode 100644 DSGVO-Dokumentation.pdf create mode 100644 generate-dsgvo-pdf.js diff --git a/DSGVO-Dokumentation.md b/DSGVO-Dokumentation.md index 7161053..deca6ba 100644 --- a/DSGVO-Dokumentation.md +++ b/DSGVO-Dokumentation.md @@ -8,21 +8,22 @@ **Verantwortlicher für die Datenverarbeitung:** SDS Systemtechnik -Rudolf-Diesel-Str. 7 +Rudolf-Diesel-Str. 7 75365 Calw -info@sds-systemtechnik.de +info@sds-systemtechnik.de +497051931540 -**Datenschutzbeauftragter (falls vorhanden):** -[Name] -[E-Mail] -[Telefon] +**Datenschutzbeauftragter (falls vorhanden):** +Matthias Herrlinger +connexo GmbH +Jägerstraße 4F +71296 Heimsheim **Kontakt für Datenschutzanfragen:** Carsten Graf Mechatronik-Ingenieur / IT-Infrastruktur SDS Systemtechnik -carsten.graf@sds-systemtechnik.de +carsten.graf@sds-systemtechnik.de +4970519315416 --- diff --git a/DSGVO-Dokumentation.pdf b/DSGVO-Dokumentation.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b1b1ac63b55d06253d6bdb28b0e650f2a5cccde3 GIT binary patch literal 24084 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�Jy+hLitTmvYbmy`JfeGHbQPNKT=ni@hwee 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 U!E|280YL(%Dpf DONE - Stunden pro Tag und wie viele Tage arbeit -> DONE - Reisen für Wochenende -> DONE -- LDAP Prüfung -> DONE TESTEn mit Jessi und Jörg +- LDAP Prüfung -> DONE GEHT?! - DSGVO Sicherheit -> DONE - Feiertage müssen als ausgefüllt zählen -> DONE - Mitarbeiter sollen PDF ansehen können. -> DONE - Wenn bereits heruntergeladen wurde und neue version da ist Meldung an Verwaltung. -> DONE Muss getestet werden -- Wenn ganzer Tag Urlaub gesetzt wird steht erst 8h (Urlaub) und dann nur noch 8h \ No newline at end of file +- Wenn ganzer Tag Urlaub gesetzt wird steht erst 8h (Urlaub) und dann nur noch 8h + +- Feiertage im PDF anzeigen -> DONE Testen noch nicht depoyed +- Oben wenn woche eingereicht anzeigen als hilfestellung +- Ausgefüllte Tage anhand der Tage pro woche gültig setzten +- Überstunden müssen anhand der Tagesstunden auch auf gültig setzten (Tag ausgefüllt wenn weniger als 8h) +- Verplante Urlaubstage müssen auf abgezogen werden, wenn die Woche die gepalnt war eingereicht wurde. \ No newline at end of file diff --git a/generate-dsgvo-pdf.js b/generate-dsgvo-pdf.js new file mode 100644 index 0000000..1ead773 --- /dev/null +++ b/generate-dsgvo-pdf.js @@ -0,0 +1,177 @@ +#!/usr/bin/env node +/** + * Erzeugt aus DSGVO-Dokumentation.md eine PDF-Datei. + * Verwendung: node generate-dsgvo-pdf.js + */ + +const PDFDocument = require('pdfkit'); +const fs = require('fs'); +const path = require('path'); + +const MARGIN = 50; +const PAGE_WIDTH = 595; // A4 +const CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN; + +function stripBold(text) { + return text.replace(/\*\*([^*]+)\*\*/g, '$1'); +} + +function isTableRow(line) { + return /^\|.+\|$/.test(line.trim()) && !/^[\s|:-]+$/.test(line.replace(/\s/g, '')); +} + +function parseTableRows(lines, startIndex) { + const rows = []; + let i = startIndex; + while (i < lines.length && isTableRow(lines[i])) { + const line = lines[i]; + if (/^[\s|:-]+$/.test(line.replace(/\s/g, ''))) { + i++; + continue; // separator line + } + const cells = line.split('|').slice(1, -1).map(c => c.trim()); + if (cells.some(c => c)) rows.push(cells); + i++; + } + return { rows, nextIndex: i }; +} + +function writeText(doc, text, options = {}) { + const opts = { width: CONTENT_WIDTH, ...options }; + doc.text(text, opts); +} + +function addParagraph(doc, line, fontSize = 10) { + doc.fontSize(fontSize).font('Helvetica'); + const text = stripBold(line.trim()); + if (!text) return; + writeText(doc, text); + doc.moveDown(0.5); +} + +function addBullet(doc, line, fontSize = 10) { + doc.fontSize(fontSize).font('Helvetica'); + const text = stripBold(line.replace(/^[-*]\s*/, '').trim()); + if (!text) return; + doc.text('• ', { continued: true }); + doc.text(text, { width: CONTENT_WIDTH - 20 }); + doc.moveDown(0.4); +} + +function addTable(doc, rows, fontSize = 9) { + if (rows.length === 0) return; + const colCount = rows[0].length; + const colWidth = CONTENT_WIDTH / colCount; + const rowHeight = fontSize * 1.4; + const startY = doc.y; + + doc.fontSize(fontSize); + rows.forEach((row, rowIndex) => { + const isHeader = rowIndex === 0; + if (isHeader) doc.font('Helvetica-Bold'); + if (doc.y > 750) { + doc.addPage(); + doc.y = MARGIN; + } + let x = MARGIN; + row.forEach((cell, cellIndex) => { + doc.text(cell, x, doc.y, { width: colWidth - 4, align: 'left' }); + x += colWidth; + }); + doc.y += rowHeight; + if (isHeader) doc.font('Helvetica'); + }); + doc.moveDown(0.5); +} + +function generateDSGVOPdf() { + const mdPath = path.join(__dirname, 'DSGVO-Dokumentation.md'); + const outPath = path.join(__dirname, 'DSGVO-Dokumentation.pdf'); + + if (!fs.existsSync(mdPath)) { + console.error('Datei nicht gefunden: DSGVO-Dokumentation.md'); + process.exit(1); + } + + const content = fs.readFileSync(mdPath, 'utf8'); + const lines = content.split(/\r?\n/); + + const doc = new PDFDocument({ margin: MARGIN, size: 'A4' }); + const stream = fs.createWriteStream(outPath); + doc.pipe(stream); + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const trimmed = line.trim(); + + // Neue Seite wenn nötig + if (doc.y > 750) doc.addPage(); + + if (trimmed.startsWith('# ')) { + doc.fontSize(22).font('Helvetica-Bold'); + writeText(doc, trimmed.slice(2).trim(), { align: 'center' }); + doc.moveDown(0.5); + doc.fontSize(12).text('Stundenerfassungssystem', { align: 'center' }); + doc.moveDown(1); + doc.font('Helvetica'); + i++; + continue; + } + + if (trimmed.startsWith('## ')) { + doc.fontSize(16).font('Helvetica-Bold'); + writeText(doc, trimmed.slice(3).trim()); + doc.moveDown(0.5); + doc.font('Helvetica').fontSize(10); + i++; + continue; + } + + if (trimmed.startsWith('### ')) { + doc.fontSize(13).font('Helvetica-Bold'); + writeText(doc, trimmed.slice(4).trim()); + doc.moveDown(0.4); + doc.font('Helvetica').fontSize(10); + i++; + continue; + } + + if (trimmed === '---') { + doc.moveDown(0.5); + i++; + continue; + } + + if (isTableRow(line)) { + const { rows, nextIndex } = parseTableRows(lines, i); + addTable(doc, rows); + i = nextIndex; + continue; + } + + if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) { + addBullet(doc, trimmed); + i++; + continue; + } + + if (trimmed) { + addParagraph(doc, trimmed); + } else { + doc.moveDown(0.3); + } + i++; + } + + doc.end(); + stream.on('finish', () => { + console.log('PDF erstellt: ' + outPath); + }); + stream.on('error', (err) => { + console.error('Fehler beim Schreiben der PDF:', err); + process.exit(1); + }); +} + +generateDSGVOPdf(); diff --git a/package.json b/package.json index a104900..d7f7e57 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", - "reset-db": "node reset-db.js" + "reset-db": "node reset-db.js", + "dsgvo-pdf": "node generate-dsgvo-pdf.js" }, "dependencies": { "archiver": "^7.0.1", diff --git a/services/feiertage-service.js b/services/feiertage-service.js index fbfb8b7..7a0cbd8 100644 --- a/services/feiertage-service.js +++ b/services/feiertage-service.js @@ -103,7 +103,34 @@ function getHolidaysForDateRange(weekStart, weekEnd) { }); } +/** + * Liefert Feiertage im Datumsbereich inkl. Namen (für PDF-Ausgabe). + * Stellt sicher, dass Daten in der DB sind, liest dann date + name. + * @param {string} weekStart YYYY-MM-DD + * @param {string} weekEnd YYYY-MM-DD + * @returns {Promise<{ holidaySet: Set, holidayNames: Map }>} + */ +function getHolidaysWithNamesForDateRange(weekStart, weekEnd) { + return getHolidaysForDateRange(weekStart, weekEnd).then((holidaySet) => { + return new Promise((resolve, reject) => { + db.all( + 'SELECT date, name FROM public_holidays WHERE date >= ? AND date <= ? ORDER BY date', + [weekStart, weekEnd], + (err, rows) => { + if (err) return reject(err); + const holidayNames = new Map(); + (rows || []).forEach((r) => { + holidayNames.set(r.date, r.name && r.name.trim() ? r.name.trim() : 'Feiertag'); + }); + resolve({ holidaySet, holidayNames }); + } + ); + }); + }); +} + module.exports = { getHolidaysForYear, getHolidaysForDateRange, + getHolidaysWithNamesForDateRange, }; diff --git a/services/ldap-service.js b/services/ldap-service.js index 3dc1144..d950570 100644 --- a/services/ldap-service.js +++ b/services/ldap-service.js @@ -479,11 +479,44 @@ class LDAPService { }); } + /** + * DN-Unescaping für Active Directory + * + * AD liefert DNs mit hex-escaped UTF-8 (z.B. \c3\9f für ß). + * Für Bind erwartet AD die unescaped UTF-8-Form. + * Siehe: https://github.com/ldapjs/node-ldapjs/issues/968 + */ + static unescapeLdapDN(dn) { + if (!dn || typeof dn !== 'string') return dn; + let result = ''; + let bytes = []; + let i = 0; + while (i < dn.length) { + if (dn[i] === '\\' && i + 2 < dn.length && /^[0-9a-fA-F]{2}$/.test(dn.slice(i + 1, i + 3))) { + bytes.push(parseInt(dn.slice(i + 1, i + 3), 16)); + i += 3; + } else { + if (bytes.length > 0) { + result += Buffer.from(bytes).toString('utf8'); + bytes = []; + } + result += dn[i]; + i++; + } + } + if (bytes.length > 0) { + result += Buffer.from(bytes).toString('utf8'); + } + return result; + } + /** * LDAP Bind durchführen (Passwort-Authentifizierung) */ static performBind(config, userDN, password, canonicalUsername, callback) { - console.log('[LDAP] Attempting bind with userDN:', userDN); + // DN unescapen: AD liefert hex-escaped (z.B. \c3\9f), Bind benötigt echte UTF-8 (ß) + const bindDN = this.unescapeLdapDN(userDN); + console.log('[LDAP] Attempting bind with userDN:', bindDN); const authClient = ldap.createClient({ url: config.url, @@ -497,7 +530,7 @@ class LDAPService { callback(err, false); }); - authClient.bind(userDN, password, (err) => { + authClient.bind(bindDN, password, (err) => { authClient.unbind(); if (err) { const errorMsg = err.message || String(err); diff --git a/services/pdf-service.js b/services/pdf-service.js index 202aca4..2383538 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -4,7 +4,7 @@ const PDFDocument = require('pdfkit'); const QRCode = require('qrcode'); const { db } = require('../database'); const { formatDate, formatDateTime } = require('../helpers/utils'); -const { getHolidaysForDateRange } = require('./feiertage-service'); +const { getHolidaysWithNamesForDateRange } = require('./feiertage-service'); // Kalenderwoche berechnen function getCalendarWeek(dateStr) { @@ -67,14 +67,14 @@ function generatePDF(timesheetId, req, res) { return new Date(a.date) - new Date(b.date); }); - // Feiertage für die Woche laden (8h pro Feiertag; Arbeit an Feiertag = Überstunden) - getHolidaysForDateRange(timesheet.week_start, timesheet.week_end) - .then((holidaySet) => { + // Feiertage für die Woche laden (mit Namen für PDF-Ausgabe) + const arbeitstage = timesheet.arbeitstage || 5; + const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8; + getHolidaysWithNamesForDateRange(timesheet.week_start, timesheet.week_end) + .then(({ holidaySet, holidayNames }) => { 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) { @@ -82,10 +82,10 @@ function generatePDF(timesheetId, req, res) { if (holidaySet.has(dateStr)) holidayHours += fullDayHours; } } - return { holidaySet, holidayHours }; + return { holidaySet, holidayNames, holidayHours }; }) - .catch(() => ({ holidaySet: new Set(), holidayHours: 0 })) - .then(({ holidaySet, holidayHours }) => { + .catch(() => ({ holidaySet: new Set(), holidayNames: new Map(), holidayHours: 0 })) + .then(({ holidaySet, holidayNames, holidayHours }) => { const doc = new PDFDocument({ margin: 50 }); // Prüfe ob inline angezeigt werden soll (für Vorschau) @@ -164,16 +164,48 @@ function generatePDF(timesheetId, req, res) { doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.5); + // Zeilen: Einträge + Feiertage ohne Eintrag, nach Datum sortiert + const allRows = []; + entries.forEach((e) => allRows.push({ type: 'entry', entry: e })); + holidaySet.forEach((dateStr) => { + if (!entriesByDate[dateStr]) { + allRows.push({ type: 'holiday', date: dateStr, holidayName: holidayNames.get(dateStr) || 'Feiertag' }); + } + }); + allRows.sort((a, b) => { + const dateA = a.type === 'entry' ? a.entry.date : a.date; + const dateB = b.type === 'entry' ? b.entry.date : b.date; + return dateA.localeCompare(dateB); + }); + // Tabellen-Daten doc.font('Helvetica'); let totalHours = 0; let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung - entries.forEach((entry) => { + allRows.forEach((row) => { + if (row.type === 'holiday') { + y = doc.y; + x = 50; + const rowData = [formatDate(row.date), '-', '-', '-', fullDayHours.toFixed(2) + ' h (Feiertag)']; + rowData.forEach((data, i) => { + doc.text(data, x, y, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + doc.moveDown(0.2); + doc.fontSize(9).font('Helvetica-Oblique'); + doc.text('Feiertag: ' + row.holidayName, 60, doc.y, { width: 360 }); + doc.fontSize(10); + doc.moveDown(0.5); + y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.3); + return; + } + + const entry = row.entry; y = doc.y; x = 50; - - // Basis-Zeile const rowData = [ formatDate(entry.date), entry.start_time || '-', @@ -224,6 +256,9 @@ function generatePDF(timesheetId, req, res) { // Überstunden und Urlaub anzeigen const overtimeInfo = []; + if (holidaySet.has(entry.date)) { + overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); + } if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); } @@ -243,28 +278,20 @@ 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 += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden - // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + vacationHours += fullDayHours; } else if (entry.vacation_type === 'half') { - vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden - // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + vacationHours += fullDayHours / 2; if (entry.total_hours) { totalHours += entry.total_hours; } } else { - // Kein Urlaub - zähle Arbeitsstunden; an Feiertagen zählt jede Stunde als Überstunde (8h Feiertag + Arbeit) if (entry.total_hours) { totalHours += entry.total_hours; } } - // Feiertag: 8h sind über holidayHours erfasst; gearbeitete Stunden oben bereits zu totalHours addiert doc.moveDown(0.5); - - // Trennlinie zwischen Einträgen y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.3); @@ -347,24 +374,24 @@ function generatePDFToBuffer(timesheetId, req) { return new Date(a.date) - new Date(b.date); }); - getHolidaysForDateRange(timesheet.week_start, timesheet.week_end) - .then((holidaySet) => { + const arbeitstageBuf = timesheet.arbeitstage || 5; + const fullDayHoursBuf = timesheet.wochenstunden > 0 && arbeitstageBuf > 0 ? timesheet.wochenstunden / arbeitstageBuf : 8; + getHolidaysWithNamesForDateRange(timesheet.week_start, timesheet.week_end) + .then(({ holidaySet, holidayNames }) => { 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 += fullDayHours; + if (holidaySet.has(dateStr)) holidayHours += fullDayHoursBuf; } } - return { holidaySet, holidayHours }; + return { holidaySet, holidayNames, holidayHours }; }) - .catch(() => ({ holidaySet: new Set(), holidayHours: 0 })) - .then(({ holidayHours }) => { + .catch(() => ({ holidaySet: new Set(), holidayNames: new Map(), holidayHours: 0 })) + .then(({ holidaySet, holidayNames, holidayHours }) => { const doc = new PDFDocument({ margin: 50 }); const buffers = []; @@ -405,15 +432,48 @@ function generatePDFToBuffer(timesheetId, req) { doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.5); + // Zeilen: Einträge + Feiertage ohne Eintrag, nach Datum sortiert + const allRowsBuf = []; + entries.forEach((e) => allRowsBuf.push({ type: 'entry', entry: e })); + holidaySet.forEach((dateStr) => { + if (!entriesByDate[dateStr]) { + allRowsBuf.push({ type: 'holiday', date: dateStr, holidayName: holidayNames.get(dateStr) || 'Feiertag' }); + } + }); + allRowsBuf.sort((a, b) => { + const dateA = a.type === 'entry' ? a.entry.date : a.date; + const dateB = b.type === 'entry' ? b.entry.date : b.date; + return dateA.localeCompare(dateB); + }); + // Tabellen-Daten doc.font('Helvetica'); let totalHours = 0; let vacationHours = 0; - entries.forEach((entry) => { + allRowsBuf.forEach((row) => { + if (row.type === 'holiday') { + y = doc.y; + x = 50; + const rowDataBuf = [formatDate(row.date), '-', '-', '-', fullDayHoursBuf.toFixed(2) + ' h (Feiertag)']; + rowDataBuf.forEach((data, i) => { + doc.text(data, x, y, { width: colWidths[i], align: 'left' }); + x += colWidths[i]; + }); + doc.moveDown(0.2); + doc.fontSize(9).font('Helvetica-Oblique'); + doc.text('Feiertag: ' + row.holidayName, 60, doc.y, { width: 360 }); + doc.fontSize(10); + doc.moveDown(0.5); + y = doc.y; + doc.moveTo(50, y).lineTo(430, y).stroke(); + doc.moveDown(0.3); + return; + } + + const entry = row.entry; y = doc.y; x = 50; - const rowData = [ formatDate(entry.date), entry.start_time || '-', @@ -427,7 +487,6 @@ function generatePDFToBuffer(timesheetId, req) { x += colWidths[i]; }); - // Tätigkeiten sammeln const activities = []; for (let i = 1; i <= 5; i++) { const desc = entry[`activity${i}_desc`]; @@ -442,13 +501,11 @@ function generatePDFToBuffer(timesheetId, req) { } } - // 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) { @@ -462,8 +519,10 @@ function generatePDFToBuffer(timesheetId, req) { doc.fontSize(10); } - // Überstunden und Urlaub anzeigen const overtimeInfo = []; + if (holidaySet.has(entry.date)) { + overtimeInfo.push('Feiertag: ' + (holidayNames.get(entry.date) || 'Feiertag')); + } if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) { overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`); } @@ -471,38 +530,30 @@ function generatePDFToBuffer(timesheetId, req) { 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) => { + overtimeInfo.forEach((info) => { doc.text(info, 70, doc.y, { width: 360 }); doc.moveDown(0.15); }); doc.fontSize(10); } - // 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 += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden - // Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt + vacationHours += fullDayHoursBuf; } else if (entry.vacation_type === 'half') { - vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden - // Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein + vacationHours += fullDayHoursBuf / 2; if (entry.total_hours) { totalHours += entry.total_hours; } } else { - // Kein Urlaub - zähle nur Arbeitsstunden if (entry.total_hours) { totalHours += entry.total_hours; } } doc.moveDown(0.5); - y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.3); @@ -513,7 +564,6 @@ function generatePDFToBuffer(timesheetId, req) { doc.moveTo(50, y).lineTo(550, y).stroke(); doc.moveDown(0.5); doc.font('Helvetica-Bold'); - // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden const totalHoursWithVacation = totalHours + vacationHours + holidayHours; doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y);