DSGVO und LDAP fix
This commit is contained in:
@@ -14,9 +14,10 @@ info@sds-systemtechnik.de
|
|||||||
+497051931540
|
+497051931540
|
||||||
|
|
||||||
**Datenschutzbeauftragter (falls vorhanden):**
|
**Datenschutzbeauftragter (falls vorhanden):**
|
||||||
[Name]
|
Matthias Herrlinger
|
||||||
[E-Mail]
|
connexo GmbH
|
||||||
[Telefon]
|
Jägerstraße 4F
|
||||||
|
71296 Heimsheim
|
||||||
|
|
||||||
**Kontakt für Datenschutzanfragen:**
|
**Kontakt für Datenschutzanfragen:**
|
||||||
Carsten Graf
|
Carsten Graf
|
||||||
|
|||||||
BIN
DSGVO-Dokumentation.pdf
Normal file
BIN
DSGVO-Dokumentation.pdf
Normal file
Binary file not shown.
@@ -3,9 +3,15 @@
|
|||||||
- Offset für die Verwaltung für Urlaubstage -> DONE
|
- Offset für die Verwaltung für Urlaubstage -> DONE
|
||||||
- Stunden pro Tag und wie viele Tage arbeit -> DONE
|
- Stunden pro Tag und wie viele Tage arbeit -> DONE
|
||||||
- Reisen für Wochenende -> DONE
|
- Reisen für Wochenende -> DONE
|
||||||
- LDAP Prüfung -> DONE TESTEn mit Jessi und Jörg
|
- LDAP Prüfung -> DONE GEHT?!
|
||||||
- DSGVO Sicherheit -> DONE
|
- DSGVO Sicherheit -> DONE
|
||||||
- Feiertage müssen als ausgefüllt zählen -> DONE
|
- Feiertage müssen als ausgefüllt zählen -> DONE
|
||||||
- Mitarbeiter sollen PDF ansehen können. -> 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 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
|
- 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.
|
||||||
177
generate-dsgvo-pdf.js
Normal file
177
generate-dsgvo-pdf.js
Normal file
@@ -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();
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "nodemon 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": {
|
"dependencies": {
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
|
|||||||
@@ -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<string>, holidayNames: Map<string, string> }>}
|
||||||
|
*/
|
||||||
|
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 = {
|
module.exports = {
|
||||||
getHolidaysForYear,
|
getHolidaysForYear,
|
||||||
getHolidaysForDateRange,
|
getHolidaysForDateRange,
|
||||||
|
getHolidaysWithNamesForDateRange,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
* LDAP Bind durchführen (Passwort-Authentifizierung)
|
||||||
*/
|
*/
|
||||||
static performBind(config, userDN, password, canonicalUsername, callback) {
|
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({
|
const authClient = ldap.createClient({
|
||||||
url: config.url,
|
url: config.url,
|
||||||
@@ -497,7 +530,7 @@ class LDAPService {
|
|||||||
callback(err, false);
|
callback(err, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
authClient.bind(userDN, password, (err) => {
|
authClient.bind(bindDN, password, (err) => {
|
||||||
authClient.unbind();
|
authClient.unbind();
|
||||||
if (err) {
|
if (err) {
|
||||||
const errorMsg = err.message || String(err);
|
const errorMsg = err.message || String(err);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const PDFDocument = require('pdfkit');
|
|||||||
const QRCode = require('qrcode');
|
const QRCode = require('qrcode');
|
||||||
const { db } = require('../database');
|
const { db } = require('../database');
|
||||||
const { formatDate, formatDateTime } = require('../helpers/utils');
|
const { formatDate, formatDateTime } = require('../helpers/utils');
|
||||||
const { getHolidaysForDateRange } = require('./feiertage-service');
|
const { getHolidaysWithNamesForDateRange } = require('./feiertage-service');
|
||||||
|
|
||||||
// Kalenderwoche berechnen
|
// Kalenderwoche berechnen
|
||||||
function getCalendarWeek(dateStr) {
|
function getCalendarWeek(dateStr) {
|
||||||
@@ -67,14 +67,14 @@ function generatePDF(timesheetId, req, res) {
|
|||||||
return new Date(a.date) - new Date(b.date);
|
return new Date(a.date) - new Date(b.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Feiertage für die Woche laden (8h pro Feiertag; Arbeit an Feiertag = Überstunden)
|
// Feiertage für die Woche laden (mit Namen für PDF-Ausgabe)
|
||||||
getHolidaysForDateRange(timesheet.week_start, timesheet.week_end)
|
const arbeitstage = timesheet.arbeitstage || 5;
|
||||||
.then((holidaySet) => {
|
const fullDayHours = timesheet.wochenstunden > 0 && arbeitstage > 0 ? timesheet.wochenstunden / arbeitstage : 8;
|
||||||
|
getHolidaysWithNamesForDateRange(timesheet.week_start, timesheet.week_end)
|
||||||
|
.then(({ holidaySet, holidayNames }) => {
|
||||||
let holidayHours = 0;
|
let holidayHours = 0;
|
||||||
const start = new Date(timesheet.week_start);
|
const start = new Date(timesheet.week_start);
|
||||||
const end = new Date(timesheet.week_end);
|
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)) {
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
if (day >= 1 && day <= 5) {
|
if (day >= 1 && day <= 5) {
|
||||||
@@ -82,10 +82,10 @@ function generatePDF(timesheetId, req, res) {
|
|||||||
if (holidaySet.has(dateStr)) holidayHours += fullDayHours;
|
if (holidaySet.has(dateStr)) holidayHours += fullDayHours;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { holidaySet, holidayHours };
|
return { holidaySet, holidayNames, holidayHours };
|
||||||
})
|
})
|
||||||
.catch(() => ({ holidaySet: new Set(), holidayHours: 0 }))
|
.catch(() => ({ holidaySet: new Set(), holidayNames: new Map(), holidayHours: 0 }))
|
||||||
.then(({ holidaySet, holidayHours }) => {
|
.then(({ holidaySet, holidayNames, holidayHours }) => {
|
||||||
const doc = new PDFDocument({ margin: 50 });
|
const doc = new PDFDocument({ margin: 50 });
|
||||||
|
|
||||||
// Prüfe ob inline angezeigt werden soll (für Vorschau)
|
// 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.moveTo(50, y).lineTo(430, y).stroke();
|
||||||
doc.moveDown(0.5);
|
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
|
// Tabellen-Daten
|
||||||
doc.font('Helvetica');
|
doc.font('Helvetica');
|
||||||
let totalHours = 0;
|
let totalHours = 0;
|
||||||
let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung
|
let vacationHours = 0; // Urlaubsstunden für Überstunden-Berechnung
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
allRows.forEach((row) => {
|
||||||
|
if (row.type === 'holiday') {
|
||||||
y = doc.y;
|
y = doc.y;
|
||||||
x = 50;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
// Basis-Zeile
|
const entry = row.entry;
|
||||||
|
y = doc.y;
|
||||||
|
x = 50;
|
||||||
const rowData = [
|
const rowData = [
|
||||||
formatDate(entry.date),
|
formatDate(entry.date),
|
||||||
entry.start_time || '-',
|
entry.start_time || '-',
|
||||||
@@ -224,6 +256,9 @@ function generatePDF(timesheetId, req, res) {
|
|||||||
|
|
||||||
// Überstunden und Urlaub anzeigen
|
// Überstunden und Urlaub anzeigen
|
||||||
const overtimeInfo = [];
|
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) {
|
if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) {
|
||||||
overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`);
|
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
|
// 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') {
|
if (entry.vacation_type === 'full') {
|
||||||
vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden
|
vacationHours += fullDayHours;
|
||||||
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
|
|
||||||
} else if (entry.vacation_type === 'half') {
|
} else if (entry.vacation_type === 'half') {
|
||||||
vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden
|
vacationHours += fullDayHours / 2;
|
||||||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours) {
|
||||||
totalHours += entry.total_hours;
|
totalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Kein Urlaub - zähle Arbeitsstunden; an Feiertagen zählt jede Stunde als Überstunde (8h Feiertag + Arbeit)
|
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours) {
|
||||||
totalHours += entry.total_hours;
|
totalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Feiertag: 8h sind über holidayHours erfasst; gearbeitete Stunden oben bereits zu totalHours addiert
|
|
||||||
|
|
||||||
doc.moveDown(0.5);
|
doc.moveDown(0.5);
|
||||||
|
|
||||||
// Trennlinie zwischen Einträgen
|
|
||||||
y = doc.y;
|
y = doc.y;
|
||||||
doc.moveTo(50, y).lineTo(430, y).stroke();
|
doc.moveTo(50, y).lineTo(430, y).stroke();
|
||||||
doc.moveDown(0.3);
|
doc.moveDown(0.3);
|
||||||
@@ -347,24 +374,24 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
return new Date(a.date) - new Date(b.date);
|
return new Date(a.date) - new Date(b.date);
|
||||||
});
|
});
|
||||||
|
|
||||||
getHolidaysForDateRange(timesheet.week_start, timesheet.week_end)
|
const arbeitstageBuf = timesheet.arbeitstage || 5;
|
||||||
.then((holidaySet) => {
|
const fullDayHoursBuf = timesheet.wochenstunden > 0 && arbeitstageBuf > 0 ? timesheet.wochenstunden / arbeitstageBuf : 8;
|
||||||
|
getHolidaysWithNamesForDateRange(timesheet.week_start, timesheet.week_end)
|
||||||
|
.then(({ holidaySet, holidayNames }) => {
|
||||||
let holidayHours = 0;
|
let holidayHours = 0;
|
||||||
const start = new Date(timesheet.week_start);
|
const start = new Date(timesheet.week_start);
|
||||||
const end = new Date(timesheet.week_end);
|
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)) {
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
const day = d.getDay();
|
const day = d.getDay();
|
||||||
if (day >= 1 && day <= 5) {
|
if (day >= 1 && day <= 5) {
|
||||||
const dateStr = d.toISOString().split('T')[0];
|
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 }))
|
.catch(() => ({ holidaySet: new Set(), holidayNames: new Map(), holidayHours: 0 }))
|
||||||
.then(({ holidayHours }) => {
|
.then(({ holidaySet, holidayNames, holidayHours }) => {
|
||||||
const doc = new PDFDocument({ margin: 50 });
|
const doc = new PDFDocument({ margin: 50 });
|
||||||
const buffers = [];
|
const buffers = [];
|
||||||
|
|
||||||
@@ -405,15 +432,48 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
doc.moveTo(50, y).lineTo(430, y).stroke();
|
doc.moveTo(50, y).lineTo(430, y).stroke();
|
||||||
doc.moveDown(0.5);
|
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
|
// Tabellen-Daten
|
||||||
doc.font('Helvetica');
|
doc.font('Helvetica');
|
||||||
let totalHours = 0;
|
let totalHours = 0;
|
||||||
let vacationHours = 0;
|
let vacationHours = 0;
|
||||||
|
|
||||||
entries.forEach((entry) => {
|
allRowsBuf.forEach((row) => {
|
||||||
|
if (row.type === 'holiday') {
|
||||||
y = doc.y;
|
y = doc.y;
|
||||||
x = 50;
|
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 = [
|
const rowData = [
|
||||||
formatDate(entry.date),
|
formatDate(entry.date),
|
||||||
entry.start_time || '-',
|
entry.start_time || '-',
|
||||||
@@ -427,7 +487,6 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
x += colWidths[i];
|
x += colWidths[i];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tätigkeiten sammeln
|
|
||||||
const activities = [];
|
const activities = [];
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const desc = entry[`activity${i}_desc`];
|
const desc = entry[`activity${i}_desc`];
|
||||||
@@ -442,13 +501,11 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tätigkeiten anzeigen
|
|
||||||
if (activities.length > 0) {
|
if (activities.length > 0) {
|
||||||
doc.moveDown(0.3);
|
doc.moveDown(0.3);
|
||||||
doc.fontSize(9).font('Helvetica-Oblique');
|
doc.fontSize(9).font('Helvetica-Oblique');
|
||||||
doc.text('Tätigkeiten:', 60, doc.y, { width: 380 });
|
doc.text('Tätigkeiten:', 60, doc.y, { width: 380 });
|
||||||
doc.moveDown(0.2);
|
doc.moveDown(0.2);
|
||||||
|
|
||||||
activities.forEach((activity, idx) => {
|
activities.forEach((activity, idx) => {
|
||||||
let activityText = `${idx + 1}. ${activity.desc}`;
|
let activityText = `${idx + 1}. ${activity.desc}`;
|
||||||
if (activity.projectNumber) {
|
if (activity.projectNumber) {
|
||||||
@@ -462,8 +519,10 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
doc.fontSize(10);
|
doc.fontSize(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überstunden und Urlaub anzeigen
|
|
||||||
const overtimeInfo = [];
|
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) {
|
if (entry.overtime_taken_hours && parseFloat(entry.overtime_taken_hours) > 0) {
|
||||||
overtimeInfo.push(`Überstunden genommen: ${parseFloat(entry.overtime_taken_hours).toFixed(2)} h`);
|
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';
|
const vacationText = entry.vacation_type === 'full' ? 'Ganzer Tag' : 'Halber Tag';
|
||||||
overtimeInfo.push(`Urlaub: ${vacationText}`);
|
overtimeInfo.push(`Urlaub: ${vacationText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overtimeInfo.length > 0) {
|
if (overtimeInfo.length > 0) {
|
||||||
doc.moveDown(0.2);
|
doc.moveDown(0.2);
|
||||||
doc.fontSize(9).font('Helvetica-Oblique');
|
doc.fontSize(9).font('Helvetica-Oblique');
|
||||||
overtimeInfo.forEach((info, idx) => {
|
overtimeInfo.forEach((info) => {
|
||||||
doc.text(info, 70, doc.y, { width: 360 });
|
doc.text(info, 70, doc.y, { width: 360 });
|
||||||
doc.moveDown(0.15);
|
doc.moveDown(0.15);
|
||||||
});
|
});
|
||||||
doc.fontSize(10);
|
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') {
|
if (entry.vacation_type === 'full') {
|
||||||
vacationHours += fullDayHours; // Ganzer Tag = (Wochenarbeitszeit / Arbeitstage) Stunden
|
vacationHours += fullDayHoursBuf;
|
||||||
// Bei vollem Tag Urlaub werden keine Arbeitsstunden gezählt
|
|
||||||
} else if (entry.vacation_type === 'half') {
|
} else if (entry.vacation_type === 'half') {
|
||||||
vacationHours += fullDayHours / 2; // Halber Tag = (Wochenarbeitszeit / Arbeitstage) / 2 Stunden
|
vacationHours += fullDayHoursBuf / 2;
|
||||||
// Bei halbem Tag Urlaub können noch Arbeitsstunden vorhanden sein
|
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours) {
|
||||||
totalHours += entry.total_hours;
|
totalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Kein Urlaub - zähle nur Arbeitsstunden
|
|
||||||
if (entry.total_hours) {
|
if (entry.total_hours) {
|
||||||
totalHours += entry.total_hours;
|
totalHours += entry.total_hours;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.moveDown(0.5);
|
doc.moveDown(0.5);
|
||||||
|
|
||||||
y = doc.y;
|
y = doc.y;
|
||||||
doc.moveTo(50, y).lineTo(430, y).stroke();
|
doc.moveTo(50, y).lineTo(430, y).stroke();
|
||||||
doc.moveDown(0.3);
|
doc.moveDown(0.3);
|
||||||
@@ -513,7 +564,6 @@ function generatePDFToBuffer(timesheetId, req) {
|
|||||||
doc.moveTo(50, y).lineTo(550, y).stroke();
|
doc.moveTo(50, y).lineTo(550, y).stroke();
|
||||||
doc.moveDown(0.5);
|
doc.moveDown(0.5);
|
||||||
doc.font('Helvetica-Bold');
|
doc.font('Helvetica-Bold');
|
||||||
// Gesamtstunden = Arbeitsstunden + Urlaubsstunden + Feiertagsstunden
|
|
||||||
const totalHoursWithVacation = totalHours + vacationHours + holidayHours;
|
const totalHoursWithVacation = totalHours + vacationHours + holidayHours;
|
||||||
doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y);
|
doc.text(`Gesamtstunden: ${totalHoursWithVacation.toFixed(2)} h`, 50, doc.y);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user