Checkin Seite erstellt

This commit is contained in:
2026-01-30 18:59:00 +01:00
parent a1ddaf5a35
commit 32f40124a8
10 changed files with 619 additions and 42 deletions

View File

@@ -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

View File

@@ -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

393
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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)">
</td>
<td><strong id="hours_${dateStr}">${isFullDayVacation ? '8.00 h (Urlaub)' : isSick ? '8.00 h (Krank)' : isHoliday && !hours ? '8.00 h (Feiertag)' : hours.toFixed(2) + ' h'}</strong></td>
<td><strong id="hours_${dateStr}">${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'}</strong></td>
</tr>
<tr class="activities-row">
<td colspan="6" class="activities-cell">
@@ -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)

View File

@@ -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

View File

@@ -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 }));

View File

@@ -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 };

56
views/checkin-result.ejs Normal file
View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - Stundenerfassung</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #333;
}
.card {
background: #fff;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 2.5rem;
max-width: 420px;
width: 100%;
text-align: center;
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
line-height: 1;
}
.card h1 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
color: #2c3e50;
}
.card .message {
color: #555;
font-size: 1rem;
line-height: 1.5;
}
.card.success .icon { color: #27ae60; }
.card.success h1 { color: #27ae60; }
.card.error .icon { color: #e74c3c; }
.card.error h1 { color: #e74c3c; }
</style>
</head>
<body>
<div class="card <%= success ? 'success' : 'error' %>">
<div class="icon" aria-hidden="true"><%= success ? '✓' : '!' %></div>
<h1><%= title %></h1>
<p class="message"><%= message %></p>
</div>
</body>
</html>

View File

@@ -116,6 +116,9 @@
<button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
</div>
</div>
<div style="margin-top: 12px;">
<a href="/api/dashboard/qr-pdf" class="btn btn-sm btn-secondary" style="padding: 8px 12px; text-decoration: none; display: inline-block;" download>QR-Code-PDF herunterladen</a>
</div>
</div>
<!-- IP-Erfassung -->