479 lines
19 KiB
JavaScript
479 lines
19 KiB
JavaScript
// PDF-Generierung Service
|
|
|
|
const PDFDocument = require('pdfkit');
|
|
const { db } = require('../database');
|
|
const { formatDate, formatDateTime } = require('../helpers/utils');
|
|
|
|
// 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);
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
|
|
// Urlaubsstunden für Überstunden-Berechnung sammeln
|
|
if (entry.vacation_type === 'full') {
|
|
vacationHours += 8; // Ganzer Tag = 8 Stunden
|
|
} else if (entry.vacation_type === 'half') {
|
|
vacationHours += 4; // Halber Tag = 4 Stunden
|
|
}
|
|
|
|
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');
|
|
doc.text(`Gesamtstunden: ${totalHours.toFixed(2)} h`, 50, doc.y);
|
|
|
|
// Überstunden berechnen und anzeigen
|
|
const wochenstunden = timesheet.wochenstunden || 0;
|
|
// Überstunden = Gesamtstunden - Wochenstunden
|
|
// Urlaub zählt als normale Arbeitszeit, daher sind Urlaubsstunden bereits in totalHours enthalten
|
|
const overtimeHours = totalHours - 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);
|
|
});
|
|
|
|
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);
|
|
}
|
|
|
|
if (entry.total_hours) {
|
|
totalHours += entry.total_hours;
|
|
}
|
|
|
|
if (entry.vacation_type === 'full') {
|
|
vacationHours += 8;
|
|
} else if (entry.vacation_type === 'half') {
|
|
vacationHours += 4;
|
|
}
|
|
|
|
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');
|
|
doc.text(`Gesamtstunden: ${totalHours.toFixed(2)} h`, 50, doc.y);
|
|
|
|
const wochenstunden = timesheet.wochenstunden || 0;
|
|
const overtimeHours = totalHours - 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();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
module.exports = { generatePDF, generatePDFToBuffer };
|