From 86cab10c707a513d011054d72532648d124fc178 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:22:42 +0200 Subject: [PATCH 01/42] Fix: sort/filter/expand nach autodate-Feldern wiederhergestellt --- app/src/routes/admin/dashboard/+page.svelte | 4 +++- app/src/routes/admin/logs/+page.svelte | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/routes/admin/dashboard/+page.svelte b/app/src/routes/admin/dashboard/+page.svelte index bd32c37..a7f3312 100644 --- a/app/src/routes/admin/dashboard/+page.svelte +++ b/app/src/routes/admin/dashboard/+page.svelte @@ -24,7 +24,9 @@ const dateStr = today.toISOString().slice(0, 19).replace('T', ' '); const result = await pb.collection('check_logs').getList(1, 50, { - sort: '-id' + filter: `tenant = '${tenantId}' && created >= '${dateStr}'`, + expand: 'station', + sort: '-created' }); logs = result.items; diff --git a/app/src/routes/admin/logs/+page.svelte b/app/src/routes/admin/logs/+page.svelte index c4c1cbb..b975cc5 100644 --- a/app/src/routes/admin/logs/+page.svelte +++ b/app/src/routes/admin/logs/+page.svelte @@ -25,7 +25,7 @@ const result = await pb.collection('check_logs').getList(p, PER_PAGE, { filter: `tenant = '${tenantId}'`, expand: 'station', - sort: '-id' + sort: '-created' }); logs = result.items; page = p; From 984f3c91620a22210f47e7baa65ead2066ab0db6 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:26:13 +0200 Subject: [PATCH 02/42] =?UTF-8?q?Fix:=20Invalid=20Date=20graceful=20handli?= =?UTF-8?q?ng=20f=C3=BCr=20Records=20ohne=20created-Feld?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/routes/admin/dashboard/+page.svelte | 5 ++++- app/src/routes/admin/logs/+page.svelte | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/routes/admin/dashboard/+page.svelte b/app/src/routes/admin/dashboard/+page.svelte index a7f3312..d6906f2 100644 --- a/app/src/routes/admin/dashboard/+page.svelte +++ b/app/src/routes/admin/dashboard/+page.svelte @@ -42,7 +42,10 @@ }); function formatTime(iso: string) { - return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); + if (!iso) return '—'; + const d = new Date(iso); + if (isNaN(d.getTime())) return '—'; + return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } diff --git a/app/src/routes/admin/logs/+page.svelte b/app/src/routes/admin/logs/+page.svelte index b975cc5..74d8224 100644 --- a/app/src/routes/admin/logs/+page.svelte +++ b/app/src/routes/admin/logs/+page.svelte @@ -38,7 +38,10 @@ } function formatDate(iso: string) { - return new Date(iso).toLocaleString('de-DE', { + if (!iso) return '—'; + const d = new Date(iso); + if (isNaN(d.getTime())) return '—'; + return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }); From c5a640911c8bd5424ddbb92c9626585fb317400f Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:36:59 +0200 Subject: [PATCH 03/42] Feature: QR-Druck, Monatsprotokoll PDF, Pricing-Sektion --- app/package-lock.json | 336 +++++++++++++++++++- app/package.json | 4 +- app/src/routes/+page.svelte | 105 ++++++ app/src/routes/admin/+layout.svelte | 7 +- app/src/routes/admin/protokoll/+page.svelte | 255 +++++++++++++++ app/src/routes/admin/stations/+page.svelte | 171 ++++++---- 6 files changed, 808 insertions(+), 70 deletions(-) create mode 100644 app/src/routes/admin/protokoll/+page.svelte diff --git a/app/package-lock.json b/app/package-lock.json index 340f1ac..41cbc0a 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,13 +8,15 @@ "name": "app", "version": "0.0.1", "dependencies": { - "pocketbase": "^0.26.9" + "pocketbase": "^0.26.9", + "qrcode": "^1.5.4" }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.1", "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@types/qrcode": "^1.5.6", "svelte": "^5.55.2", "svelte-check": "^4.4.6", "typescript": "^6.0.2", @@ -2660,6 +2662,26 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -2704,6 +2726,30 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", @@ -2975,6 +3021,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/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -3012,6 +3067,17 @@ "url": "https://paulmillr.com/funding/" } }, + "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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -3022,6 +3088,24 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -3174,6 +3258,15 @@ } } }, + "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/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -3237,6 +3330,12 @@ "dev": true, "license": "MIT" }, + "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", @@ -3275,6 +3374,12 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -3565,6 +3670,19 @@ "node": ">=10" } }, + "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/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3690,6 +3808,15 @@ "node": ">=6.9.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", @@ -4073,6 +4200,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -4730,6 +4866,18 @@ "dev": true, "license": "MIT" }, + "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.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -4926,6 +5074,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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-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", @@ -4933,6 +5117,15 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "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-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -4997,6 +5190,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "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/pocketbase": { "version": "0.26.9", "resolved": "https://registry.npmjs.org/pocketbase/-/pocketbase-0.26.9.tgz", @@ -5065,6 +5267,23 @@ "node": ">=6" } }, + "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/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5181,6 +5400,15 @@ "regjsparser": "bin/parser" } }, + "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-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -5191,6 +5419,12 @@ "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/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -5387,6 +5621,12 @@ "node": ">=20.0.0" } }, + "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==", + "license": "ISC" + }, "node_modules/set-cookie-parser": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", @@ -5639,6 +5879,20 @@ "node": ">= 0.4" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -5741,6 +5995,18 @@ "node": ">=4" } }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -6033,6 +6299,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -6373,6 +6646,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", @@ -6615,6 +6894,26 @@ "workbox-core": "7.4.1" } }, + "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/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": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -6622,6 +6921,41 @@ "dev": true, "license": "ISC" }, + "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/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", diff --git a/app/package.json b/app/package.json index aa45009..10855aa 100644 --- a/app/package.json +++ b/app/package.json @@ -16,6 +16,7 @@ "@sveltejs/adapter-node": "^5.5.4", "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@types/qrcode": "^1.5.6", "svelte": "^5.55.2", "svelte-check": "^4.4.6", "typescript": "^6.0.2", @@ -23,6 +24,7 @@ "vite-plugin-pwa": "^1.3.0" }, "dependencies": { - "pocketbase": "^0.26.9" + "pocketbase": "^0.26.9", + "qrcode": "^1.5.4" } } diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index f34d61b..4163129 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -84,6 +84,53 @@ + +
+
+

