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 0000000..b1b1ac6 Binary files /dev/null and b/DSGVO-Dokumentation.pdf differ diff --git a/Stunderfassung todo.txt b/Stunderfassung todo.txt index f6fe8e2..dd3e983 100644 --- a/Stunderfassung todo.txt +++ b/Stunderfassung todo.txt @@ -3,9 +3,15 @@ - Offset für die Verwaltung für Urlaubstage -> 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);