601 lines
24 KiB
JavaScript
601 lines
24 KiB
JavaScript
// PDF-Generierung Service
|
||
|
||
const PDFDocument = require('pdfkit');
|
||
const QRCode = require('qrcode');
|
||
const { db } = require('../database');
|
||
const { formatDate, formatDateTime } = require('../helpers/utils');
|
||
const { getHolidaysForDateRange } = 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
|
||
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 (8h pro Feiertag; Arbeit an Feiertag = Überstunden)
|
||
getHolidaysForDateRange(timesheet.week_start, timesheet.week_end)
|
||
.then((holidaySet) => {
|
||
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 += 8;
|
||
}
|
||
}
|
||
return { holidaySet, holidayHours };
|
||
})
|
||
.catch(() => ({ holidaySet: new Set(), holidayHours: 0 }))
|
||
.then(({ holidaySet, 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);
|
||
|
||
// Tabellen-Daten
|
||
doc.font('Helvetica');
|
||
let totalHours = 0;
|
||
let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung
|
||
|
||
entries.forEach((entry) => {
|
||
y = doc.y;
|
||
x = 50;
|
||
|
||
// Basis-Zeile
|
||
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 (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 += 8; // Ganzer Tag = 8 Stunden
|
||
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
|
||
} else if (entry.vacation_type === 'half') {
|
||
vacationHours += 4; // Halber Tag = 4 Stunden
|
||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
||
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);
|
||
});
|
||
|
||
// 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
|
||
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);
|
||
});
|
||
|
||
getHolidaysForDateRange(timesheet.week_start, timesheet.week_end)
|
||
.then((holidaySet) => {
|
||
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 += 8;
|
||
}
|
||
}
|
||
return { holidaySet, holidayHours };
|
||
})
|
||
.catch(() => ({ holidaySet: new Set(), holidayHours: 0 }))
|
||
.then(({ 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);
|
||
|
||
// Tabellen-Daten
|
||
doc.font('Helvetica');
|
||
let totalHours = 0;
|
||
let vacationHours = 0;
|
||
|
||
entries.forEach((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 (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 += 8; // Ganzer Tag = 8 Stunden
|
||
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
|
||
} else if (entry.vacation_type === 'half') {
|
||
vacationHours += 4; // Halber Tag = 4 Stunden
|
||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
||
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);
|
||
});
|
||
|
||
// Summe
|
||
y = doc.y;
|
||
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);
|
||
|
||
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) {
|
||
const baseUrl = `${req.protocol}://${req.get('host')}`;
|
||
return baseUrl.replace(/:\d+$/, ':3334');
|
||
}
|
||
|
||
// PDF mit Check-in- und Check-out-QR-Codes (A4)
|
||
async function generateCheckinCheckoutQRPDF(req, res) {
|
||
const userId = req.session.userId;
|
||
if (!userId) {
|
||
return res.status(401).send('Nicht angemeldet');
|
||
}
|
||
const checkinBaseUrl = getCheckinBaseUrl(req);
|
||
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 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_${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);
|
||
|
||
doc.fontSize(18).text('Check-in / Check-out – Zeiterfassung', { align: 'center' });
|
||
doc.moveDown(1.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 };
|