Einfache, transparente Preise

+

Keine Einrichtungsgebühr. Monatlich kündbar.

+
+
+
Basic
+
249 €/Monat
+
    +
  • ✓ Bis zu 500 Assets / QR-Tags
  • +
  • ✓ Eigenes Logo & CI-Farben
  • +
  • ✓ Standard-Protokoll-Templates
  • +
  • ✓ PDF-Export
  • +
  • ✓ Subdomain (firma.checkflo.de)
  • +
+ Demo anfragen +
+ +
+
Enterprise
+
ab 1.500 €/Monat
+
    +
  • ✓ Eigene Server-Infrastruktur
  • +
  • ✓ SAP / MS Dynamics Anbindung
  • +
  • ✓ Dedizierter Support & SLA
  • +
  • ✓ On-Premise möglich
  • +
  • ✓ Individuelle Entwicklung
  • +
+ Angebot anfragen +
+
+
+
+
@@ -254,6 +301,64 @@ .feature-card h3 { font-size: 1.1rem; margin-bottom: 0.5rem; } .feature-card p { color: #555; font-size: 0.95rem; } + /* PRICING */ + .pricing { padding: 5rem 2rem; background: #F5F7FA; } + .pricing-sub { text-align: center; color: #666; margin-top: -2rem; margin-bottom: 3rem; } + + .pricing-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 1.5rem; + align-items: stretch; + } + + .plan { + background: #fff; + border-radius: 16px; + padding: 2rem; + border: 1.5px solid #eee; + display: flex; + flex-direction: column; + position: relative; + } + .plan-featured { + border-color: #F97316; + box-shadow: 0 4px 24px rgba(249,115,22,0.12); + } + .plan-badge { + position: absolute; + top: -12px; + left: 50%; + transform: translateX(-50%); + background: #F97316; + color: #fff; + padding: 0.2rem 0.9rem; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 700; + } + .plan-name { font-size: 0.85rem; font-weight: 700; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 0.75rem; } + .plan-price { font-size: 2rem; font-weight: 800; color: #0B1023; margin-bottom: 1.5rem; } + .plan-price span { font-size: 1rem; font-weight: 400; color: #888; } + + .plan-features { list-style: none; margin: 0 0 2rem; padding: 0; display: flex; flex-direction: column; gap: 0.6rem; flex: 1; } + .plan-features li { font-size: 0.9rem; color: #444; } + + .btn-plan { + display: block; + text-align: center; + padding: 0.75rem; + border-radius: 8px; + font-weight: 700; + font-size: 0.95rem; + border: 1.5px solid #0B1023; + color: #0B1023; + transition: all 0.15s; + } + .btn-plan:hover { background: #0B1023; color: #fff; } + .btn-plan-featured { background: #F97316; border-color: #F97316; color: #fff; } + .btn-plan-featured:hover { background: #ea6c10; border-color: #ea6c10; } + /* CTA */ .cta { background: #0B1023; diff --git a/app/src/routes/admin/+layout.svelte b/app/src/routes/admin/+layout.svelte index ebf934a..a7013b9 100644 --- a/app/src/routes/admin/+layout.svelte +++ b/app/src/routes/admin/+layout.svelte @@ -14,9 +14,10 @@ } const navItems = [ - { href: '/admin/dashboard', label: 'Dashboard', icon: '◈' }, - { href: '/admin/logs', label: 'Protokoll', icon: '≡' }, - { href: '/admin/stations', label: 'Stationen', icon: '⊞' } + { href: '/admin/dashboard', label: 'Dashboard', icon: '◈' }, + { href: '/admin/logs', label: 'Einträge', icon: '≡' }, + { href: '/admin/protokoll', label: 'Protokoll', icon: '📄' }, + { href: '/admin/stations', label: 'Stationen', icon: '⊞' } ]; diff --git a/app/src/routes/admin/protokoll/+page.svelte b/app/src/routes/admin/protokoll/+page.svelte new file mode 100644 index 0000000..41dce43 --- /dev/null +++ b/app/src/routes/admin/protokoll/+page.svelte @@ -0,0 +1,255 @@ + + +Protokoll {months[selectedMonth-1]} {selectedYear} — checkflo + + +
+
+

Monatsprotokoll

+
+
+ + + +
+
+ + +
+
+
+
HACCP-Temperatur & Hygieneprotokoll
+
{months[selectedMonth-1]} {selectedYear}
+
+
{tenantName}
+
+ + {#if loading} +

Lädt…

+ {:else if logs.length === 0} +

Keine Einträge im gewählten Zeitraum.

+

Keine Einträge im {months[selectedMonth-1]} {selectedYear}.

+ {:else} + {#each Object.entries(byStation()) as [stationName, stationLogs]} +
+
{stationName}
+ + + + + + + + + + + + {#each stationLogs as log} + + + + + + + + {/each} + +
Datum / ZeitTemp. (°C)StatusNotizGeprüft von
{formatDate(log.created)}{log.temperature ? log.temperature + ' °C' : '—'} + + {STATUS_LABEL[log.status] ?? log.status} + + {log.notes || '—'}{log.checked_by}
+
+ {/each} + +
+ Gesamt: {logs.length} Einträge · + {logs.filter(l=>l.status==='ok').length} OK · + {logs.filter(l=>l.status==='abweichung').length} Abweichungen · + {logs.filter(l=>l.status==='kritisch').length} Kritisch +
+ {/if} + + +
+ + diff --git a/app/src/routes/admin/stations/+page.svelte b/app/src/routes/admin/stations/+page.svelte index c46836d..5df72ad 100644 --- a/app/src/routes/admin/stations/+page.svelte +++ b/app/src/routes/admin/stations/+page.svelte @@ -1,6 +1,7 @@ Stationen — checkflo -
-

Stationen

-

Jeden QR-Code ausdrucken und an der Station befestigen. Der Link öffnet die Checkliste direkt im Browser.

+
+
+
+

Stationen

+

QR-Codes ausdrucken und an den Stationen befestigen.

+
+ {#if stations.length > 0} + + {/if} +
{#if loading}

Lädt…

@@ -53,29 +71,19 @@
{#each stations as s}
-
-
+
+
{s.name}
{TYPE_LABEL[s.type] ?? s.type} {#if s.target_temp_min || s.target_temp_max} · {s.target_temp_min}° bis {s.target_temp_max}°C {/if}
+ {qrUrl(s.qr_id)}
-
- -
- {qrUrl(s.qr_id)} - - - QR öffnen - + {#if qrCodes[s.qr_id]} + QR {s.name} + {/if}
{/each} @@ -83,65 +91,98 @@ {/if}
+ +{#if !loading} + +{/if} + From c983750dcdfb217728534b069aaa2ad18cae6a76 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:39:47 +0200 Subject: [PATCH 04/42] Feature: Phosphor-Icons in Admin-Nav, Druck-Button mit Printer-Icon --- app/src/lib/assets/icons/dashboard.svg | 1 + app/src/lib/assets/icons/logout.svg | 1 + app/src/lib/assets/icons/logs.svg | 1 + app/src/lib/assets/icons/printer.svg | 1 + app/src/lib/assets/icons/protokoll.svg | 1 + app/src/lib/assets/icons/stations.svg | 1 + app/src/routes/admin/+layout.svelte | 48 ++++++++++++++++++++------ 7 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 app/src/lib/assets/icons/dashboard.svg create mode 100644 app/src/lib/assets/icons/logout.svg create mode 100644 app/src/lib/assets/icons/logs.svg create mode 100644 app/src/lib/assets/icons/printer.svg create mode 100644 app/src/lib/assets/icons/protokoll.svg create mode 100644 app/src/lib/assets/icons/stations.svg diff --git a/app/src/lib/assets/icons/dashboard.svg b/app/src/lib/assets/icons/dashboard.svg new file mode 100644 index 0000000..c9cf470 --- /dev/null +++ b/app/src/lib/assets/icons/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/lib/assets/icons/logout.svg b/app/src/lib/assets/icons/logout.svg new file mode 100644 index 0000000..7bf5110 --- /dev/null +++ b/app/src/lib/assets/icons/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/lib/assets/icons/logs.svg b/app/src/lib/assets/icons/logs.svg new file mode 100644 index 0000000..9abf1d0 --- /dev/null +++ b/app/src/lib/assets/icons/logs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/lib/assets/icons/printer.svg b/app/src/lib/assets/icons/printer.svg new file mode 100644 index 0000000..7befdfd --- /dev/null +++ b/app/src/lib/assets/icons/printer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/lib/assets/icons/protokoll.svg b/app/src/lib/assets/icons/protokoll.svg new file mode 100644 index 0000000..cbaca75 --- /dev/null +++ b/app/src/lib/assets/icons/protokoll.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/lib/assets/icons/stations.svg b/app/src/lib/assets/icons/stations.svg new file mode 100644 index 0000000..249f544 --- /dev/null +++ b/app/src/lib/assets/icons/stations.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/routes/admin/+layout.svelte b/app/src/routes/admin/+layout.svelte index a7013b9..6430bed 100644 --- a/app/src/routes/admin/+layout.svelte +++ b/app/src/routes/admin/+layout.svelte @@ -3,6 +3,11 @@ import { goto } from '$app/navigation'; import { pb } from '$lib/pb'; import logo from '$lib/assets/checkflo-logo.png'; + import iconDashboard from '$lib/assets/icons/dashboard.svg'; + import iconLogs from '$lib/assets/icons/logs.svg'; + import iconProtokoll from '$lib/assets/icons/protokoll.svg'; + import iconStations from '$lib/assets/icons/stations.svg'; + import iconLogout from '$lib/assets/icons/logout.svg'; let { children } = $props(); @@ -14,10 +19,10 @@ } const navItems = [ - { href: '/admin/dashboard', label: 'Dashboard', icon: '◈' }, - { href: '/admin/logs', label: 'Einträge', icon: '≡' }, - { href: '/admin/protokoll', label: 'Protokoll', icon: '📄' }, - { href: '/admin/stations', label: 'Stationen', icon: '⊞' } + { href: '/admin/dashboard', label: 'Dashboard', icon: iconDashboard }, + { href: '/admin/logs', label: 'Einträge', icon: iconLogs }, + { href: '/admin/protokoll', label: 'Protokoll', icon: iconProtokoll }, + { href: '/admin/stations', label: 'Stationen', icon: iconStations } ]; @@ -36,12 +41,15 @@ class="nav-item" class:active={$page.url.pathname.startsWith(item.href)} > - {item.icon} + {item.label} {/each} - +
@@ -85,20 +93,39 @@ } .nav-item:hover { background: rgba(255,255,255,0.06); color: #fff; } .nav-item.active { background: rgba(249,115,22,0.15); color: #F97316; } - .nav-icon { font-size: 1.1rem; } + + .nav-icon { + width: 18px; + height: 18px; + flex-shrink: 0; + filter: brightness(0) saturate(100%) invert(55%) sepia(14%) saturate(400%) hue-rotate(182deg); + } + .nav-item.active .nav-icon { + filter: brightness(0) saturate(100%) invert(56%) sepia(85%) saturate(600%) hue-rotate(359deg); + } + .nav-item:hover .nav-icon { + filter: brightness(0) invert(1); + } .logout { + display: flex; + align-items: center; + gap: 0.75rem; background: none; border: 1px solid #2a3347; color: #8892a4; - padding: 0.6rem; + padding: 0.6rem 0.75rem; border-radius: 8px; cursor: pointer; font-size: 0.85rem; transition: all 0.15s; margin-top: 1rem; + width: 100%; } .logout:hover { border-color: #F97316; color: #F97316; } + .logout:hover .nav-icon { + filter: brightness(0) saturate(100%) invert(56%) sepia(85%) saturate(600%) hue-rotate(359deg); + } main { flex: 1; @@ -116,8 +143,9 @@ } .logo-link { margin-bottom: 0; margin-right: auto; } nav { flex-direction: row; flex: 0; gap: 0.5rem; } - .nav-item span:last-child { display: none; } - .logout { margin-top: 0; padding: 0.5rem 0.75rem; } + .nav-item span { display: none; } + .logout { margin-top: 0; padding: 0.5rem 0.75rem; width: auto; } + .logout :global(span) { display: none; } main { padding: 1rem; } } From 65c36062fad5da7d793bedc8a0e48110b543f89c Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:43:12 +0200 Subject: [PATCH 05/42] Fix: Sidebar beim Drucken ausblenden --- app/src/routes/admin/+layout.svelte | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/routes/admin/+layout.svelte b/app/src/routes/admin/+layout.svelte index 6430bed..b070c44 100644 --- a/app/src/routes/admin/+layout.svelte +++ b/app/src/routes/admin/+layout.svelte @@ -133,6 +133,12 @@ padding: 2rem; } + @media print { + aside { display: none !important; } + :global(.shell) { display: block !important; } + main { padding: 0 !important; overflow: visible !important; } + } + @media (max-width: 640px) { .shell { flex-direction: column; } aside { From 683c645f85bc2e74f77cd8a8d143f82f388799a6 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:45:12 +0200 Subject: [PATCH 06/42] Feature: QR-Codes mit checkflo-Logo in der Mitte --- app/src/routes/admin/stations/+page.svelte | 41 +++++++++++++++++++--- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/app/src/routes/admin/stations/+page.svelte b/app/src/routes/admin/stations/+page.svelte index 5df72ad..1802b0b 100644 --- a/app/src/routes/admin/stations/+page.svelte +++ b/app/src/routes/admin/stations/+page.svelte @@ -29,12 +29,10 @@ }); stations = result; - // QR-Codes generieren + // QR-Codes mit Logo generieren const codes: Record = {}; for (const s of result) { - codes[s.qr_id] = await QRCode.toDataURL(qrUrl(s.qr_id), { - width: 200, margin: 1, color: { dark: '#0B1023', light: '#ffffff' } - }); + codes[s.qr_id] = await generateQRWithLogo(qrUrl(s.qr_id)); } qrCodes = codes; } catch { @@ -47,6 +45,41 @@ return `https://checkflo.de/s/${qrId}`; } + async function generateQRWithLogo(url: string): Promise { + const size = 400; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + await QRCode.toCanvas(canvas, url, { + width: size, + margin: 1, + errorCorrectionLevel: 'H', + color: { dark: '#0B1023', light: '#ffffff' } + }); + + const ctx = canvas.getContext('2d')!; + const logoSize = Math.round(size * 0.22); + const x = (size - logoSize) / 2; + const y = (size - logoSize) / 2; + const pad = 6; + const r = 10; + + // Weißer abgerundeter Hintergrund + ctx.fillStyle = '#ffffff'; + ctx.beginPath(); + ctx.roundRect(x - pad, y - pad, logoSize + pad * 2, logoSize + pad * 2, r); + ctx.fill(); + + // Logo laden und zeichnen + const img = new Image(); + img.src = '/favicon.svg'; + await new Promise(resolve => { img.onload = () => resolve(); }); + ctx.drawImage(img, x, y, logoSize, logoSize); + + return canvas.toDataURL('image/png'); + } + function printAll() { window.print(); } From 90973ca5afdbe3be68a1245049b3006ef3e0693b Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 14:46:56 +0200 Subject: [PATCH 07/42] Fix: Phosphor Printer-Icon in Druck-Buttons --- app/src/routes/admin/protokoll/+page.svelte | 6 +++++- app/src/routes/admin/stations/+page.svelte | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/src/routes/admin/protokoll/+page.svelte b/app/src/routes/admin/protokoll/+page.svelte index 41dce43..6a4cf0b 100644 --- a/app/src/routes/admin/protokoll/+page.svelte +++ b/app/src/routes/admin/protokoll/+page.svelte @@ -1,6 +1,7 @@ @@ -31,15 +31,16 @@
-

HACCP-Dokumentation für die Gastronomie

-

Prüfprotokolle.
Ohne Papier.
Ohne App-Store.

+

White-Label-Software für Prüf- und Wartungsbetriebe

+

Ihre eigene
HACCP-App —
unter Ihrem Namen.

- Kühlschranktemperaturen, Hygienekontrollen und Wartungsnachweise — - digital erfasst, automatisch protokolliert, jederzeit abrufbar. + Bieten Sie Ihren Kunden digitale Prüfprotokolle als gebrandete App an. + Kein App-Store. Keine Installation. Läuft offline. + Und Ihre Kunden sehen nur Ihr Logo.

@@ -47,22 +48,22 @@
-

In 3 Schritten zum digitalen Protokoll

+

In 3 Schritten zur eigenen HACCP-App

1
-

QR-Code aufkleben

-

Robuste NFC/QR-Tags direkt am Kühlschrank, Warmhaltebehälter oder an der Hygiene-Station.

+

Lizenz buchen & branden

+

Sie erhalten Ihre App unter Ihrem Logo, Ihren Farben und Ihrer Domain — fertig in 48 Stunden.

2
-

Scannen & ausfüllen

-

Techniker scannt den Code, trägt Temperatur ein, macht bei Abweichung ein Foto — fertig.

+

Kunden einrichten

+

Stationen anlegen, QR-Codes ausdrucken, beim Kunden aufkleben. Ihr Kunde braucht nichts installieren.

3
-

Protokoll abrufen

-

Vollständiges Monatsprotokoll als PDF — für Lebensmittelkontrolle, Franchise-Geber oder eigene Dokumentation.

+

Protokolle abrufen

+

Ihre Kunden erfassen täglich — Sie sehen alles in Echtzeit und exportieren Protokolle per Knopfdruck als PDF.

@@ -71,7 +72,7 @@
-

Warum checkflo?

+

Was checkflo für Sie leistet

{#each features as f}
From d1b2d41fd9ed5803aeee3a3e6b4c657e0fa1a65d Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 15:37:14 +0200 Subject: [PATCH 10/42] Fix: seed-testdata ohne bash4 assoc arrays (macOS kompatibel) --- scripts/seed-testdata.sh | 140 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100755 scripts/seed-testdata.sh diff --git a/scripts/seed-testdata.sh b/scripts/seed-testdata.sh new file mode 100755 index 0000000..8c0a232 --- /dev/null +++ b/scripts/seed-testdata.sh @@ -0,0 +1,140 @@ +#!/bin/bash +# Generiert realistische HACCP-Testdaten für die letzten 30 Tage + +set -euo pipefail + +PB_URL="${PB_URL:-https://api.checkflo.de}" + +if [ -z "${PB_EMAIL:-}" ] || [ -z "${PB_PASSWORD:-}" ]; then + echo "Aufruf: PB_EMAIL=... PB_PASSWORD=... ./seed-testdata.sh" + exit 1 +fi + +TOKEN=$(curl -sf -X POST "$PB_URL/api/collections/_superusers/auth-with-password" \ + -H "Content-Type: application/json" \ + -d "{\"identity\":\"$PB_EMAIL\",\"password\":\"$PB_PASSWORD\"}" | jq -r '.token') + +echo "✓ Eingeloggt" + +TENANT_ID="mengbzc3ajxpccz" + +# Stationen als JSON laden +STATIONS_JSON=$(curl -sf "$PB_URL/api/collections/stations/records?filter=tenant%3D'$TENANT_ID'&perPage=50" \ + -H "Authorization: $TOKEN") + +STATION_COUNT=$(echo "$STATIONS_JSON" | jq '.items | length') +echo "→ $STATION_COUNT Stationen gefunden" + +NAMES=("Maria S." "Klaus B." "Sandra M." "Tobias R." "Anna K." "Michael F." "Lisa W.") +NOTES_ABW=("Tür stand leicht offen" "Dichtung prüfen" "Kompressor läuft laut" "Temperatur leicht erhöht" "Nach Lieferung noch warm") +NOTES_KRIT=("Tür war 2h offen" "Kompressor ausgefallen" "Sofortmaßnahme eingeleitet" "Produkte aussortiert" "Techniker verständigt") + +create_log() { + local station_id=$1 + local date_str=$2 + local status=$3 + local temp=$4 + local notes=$5 + local person=$6 + + curl -sf -X POST "$PB_URL/api/collections/check_logs/records" \ + -H "Authorization: $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"station\": \"$station_id\", + \"tenant\": \"$TENANT_ID\", + \"temperature\": $temp, + \"status\": \"$status\", + \"notes\": \"$notes\", + \"checked_by\": \"$person\", + \"created\": \"$date_str\", + \"updated\": \"$date_str\" + }" > /dev/null +} + +rand_between() { + local min=$1 + local max=$2 + echo $(( min + RANDOM % (max - min + 1) )) +} + +echo "→ Erstelle Einträge für die letzten 30 Tage..." +COUNT=0 + +for DAY in $(seq 29 -1 0); do + # Datum berechnen (macOS + Linux kompatibel) + if date -v -${DAY}d +"%Y-%m-%d" > /dev/null 2>&1; then + DATE=$(date -v -${DAY}d +"%Y-%m-%d") + DOW=$(date -v -${DAY}d +"%u") + else + DATE=$(date -d "-${DAY} days" +"%Y-%m-%d") + DOW=$(date -d "-${DAY} days" +"%u") + fi + + for i in $(seq 0 $((STATION_COUNT - 1))); do + SID=$(echo "$STATIONS_JSON" | jq -r ".items[$i].id") + TYPE=$(echo "$STATIONS_JSON" | jq -r ".items[$i].type") + TMIN=$(echo "$STATIONS_JSON" | jq -r ".items[$i].target_temp_min // 0") + TMAX=$(echo "$STATIONS_JSON" | jq -r ".items[$i].target_temp_max // 0") + + PERSON=${NAMES[$((RANDOM % ${#NAMES[@]}))]} + + if [ "$TYPE" = "kuehlschrank" ] || [ "$TYPE" = "tiefkuehl" ]; then + # Morgencheck + HOUR=$(rand_between 7 8) + MIN_T=$(rand_between 0 5) + SEC_T=$(rand_between 0 59) + TIME="${DATE} 0${HOUR}:$(printf "%02d" $MIN_T):$(printf "%02d" $SEC_T)" + + RAND=$((RANDOM % 100)) + if [ $RAND -lt 90 ]; then + TEMP=$(rand_between $TMIN $TMAX) + create_log "$SID" "$TIME" "ok" "$TEMP" "" "$PERSON" + elif [ $RAND -lt 98 ]; then + TEMP=$(( TMAX + 1 + RANDOM % 3 )) + NOTE=${NOTES_ABW[$((RANDOM % ${#NOTES_ABW[@]}))]} + create_log "$SID" "$TIME" "abweichung" "$TEMP" "$NOTE" "$PERSON" + else + TEMP=$(( TMAX + 4 + RANDOM % 5 )) + NOTE=${NOTES_KRIT[$((RANDOM % ${#NOTES_KRIT[@]}))]} + create_log "$SID" "$TIME" "kritisch" "$TEMP" "$NOTE" "$PERSON" + fi + COUNT=$((COUNT + 1)) + + # Abendcheck (Mo-Fr) + if [ "$DOW" -lt 6 ]; then + HOUR=$(rand_between 17 19) + TIME="${DATE} ${HOUR}:$(printf "%02d" $(rand_between 0 59)):00" + TEMP=$(rand_between $TMIN $TMAX) + create_log "$SID" "$TIME" "ok" "$TEMP" "" "$PERSON" + COUNT=$((COUNT + 1)) + fi + + elif [ "$TYPE" = "warmhalte" ]; then + TIME="${DATE} 11:$(printf "%02d" $(rand_between 0 30)):00" + RAND=$((RANDOM % 100)) + if [ $RAND -lt 85 ]; then + TEMP=$(rand_between $TMIN $TMAX) + create_log "$SID" "$TIME" "ok" "$TEMP" "" "$PERSON" + else + TEMP=$(( TMIN - 3 - RANDOM % 5 )) + NOTE=${NOTES_ABW[$((RANDOM % ${#NOTES_ABW[@]}))]} + create_log "$SID" "$TIME" "abweichung" "$TEMP" "$NOTE" "$PERSON" + fi + COUNT=$((COUNT + 1)) + + elif [ "$TYPE" = "hygiene" ]; then + if [ "$DOW" -eq 1 ]; then + TIME="${DATE} 09:$(printf "%02d" $(rand_between 0 30)):00" + create_log "$SID" "$TIME" "ok" "0" "" "$PERSON" + COUNT=$((COUNT + 1)) + fi + fi + done + + printf " Tag -%02d: %s (%s)..." "$DAY" "$DATE" "DOW $DOW" + echo " OK" +done + +echo "" +echo " ✓ $COUNT Einträge erstellt" From 678a5f9a03da823530f4741536d447f92d18a092 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 15:39:42 +0200 Subject: [PATCH 11/42] Feature: Device-Mockups auf Landing Page (CSS Dashboard + Live iPhone-Iframe) --- app/src/routes/+page.svelte | 284 ++++++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 15f5808..10501dc 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -45,6 +45,93 @@
+ +
+
+

So sieht Ihre gebrandete App aus

+

Dashboard für Sie — Checkliste für Ihre Kunden

+ +
+ + +
+
+
+ +
+
app.musterprüfbetrieb.de/admin
+
+
+
+ +
Dashboard
+
Protokoll
+
Stationen
+
+
+
Dashboard
+
Sonntag, 17. Mai
+
+
+
184
+
Checks gesamt
+
+
+
171
+
In Ordnung
+
+
+
11
+
Abweichungen
+
+
+
2
+
Kritisch
+
+
+
Heutige Einträge
+
+ OK + Kühlschrank 1 (Küche) + 5 °C + Maria S. · 07:42 +
+
+ OK + Kühlschrank 2 (Getränke) + 6 °C + Klaus B. · 08:05 +
+
+ Abweichung + Tiefkühlzelle + −15 °C + Sandra M. · 08:31 +
+
+
+
+ + +
+
+
+
+ +
+
+
+

Live-Demo — einfach ausprobieren

+
+ +
+
+
+
@@ -195,6 +282,203 @@ } .btn-nav:hover { background: #0B1023; color: #fff; } + /* MOCKUPS */ + .mockups { + padding: 5rem 2rem; + background: #0B1023; + color: #fff; + overflow: hidden; + } + .mockups .eyebrow-dark { + font-size: 0.8rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1.5px; + color: #F97316; + text-align: center; + margin-bottom: 1rem; + } + .mockups h2 { color: #fff; text-align: center; margin-bottom: 3rem; } + + .devices { + display: flex; + align-items: flex-start; + justify-content: center; + gap: 3rem; + flex-wrap: wrap; + } + + /* Browser Mockup */ + .device-browser { + flex: 1; + min-width: 320px; + max-width: 580px; + background: #1a2035; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 24px 60px rgba(0,0,0,0.5); + border: 1px solid #2a3347; + } + .browser-bar { + background: #252d42; + padding: 0.6rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + } + .browser-dots { display: flex; gap: 5px; } + .browser-dots span { + width: 10px; height: 10px; + border-radius: 50%; + background: #3a4055; + } + .browser-url { + flex: 1; + background: #1a2035; + border-radius: 4px; + padding: 0.2rem 0.6rem; + font-size: 0.7rem; + color: #6b7a9a; + font-family: monospace; + } + .browser-screen { display: flex; height: 300px; overflow: hidden; } + + /* CSS Dashboard */ + .dash-sidebar { + width: 120px; + background: #0B1023; + padding: 1rem 0.75rem; + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 0.25rem; + } + .dash-logo { + font-size: 0.8rem; + font-weight: 800; + color: #fff; + margin-bottom: 1rem; + padding: 0 0.25rem; + } + .dash-logo span { color: #F97316; } + .dash-nav-item { + padding: 0.4rem 0.5rem; + border-radius: 5px; + font-size: 0.65rem; + color: #6b7a9a; + } + .dash-nav-item.active { + background: rgba(249,115,22,0.15); + color: #F97316; + } + + .dash-main { + flex: 1; + padding: 1rem; + background: #F5F7FA; + overflow: hidden; + } + .dash-title { font-size: 0.9rem; font-weight: 800; color: #0B1023; } + .dash-date { font-size: 0.65rem; color: #888; margin-bottom: 0.75rem; } + + .dash-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 0.4rem; + margin-bottom: 0.75rem; + } + .dash-stat { + background: #fff; + border-radius: 6px; + padding: 0.4rem; + border-left: 2.5px solid #ddd; + } + .dash-stat.ok { border-color: #16a34a; } + .dash-stat.warn { border-color: #ea580c; } + .dash-stat.crit { border-color: #dc2626; } + .dash-num { font-size: 1rem; font-weight: 800; color: #0B1023; line-height: 1; } + .dash-label { font-size: 0.5rem; color: #888; margin-top: 0.15rem; } + + .dash-entries-title { font-size: 0.7rem; font-weight: 700; color: #0B1023; margin-bottom: 0.4rem; } + .dash-entry { + background: #fff; + border-radius: 6px; + padding: 0.4rem 0.5rem; + font-size: 0.6rem; + display: flex; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.3rem; + } + .entry-badge { + padding: 0.1rem 0.35rem; + border-radius: 10px; + font-weight: 700; + font-size: 0.55rem; + white-space: nowrap; + } + .entry-badge.ok { background: #dcfce7; color: #16a34a; } + .entry-badge.warn { background: #ffedd5; color: #ea580c; } + .entry-station { flex: 1; font-weight: 600; color: #0B1023; } + .entry-temp { color: #555; } + .entry-who { color: #aaa; white-space: nowrap; } + + /* Phone Mockup */ + .device-phone { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + } + .phone-frame { + width: 220px; + background: #1a1a2e; + border-radius: 36px; + padding: 12px; + box-shadow: 0 24px 60px rgba(0,0,0,0.6), inset 0 0 0 1px #2a2a4a; + } + .phone-notch { + width: 60px; + height: 20px; + background: #1a1a2e; + border-radius: 10px; + margin: 0 auto 8px; + border: 1.5px solid #2a2a4a; + } + .phone-screen { + background: #fff; + border-radius: 24px; + overflow: hidden; + height: 380px; + } + .phone-screen iframe { + width: 100%; + height: 100%; + border: none; + transform: scale(0.85); + transform-origin: top left; + width: 117%; + height: 117%; + pointer-events: auto; + } + .phone-home { + width: 60px; + height: 4px; + background: #3a3a5a; + border-radius: 2px; + margin: 10px auto 0; + } + .phone-caption { + font-size: 0.8rem; + color: #6b7a9a; + text-align: center; + } + + @media (max-width: 768px) { + .device-browser { display: none; } + .devices { justify-content: center; } + } + /* HERO */ .hero { background: linear-gradient(135deg, #0B1023 0%, #16213e 100%); From 533cb905fa92417d404fc4254fce37aafc5ecc72 Mon Sep 17 00:00:00 2001 From: rene Date: Sun, 17 May 2026 15:49:06 +0200 Subject: [PATCH 12/42] Feature: Schmidt Hygiene GmbH als White-Label Demo (blaue CI, eigenes Logo, Live-Iframe) --- app/src/lib/assets/schmidt-hygiene-logo.svg | 9 ++++ app/src/routes/+page.svelte | 35 +++++++------- scripts/seed-schmidt.sh | 53 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 app/src/lib/assets/schmidt-hygiene-logo.svg create mode 100755 scripts/seed-schmidt.sh diff --git a/app/src/lib/assets/schmidt-hygiene-logo.svg b/app/src/lib/assets/schmidt-hygiene-logo.svg new file mode 100644 index 0000000..5c23b22 --- /dev/null +++ b/app/src/lib/assets/schmidt-hygiene-logo.svg @@ -0,0 +1,9 @@ + + + + + + + Schmidt + Hygiene GmbH + diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index 10501dc..3a4f161 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -1,5 +1,6 @@ diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index f53973e..9252a50 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -4,6 +4,7 @@ import { pb } from '$lib/pb'; import { onMount } from 'svelte'; import { browser } from '$app/environment'; + const PUBLIC_TURNSTILE_SITE_KEY = '0x4AAAAAADRLILtAf9XMk-g0'; const DEMO_TENANT = 'mengbzc3ajxpccz'; @@ -56,14 +57,22 @@ if (!formName.trim() || !formEmail.trim()) { formError = 'Bitte Name und E-Mail ausfüllen.'; return; } formSending = true; formError = ''; try { - await pb.collection('inquiries').create({ - name: formName.trim(), - company: formCompany.trim(), - email: formEmail.trim(), - phone: formPhone.trim(), - message: formMsg.trim(), - plan: modalPlan + const token = (window as any).turnstile?.getResponse() ?? ''; + const res = await fetch('/api/contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token, name: formName.trim(), email: formEmail.trim(), + company: formCompany.trim(), phone: formPhone.trim(), + message: formMsg.trim(), plan: modalPlan + }) }); + if (!res.ok) { + const d = await res.json(); + formError = d.error ?? 'Fehler beim Senden.'; + (window as any).turnstile?.reset(); + return; + } formDone = true; formName = formCompany = formEmail = formPhone = formMsg = ''; } catch { @@ -438,6 +447,7 @@
+
{#if formError}{/if}