diff --git a/checkin-server.js b/checkin-server.js index ef3a1f2..fddc806 100644 --- a/checkin-server.js +++ b/checkin-server.js @@ -1,15 +1,44 @@ // Check-in Server (separater Express-App auf Port 3334) const express = require('express'); +const path = require('path'); const { db } = require('./database'); const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); const checkinApp = express(); -const CHECKIN_PORT = 3336; +const CHECKIN_PORT = 3334; + +// View-Engine für Browser-Aufrufe (Bestätigungsseiten) +checkinApp.set('view engine', 'ejs'); +checkinApp.set('views', path.join(__dirname, 'views')); // Middleware für Check-in-Server checkinApp.use(express.json()); +/** Erkennt Browser-Aufruf: Accept-Header enthält text/html (z. B. beim Aufruf per QR/Link im Browser). */ +function wantsHtml(req) { + const accept = req.get('Accept') || ''; + return /text\/html/i.test(accept); +} + +/** Sendet je nach Client entweder HTML-Seite oder JSON. */ +function sendResponse(req, res, success, data) { + if (wantsHtml(req)) { + const title = data.title || (success ? 'Erfolg' : 'Fehler'); + const message = data.message || data.error || (success ? 'Aktion erfolgreich.' : 'Ein Fehler ist aufgetreten.'); + if (!success && data.status) { + res.status(data.status); + } + res.render('checkin-result', { success, title, message }); + } else { + if (success) { + res.json({ success: true, ...data }); + } else { + res.status(data.status || 500).json({ success: false, error: data.error }); + } + } +} + // API: Check-in (Kommen) checkinApp.get('/api/checkin/:userId', (req, res) => { const userId = parseInt(req.params.userId); @@ -19,25 +48,27 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { // Prüfe ob User existiert db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { - return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); + return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 }); } // Prüfe ob bereits ein Eintrag für heute existiert db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, currentDate], (err, entry) => { if (err) { - return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); + return sendResponse(req, res, false, { error: 'Fehler beim Abrufen des Eintrags', status: 500 }); } + const successTitle = 'Hallo, du wurdest erfolgreich eingecheckt'; + if (!entry) { // Kein Eintrag existiert → Erstelle neuen mit start_time db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, [userId, currentDate, currentTime], (err) => { if (err) { - return res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Eintrags' }); + return sendResponse(req, res, false, { error: 'Fehler beim Erstellen des Eintrags', status: 500 }); } - res.json({ - success: true, + sendResponse(req, res, true, { + title: successTitle, message: `Start-Zeit erfasst: ${currentTime}`, start_time: currentTime, date: currentDate @@ -48,10 +79,10 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [currentTime, entry.id], (err) => { if (err) { - return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); + return sendResponse(req, res, false, { error: 'Fehler beim Aktualisieren', status: 500 }); } - res.json({ - success: true, + sendResponse(req, res, true, { + title: successTitle, message: `Start-Zeit erfasst: ${currentTime}`, start_time: currentTime, date: currentDate @@ -59,8 +90,8 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { }); } else { // Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in - res.json({ - success: true, + sendResponse(req, res, true, { + title: successTitle, message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`, start_time: entry.start_time, date: currentDate @@ -79,21 +110,20 @@ checkinApp.get('/api/checkout/:userId', (req, res) => { // Prüfe ob User existiert db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { if (err || !user) { - return res.status(404).json({ success: false, error: 'Benutzer nicht gefunden' }); + return sendResponse(req, res, false, { error: 'Benutzer nicht gefunden', status: 404 }); } // Prüfe ob bereits ein Eintrag für heute existiert db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1', [userId, currentDate], (err, entry) => { if (err) { - return res.status(500).json({ success: false, error: 'Fehler beim Abrufen des Eintrags' }); + return sendResponse(req, res, false, { error: 'Fehler beim Abrufen des Eintrags', status: 500 }); } if (!entry || !entry.start_time) { - // Kein Eintrag oder keine Start-Zeit → Fehler - return res.status(400).json({ - success: false, - error: 'Bitte zuerst einchecken (Kommen).' + return sendResponse(req, res, false, { + error: 'Bitte zuerst einchecken (Kommen).', + status: 400 }); } @@ -105,11 +135,13 @@ checkinApp.get('/api/checkout/:userId', (req, res) => { db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [currentTime, totalHours, entry.id], (err) => { if (err) { - return res.status(500).json({ success: false, error: 'Fehler beim Aktualisieren' }); + return sendResponse(req, res, false, { error: 'Fehler beim Aktualisieren', status: 500 }); } - res.json({ - success: true, - message: `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`, + const successTitle = 'Schönen Feierabend, du wurdest erfolgreich ausgecheckt'; + const successMessage = `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`; + sendResponse(req, res, true, { + title: successTitle, + message: successMessage, end_time: currentTime, total_hours: totalHours, date: currentDate diff --git a/email-mitarbeiter-stundenerfassung.txt b/email-mitarbeiter-stundenerfassung.txt index 1377e05..7df208f 100644 --- a/email-mitarbeiter-stundenerfassung.txt +++ b/email-mitarbeiter-stundenerfassung.txt @@ -5,13 +5,16 @@ Hallo zusammen, Mara ist auf mich mit einer Bitte herangetreten, ob ich die Stundenerfassung digitalisieren kann. Das habe ich die letzten 2 Wochen am abend und am WE gemacht. -Ich gleube, dass das System jetzt fest fertig ist und ihr es testen könnt -Der test soll kleinere Fehler finden und mir noch die möglichkeit geben diese dann zu beheben. +Ich glaube, dass das System jetzt fest fertig ist und ihr es testen könnt +Der test soll Fehler finden und mir noch die möglichkeit geben diese dann zu beheben. Am Montag würde ich gerne eine kurze Einführung für die Leute im Büro geben. Um ca. 11:00 Uhr für so 10-15 Minuten. -Achtet bitte auf die Überstundenerechnung, da könnte noch der ein oder andere Fehler drin sein. +Achtet bitte am Anfangauf die Überstundenerechnung, da könnte noch der ein oder andere Fehler drin sein. + +Die Seite ist im Browser zu finden unter http://stunden.sds-systemtechnik.de:3333 oder http://192.168.120.64:3333 + Viele Grüße Carsten Graf diff --git a/package-lock.json b/package-lock.json index 25996ce..c58c19d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "node-cron": "^3.0.3", "pdfkit": "^0.13.0", "ping": "^0.4.4", + "qrcode": "^1.5.4", "sqlite3": "^5.1.6" }, "devDependencies": { @@ -841,6 +842,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -882,6 +892,46 @@ "node": ">=6" } }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -1104,6 +1154,15 @@ "ms": "2.0.0" } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -1225,6 +1284,12 @@ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1528,6 +1593,19 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fontkit": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", @@ -1672,6 +1750,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2474,6 +2561,18 @@ "node": ">= 0.4" } }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -2975,6 +3074,33 @@ "wrappy": "1" } }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -2990,6 +3116,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -3008,6 +3143,15 @@ "node": ">= 0.8" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -3099,6 +3243,15 @@ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -3204,6 +3357,23 @@ "once": "^1.3.1" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -3314,6 +3484,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/restructure": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", @@ -3440,8 +3625,7 @@ "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "node_modules/set-function-length": { "version": "1.2.2", @@ -4132,6 +4316,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -4259,11 +4449,52 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", @@ -4913,6 +5144,11 @@ "get-intrinsic": "^1.3.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -4940,6 +5176,36 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "optional": true }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -5099,6 +5365,11 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5183,6 +5454,11 @@ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -5427,6 +5703,15 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, "fontkit": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", @@ -5529,6 +5814,11 @@ "wide-align": "^1.1.5" } }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -6095,6 +6385,14 @@ } } }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, "lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -6451,6 +6749,22 @@ "wrappy": "1" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, "p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -6460,6 +6774,11 @@ "aggregate-error": "^3.0.0" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, "package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6475,6 +6794,11 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6544,6 +6868,11 @@ "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6628,6 +6957,16 @@ "once": "^1.3.1" } }, + "qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "requires": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + } + }, "qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -6708,6 +7047,16 @@ "set-function-name": "^2.0.2" } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, "restructure": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", @@ -6794,8 +7143,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "optional": true + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, "set-function-length": { "version": "1.2.2", @@ -7301,6 +7649,11 @@ "is-weakset": "^2.0.3" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "which-typed-array": { "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", @@ -7389,11 +7742,43 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + }, "zip-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", diff --git a/package.json b/package.json index 91e23b8..a104900 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,18 @@ "reset-db": "node reset-db.js" }, "dependencies": { - "express": "^4.18.2", - "express-session": "^1.17.3", + "archiver": "^7.0.1", "bcryptjs": "^2.4.3", - "sqlite3": "^5.1.6", "body-parser": "^1.20.2", "ejs": "^3.1.9", - "pdfkit": "^0.13.0", + "express": "^4.18.2", + "express-session": "^1.17.3", "ldapjs": "^3.0.7", "node-cron": "^3.0.3", + "pdfkit": "^0.13.0", "ping": "^0.4.4", - "archiver": "^7.0.1" + "qrcode": "^1.5.4", + "sqlite3": "^5.1.6" }, "devDependencies": { "nodemon": "^3.0.1" diff --git a/public/js/dashboard.js b/public/js/dashboard.js index ae8d933..3104b96 100644 --- a/public/js/dashboard.js +++ b/public/js/dashboard.js @@ -372,8 +372,13 @@ function renderWeek() { // Stunden zur Summe hinzufügen // Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) + // Feiertag: 8h Basis + gearbeitete Stunden (jede gearbeitete Stunde = Überstunde) // Bei halbem Tag Urlaub werden die Urlaubsstunden später in der Überstunden-Berechnung hinzugezählt - totalHours += hours; + if (isHoliday) { + totalHours += 8 + (hours || 0); // 8h Feiertag + gearbeitete Stunden (= Überstunden) + } else { + totalHours += hours; + } // Bearbeitung ist immer möglich, auch nach Abschicken // Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden) @@ -404,7 +409,7 @@ function renderWeek() { data-date="${dateStr}" data-field="break_minutes" ${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)"> - ${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : isHoliday && !hours ? '8.00 h (Feiertag)' : hours.toFixed(2) + ' h'} + ${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : isHoliday && !hours ? '8.00 h (Feiertag)' : isHoliday && hours ? '8.00 + ' + hours.toFixed(2) + ' h (Überst.)' : hours.toFixed(2) + ' h'} @@ -576,7 +581,7 @@ function renderWeek() { vacationHours += 4; // Halber Tag = 4 Stunden } }); - + // Überstunden berechnen let overtimeTaken = 0; Object.values(currentEntries).forEach(e => { @@ -664,6 +669,23 @@ function updateOvertimeDisplay() { } } else if (sickStatus) { totalHours += 8; // Krank = 8 Stunden + } else if (currentHolidayDates.has(dateStr)) { + // Feiertag: 8h Basis + gearbeitete Stunden (jede Stunde = Überstunde) + const startInput = document.querySelector(`input[data-date="${dateStr}"][data-field="start_time"]`); + const endInput = document.querySelector(`input[data-date="${dateStr}"][data-field="end_time"]`); + const startTime = startInput ? startInput.value : ''; + const endTime = endInput ? endInput.value : ''; + let worked = 0; + if (startTime && endTime) { + const breakInput = document.querySelector(`input[data-date="${dateStr}"][data-field="break_minutes"]`); + const breakMinutes = parseInt(breakInput ? breakInput.value : 0) || 0; + const start = new Date(`2000-01-01T${startTime}`); + const end = new Date(`2000-01-01T${endTime}`); + worked = (end - start) / (1000 * 60 * 60) - (breakMinutes / 60); + } else if (currentEntries[dateStr]?.total_hours) { + worked = parseFloat(currentEntries[dateStr].total_hours) || 0; + } + totalHours += 8 + worked; // 8h Feiertag + gearbeitete Stunden (= Überstunden) } else { // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden if (isFullDayOvertime) { @@ -710,10 +732,7 @@ function updateOvertimeDisplay() { } // Überstunden berechnen (wie im Backend: mit adjustedSollStunden) - // Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden - // Die negativen Stunden (wegen 0 statt Sollstunden) werden durch die verbrauchten Überstunden ausgeglichen - // Daher: adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours) - // So werden die Tage mit 8 Überstunden nicht zu negativen Überstunden führen + // totalHours enthält bereits Feiertagsstunden (8h oder gearbeitete Stunden) aus dem Feiertag-Zweig oben const totalHoursWithVacation = totalHours + vacationHours; const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); // overtimeHours = Überstunden diese Woche (wie im Backend berechnet) diff --git a/routes/dashboard.js b/routes/dashboard.js index 3f0e256..b97f1d7 100644 --- a/routes/dashboard.js +++ b/routes/dashboard.js @@ -2,9 +2,18 @@ const { hasRole } = require('../helpers/utils'); const { requireAuth } = require('../middleware/auth'); +const { generateCheckinCheckoutQRPDF } = require('../services/pdf-service'); // Routes registrieren function registerDashboardRoutes(app) { + // QR-Code-PDF (Check-in/Check-out) – nur für eingeloggte Nutzer mit Mitarbeiter-Rolle + app.get('/api/dashboard/qr-pdf', requireAuth, (req, res) => { + if (!hasRole(req, 'mitarbeiter')) { + return res.status(403).send('Zugriff verweigert'); + } + generateCheckinCheckoutQRPDF(req, res); + }); + // Dashboard für Mitarbeiter app.get('/dashboard', requireAuth, (req, res) => { // Prüfe ob User Mitarbeiter-Rolle hat diff --git a/server.js b/server.js index a9af914..661f7c7 100644 --- a/server.js +++ b/server.js @@ -6,7 +6,7 @@ const { initDatabase } = require('./database'); const { getDefaultRole } = require('./helpers/utils'); const app = express(); -const PORT = 3335; +const PORT = 3333; // Middleware app.use(bodyParser.urlencoded({ extended: true })); diff --git a/services/pdf-service.js b/services/pdf-service.js index 975df4d..73700a9 100644 --- a/services/pdf-service.js +++ b/services/pdf-service.js @@ -1,6 +1,7 @@ // 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'); @@ -528,4 +529,72 @@ function generatePDFToBuffer(timesheetId, req) { }); } -module.exports = { generatePDF, generatePDFToBuffer }; +// 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 }; diff --git a/views/checkin-result.ejs b/views/checkin-result.ejs new file mode 100644 index 0000000..9174944 --- /dev/null +++ b/views/checkin-result.ejs @@ -0,0 +1,56 @@ + + + + + + <%= title %> - Stundenerfassung + + + +
+ +

<%= title %>

+

<%= message %>

+
+ + diff --git a/views/dashboard.ejs b/views/dashboard.ejs index 31c7fc9..d318924 100644 --- a/views/dashboard.ejs +++ b/views/dashboard.ejs @@ -116,6 +116,9 @@ +
+ QR-Code-PDF herunterladen +