// PDF-Generierung Service const PDFDocument = require('pdfkit'); const QRCode = require('qrcode'); const { db } = require('../database'); const { formatDate, formatDateTime } = require('../helpers/utils'); const { getHolidaysWithNamesForDateRange } = require('./feiertage-service'); // Kalenderwoche berechnen function getCalendarWeek(dateStr) { const date = new Date(dateStr); const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); const dayNum = d.getUTCDay() || 7; d.setUTCDate(d.getUTCDate() + 4 - dayNum); const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); const weekNo = Math.ceil((((d - yearStart) / 86400000) + 1) / 7); return weekNo; } // PDF generieren function generatePDF(timesheetId, req, res) { db.get(`SELECT wt.*, u.firstname, u.lastname, u.username, u.wochenstunden, u.arbeitstage FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { if (err || !timesheet) { return res.status(404).send('Stundenzettel nicht gefunden'); } // Hole Einträge die zum Zeitpunkt der Einreichung existierten // Filtere nach submitted_at der Version, damit jede Version ihre eigenen Daten zeigt // Logik: Wenn updated_at existiert, verwende das, sonst created_at, sonst zeige Eintrag (für alte Daten ohne Timestamps) db.all(`SELECT * FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? AND ( (updated_at IS NOT NULL AND updated_at <= ?) OR (updated_at IS NULL AND created_at IS NOT NULL AND created_at <= ?) OR (updated_at IS NULL AND created_at IS NULL) ) ORDER BY date, updated_at DESC, id DESC`, [timesheet.user_id, timesheet.week_start, timesheet.week_end, timesheet.submitted_at, timesheet.submitted_at], (err, allEntries) => { if (err) { return res.status(500).send('Fehler beim Abrufen der Einträge'); } // Filtere auf neuesten Eintrag pro Tag (basierend auf updated_at oder id) const entriesByDate = {}; (allEntries || []).forEach(entry => { const existing = entriesByDate[entry.date]; if (!existing) { entriesByDate[entry.date] = entry; } else { // Vergleiche updated_at (falls vorhanden) oder id (höhere ID = neuer) const existingTime = existing.updated_at ? new Date(existing.updated_at).getTime() : 0; const currentTime = entry.updated_at ? new Date(entry.updated_at).getTime() : 0; if (currentTime > existingTime || (currentTime === existingTime && entry.id > existing.id)) { entriesByDate[entry.date] = entry; } } }); // Konvertiere zu Array und sortiere nach Datum const entries = Object.values(entriesByDate).sort((a, b) => { return new Date(a.date) - new Date(b.date); }); // 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); 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; } } return { holidaySet, holidayNames, 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) const inline = req.query.inline === 'true'; // Dateinamen generieren: Stundenzettel_KWxxx_NameMitarbeiter_heutigesDatum.pdf const calendarWeek = getCalendarWeek(timesheet.week_start); const today = new Date(); const todayStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); const employeeName = `${timesheet.firstname}${timesheet.lastname}`.replace(/\s+/g, ''); const filename = `Stundenzettel_KW${String(calendarWeek).padStart(2, '0')}_${employeeName}_${todayStr}.pdf`; res.setHeader('Content-Type', 'application/pdf'); res.setHeader('X-Content-Type-Options', 'nosniff'); if (inline) { res.setHeader('Content-Disposition', `inline; filename="${filename}"`); // Zusätzliche Header für iframe-Unterstützung res.setHeader('X-Frame-Options', 'SAMEORIGIN'); } else { res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); // Marker setzen, dass PDF heruntergeladen wurde (nur bei Download, nicht bei Vorschau) const downloadedBy = req.session.userId; // User der die PDF herunterlädt console.log('PDF Download - User ID:', downloadedBy, 'Timesheet ID:', timesheetId); if (downloadedBy) { db.run(`UPDATE weekly_timesheets SET pdf_downloaded_at = CURRENT_TIMESTAMP, pdf_downloaded_by = ? WHERE id = ?`, [downloadedBy, timesheetId], (err) => { if (err) { console.error('Fehler beim Setzen des Download-Markers:', err); } else { console.log('Download-Marker erfolgreich gesetzt für User:', downloadedBy); } // Fehler wird ignoriert, damit PDF trotzdem generiert wird }); } else { console.warn('PDF Download - Keine User ID in Session gefunden!'); } } doc.pipe(res); // Header (Kalenderwoche wurde bereits oben berechnet) doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' }); doc.moveDown(); // Mitarbeiter-Info doc.fontSize(12); doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`); doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`); doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`); doc.moveDown(); // Tabelle - Basis-Informationen const tableTop = doc.y; const colWidths = [80, 80, 80, 60, 80]; const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden']; // Tabellen-Header doc.fontSize(10).font('Helvetica-Bold'); let x = 50; headers.forEach((header, i) => { doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); doc.moveDown(); let y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.5); // 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 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; const rowData = [ formatDate(entry.date), entry.start_time || '-', entry.end_time || '-', entry.break_minutes ? `${entry.break_minutes} min` : '-', entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' ]; rowData.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); // Tätigkeiten sammeln const activities = []; for (let i = 1; i <= 5; i++) { const desc = entry[`activity${i}_desc`]; const hours = entry[`activity${i}_hours`]; const projectNumber = entry[`activity${i}_project_number`]; if (desc && desc.trim() && hours > 0) { activities.push({ desc: desc.trim(), hours: parseFloat(hours), projectNumber: projectNumber ? projectNumber.trim() : null }); } } // Tätigkeiten anzeigen if (activities.length > 0) { doc.moveDown(0.3); doc.fontSize(9).font('Helvetica-Oblique'); doc.text('Tätigkeiten:', 60, doc.y, { width: 380 }); doc.moveDown(0.2); activities.forEach((activity, idx) => { let activityText = `${idx + 1}. ${activity.desc}`; if (activity.projectNumber) { activityText += ` (Projekt: ${activity.projectNumber})`; } activityText += ` - ${activity.hours.toFixed(2)} h`; doc.fontSize(9).font('Helvetica'); doc.text(activityText, 70, doc.y, { width: 360 }); doc.moveDown(0.2); }); doc.fontSize(10); } // Überstunden und Urlaub anzeigen const overtimeInfo = []; if (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`); } if (entry.vacation_type) { const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; overtimeInfo.push(`Urlaub: ${vacationText}`); } if (overtimeInfo.length > 0) { doc.moveDown(0.2); doc.fontSize(9).font('Helvetica-Oblique'); overtimeInfo.forEach((info, idx) => { doc.text(info, 70, doc.y, { width: 360 }); doc.moveDown(0.15); }); doc.fontSize(10); } // Urlaub hat Priorität - wenn Urlaub, zähle nur Urlaubsstunden, nicht zusätzlich Arbeitsstunden if (entry.vacation_type === 'full') { vacationHours += fullDayHours; } else if (entry.vacation_type === 'half') { vacationHours += fullDayHours / 2; if (entry.total_hours) { totalHours += entry.total_hours; } } else { 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); }); // Summe y = doc.y; doc.moveTo(50, y).lineTo(550, y).stroke(); doc.moveDown(0.5); doc.font('Helvetica-Bold'); // Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden (8h pro Feiertag) const totalHoursWithVacation = totalHours + vacationHours + holidayHours; doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); // Überstunden berechnen und anzeigen const wochenstunden = timesheet.wochenstunden || 0; // Überstunden = (Tatsächliche Stunden + Urlaubsstunden) - Wochenstunden const overtimeHours = totalHoursWithVacation - wochenstunden; doc.moveDown(0.3); doc.font('Helvetica-Bold'); if (overtimeHours > 0) { doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); } else if (overtimeHours < 0) { doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); } else { doc.text(`Überstunden: 0.00 h`, 50, doc.y); } doc.end(); }); }); }); } // 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, u.arbeitstage FROM weekly_timesheets wt JOIN users u ON wt.user_id = u.id WHERE wt.id = ?`, [timesheetId], (err, timesheet) => { if (err || !timesheet) { return reject(new Error('Stundenzettel nicht gefunden')); } // Hole Einträge die zum Zeitpunkt der Einreichung existierten db.all(`SELECT * FROM timesheet_entries WHERE user_id = ? AND date >= ? AND date <= ? AND ( (updated_at IS NOT NULL AND updated_at <= ?) OR (updated_at IS NULL AND created_at IS NOT NULL AND created_at <= ?) OR (updated_at IS NULL AND created_at IS NULL) ) ORDER BY date, updated_at DESC, id DESC`, [timesheet.user_id, timesheet.week_start, timesheet.week_end, timesheet.submitted_at, timesheet.submitted_at], (err, allEntries) => { if (err) { return reject(new 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 { 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; } } }); const entries = Object.values(entriesByDate).sort((a, b) => { return new Date(a.date) - new Date(b.date); }); 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); 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 += fullDayHoursBuf; } } return { holidaySet, holidayNames, holidayHours }; }) .catch(() => ({ holidaySet: new Set(), holidayNames: new Map(), holidayHours: 0 })) .then(({ holidaySet, holidayNames, holidayHours }) => { const doc = new PDFDocument({ margin: 50 }); const buffers = []; doc.on('data', buffers.push.bind(buffers)); doc.on('end', () => { const pdfBuffer = Buffer.concat(buffers); resolve(pdfBuffer); }); doc.on('error', reject); // Header const calendarWeek = getCalendarWeek(timesheet.week_start); doc.fontSize(20).text(`Stundenzettel für KW ${calendarWeek}`, { align: 'center' }); doc.moveDown(); // Mitarbeiter-Info doc.fontSize(12); doc.text(`Mitarbeiter: ${timesheet.firstname} ${timesheet.lastname}`); doc.text(`Zeitraum: ${formatDate(timesheet.week_start)} - ${formatDate(timesheet.week_end)}`); doc.text(`Eingereicht am: ${formatDateTime(timesheet.submitted_at)}`); doc.moveDown(); // Tabelle - Basis-Informationen const tableTop = doc.y; const colWidths = [80, 80, 80, 60, 80]; const headers = ['Datum', 'Start', 'Ende', 'Pause', 'Stunden']; // Tabellen-Header doc.fontSize(10).font('Helvetica-Bold'); let x = 50; headers.forEach((header, i) => { doc.text(header, x, tableTop, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); doc.moveDown(); let y = doc.y; doc.moveTo(50, y).lineTo(430, y).stroke(); doc.moveDown(0.5); // 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; 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 || '-', entry.end_time || '-', entry.break_minutes ? `${entry.break_minutes} min` : '-', entry.total_hours ? entry.total_hours.toFixed(2) + ' h' : '-' ]; rowData.forEach((data, i) => { doc.text(data, x, y, { width: colWidths[i], align: 'left' }); x += colWidths[i]; }); const activities = []; for (let i = 1; i <= 5; i++) { const desc = entry[`activity${i}_desc`]; const hours = entry[`activity${i}_hours`]; const projectNumber = entry[`activity${i}_project_number`]; if (desc && desc.trim() && hours > 0) { activities.push({ desc: desc.trim(), hours: parseFloat(hours), projectNumber: projectNumber ? projectNumber.trim() : null }); } } if (activities.length > 0) { doc.moveDown(0.3); doc.fontSize(9).font('Helvetica-Oblique'); doc.text('Tätigkeiten:', 60, doc.y, { width: 380 }); doc.moveDown(0.2); activities.forEach((activity, idx) => { let activityText = `${idx + 1}. ${activity.desc}`; if (activity.projectNumber) { activityText += ` (Projekt: ${activity.projectNumber})`; } activityText += ` - ${activity.hours.toFixed(2)} h`; doc.fontSize(9).font('Helvetica'); doc.text(activityText, 70, doc.y, { width: 360 }); doc.moveDown(0.2); }); doc.fontSize(10); } 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`); } if (entry.vacation_type) { const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag'; overtimeInfo.push(`Urlaub: ${vacationText}`); } if (overtimeInfo.length > 0) { doc.moveDown(0.2); doc.fontSize(9).font('Helvetica-Oblique'); overtimeInfo.forEach((info) => { doc.text(info, 70, doc.y, { width: 360 }); doc.moveDown(0.15); }); doc.fontSize(10); } if (entry.vacation_type === 'full') { vacationHours += fullDayHoursBuf; } else if (entry.vacation_type === 'half') { vacationHours += fullDayHoursBuf / 2; if (entry.total_hours) { totalHours += entry.total_hours; } } else { 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); }); // Summe y = doc.y; doc.moveTo(50, y).lineTo(550, y).stroke(); doc.moveDown(0.5); doc.font('Helvetica-Bold'); const totalHoursWithVacation = totalHours + vacationHours + holidayHours; doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y); const wochenstunden = timesheet.wochenstunden || 0; const overtimeHours = totalHoursWithVacation - wochenstunden; doc.moveDown(0.3); doc.font('Helvetica-Bold'); if (overtimeHours > 0) { doc.text(`Überstunden: +${overtimeHours.toFixed(2)} h`, 50, doc.y); } else if (overtimeHours < 0) { doc.text(`Überstunden: ${overtimeHours.toFixed(2)} h`, 50, doc.y); } else { doc.text(`Überstunden: 0.00 h`, 50, doc.y); } doc.end(); }); }); }); }); } // Check-in/Check-out URL-Basis (wie im Dashboard-Frontend) function getCheckinBaseUrl(req, callback) { // Versuche Root URL aus Datenbank zu laden db.get('SELECT checkin_root_url FROM system_options WHERE id = 1', (err, options) => { if (err) { console.warn('Fehler beim Laden der Root URL, verwende Fallback:', err); } let checkinBaseUrl = null; if (options && options.checkin_root_url && options.checkin_root_url.trim() !== '') { checkinBaseUrl = options.checkin_root_url.trim(); // Stelle sicher, dass kein trailing slash vorhanden ist checkinBaseUrl = checkinBaseUrl.replace(/\/+$/, ''); } // Fallback: Konstruiere URL aus Request (Port 3334 für Check-in) if (!checkinBaseUrl) { const baseUrl = `${req.protocol}://${req.get('host')}`; checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334'); } callback(checkinBaseUrl); }); } // PDF mit Check-in- und Check-out-QR-Codes (A4) // urlType: 'internal' oder 'external' async function generateCheckinCheckoutQRPDF(req, res, urlType = 'internal') { const userId = req.session.userId; if (!userId) { return res.status(401).send('Nicht angemeldet'); } let checkinBaseUrl; if (urlType === 'external') { // Externe URL: Lade aus Datenbank checkinBaseUrl = await new Promise((resolve) => { getCheckinBaseUrl(req, resolve); }); } else { // Interne URL: Konstruiere aus Request (Port 3334) const baseUrl = `${req.protocol}://${req.get('host')}`; if (baseUrl.match(/:\d+$/)) { checkinBaseUrl = baseUrl.replace(/:\d+$/, ':3334'); } else { const url = new URL(baseUrl); checkinBaseUrl = `${url.protocol}//${url.hostname}:3334`; } } const checkinUrl = `${checkinBaseUrl}/api/checkin/${userId}`; const checkoutUrl = `${checkinBaseUrl}/api/checkout/${userId}`; try { const [checkinQRBuffer, checkoutQRBuffer] = await Promise.all([ QRCode.toBuffer(checkinUrl, { type: 'png', width: 400, margin: 1 }), QRCode.toBuffer(checkoutUrl, { type: 'png', width: 400, margin: 1 }) ]); const firstname = (req.session.firstname || '').replace(/\s+/g, ''); const lastname = (req.session.lastname || '').replace(/\s+/g, ''); const namePart = [firstname, lastname].filter(Boolean).join('_') || 'User'; const urlTypeLabel = urlType === 'external' ? 'Extern' : 'Intern'; const today = new Date(); const dateStr = today.getFullYear() + '-' + String(today.getMonth() + 1).padStart(2, '0') + '-' + String(today.getDate()).padStart(2, '0'); const filename = `Check-in_Check-out_QR_${urlTypeLabel}_${namePart}_${dateStr}.pdf`; res.setHeader('Content-Type', 'application/pdf'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); const doc = new PDFDocument({ size: 'A4', margin: 50 }); doc.pipe(res); const pageWidth = 595.28 - 100; const qrSize = 160; const gap = 40; const left1 = 50 + (pageWidth / 2 - qrSize - gap / 2); const left2 = 50 + (pageWidth / 2 + gap / 2); const title = urlType === 'external' ? 'Check-in / Check-out – Zeiterfassung (Externe URLs)' : 'Check-in / Check-out – Zeiterfassung (Interne URLs)'; doc.fontSize(18).text(title, { align: 'center' }); doc.moveDown(1.5); const displayFirst = (req.session.firstname || '').trim(); const displayLast = (req.session.lastname || '').trim(); const displayName = [displayFirst, displayLast].filter(Boolean).join(' ') || 'Mitarbeiter'; doc.fontSize(12).font('Helvetica').text('Mitarbeiter: ' + displayName, { align: 'center' }); doc.moveDown(0.5); const topY = doc.y; doc.image(checkinQRBuffer, left1, topY, { width: qrSize, height: qrSize }); doc.image(checkoutQRBuffer, left2, topY, { width: qrSize, height: qrSize }); doc.fontSize(12).font('Helvetica-Bold'); doc.text('Check-in', left1, topY + qrSize + 8, { width: qrSize, align: 'center' }); doc.text('Check-out', left2, topY + qrSize + 8, { width: qrSize, align: 'center' }); doc.moveDown(2); doc.font('Helvetica').fontSize(10); doc.text('Scannen Sie den jeweiligen QR-Code zum Erfassen von Arbeitsbeginn (Check-in) bzw. Arbeitsende (Check-out).', 50, doc.y, { width: pageWidth, align: 'center' }); doc.end(); } catch (err) { console.error('Fehler beim Generieren des QR-PDFs:', err); if (!res.headersSent) { res.status(500).send('Fehler beim Erstellen des PDFs'); } } } module.exports = { generatePDF, generatePDFToBuffer, generateCheckinCheckoutQRPDF };