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) // Check-in Server (separater Express-App auf Port 3334)
const express = require('express'); const express = require('express');
const path = require('path');
const { db } = require('./database'); const { db } = require('./database');
const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils'); const { getCurrentDate, getCurrentTime, updateTotalHours } = require('./helpers/utils');
const checkinApp = express(); 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 // Middleware für Check-in-Server
checkinApp.use(express.json()); 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) // API: Check-in (Kommen)
checkinApp.get('/api/checkin/:userId', (req, res) => { checkinApp.get('/api/checkin/:userId', (req, res) => {
const userId = parseInt(req.params.userId); const userId = parseInt(req.params.userId);
@@ -19,25 +48,27 @@ checkinApp.get('/api/checkin/:userId', (req, res) => {
// Prüfe ob User existiert // Prüfe ob User existiert
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
if (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 // 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', db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
[userId, currentDate], (err, entry) => { [userId, currentDate], (err, entry) => {
if (err) { 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) { if (!entry) {
// Kein Eintrag existiert → Erstelle neuen mit start_time // Kein Eintrag existiert → Erstelle neuen mit start_time
db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`, db.run(`INSERT INTO timesheet_entries (user_id, date, start_time, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP)`,
[userId, currentDate, currentTime], (err) => { [userId, currentDate, currentTime], (err) => {
if (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({ sendResponse(req, res, true, {
success: true, title: successTitle,
message: `Start-Zeit erfasst: ${currentTime}`, message: `Start-Zeit erfasst: ${currentTime}`,
start_time: currentTime, start_time: currentTime,
date: currentDate 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 = ?', db.run('UPDATE timesheet_entries SET start_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[currentTime, entry.id], (err) => { [currentTime, entry.id], (err) => {
if (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({ sendResponse(req, res, true, {
success: true, title: successTitle,
message: `Start-Zeit erfasst: ${currentTime}`, message: `Start-Zeit erfasst: ${currentTime}`,
start_time: currentTime, start_time: currentTime,
date: currentDate date: currentDate
@@ -59,8 +90,8 @@ checkinApp.get('/api/checkin/:userId', (req, res) => {
}); });
} else { } else {
// Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in // Start-Zeit bereits vorhanden → Ignoriere weiteren Check-in
res.json({ sendResponse(req, res, true, {
success: true, title: successTitle,
message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`, message: `Bereits eingecheckt um ${entry.start_time}. Check-in ignoriert.`,
start_time: entry.start_time, start_time: entry.start_time,
date: currentDate date: currentDate
@@ -79,21 +110,20 @@ checkinApp.get('/api/checkout/:userId', (req, res) => {
// Prüfe ob User existiert // Prüfe ob User existiert
db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => { db.get('SELECT id FROM users WHERE id = ?', [userId], (err, user) => {
if (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 // 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', db.get('SELECT * FROM timesheet_entries WHERE user_id = ? AND date = ? ORDER BY updated_at DESC, id DESC LIMIT 1',
[userId, currentDate], (err, entry) => { [userId, currentDate], (err, entry) => {
if (err) { 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) { if (!entry || !entry.start_time) {
// Kein Eintrag oder keine Start-Zeit → Fehler return sendResponse(req, res, false, {
return res.status(400).json({ error: 'Bitte zuerst einchecken (Kommen).',
success: false, status: 400
error: 'Bitte zuerst einchecken (Kommen).'
}); });
} }
@@ -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 = ?', db.run('UPDATE timesheet_entries SET end_time = ?, total_hours = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[currentTime, totalHours, entry.id], (err) => { [currentTime, totalHours, entry.id], (err) => {
if (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({ const successTitle = 'Schönen Feierabend, du wurdest erfolgreich ausgecheckt';
success: true, const successMessage = `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`;
message: `End-Zeit erfasst: ${currentTime}. Gesamtstunden: ${totalHours.toFixed(2)} h`, sendResponse(req, res, true, {
title: successTitle,
message: successMessage,
end_time: currentTime, end_time: currentTime,
total_hours: totalHours, total_hours: totalHours,
date: currentDate date: currentDate

View File

@@ -5,13 +5,16 @@ Hallo zusammen,
Mara ist auf mich mit einer Bitte herangetreten, ob ich die Stundenerfassung digitalisieren kann. 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. 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 Ich glaube, 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. 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. 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. 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 Viele Grüße
Carsten Graf Carsten Graf

393
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pdfkit": "^0.13.0", "pdfkit": "^0.13.0",
"ping": "^0.4.4", "ping": "^0.4.4",
"qrcode": "^1.5.4",
"sqlite3": "^5.1.6" "sqlite3": "^5.1.6"
}, },
"devDependencies": { "devDependencies": {
@@ -841,6 +842,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -882,6 +892,46 @@
"node": ">=6" "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": { "node_modules/clone": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -1104,6 +1154,15 @@
"ms": "2.0.0" "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": { "node_modules/decompress-response": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" "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": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1528,6 +1593,19 @@
"node": ">= 0.8" "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": { "node_modules/fontkit": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", "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": "^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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -2474,6 +2561,18 @@
"node": ">= 0.4" "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": { "node_modules/lodash": {
"version": "4.17.23", "version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
@@ -2975,6 +3074,33 @@
"wrappy": "1" "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": { "node_modules/p-map": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
@@ -2990,6 +3116,15 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "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": ">= 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": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" "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": { "node_modules/possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "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" "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": { "node_modules/qs": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@@ -3314,6 +3484,21 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/restructure": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
@@ -3440,8 +3625,7 @@
"node_modules/set-blocking": { "node_modules/set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"optional": true
}, },
"node_modules/set-function-length": { "node_modules/set-function-length": {
"version": "1.2.2", "version": "1.2.2",
@@ -4132,6 +4316,12 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/which-typed-array": {
"version": "1.1.20", "version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "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", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "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": { "node_modules/yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
@@ -4913,6 +5144,11 @@
"get-intrinsic": "^1.3.0" "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": { "chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -4940,6 +5176,36 @@
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"optional": true "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": { "clone": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
@@ -5099,6 +5365,11 @@
"ms": "2.0.0" "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": { "decompress-response": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" "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": { "dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -5427,6 +5703,15 @@
"unpipe": "~1.0.0" "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": { "fontkit": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz", "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.9.0.tgz",
@@ -5529,6 +5814,11 @@
"wide-align": "^1.1.5" "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": { "get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "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": { "lodash": {
"version": "4.17.23", "version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
@@ -6451,6 +6749,22 @@
"wrappy": "1" "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": { "p-map": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
@@ -6460,6 +6774,11 @@
"aggregate-error": "^3.0.0" "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": { "package-json-from-dist": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" "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": { "path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" "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": { "possible-typed-array-names": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "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" "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": { "qs": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@@ -6708,6 +7047,16 @@
"set-function-name": "^2.0.2" "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": { "restructure": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz", "resolved": "https://registry.npmjs.org/restructure/-/restructure-2.0.1.tgz",
@@ -6794,8 +7143,7 @@
"set-blocking": { "set-blocking": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="
"optional": true
}, },
"set-function-length": { "set-function-length": {
"version": "1.2.2", "version": "1.2.2",
@@ -7301,6 +7649,11 @@
"is-weakset": "^2.0.3" "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": { "which-typed-array": {
"version": "1.1.20", "version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "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", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" "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": { "yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "zip-stream": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "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" "reset-db": "node reset-db.js"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "archiver": "^7.0.1",
"express-session": "^1.17.3",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"sqlite3": "^5.1.6",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"pdfkit": "^0.13.0", "express": "^4.18.2",
"express-session": "^1.17.3",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"pdfkit": "^0.13.0",
"ping": "^0.4.4", "ping": "^0.4.4",
"archiver": "^7.0.1" "qrcode": "^1.5.4",
"sqlite3": "^5.1.6"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.0.1" "nodemon": "^3.0.1"

View File

@@ -372,8 +372,13 @@ function renderWeek() {
// Stunden zur Summe hinzufügen // Stunden zur Summe hinzufügen
// Bei ganztägigem Urlaub oder Krank sollten es bereits 8 Stunden sein (vom Backend gesetzt) // 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 // 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 // Bearbeitung ist immer möglich, auch nach Abschicken
// Bei ganztägigem Urlaub oder Krank werden Zeitfelder deaktiviert; Feiertag: Anzeige, Zeitfelder optional (Überstunden) // 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" data-date="${dateStr}" data-field="break_minutes"
${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)"> ${timeFieldsDisabled} ${disabled} oninput="saveEntry(this)" onchange="saveEntry(this)">
</td> </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>
<tr class="activities-row"> <tr class="activities-row">
<td colspan="6" class="activities-cell"> <td colspan="6" class="activities-cell">
@@ -576,7 +581,7 @@ function renderWeek() {
vacationHours += 4; // Halber Tag = 4 Stunden vacationHours += 4; // Halber Tag = 4 Stunden
} }
}); });
// Überstunden berechnen // Überstunden berechnen
let overtimeTaken = 0; let overtimeTaken = 0;
Object.values(currentEntries).forEach(e => { Object.values(currentEntries).forEach(e => {
@@ -664,6 +669,23 @@ function updateOvertimeDisplay() {
} }
} else if (sickStatus) { } else if (sickStatus) {
totalHours += 8; // Krank = 8 Stunden 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 { } else {
// Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden // Wenn 8 Überstunden (ganzer Tag) eingetragen sind, zählt der Tag als 0 Stunden
if (isFullDayOvertime) { if (isFullDayOvertime) {
@@ -710,10 +732,7 @@ function updateOvertimeDisplay() {
} }
// Überstunden berechnen (wie im Backend: mit adjustedSollStunden) // Überstunden berechnen (wie im Backend: mit adjustedSollStunden)
// Wenn 8 Überstunden genommen wurden, zählen diese Tage als 0 Stunden // totalHours enthält bereits Feiertagsstunden (8h oder gearbeitete Stunden) aus dem Feiertag-Zweig oben
// 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
const totalHoursWithVacation = totalHours + vacationHours; const totalHoursWithVacation = totalHours + vacationHours;
const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours); const adjustedSollStunden = sollStunden - (fullDayOvertimeDays * fullDayHours);
// overtimeHours = Überstunden diese Woche (wie im Backend berechnet) // overtimeHours = Überstunden diese Woche (wie im Backend berechnet)

View File

@@ -2,9 +2,18 @@
const { hasRole } = require('../helpers/utils'); const { hasRole } = require('../helpers/utils');
const { requireAuth } = require('../middleware/auth'); const { requireAuth } = require('../middleware/auth');
const { generateCheckinCheckoutQRPDF } = require('../services/pdf-service');
// Routes registrieren // Routes registrieren
function registerDashboardRoutes(app) { 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 // Dashboard für Mitarbeiter
app.get('/dashboard', requireAuth, (req, res) => { app.get('/dashboard', requireAuth, (req, res) => {
// Prüfe ob User Mitarbeiter-Rolle hat // Prüfe ob User Mitarbeiter-Rolle hat

View File

@@ -6,7 +6,7 @@ const { initDatabase } = require('./database');
const { getDefaultRole } = require('./helpers/utils'); const { getDefaultRole } = require('./helpers/utils');
const app = express(); const app = express();
const PORT = 3335; const PORT = 3333;
// Middleware // Middleware
app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.urlencoded({ extended: true }));

View File

@@ -1,6 +1,7 @@
// PDF-Generierung Service // PDF-Generierung Service
const PDFDocument = require('pdfkit'); const PDFDocument = require('pdfkit');
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 { 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> <button onclick="copyToClipboard('checkoutUrl')" class="btn btn-sm btn-secondary" style="padding: 8px 12px;">Kopieren</button>
</div> </div>
</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> </div>
<!-- IP-Erfassung --> <!-- IP-Erfassung -->