Files
SDSStundenerfassung/services/pdf-service.js
2026-02-25 19:47:56 +01:00

715 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PDF-Generierung Service
const PDFDocument = require('pdfkit');
const QRCode = require('qrcode');
const { db } = require('../database');
const { formatDate, formatDateTime, formatHoursMin } = 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;
// Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen
const holidayDay = new Date(row.date + 'T12:00:00').getDay();
const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6);
const holidayHoursStr = isWeekendHoliday ? '0h 0min (Feiertag)' : formatHoursMin(fullDayHours) + ' (Feiertag)';
const rowData = [formatDate(row.date), '-', '-', '-', holidayHoursStr];
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 ? formatHoursMin(entry.total_hours) : '-'
];
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()) {
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 += ` - ${formatHoursMin(activity.hours)}`;
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: ${formatHoursMin(parseFloat(entry.overtime_taken_hours))}`);
}
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: ${formatHoursMin(totalHoursWithVacation)}`, 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: +${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else if (overtimeHours < 0) {
doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else {
doc.text(`Überstunden: ${formatHoursMin(0)}`, 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;
// Feiertag am Wochenende: keine Tagesarbeitsstunden anzeigen
const holidayDay = new Date(row.date + 'T12:00:00').getDay();
const isWeekendHoliday = (holidayDay === 0 || holidayDay === 6);
const holidayHoursStr = isWeekendHoliday ? '0h 0min (Feiertag)' : formatHoursMin(fullDayHoursBuf) + ' (Feiertag)';
const rowDataBuf = [formatDate(row.date), '-', '-', '-', holidayHoursStr];
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 ? formatHoursMin(entry.total_hours) : '-'
];
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()) {
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 += ` - ${formatHoursMin(activity.hours)}`;
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: ${formatHoursMin(parseFloat(entry.overtime_taken_hours))}`);
}
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: ${formatHoursMin(totalHoursWithVacation)}`, 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: +${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else if (overtimeHours < 0) {
doc.text(`Überstunden: ${formatHoursMin(overtimeHours)}`, 50, doc.y);
} else {
doc.text(`Überstunden: ${formatHoursMin(0)}`, 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 };