Compare commits

...

59 Commits

Author SHA1 Message Date
2eb80e0da9 added url, apikey to lockscreen 2025-05-03 15:12:58 +02:00
dbcdce5296 set new gallery url 2025-05-03 15:07:08 +02:00
7482c329ed Nav fix 2025-05-03 14:36:48 +02:00
aafaf4dd9e Minor fixes with lockscreen 2025-05-03 14:34:09 +02:00
7b08d6e03f switch logger to db 2025-04-21 21:44:19 +02:00
fe5cbabd46 Fix spelling 2025-04-21 21:31:38 +02:00
141f75717b Fix burgernav icon 2025-04-21 21:25:24 +02:00
c38be00f73 Reintroduce burger nav 2025-04-21 21:24:52 +02:00
ccbcb94449 make user_select buttons dark 2025-04-21 21:08:39 +02:00
86b9595665 Add navlink to user_select 2025-04-21 21:04:10 +02:00
c6e441dc26 Refactor navbar to dynamically inject buttons 2025-04-21 21:02:11 +02:00
c89eb37361 Implement kiosk mode functionality and update external link handling in footer and credits 2025-04-21 21:01:43 +02:00
5cfd8b2319 Change user_select style 2025-04-21 18:18:33 +02:00
d44900435f Fix lockscreen for different screen sizes 2025-04-21 18:17:21 +02:00
1e4ebc2a3c Move foother to bottom 2025-04-21 17:36:26 +02:00
5ce521c8a7 Remove invalid unsplash api key - Never commit such keys! / Remove unsplash branding from credits 2025-04-21 17:35:13 +02:00
ef16f045f7 Implement AbortError class and enhance transaction handling with stock and existence checks 2025-04-21 17:03:32 +02:00
bf561f8c7f Rename "Bild hinterlegt" to "Bild" 2025-04-21 14:33:27 +02:00
5da8060857 Frontend Products -> Change Upload text to Icon 2025-04-21 14:30:45 +02:00
b7d12d18d4 Prisma schema-> Increase the decimal places for total and price from 5,2 to 8,2 2025-04-21 14:20:49 +02:00
16ee092b35 Migrate config to subobj (http and mysql) / .env fort prisma is written on startup / Add http.enable_csp config option 2025-04-21 00:26:01 +02:00
366f3297da Streamline some variable names 2025-04-20 21:20:57 +02:00
475690ca2b Minor bugfixes 2025-03-19 23:16:11 +01:00
03fec1ebd7 reports view 2025-03-19 23:15:28 +01:00
8cd011fc01 added old transactions to payup view 2025-03-19 23:15:12 +01:00
9066397cd4 fixed layout 2025-03-19 21:37:14 +01:00
cf7bd8da9c fixed lockscreen up a little 2025-03-19 21:36:47 +01:00
14cf8af14b restock mode 2025-03-19 20:45:05 +01:00
d491033c29 pay up implementation 2025-03-19 20:44:26 +01:00
fd7d1ffd47 products -> removes id from POST 2025-03-09 23:12:54 +01:00
fe04ad9ce3 custom validation -> parse arrays from string / Rough transaction implementaion 2025-03-09 23:11:59 +01:00
bd43f03507 implemented ui functions for managing products, uploading images and editiing users 2025-03-09 23:09:49 +01:00
fa7f3004fa DB schema updates 2025-03-09 23:07:26 +01:00
42f6e0b22d TODO: replace parseDynamicSortBy 2025-03-09 23:07:02 +01:00
18d3646315 Add images to .gitignore, except default.png 2025-03-09 21:39:41 +01:00
3bf0e2fdd5 optimize image route / implement check if img exists for frontend purposes 2025-03-09 21:36:00 +01:00
2cbde4e344 devmode warning(error) messages 2025-03-09 18:30:23 +01:00
66f0bebc9d Add dedicated devmode for express-fileupload 2025-03-09 17:46:32 +01:00
50ad684ad3 Add email to user 2025-03-03 16:43:49 +01:00
0233196276 Add image endpoint / query args are now undefined om empty string 2025-03-03 16:43:29 +01:00
551f72f3e0 new directory structure for api endpoints / prepare image endpoint / default product image 2025-03-02 22:11:48 +01:00
5ab2df351c Fix codecheck -> Handle all nullish things possible 2025-03-02 18:52:35 +01:00
bca596fa75 Fix codecheck again 2025-03-02 18:18:05 +01:00
f1ddaa1cc2 Dependency upgrade & cleanup 2025-03-02 17:25:18 +01:00
3706ed07d2 Frontend product + user select 2025-02-24 23:34:49 +01:00
5a68622b7a commented out navbar-end 2025-02-24 23:34:15 +01:00
d5eecd2d0c Change favicon / logo 2025-02-24 23:33:56 +01:00
99371e96a9 Fix user route (user code bool) 2025-02-24 23:28:17 +01:00
45cd5b10e6 Frontend stuff 2025-02-24 22:55:28 +01:00
611a4a0ead configManager now supports logging via tslog / cleanup / loglevel 0 in devmode 3 in prod 2025-02-24 22:55:18 +01:00
744ab40a6b Streamline nameing to "user codecheck" for debug 2025-02-22 21:59:11 +01:00
0885209502 refine and explain user codecheck 2025-02-22 21:55:39 +01:00
6b665bb41d Add Security disclaimer / reformat readme 2025-02-22 21:46:44 +01:00
5720b45c6b Fix formatting 2025-02-22 21:45:17 +01:00
de5a4b9f19 Add codecheck endpoint 2025-02-22 21:45:00 +01:00
7b9fe95cd4 user POST -> If code is 0000 deactivate code (write null to db) similar to PATCH 2025-02-21 22:56:34 +01:00
09f06fb2ea products PATCh schema -> id is now required 2025-02-21 22:54:31 +01:00
6844b1fb24 products GET-> add gtin to if and dont get all if gtin is specified 2025-02-21 22:53:59 +01:00
0d6b41abcf Add P2025to prisma errorhandler 2025-02-21 22:52:52 +01:00
62 changed files with 2664 additions and 934 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ dist/
.env
config.json
.vsls.json
images/*.png
!images/default.png

View File

@ -1,9 +1,13 @@
# HydrationHUB
HydrationHUB - TODO: Luistiger slogan?
## Serving static files from node_modules
## Security Disclaimer
This application is not designed for security and therefore has exploitable vulnerabilities.
This is due to the simplicity of the application and is sufficient for the intended purpose.
If you plan to use this application for a different purpose where security vulnerabilities are a concern, we would greatly appreciate your contributions.
## Static handling
### Serving static files from node_modules
Files from explicit dirs inside `node_modules` will be served below `/libs`.
## Serving static files from /static
### Serving static files from /static
Files from the `/static` folder will be served below `/static`.

BIN
images/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

711
package-lock.json generated
View File

@ -9,7 +9,8 @@
"version": "1.0.0",
"license": "GPL-3.0",
"dependencies": {
"@prisma/client": "^6.4.0",
"@hapi/bourne": "^3.0.0",
"@prisma/client": "^6.4.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",
"eta": "^3.5.0",
@ -22,21 +23,16 @@
"tslog": "^4.9.3"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.1",
"@types/express-session": "^1.18.1",
"@types/joi": "^17.2.2",
"@types/lodash": "^4.17.14",
"@types/node": "^22.10.5",
"@types/passport": "^1.0.17",
"@types/passport-local": "^1.0.38",
"@types/signale": "^1.4.7",
"eslint": "^9.18.0",
"@types/lodash": "^4.17.16",
"@types/node": "^22.13.8",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"prisma": "^6.2.1",
"tsx": "^3.12.10",
"typescript": "^5.7.3"
"prisma": "^6.4.1",
"tsx": "^4.19.3",
"typescript": "^5.8.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
@ -522,9 +518,9 @@
}
},
"node_modules/@eslint/core": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
"integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@ -535,9 +531,9 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz",
"integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==",
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz",
"integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -559,9 +555,9 @@
}
},
"node_modules/@eslint/js": {
"version": "9.20.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
"integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz",
"integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==",
"dev": true,
"license": "MIT",
"engines": {
@ -579,19 +575,25 @@
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.6.tgz",
"integrity": "sha512-+0TjwR1eAUdZtvv/ir1mGX+v0tUoR3VEPB8Up0LLJC+whRW0GgBBtpbOkg/a/U4Dxa6l5a3l9AJ1aWIQVyoWJA==",
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.11.0",
"@eslint/core": "^0.12.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hapi/bourne": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz",
"integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -660,9 +662,9 @@
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz",
"integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==",
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
"integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@ -674,9 +676,9 @@
}
},
"node_modules/@prisma/client": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.0.tgz",
"integrity": "sha512-48tLb+VL7iuuqJXjD4Xbqa622fuh8UtqmjTf39AKrQjlTUdNaMc9sC/c49eXQkcnrAdh9FoS1qVupmZSYiZ9TQ==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz",
"integrity": "sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
@ -696,24 +698,24 @@
}
},
"node_modules/@prisma/debug": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.0.tgz",
"integrity": "sha512-zXsLpNXLypdThJjItqk0u/3uitcD9+9rNynZPPu4Xp7664yp8VbxCGTVBS696vA0e3kw0Xv/4muR+cNHkxspcw==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz",
"integrity": "sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.0.tgz",
"integrity": "sha512-SeUigmWj0uhnSjYKWkG//Gzj4XHy6bi1TXgTZ+pPujxGELJgOvecgaA/bHmWokyHPnbYWRaoSwQDxPjihYzSZg==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz",
"integrity": "sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.0",
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/fetch-engine": "6.4.0",
"@prisma/get-platform": "6.4.0"
"@prisma/fetch-engine": "6.4.1",
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/engines-version": {
@ -724,25 +726,25 @@
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.0.tgz",
"integrity": "sha512-CN/Qb/+n+15gfhN0ipeUcGu/oGw7HIwVw4DCKD11190dneVGpa+n0wxwXTFJlV0vkAeSBSkQ3y1ugKAa5s/VPg==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz",
"integrity": "sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.0",
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/get-platform": "6.4.0"
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.0.tgz",
"integrity": "sha512-khANH9QbCRhFWiGj3qmR0clxLTU76Cimxf4JAhjhtpsc1jdG1A9geGe0kU4WAQ1YpiKFJ10s9j2wbbE/jSP99Q==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz",
"integrity": "sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.0"
"@prisma/debug": "6.4.1"
}
},
"node_modules/@sideway/address": {
@ -797,16 +799,6 @@
"@types/node": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@ -851,16 +843,6 @@
"@types/send": "*"
}
},
"node_modules/@types/express-session": {
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz",
"integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/http-errors": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
@ -886,9 +868,9 @@
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
"version": "4.17.16",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz",
"integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==",
"dev": true,
"license": "MIT"
},
@ -900,48 +882,15 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"version": "22.13.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz",
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/passport": {
"version": "1.0.17",
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/passport-local": {
"version": "1.0.38",
"resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.38.tgz",
"integrity": "sha512-nsrW4A963lYE7lNTv9cr5WmiUD1ibYJvWrpE13oxApFsRt77b0RdtZvKbCdNIY4v/QZ6TRQWaDDEwV1kCTmcXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-strategy": "*"
}
},
"node_modules/@types/passport-strategy": {
"version": "0.2.38",
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/passport": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@ -979,16 +928,6 @@
"@types/send": "*"
}
},
"node_modules/@types/signale": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/@types/signale/-/signale-1.4.7.tgz",
"integrity": "sha512-nc0j37QupTT7OcYeH3gRE1ZfzUalEUsDKJsJ3IsJr0pjjFZTjtrX1Bsn6Kv56YXI/H9rNSwAkIPRxNlZI8GyQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -1144,13 +1083,6 @@
"concat-map": "0.0.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bulma": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bulma/-/bulma-1.0.3.tgz",
@ -1488,22 +1420,22 @@
}
},
"node_modules/eslint": {
"version": "9.20.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
"integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
"version": "9.21.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz",
"integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.0",
"@eslint/core": "^0.11.0",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "9.20.0",
"@eslint/plugin-kit": "^0.2.5",
"@eslint/config-array": "^0.19.2",
"@eslint/core": "^0.12.0",
"@eslint/eslintrc": "^3.3.0",
"@eslint/js": "9.21.0",
"@eslint/plugin-kit": "^0.2.7",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"ajv": "^6.12.4",
@ -1846,16 +1778,6 @@
"node": ">=16"
}
},
"node_modules/flat-cache/node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
@ -1906,17 +1828,17 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
@ -2107,6 +2029,15 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -2190,6 +2121,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -2470,14 +2411,14 @@
}
},
"node_modules/prisma": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.0.tgz",
"integrity": "sha512-UxEaEo1ajnEvwT9UQRyRfq0zZ9pvOmlZ6kShY8Hgu4jxgkHo1mg85IEP8yBgFRiRBA2o2OIt1nxzcllt86D4Mw==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz",
"integrity": "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/engines": "6.4.0",
"@prisma/engines": "6.4.1",
"esbuild": ">=0.12 <1",
"esbuild-register": "3.6.0"
},
@ -2512,15 +2453,6 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-addr/node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@ -2780,27 +2712,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
@ -2866,435 +2777,25 @@
}
},
"node_modules/tsx": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-3.14.0.tgz",
"integrity": "sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==",
"version": "4.19.3",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz",
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
"get-tsconfig": "^4.7.2",
"source-map-support": "^0.5.21"
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/tsx/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -3322,9 +2823,9 @@
}
},
"node_modules/typescript": {
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {

View File

@ -21,24 +21,20 @@
"pos"
],
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/express-fileupload": "^1.5.1",
"@types/express-session": "^1.18.1",
"@types/joi": "^17.2.2",
"@types/lodash": "^4.17.14",
"@types/node": "^22.10.5",
"@types/passport": "^1.0.17",
"@types/passport-local": "^1.0.38",
"@types/signale": "^1.4.7",
"eslint": "^9.18.0",
"@types/lodash": "^4.17.16",
"@types/node": "^22.13.8",
"eslint": "^9.21.0",
"eslint-config-prettier": "^9.1.0",
"prisma": "^6.2.1",
"tsx": "^3.12.10",
"typescript": "^5.7.3"
"prisma": "^6.4.1",
"tsx": "^4.19.3",
"typescript": "^5.8.2"
},
"dependencies": {
"@prisma/client": "^6.4.0",
"@hapi/bourne": "^3.0.0",
"@prisma/client": "^6.4.1",
"bootstrap-icons": "^1.11.3",
"bulma": "^1.0.3",
"eta": "^3.5.0",

View File

@ -17,11 +17,12 @@ model user {
id Int @id @unique @default(autoincrement())
name String @unique
code String?
email String?
// TODO: Prüfen ob nötig, erstmal vorbereitet.
transactions transactions[]
@@fulltext([name])
@@fulltext([name, email])
}
model transactions {
@ -32,9 +33,9 @@ model transactions {
user user @relation(fields: [userId], references: [id])
userId Int
total Float
total Decimal @db.Decimal(8,2)
paid Boolean @default(false)
paidAt Boolean?
paidAt DateTime?
createdAt DateTime @default(now())
}
@ -54,7 +55,7 @@ model products {
id Int @id @unique @default(autoincrement())
gtin String @unique // Dont try to use BigInt -> https://github.com/prisma/studio/issues/614
name String @unique
price Decimal @db.Decimal(5,2) // FIXME: input: 77.80 -> output: 77.8
price Decimal @db.Decimal(8,2)
stock Int
visible Boolean @default(true)
@ -62,3 +63,5 @@ model products {
@@fulltext([name])
}
// TODO: migrate all ids to uuid?

View File

@ -1,18 +1,30 @@
import ConfigManager from '../libs/configManager.js';
import __path from './path.js';
import _ from 'lodash';
import log from './log.js';
// Create a new config instance.
const config = new ConfigManager(__path + '/config.json', true, {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '0.0.0.0',
http_port: 3000,
http_domain: 'example.org',
http_enable_hsts: false,
devmode: true
});
// db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http: {
listen_address: '0.0.0.0',
port: 3000,
domain: 'example.org',
enable_hsts: false,
enable_csp: false
},
mysql: {
host: '',
port: 3306,
user: '',
password: '',
database: 'hydrationhub'
},
devmode: false,
devmode_fileupload: false,
galleryApiKey: '',
});//, log.core); // Disabled due to Cyclic dependencies with log handler (specifically-> devmode for loglevel)
!config.global.devmode && log.core.error('devmode active! Do NOT use this in prod!');
export default config;

View File

@ -1,13 +1,27 @@
import { PrismaClient, Prisma } from '@prisma/client'; // Database
import { Response } from 'express';
import config from './config.js';
import __path from './path.js';
import log from './log.js';
import fs from 'fs';
import path from 'path';
// Generate .env file for Prisma commands
const dotEnvPath = path.join(__path, '/.env')
const dotEnvExist = !fs.existsSync(dotEnvPath);
fs.writeFileSync(dotEnvPath, `DATABASE_URL="mysql://${config.global.mysql.user}:${config.global.mysql.password}@${config.global.mysql.host}:${config.global.mysql.port}/${config.global.mysql.database}"`);
log.core.info('Generated .env file for Prisma.');
if (dotEnvExist) {
log.db.error('Please run "npx prisma db push" to synchronize the database.');
process.exit(1);
}
// TODO: Add errorhandling with some sort of message.
const prisma = new PrismaClient({
datasources: {
db: {
url: config.global.db_connection_string
url: `mysql://${config.global.mysql.user}:${config.global.mysql.password}@${config.global.mysql.host}:${config.global.mysql.port}/${config.global.mysql.database}`
}
}
});
@ -17,7 +31,6 @@ export function handlePrismaError(errorObj: any, res: Response, source: string)
log.db.error(source, errorObj);
if (errorObj instanceof Prisma.PrismaClientKnownRequestError) {
switch (errorObj.code) {
// P2002 -> "Unique constraint failed on the {constraint}"
case 'P2002':
res.status(409).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'The object needs to be unique', meta: errorObj.meta });
@ -28,6 +41,11 @@ export function handlePrismaError(errorObj: any, res: Response, source: string)
res.status(404).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'Relation object does not exist', meta: errorObj.meta });
break;
// P2025 -> "An operation failed because it depends on one or more records that were required but not found. {cause}"
case 'P2025':
res.status(404).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'Object does not exist', meta: errorObj.meta });
break;
default:
res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'An error occurred during the database operation' });
break;
@ -40,3 +58,10 @@ export function handlePrismaError(errorObj: any, res: Response, source: string)
export default prisma;
//FIXME: https://www.prisma.io/docs/orm/prisma-client/special-fields-and-types/null-and-undefined
// // Simulate a Prisma error for testing purposes
// throw new Prisma.PrismaClientKnownRequestError(
// 'Simulated Prisma error for testing',
// { code: 'P2000', clientVersion: 'unknown' } // Example error parameters
// );

View File

@ -1,38 +1,39 @@
import { Logger,ISettingsParam } from "tslog";
import config from './config.js';
import { Logger, ISettingsParam } from 'tslog';
// You can ignore every log message from being processed until a certain severity. Default severities are:
// 0: silly, 1: trace, 2: debug, 3: info, 4: warn, 5: error, 6: fatal
function loggerConfig(name: string): ISettingsParam<unknown> {
return {
type: "pretty", // pretty, json, hidden
type: 'pretty', // pretty, json, hidden
name: name,
hideLogPositionForProduction: true,
prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} "
}
prettyLogTemplate: '{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} ',
minLevel: config.global.devmode ? 0 : 3 // Only display info, warn, error, fatal in production mode
};
}
type log = {
core: Logger<unknown>
db: Logger<unknown>
web: Logger<unknown>
S3: Logger<unknown>
auth: Logger<unknown>
api?: Logger<unknown>
frontend?: Logger<unknown>
core: Logger<unknown>;
db: Logger<unknown>;
web: Logger<unknown>;
auth: Logger<unknown>;
api?: Logger<unknown>;
frontend?: Logger<unknown>;
};
// FIXME: any type
let log: log = {
core: new Logger(loggerConfig("Core")),
db: new Logger(loggerConfig("DB")),
web: new Logger(loggerConfig("Web")),
S3: new Logger(loggerConfig("S3")),
auth: new Logger(loggerConfig("Auth")),
core: new Logger(loggerConfig('Core')),
db: new Logger(loggerConfig('DB')),
web: new Logger(loggerConfig('Web')),
auth: new Logger(loggerConfig('Auth'))
// helper: new Logger(loggerConfig("HELPER")),
};
log["api"] = log.web.getSubLogger({ name: "API" });
log["frontend"] = log.web.getSubLogger({ name: "Frontend" });
log['api'] = log.web.getSubLogger({ name: 'API' });
log['frontend'] = log.web.getSubLogger({ name: 'Frontend' });
// log.core.silly("Hello from core");
// log.api.trace("Hello from api");

View File

@ -0,0 +1,23 @@
import Bourne from '@hapi/bourne';
import Joi from 'joi';
const validator = Joi.extend((joi) => ({
type: 'array',
base: Joi.array(),
coerce: {
from: 'string',
method(value, helpers) {
if (typeof value !== 'string' || (value[0] !== '[' && !/^\s*\[/.test(value))) {
return { value };
}
try {
return { value: Bourne.parse(value) };
} catch (ignoreErr) {
return { value };
}
}
}
}));
export default validator;

View File

@ -7,6 +7,7 @@
* @returns {object}
*/
export function parseDynamicSortBy(SortField: string, Order: string) {
// TODO: { [value.sort.toString()]: value.order.toString() }
return JSON.parse(`{ "${SortField}": "${Order}" }`);
}

View File

@ -1,28 +1,22 @@
// MARK: Imports
import path from 'node:path';
import fs from 'node:fs';
import ChildProcess from 'child_process';
import __path from './handlers/path.js';
import log from './handlers/log.js';
import db from './handlers/db.js';
//import db from './handlers/db.js';
import config from './handlers/config.js';
// Express & more
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import session from 'express-session';
import fileUpload from 'express-fileupload';
import bodyParser, { Options } from 'body-parser';
import { Eta } from 'eta';
import passport from 'passport';
import ChildProcess from 'child_process';
import bodyParser from 'body-parser';
import { Eta, Options } from 'eta';
import routes from './routes/index.js';
import fs from 'node:fs';
log.core.trace('Running from path: ' + __path);
// MARK: Express
const app = express();
@ -67,15 +61,15 @@ app.set('view engine', 'eta');
// MARK: Express Middleware & Config
app.set('x-powered-by', false); // helmet does this too. But not in devmode
if (!config.global.devmode) {
if (!config.global.devmode && config.global.http.enable_csp) {
app.use(
helmet({
strictTransportSecurity: config.global.http_enable_hsts,
strictTransportSecurity: config.global.http.enable_hsts,
contentSecurityPolicy: {
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", config.global.http_domain],
scriptSrc: ["'self'", config.global.http.domain],
objectSrc: ["'none'"],
upgradeInsecureRequests: config.global.devmode ? null : []
}
@ -84,17 +78,22 @@ if (!config.global.devmode) {
); // Add headers
}
app.use(fileUpload());
app.use(fileUpload({ useTempFiles: false, debug: config.global.devmode_fileupload }));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(routes);
// TODO: Remove hardcoded http
app.listen(config.global.http_port, config.global.http_listen_address, () => {
log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`);
app.listen(config.global.http.port, config.global.http.listen_address, () => {
log.web.info(`Listening at http://${config.global.http.listen_address}:${config.global.http.port}`);
});
log.core.trace('Running from path: ' + __path);
config.global.devmode && log.core.error('DEVMODE ACTIVE! Do NOT use this in prod! (silly/trace/debug logging enabled, test route enabled, security features disabled )');
config.global.devmode_fileupload && log.core.error('DEVMODE ACTIVE! Do NOT use this in prod! (express-fileupload debug mode)');
// MARK: Helper Functions
function buildEtaEngine() {
return (path: string, opts: Options, callback: CallableFunction) => {

View File

@ -1,6 +1,7 @@
import fs from 'node:fs';
import _ from 'lodash';
import { randomUUID, randomBytes } from 'crypto';
import { randomBytes } from 'crypto';
import { Logger } from 'tslog';
export type configObject = Record<any, any>;
@ -17,18 +18,25 @@ export default class config {
global: configObject;
replaceSecrets: boolean;
#logger: Logger<unknown> | typeof console;
/**
* Creates an instance of config.
*
* @constructor
* @param {string} configPath Path to config file.
* @param {boolean} replaceSecrets Whether to replace secrets with generated values.
* @param {object} configPreset Default config object with default values.
* @param {Logger<unknown> | typeof console} [logger] Optional (tslog) logger.
*/
constructor(configPath: string, replaceSecrets: boolean, configPreset: object) {
constructor(configPath: string, replaceSecrets: boolean, configPreset: object, logger?: Logger<unknown> | typeof console) {
this.#configPath = configPath;
this.global = configPreset;
this.replaceSecrets = replaceSecrets;
this.#logger = logger ?? console;
this.#logger.info(`Initializing config manager with path: ${this.#configPath}`);
try {
// Read config
const data = fs.readFileSync(this.#configPath, 'utf8');
@ -40,11 +48,11 @@ export default class config {
} catch (err: any) {
// If file does not exist, create it.
if (err.code === 'ENOENT') {
console.log(`Config file does not exist. Creating it at ${this.#configPath} now.`);
this.#logger.info(`Config file does not exist. Creating it at ${this.#configPath} now.`);
this.save_config();
return;
}
console.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
this.#logger.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
// Exit process.
process.exit(1);
}
@ -58,15 +66,15 @@ export default class config {
// If enabled replace tokens defines as "gen" with random token
if (this.replaceSecrets) {
// Replace tokens with value "gen"
this.generate_secrets(this.global, 'gen')
this.generate_secrets(this.global, 'gen');
}
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
} catch (err) {
console.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
this.#logger.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
return;
}
console.log(`Successfully written config file to ${this.#configPath}`);
this.#logger.info(`Successfully written config file to ${this.#configPath}`);
}
/**
@ -79,9 +87,8 @@ export default class config {
while (stack?.length > 0) {
const currentObj: any = stack.pop();
Object.keys(currentObj).forEach((key) => {
if (currentObj[key] === placeholder) {
console.log('Generating secret: ' + key);
this.#logger.info('Generating secret: ' + key);
currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
}
@ -93,7 +100,6 @@ export default class config {
}
}
/*
**** Example ****

View File

@ -1,11 +1,11 @@
import express from 'express';
// Route imports
import v1_routes from './v1/index.js';
import v1_router from './v1/index.js';
// Router base is '/api'
const Router = express.Router({ strict: false });
Router.use('/v1', v1_routes);
Router.use('/v1', v1_router);
export default Router;

View File

@ -0,0 +1,142 @@
import { Request, Response } from 'express';
import path from 'node:path';
import fs from 'node:fs';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import __path from '../../../../handlers/path.js';
import { schema_get, schema_post, schema_del } from './image_schema.js';
import { UploadedFile } from 'express-fileupload';
// MARK: GET image
async function get(req: Request, res: Response) {
const { error, value } = schema_get.validate(req.query);
if (error) {
log.api?.debug('GET image Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('GET image Success:', req.query, value);
const img_path = path.join(__path, 'images', `${value.id}.png`);
const img_default_path = path.join(__path, 'images', 'default.png');
const img_existing = fs.existsSync(img_path);
const img_default_existing = fs.existsSync(img_default_path);
if (value.check) {
res.status(200).json(img_existing);
return;
}
if (!img_default_existing) {
log.api?.warn('Default image not found! Please make sure the following path contains an png file:', img_default_path);
}
await db.products
.findUnique({
where: {
id: value.id
}
})
.then((result) => {
if (result) {
// Serve stored or default image
log.api?.debug('Image exists:', img_existing);
img_existing ? res.sendFile(img_path) : res.sendFile(img_default_path);
} else {
// Product does not exist
log.api?.debug('Product does not exist, using default image ');
res.sendFile(img_default_path);
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET image');
});
}
}
// MARK: CREATE/UPDATE image (upload)
async function post(req: Request, res: Response) {
const { error, value } = schema_post.validate(req.query);
if (error) {
log.api?.debug('POST image Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
// Check if multipart has image with file
if (!req.files || !req.files.image) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'Missing image' });
return;
}
const upload_file = req.files.image as UploadedFile;
const upload_path = path.join(__path, 'images', `${value.id}.png`);
const allowedMimeTypes = ['image/png'];
// Check if mimetype is allowed
if (!allowedMimeTypes.includes(upload_file.mimetype)) {
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: 'Only image/png is allowed' });
return;
}
// Check if product exists
const result = await db.products.findUnique({
where: {
id: value.id
}
});
if (!result) {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product' });
return;
}
// Write file
upload_file.mv(upload_path, (err) => {
if (err) {
res.status(500).json({ status: 'ERROR', errorcode: 'IO_ERROR', message: 'Could not write image to disk' });
return;
}
log.api?.debug('File uploaded to:', upload_path);
res.status(200).json({ status: 'CREATED', message: 'Successfully uploaded image', id: result.id });
});
}
}
// MARK: DELETE image
async function del(req: Request, res: Response) {
const { error, value } = schema_del.validate(req.query);
if (error) {
log.api?.debug('DEL image Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('DEL image Success:', req.query, value);
const del_path = path.join(__path, 'images', `${value.id}.png`);
await db.products
.findUnique({
where: {
id: value.id
}
})
.then((result) => {
if (result) {
if (!fs.existsSync(del_path)) {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Product exists but no image found' });
return;
}
fs.unlink(del_path, (err) => {
if (err) {
res.status(500).json({ status: 'ERROR', errorcode: 'IO_ERROR', message: 'Could not delete image' });
return;
}
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted image', id: result.id });
log.api?.debug('File removed from:', del_path);
});
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified product (image)' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'DEL products');
});
}
}
export default { get, post, del };

View File

@ -0,0 +1,37 @@
import { Request, Response } from 'express';
import validator from 'joi'; // DOCS: https://joi.dev/api
// MARK: GET image
const schema_get = validator.object({
id: validator.number().positive().precision(0).default(0).note('product id'), // id 0 should never exist, since id autoincrement starts at 1
check: validator.boolean().default(false)
});
// MARK: CREATE / UPDATE image
const schema_post = validator.object({
id: validator.number().positive().precision(0).required().note('product id')
//image: validator.string().required().note('product image as multipart/form-data')
});
// MARK: DELETE products
const schema_del = validator.object({
id: validator.number().positive().precision(0).required().note('product id')
});
// Describe all schemas
const schema_get_desc = schema_get.describe();
const schema_post_desc = schema_post.describe();
const schema_patch_desc = schema_post.describe(); // Just for show (POST = PATCH)
const schema_del_desc = schema_del.describe();
// GET route
export default async function get(req: Request, res: Response) {
res.status(200).json({
GET: schema_get_desc,
POST: schema_post_desc,
PATCH: schema_patch_desc,
DELETE: schema_del_desc
});
}
export { schema_get, schema_post, schema_del };

View File

@ -1,15 +1,23 @@
import express from 'express';
import passport from 'passport';
// Route imports
import testRoute from './test.js';
import versionRoute from './version.js';
import test_route from './test.js';
import version_route from './version.js';
import user_route from './user.js';
import user_schema from './user_schema.js';
import user_route from './user/user.js';
import user_schema from './user/user_schema.js';
import products_route from './products.js';
import products_schema from './products_schema.js';
import user_codecheck_route from './user/user_codecheck.js';
import user_codecheck_schema from './user/user_codecheck_schema.js';
import products_route from './products/products.js';
import products_schema from './products/products_schema.js';
import image_route from './image/image.js';
import image_schema from './image/image_schema.js';
import transaction_route from './transaction/transaction.js';
import transaction_schema from './transaction/transaction_schema.js';
// Router base is '/api/v1'
const Router = express.Router({ strict: false });
@ -24,14 +32,33 @@ Router.use('*', function (req, res, next) {
next();
});
// All empty strings are undefined (not null!) values (query)
Router.use('*', function (req, res, next) {
for (let key in req.query) {
if (req.query[key] === '') {
req.query[key] = undefined;
}
}
next();
});
// All api routes lowercase! Yea I know but when strict: true it matters.
Router.route('/user').get(user_route.get).post(user_route.post).patch(user_route.patch).delete(user_route.del);
Router.route('/user/describe').get(user_schema);
Router.route('/user/codecheck').get(user_codecheck_route.get);
Router.route('/user/codecheck/describe').get(user_codecheck_schema);
Router.route('/products').get(products_route.get).post(products_route.post).patch(products_route.patch).delete(products_route.del);
Router.route('/products/describe').get(products_schema);
Router.route('/version').get(versionRoute.get);
Router.route('/test').get(testRoute.get);
Router.route('/image').get(image_route.get).post(image_route.post).patch(image_route.post).delete(image_route.del); // POST and PATCH are handled in 'image_route.post'
Router.route('/image/describe').get(image_schema);
Router.route('/transaction').get(transaction_route.get).post(transaction_route.post).patch(transaction_route.patch).delete(transaction_route.del);
Router.route('/transaction/describe').get(transaction_schema);
Router.route('/version').get(version_route.get);
Router.route('/test').get(test_route.get);
export default Router;

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
import log from '../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../../helpers/prisma_helpers.js';
import { schema_get, schema_post, schema_patch, schema_del } from './products_schema.js';
// MARK: GET products
@ -13,8 +13,8 @@ async function get(req: Request, res: Response) {
} else {
log.api?.debug('GET products Success:', req.query, value);
if (value.search !== undefined || value.id !== undefined) {
// if search or get by id
if (value.search !== undefined || value.id !== undefined || value.gtin !== undefined) {
// if search or get by id/gtin
await db
.$transaction([
// Same query for count and findMany
@ -86,7 +86,6 @@ async function post(req: Request, res: Response) {
await db.products
.create({
data: {
id: value.id,
gtin: value.gtin,
name: value.name,
price: value.price,

View File

@ -47,7 +47,7 @@ const schema_post = validator.object({
// MARK: UPDATE products
const schema_patch = validator
.object({
id: validator.number().positive().precision(0),
id: validator.number().positive().precision(0).required(),
gtin: validator
.string()
.min(8)

View File

@ -0,0 +1,265 @@
import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../../helpers/prisma_helpers.js';
import { schema_get, schema_post, schema_patch, schema_del } from './transaction_schema.js';
import { Prisma } from '@prisma/client';
class AbortError extends Error {
constructor(public http_status: number, public status: string, public errorcode: string, public message: string, public details?: any) {
super(message);
this.name = 'AbortError';
}
}
// MARK: GET transaction
async function get(req: Request, res: Response) {
const { error, value } = schema_get.validate(req.query);
if (error) {
log.api?.debug('GET transaction Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('GET transaction Success:', req.query, value);
if (value.id !== undefined || value.user_id !== undefined) {
// get by id or user_id
await db
.$transaction([
// Same query for count and findMany
db.transactions.count({
where: {
OR: [{ id: value.id }, { userId: value.user_id }],
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
}),
db.transactions.findMany({
where: {
OR: [{ id: value.id }, { userId: value.user_id }],
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
})
])
.then(([count, result]) => {
if (result.length !== 0) {
res.status(200).json({ count, result });
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified transaction' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET transaction');
});
} else {
// get all
await db
.$transaction([
// Same query for count and findMany
db.transactions.count({
where: {
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
}),
db.transactions.findMany({
where: {
paid: value.paid
},
orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
skip: value.skip,
take: value.take
})
])
.then(([count, result]) => {
if (result.length !== 0) {
res.status(200).json({ count, result });
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified transaction' });
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET transaction');
});
}
}
}
// MARK: CREATE transaction
async function post(req: Request, res: Response) {
const { error, value } = schema_post.validate(req.body);
if (error) {
log.api?.debug('POST transaction Error:', req.body, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('POST transaction Success:', req.body, value);
const products: Array<number> = value.products;
const outOfStockProducts: { id: number; name: string }[] = [];
const notFoundProducts: { id: number; name: string }[] = [];
const salesData: { productId: number; price: number }[] = [];
let total = new Prisma.Decimal(0);
// Start Prisma transaction
await db
.$transaction(async (prisma) => {
// Iterate over all products for this transaction(not prisma)
for (let i = 0; i < products.length; i++) {
log.api?.debug('Product:', i + 1, 'of', products.length, '(Loop)');
// Get product (price, stock, name)
const product = await prisma.products.findUnique({
where: { id: products[i] },
select: { price: true, stock: true, name: true }
});
// Check if product exists
if (product) {
log.api?.debug('Price:', product.price, '[Name:' + product.name + ']', '[ID:' + products[i] + ']');
if (product.stock > 0) {
// Add price of current product to total
total = total.add(product.price);
// Add product to salesData -> Later generate sales entry for each product
salesData.push({
productId: products[i],
price: Number(product.price)
});
// Reduce stock by 1
await prisma.products.update({
where: { id: products[i] },
data: { stock: { decrement: 1 } }
});
} else {
// Product is out of stock
outOfStockProducts.push({ id: products[i], name: product.name });
}
} else {
// Product not found
notFoundProducts.push({ id: products[i], name: 'unknown' });
}
}
log.api?.debug('Total:', total.toFixed(2));
// Abort the Prisma transaction if there are not existing products
if (notFoundProducts.length > 0) {
log.api?.debug('Aborting. missing products:', notFoundProducts);
throw new AbortError(
400, // http_status
'ERROR', // status
'NOT_FOUND', // errorcode
'Some of the products included in the transaction do not exist, therefore the transaction has not been processed.', // message
notFoundProducts // details
);
}
// Abort the Prisma transaction if there are products with insufficient stock
if (outOfStockProducts.length > 0) {
log.api?.debug('Aborting. out of stock products:', outOfStockProducts);
throw new AbortError(
400, // http_status
'ERROR', // status
'OUT_OF_STOCK', // errorcode
'Some of the products included in the transaction are out of stock, therefore the transaction has not been processed.', // message
outOfStockProducts // details
);
}
// Create transaction with salesData
const transaction = await prisma.transactions.create({
data: {
userId: value.user_id,
total: total,
paid: false,
sales: {
create: salesData
}
},
select: {
id: true
}
});
// Everything went well
res.status(201).json({ status: 'CREATED', message: 'Successfully created transaction', id: transaction.id });
})
.catch((err) => {
if (err instanceof AbortError) {
res.status(err.http_status).json({
status: err.status,
errorcode: err.errorcode,
message: err.message,
details: err.details
});
} else {
handlePrismaError(err, res, 'POST transaction');
}
});
}
}
// MARK: UPDATE transaction
async function patch(req: Request, res: Response) {
const { error, value } = schema_patch.validate(req.body);
if (error) {
log.api?.debug('PATCH transaction Error:', req.body, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('PATCH transaction Success:', req.body, value);
await db.transactions
.update({
where: {
id: value.id
},
data: {
userId: value.user_id,
paid: value.paid,
paidAt: value.paid ? new Date() : undefined
},
select: {
id: true
}
})
.then((result) => {
res.status(200).json({ status: 'UPDATED', message: 'Successfully updated transaction', id: result.id });
})
.catch((err) => {
handlePrismaError(err, res, 'PATCH transaction');
});
}
}
// MARK: DELETE transaction
async function del(req: Request, res: Response) {
const { error, value } = schema_del.validate(req.body);
if (error) {
log.api?.debug('DEL transaction Error:', req.body, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('DEL transaction Success:', req.body, value);
await db.transactions
.delete({
where: {
id: value.id
}
})
.then((result) => {
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted transaction', id: result.id });
})
.catch((err) => {
handlePrismaError(err, res, 'DEL transaction');
});
}
}
export default { get, post, patch, del };

View File

@ -0,0 +1,59 @@
import { Request, Response } from 'express';
//import validator from 'joi'; // DOCS: https://joi.dev/api
import validator from '../../../../handlers/validation.js';
import { Prisma } from '@prisma/client';
// MARK: GET transaction
const schema_get = validator
.object({
sort: validator
.string()
.valid(...Object.keys(Prisma.TransactionsScalarFieldEnum))
.default('id'),
order: validator.string().valid('asc', 'desc').default('asc'),
take: validator.number().min(1).max(512),
skip: validator.number().min(0),
id: validator.number().positive().precision(0),
user_id: validator.number().positive().precision(0),
paid: validator.boolean().note('true-> Only paid / false-> Only unpaid / undefined-> both')
})
.nand('id', 'user_id'); // Allow id or user_id. not both.
// MARK: CREATE transaction
const schema_post = validator.object({
products: validator.array().items(validator.number().positive().precision(0)).required(),
user_id: validator.number().positive().precision(0).required(),
paid: validator.boolean().default(false)
});
// MARK: UPDATE transaction
const schema_patch = validator.object({
id: validator.number().positive().precision(0).required(),
user_id: validator.number().positive().precision(0).required(),
paid: validator.boolean().default(false)
});
// MARK: DELETE transaction
const schema_del = validator.object({
id: validator.number().positive().precision(0).required()
});
// Describe all schemas
const schema_get_desc = schema_get.describe();
const schema_post_desc = schema_post.describe();
const schema_patch_desc = schema_patch.describe();
const schema_del_desc = schema_del.describe();
// GET route
export default async function get(req: Request, res: Response) {
res.status(200).json({
GET: schema_get_desc,
POST: schema_post_desc,
PATCH: schema_patch_desc,
DELETE: schema_del_desc
});
}
export { schema_get, schema_post, schema_patch, schema_del };

View File

@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
import log from '../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import { parseDynamicSortBy } from '../../../../helpers/prisma_helpers.js';
import { schema_get, schema_post, schema_patch, schema_del } from './user_schema.js';
// MARK: GET user
@ -39,11 +39,11 @@ async function get(req: Request, res: Response) {
if (result.length !== 0) {
result.forEach((element: { id: number; name: string; code: string | null | boolean }) => {
// code-> true if code is set
element.code = element.code !== null;
element.code = !(element.code === '' || element.code === null || element.code === undefined); // Check if nullish
});
res.status(200).json({ count, result });
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified user' });
}
})
.catch((err) => {
@ -69,11 +69,13 @@ async function get(req: Request, res: Response) {
if (result.length !== 0) {
result.forEach((element: { id: number; name: string; code: string | null | boolean }) => {
// code-> true if code is set
element.code = element.code !== null;
//log.api?.debug('"' + element.code + '"');
//log.api?.debug(!(element.code === ''), !(element.code === null), !(element.code === undefined));
element.code = !(element.code === '' || element.code === null || element.code === undefined); // Check if nullish
});
res.status(200).json({ count, result });
} else {
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' });
res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find any users' });
}
})
.catch((err) => {
@ -95,7 +97,8 @@ async function post(req: Request, res: Response) {
.create({
data: {
name: value.name,
code: value.code
email: value.email,
code: value.code === '0000' ? null : value.code
},
select: {
id: true
@ -125,7 +128,8 @@ async function patch(req: Request, res: Response) {
},
data: {
name: value.name,
code: (value.code === '0000') ? null : value.code
email: value.email,
code: value.code === '0000' ? null : value.code
},
select: {
id: true
@ -156,7 +160,8 @@ async function del(req: Request, res: Response) {
})
.then((result) => {
res.status(200).json({ status: 'DELETED', message: 'Successfully deleted user', id: result.id });
}).catch((err) => {
})
.catch((err) => {
handlePrismaError(err, res, 'DEL user');
});
}

View File

@ -0,0 +1,36 @@
import { Request, Response } from 'express';
import db, { handlePrismaError } from '../../../../handlers/db.js'; // Database
import log from '../../../../handlers/log.js';
import { schema_get } from './user_codecheck_schema.js';
// MARK: GET user codecheck
async function get(req: Request, res: Response) {
const { error, value } = schema_get.validate(req.query);
if (error) {
log.api?.debug('GET user codecheck Error:', req.query, value, error.details[0].message);
res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
} else {
log.api?.debug('GET user codecheck Success:', req.query, value);
await db.user
.findUnique({
where: {
id: value.id
}
})
.catch((err) => {
handlePrismaError(err, res, 'GET user codecheck');
})
.then((result) => {
// user has no code OR code must match
// result?.code === '' -> user exists and has no code
// result?.code === null -> user exists and has no code
// result?.code === undefined -> user does not exists
// value.code === result?.code -> If user exists and has no code this matches
res.status(200).json(result?.code === '' || result?.code === null || result?.code === undefined || result?.code === value.code);
//log.api?.debug(result, result?.code);
});
}
}
export default { get };

View File

@ -0,0 +1,32 @@
import { Request, Response } from 'express';
import validator from 'joi'; // DOCS: https://joi.dev/api
// MARK: GET user codecheck
const schema_get = validator.object({
id: validator.number().positive().precision(0).required(),
code: validator
.string()
.min(4)
.max(4)
.trim()
.regex(new RegExp(/^[0-9]+$/))
.required()
});
// Describe all schemas
const schema_get_desc = schema_get.describe();
// const schema_post_desc = schema_post.describe();
// const schema_patch_desc = schema_patch.describe();
// const schema_del_desc = schema_del.describe();
// GET route
export default async function get(req: Request, res: Response) {
res.status(200).json({
GET: schema_get_desc
// POST: schema_post_desc,
// PATCH: schema_patch_desc,
// DELETE: schema_del_desc
});
}
export { schema_get }; //, schema_post, schema_patch, schema_del };

View File

@ -22,6 +22,7 @@ const schema_get = validator
// MARK: CREATE user
const schema_post = validator.object({
name: validator.string().min(1).max(32).required(),
email: validator.string().email().trim().required(),
code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
});
@ -30,9 +31,10 @@ const schema_patch = validator
.object({
id: validator.number().positive().precision(0).required(),
name: validator.string().min(1).max(32),
email: validator.string().email().trim(),
code: validator.string().min(4).max(4).trim().regex(new RegExp(/^[0-9]+$/))
})
.or('name', 'code');
.or('name', 'email', 'code');
// MARK: DELETE user
const schema_del = validator.object({

View File

@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("lockscreen", { message: "Hello world from eta!" })
res.render("admin/dashboard")
}
export default { get };

View File

@ -0,0 +1,17 @@
import express from 'express';
// Route imports
import dashboard_route from './dashboard.js';
import users_route from './users.js';
import products_route from './products.js';
import report_route from './report.js';
// Router base is '/admin'
const Router = express.Router({ strict: false });
Router.route('/').get(dashboard_route.get);
Router.route('/users').get(users_route.get);
Router.route('/products').get(products_route.get);
Router.route('/report').get(report_route.get);
export default Router;

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("admin/products")
}
export default { get };

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("admin/reports")
}
export default { get };

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("admin/users")
}
export default { get };

View File

@ -1,25 +1,25 @@
import express from 'express';
import config from '../../handlers/config.js';
// Route imports
import dashboardRoute from './dashboard.js';
import testRoute from './test.js';
import contactRoute from './contact.js';
// import itemsRoute from './items.js';
// import manage_routes from './manage/index.js';
import screensaver_route from './screensaver.js';
import user_select_route from './user_select.js';
import product_select_route from './product_select.js';
import pay_up_route from './pay_up.js';
import test_Route from './test.js';
import adminRouter from './admin/index.js';
// Router base is '/'
const Router = express.Router({ strict: false });
// Router.route('/test').get(testRoute.get);
// Router.route('/items').get(itemsRoute.get);
Router.route('/').get(screensaver_route.get);
Router.route('/user_select').get(user_select_route.get);
Router.route('/product_select').get(product_select_route.get);
Router.route('/pay_up').get(pay_up_route.get);
// Router.route('/:id(\\w{8})').get(skuRoute.get);
// Router.route('/s/:id').get(skuRouteDash.get);
Router.use('/admin', adminRouter);
// Router.use('/manage', manage_routes);
Router.route('/').get(dashboardRoute.get);
Router.route('/dbTest').get(testRoute.get);
Router.route('/contact').get(contactRoute.get);
config.global.devmode && Router.route('/test').get(test_Route.get);
export default Router;

View File

@ -1,7 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("contacts")
res.render("payup")
}
export default { get };

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("product_select")
}
export default { get };

View File

@ -0,0 +1,8 @@
import express, { Request, Response } from 'express';
import config from '../../handlers/config.js';
function get(req: Request, res: Response) {
res.render("screensaver", { apikey: config.global.galleryApiKey })
}
export default { get };

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("user_select")
}
export default { get };

View File

@ -1,25 +1,23 @@
import express from 'express';
import path from 'node:path';
import __path from "../handlers/path.js";
import log from "../handlers/log.js";
import __path from '../handlers/path.js';
import log from '../handlers/log.js';
// Route imports
import frontend_routes from './frontend/index.js';
import api_routes from './api/index.js';
import frontend_router from './frontend/index.js';
import api_router from './api/index.js';
const Router = express.Router({ strict: false });
// static / libs routes
Router.use('/static', express.static(__path + '/static'));
Router.use('/libs/bulma', express.static(path.join(__path, 'node_modules', 'bulma', 'css'))); // http://192.168.221.10:3000/libs/bulma/bulma.css
Router.use('/libs/bulma', express.static(path.join(__path, 'node_modules', 'bulma', 'css'))); // /libs/bulma/bulma.css
Router.use('/libs/jquery', express.static(path.join(__path, 'node_modules', 'jquery', 'dist')));
Router.use('/libs/bootstrap-icons', express.static(path.join(__path, 'node_modules', 'bootstrap-icons')));
// Other routers
Router.use('/api', api_routes);
Router.use('/', frontend_routes);
Router.use('/api', api_router);
Router.use('/', frontend_router);
// Default route.
Router.all('*', function (req, res) {

View File

@ -19,6 +19,12 @@ let _api = {
headers: new Headers({ 'content-type': 'application/json' })
};
const response = await fetch(_apiConfig.basePath + path, options);
if(response.status == 404) {
return {
count: 0,
result: []
}
}
// Handle the response
if (!response.ok) {
console.error('Failed to fetch:', response.statusText);
@ -32,6 +38,9 @@ let _api = {
if (typeof result === 'number') {
return result;
}
if (typeof result === 'boolean') {
return result;
}
console.error('Invalid JSON response');
_testPageFail('Invalid JSON response');
return;
@ -91,13 +100,13 @@ let _api = {
// Handle the response
if (!response.ok) {
_testPageFail(response.statusText);
return;
return -1;
}
const result = await response.json();
// Handle the result, was json valid?
if (!result) {
_testPageFail('Invalid JSON response');
return;
return -1;
}
return result;
@ -163,7 +172,7 @@ function getApiDescriptionByTable(tableName) {
}
}
function returnTableDataByTableName(tableName, search="", orderBy="asc", sort="", take=-1, skip=0) {
function returnTableDataByTableName(tableName, search="", orderBy="asc", sort="", take=-1, skip=0, filters=[]) {
var orderBy = orderBy.toLowerCase();
if(orderBy == "") {
orderBy = "asc";
@ -178,6 +187,10 @@ function returnTableDataByTableName(tableName, search="", orderBy="asc", sort=""
if(skip > 0) {
baseString += "&skip=" + skip;
}
filterKeys = Object.keys(filters);
for(var i = 0; i < filterKeys.length; i++) {
baseString += "&" + filterKeys[i] + "=" + filters[filterKeys[i]];
}
if (search && search.length > 0) {
return _api.get(baseString + '&search=' + search);
@ -187,13 +200,14 @@ function returnTableDataByTableName(tableName, search="", orderBy="asc", sort=""
}
async function getCountByTable(tableName, search="") {
let baseString = tableName + '?count=true';
let baseString = tableName + '';
if (search && search.length > 0) {
baseString += '&search=' + search;
baseString += '?search=' + search;
}
// Stored in `data:count:${tableName}`
let result = await _api.get(baseString);
console.debug('Count result:', result);
result = result.count;
if (typeof result !== 'number') {
_testPageWarn('Count was not a number, was: ' + result);
console.warn('Count was not a number, was: ' + result);
@ -204,6 +218,7 @@ async function getCountByTable(tableName, search="") {
function _testPageFail(reason) {
return;
document.getElementById('heroStatus').classList.remove('is-success');
document.getElementById('heroStatus').classList.add('is-danger');
@ -211,6 +226,8 @@ function _testPageFail(reason) {
}
function _testPageWarn(reason) {
console.warn('API Wrapper Test Warning, reason: ' + reason);
return;
document.getElementById('heroStatus').classList.remove('is-success');
document.getElementById('heroStatus').classList.add('is-warning');

View File

@ -4,7 +4,7 @@
color: #fff;
text-align: center;
bottom: 10%;
right: 5%;
right: 5%; /* Verschiebt die Flexbox weiter nach links */
z-index: 900010;
position: absolute;
display: flex;
@ -12,15 +12,15 @@
align-items: center;
flex-direction: row;
flex-wrap: wrap;
width: 30%;
width: 40%;
margin: 0 auto; /* Stellt sicher, dass die Box zentriert bleibt */
}
#time {
margin-bottom: 0px;
padding-bottom: 0px;
text-align: center;
width: 95%;
width: 140%;
vertical-align: middle;
font-family: monospace;
}
@ -29,3 +29,31 @@ flex-wrap: wrap;
font-size: 50px;
margin-top: -40px;
}
/* HTML: <div class="loader"></div> */
.loader {
height: 200px;
aspect-ratio: 2/3;
--c:no-repeat linear-gradient(#fff 0 0);
background: var(--c), var(--c), var(--c), var(--c);
background-size: 50% 33.4%;
animation: l8 1.5s infinite linear;
}
@keyframes l8 {
0%,
5% {background-position:0 25%,100% 25%,0 75%,100% 75%}
33% {background-position:0 50%,100% 0,0 100%,100% 50%}
66% {background-position:0 50%,0 0,100% 100%,100% 50%}
95%,
100% {background-position:0 75%,0 25%,100% 75%,100% 25%}
}
#credits {
position: absolute;
bottom: 1px;
left: 1px;
mix-blend-mode: difference;
color: white;
font-weight: bold;
}

View File

@ -1,3 +1,3 @@
<svg id="favicon" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="138.654" height="146.519" viewBox="0 0 36.685 38.767">
<path d="M18.775 0A24.388 24.388 0 0 0 6.82 3.115C3.15 5.165-1.91 9.252.736 13.985c.37.66.9 1.221 1.47 1.713 1.532 1.322 2.98.222 4.554-.457.975-.42 1.95-.842 2.922-1.27.434-.19 1.01-.33 1.328-.698.858-.99.494-2.994.05-4.095a27.25 27.25 0 0 1 3.65-1.24v30.828h7.215V7.671c1.05.184 2.438.432 3.266 1.041.387.284.113.908.076 1.297-.08.827-.027 1.817.344 2.581.308.632 1.16.784 1.765 1.008l4.564 1.704c.628.232 1.33.643 1.979.297 2.822-1.507 3.574-5.39 1.843-8.023-1.165-1.77-3.255-3.13-5.035-4.216C27.037 1.107 22.906.014 18.775 0z"/>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cup-straw" viewBox="0 0 16 16">
<path d="M13.902.334a.5.5 0 0 1-.28.65l-2.254.902-.4 1.927c.376.095.715.215.972.367.228.135.56.396.56.82q0 .069-.011.132l-.962 9.068a1.28 1.28 0 0 1-.524.93c-.488.34-1.494.87-3.01.87s-2.522-.53-3.01-.87a1.28 1.28 0 0 1-.524-.93L3.51 5.132A1 1 0 0 1 3.5 5c0-.424.332-.685.56-.82.262-.154.607-.276.99-.372C5.824 3.614 6.867 3.5 8 3.5c.712 0 1.389.045 1.985.127l.464-2.215a.5.5 0 0 1 .303-.356l2.5-1a.5.5 0 0 1 .65.278M9.768 4.607A14 14 0 0 0 8 4.5c-1.076 0-2.033.11-2.707.278A3.3 3.3 0 0 0 4.645 5c.146.073.362.15.648.222C5.967 5.39 6.924 5.5 8 5.5c.571 0 1.109-.03 1.588-.085zm.292 1.756C9.445 6.45 8.742 6.5 8 6.5c-1.133 0-2.176-.114-2.95-.308a6 6 0 0 1-.435-.127l.838 8.03c.013.121.06.186.102.215.357.249 1.168.69 2.438.69s2.081-.441 2.438-.69c.042-.029.09-.094.102-.215l.852-8.03a6 6 0 0 1-.435.127 9 9 0 0 1-.89.17zM4.467 4.884s.003.002.005.006zm7.066 0-.005.006zM11.354 5a3 3 0 0 0-.604-.21l-.099.445.055-.013c.286-.072.502-.149.648-.222"/>
</svg>

Before

Width:  |  Height:  |  Size: 678 B

After

Width:  |  Height:  |  Size: 1.1 KiB

13
static/js/kiosk_mode.js Normal file
View File

@ -0,0 +1,13 @@
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
// TODO: How to start kiosk mode?
if (true) {
console.info('Kiosk mode -> Disabled all external links');
document.querySelectorAll('a').forEach((link) => {
if (link.classList.contains('external-link')) {
link.style.pointerEvents = 'none';
}
});
}
}, 1000);
});

View File

@ -1,22 +1,40 @@
// Image Handler
const baseUrl = "https://api.unsplash.com/photos/random?client_id=[KEY]&orientation=landscape&topics=nature";
const apiKey = "tYOt7Jo94U7dunVcP5gt-kDKDMjWFOGQNsHuhLDLV8k"; // Take from config
const fullUrl = baseUrl.replace("[KEY]", apiKey);
const showModeImage = '/static/media/showModeLockscreen.jpg';
const showModeImage = "/static/media/showModeLockscreen.jpg"
let credits = document.getElementById("credits");
let credits = document.getElementById('credits');
let currentImageHandle;
document.body.addEventListener('click', () => {
window.location.href = '/user_select';
});
// Lock screen or show mode
let screenState = "lock";
let screenState = 'lock';
let cookieScreen = getCookie('screen');
if (cookieScreen) {
screenState = cookieScreen;
}
function handleImage() {
if(screenState === "lock") {
fetch("https://staging.thegreydiamond.de/projects/photoPortfolio/api/getRand.php?uuid=01919dec-b2cd-7adc-8ca2-a071d1169cbc&unsplash=true")
.then(response => response.json())
.then(data => {
if (screenState === 'lock') {
const apiParams = {
// default galery; spring awakens
uuid: '01919dec-b2cd-7adc-8ca2-a071d1169cbc;01953de0-3aa7-71f1-bfff-cbf9488efa64',
unsplash: true,
orientation: 'landscape',
height: window.screen.availHeight,
width: window.screen.availWidth,
cropCenteringMode: 'sm',
apikey: apiKey
};
const apiUrl = `https://photo.thegreydiamond.de/api/images/random.php?${new URLSearchParams(apiParams).toString()}`;
fetch(apiUrl)
.then((response) => response.json())
.then((data) => {
// data = {
// urls: {
// regular: "https://imageproxy.thegreydiamond.de/ra5iqxlyve6HpjNvC1tzG50a14oIOgiWP95CxIvbBC8/sm:1/kcr:1/aHR0cHM6Ly9zdGFn/aW5nLnRoZWdyZXlk/aWFtb25kLmRlL3By/b2plY3RzL3Bob3Rv/UG9ydGZvbGlvL2Rl/bW9IaVJlcy9QMTE5/MDgzMC1zY2hpbGQu/anBn.webp"
@ -30,27 +48,27 @@ function handleImage() {
// }
if (!currentImageHandle) {
// Create a page filling div which contains the image
currentImageHandle = document.createElement("div");
currentImageHandle.style.position = "absolute";
currentImageHandle.style.top = "0";
currentImageHandle.style.left = "0";
currentImageHandle.style.width = "100%";
currentImageHandle.style.height = "100%";
currentImageHandle = document.createElement('div');
currentImageHandle.style.position = 'absolute';
currentImageHandle.style.top = '0';
currentImageHandle.style.left = '0';
currentImageHandle.style.width = '100%';
currentImageHandle.style.height = '100%';
currentImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
currentImageHandle.style.backgroundSize = "cover";
currentImageHandle.style.backgroundSize = 'cover';
currentImageHandle.style.opacity = 1;
} else {
// Create a new div behind the current one and delete the old one when the new one is loaded
let newImageHandle = document.createElement("div");
newImageHandle.style.position = "absolute";
newImageHandle.style.top = "0";
newImageHandle.style.left = "0";
newImageHandle.style.width = "100%";
newImageHandle.style.height = "100%";
let newImageHandle = document.createElement('div');
newImageHandle.style.position = 'absolute';
newImageHandle.style.top = '0';
newImageHandle.style.left = '0';
newImageHandle.style.width = '100%';
newImageHandle.style.height = '100%';
newImageHandle.style.backgroundImage = `url(${data.urls.regular})`;
newImageHandle.style.backgroundSize = "cover";
newImageHandle.style.backgroundSize = 'cover';
newImageHandle.style.opacity = 1;
newImageHandle.style.transition = "1s";
newImageHandle.style.transition = '1s';
newImageHandle.style.zIndex = 19999;
document.body.appendChild(newImageHandle);
@ -61,15 +79,13 @@ function handleImage() {
currentImageHandle = newImageHandle;
}, 1000);
// Set the credits
credits.innerHTML = `Photo by <a href="${data.user.links.html}" target="_blank">${data.user.name}</a> on <a href="https://unsplash.com" target="_blank">Unsplash</a>`;
credits.innerHTML = `"${data.title}" by <a href="${data.user.links.html}" class="external-link" target="_blank">${data.user.name}</a>`;
credits.style.zIndex = 300000;
}
})
.catch(error => {
console.error("Error fetching image: ", error);
.catch((error) => {
console.error('Error fetching image: ', error);
});
} else {
if (currentImageHandle) {
@ -78,16 +94,16 @@ function handleImage() {
return;
}
// Create a new div behind the current one and delete the old one when the new one is loaded
let newImageHandle = document.createElement("div");
newImageHandle.style.position = "absolute";
newImageHandle.style.top = "0";
newImageHandle.style.left = "0";
newImageHandle.style.width = "100%";
newImageHandle.style.height = "100%";
let newImageHandle = document.createElement('div');
newImageHandle.style.position = 'absolute';
newImageHandle.style.top = '0';
newImageHandle.style.left = '0';
newImageHandle.style.width = '100%';
newImageHandle.style.height = '100%';
newImageHandle.style.backgroundImage = `url(${showModeImage})`;
newImageHandle.style.backgroundSize = "cover";
newImageHandle.style.backgroundSize = 'cover';
newImageHandle.style.opacity = 1;
newImageHandle.style.transition = "1s";
newImageHandle.style.transition = '1s';
document.body.appendChild(newImageHandle);
setTimeout(() => {
@ -107,37 +123,22 @@ function handleTimeAndDate() {
month += 1;
let year = time.getFullYear();
let timeHandle = document.getElementById("time");
let dateHandle = document.getElementById("date");
let timeHandle = document.getElementById('time');
let dateHandle = document.getElementById('date');
timeHandle.innerHTML = `${hours < 10 ? "0" + hours : hours}:${minutes < 10 ? "0" + minutes : minutes}:${time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds()}`;
timeHandle.innerHTML = `${hours < 10 ? '0' + hours : hours}:${minutes < 10 ? '0' + minutes : minutes}:${time.getSeconds() < 10 ? '0' + time.getSeconds() : time.getSeconds()}`;
// Datum in format Montag, 22.12.2024
dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? "0" + day : day}.${month < 10 ? "0" + month : month}.${year}`;
dateHandle.innerHTML = `${getDay(time.getDay())}, ${day < 10 ? '0' + day : day}.${month < 10 ? '0' + month : month}.${year}`;
}
function getDay(day) {
switch(day) {
case 0:
return "Sonntag";
case 1:
return "Montag";
case 2:
return "Dienstag";
case 3:
return "Mittwoch";
case 4:
return "Donnerstag";
case 5:
return "Freitag";
case 6:
return "Samstag";
}
return ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'][day];
}
// Set the image handler to run every 10 minutes
setInterval(handleImage, 60 * 1000 * 10);
handleImage();
handleImage()
handleImage();
// Set the time and date handler to run every minute
setInterval(handleTimeAndDate, 500);

View File

@ -1,3 +0,0 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393">
<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,3 +1,27 @@
html, body {
height: 100%;
margin: 0;
display: flex;
flex-direction: column;
}
body {
min-height: 100vh;
}
hidden {
display: none;
}
.notification-container {
position: fixed;
top: 0;
right: 0;
z-index: 1000;
margin: 20px;
}
footer {
margin-top: auto;
padding: 1rem !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@ -26,6 +26,13 @@ var searchFields = document.querySelectorAll('input[data-searchTargetId]');
// Find all modalForms
var modalForms = document.querySelectorAll('form[data-targetTable]');
// Create a floating container for notifications
const notificationContainer = document.createElement('div');
notificationContainer.classList.add('notification-container');
document.body.appendChild(notificationContainer);
let notifications = [];
console.info('Processing single values');
console.info(singleValues);
@ -67,12 +74,13 @@ tables.forEach(async (table) => {
refreshTable(table);
});
});
if(table.getAttribute("data-loadmode") == "manual") {
return;
}
refreshTable(table);
});
async function writeSingelton(element) {
const table = element.getAttribute('data-dataSource');
console.log('Table: ', table, ' Action: ', element.getAttribute('data-dataAction'), ' Element: ', element);
@ -181,6 +189,10 @@ modalForms.forEach((modalForm) => {
console.log('Type: ', rule['args']['type']);
break;
}
case 'email': {
field.setAttribute('type', 'email');
break;
}
}
});
if (flags) {
@ -231,6 +243,7 @@ modalForms.forEach((modalForm) => {
console.log('Response: ', resp);
if (resp['status'] == 'CREATED' || resp['status'] == 'UPDATED') {
console.log('Entry created successfully');
createTemporaryNotification('Eintrag erfolgreich aktualisiert', 'is-success');
modalForm.closest('.modal').classList.remove('is-active');
modalForm.reset();
// Hide loadPhase
@ -251,12 +264,18 @@ modalForms.forEach((modalForm) => {
entryPhase.classList.remove('is-hidden');
}
// TODO: Show error message
createTemporaryNotification('Error while creating entry', 'is-danger');
}
// Find all tables with data-searchTargetId set to table
setTimeout(() => {
if(modalForm.getAttribute('data-extTable') != null) {
refreshTableByName(table);
updateSingeltonsByTableName(table);
} else {
refreshTableByName(document.getElementById(modalForm.getAttribute('data-extTable')));
}
}, 500);
});
});
@ -273,6 +292,7 @@ async function refreshTable(table) {
});
let order = '';
let column = '';
let filters = JSON.parse(table.getAttribute('data-filters')) || {};
ths.forEach((th) => {
if (th.hasAttribute('data-order')) {
order = th.getAttribute('data-order');
@ -301,7 +321,7 @@ async function refreshTable(table) {
if (searchField) {
const value = searchField.value;
const dbTable = table.getAttribute('data-dataSource');
const result = await returnTableDataByTableName(dbTable, value, order, column, take= maxLinesPerPage, skip= start);
const result = await returnTableDataByTableName(dbTable, value, order, column, take=maxLinesPerPage, skip=start, filters);
const totalResultCount = await getCountByTable(dbTable, value);
paginationPassOnPre['dataLength'] = totalResultCount;
var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
@ -310,7 +330,7 @@ async function refreshTable(table) {
clearTable(table);
writeDataToTable(table, data, paginationPassOn);
} else {
const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start);
const result = await returnTableDataByTableName(table.getAttribute('data-dataSource'), undefined, order, column, take= maxLinesPerPage, skip= start, filters);
const resultCount = await getCountByTable(table.getAttribute('data-dataSource'));
paginationPassOnPre['dataLength'] = resultCount;
var magMiddl = managePaginationMiddleware(result, paginationPassOnPre);
@ -355,6 +375,7 @@ function writeDataToTable(table, data, paginationPassOn) {
if(data == undefined || data == null || data.length == 0) {
return;
}
data = data.result
console.log('Writing data to table: ', table, data);
// Get THEAD and TBODY elements
const thead = table.querySelector('thead');
@ -377,6 +398,9 @@ function writeDataToTable(table, data, paginationPassOn) {
actionFields.push(column);
return;
}
if(column.getAttribute('data-type') == 'hidden') {
return;
}
requiredCols.push(column.getAttribute('data-dataCol'));
});
@ -472,9 +496,37 @@ function writeDataToTable(table, data, paginationPassOn) {
const row = data[resultIndex];
const tr = document.createElement('tr');
requiredCols.forEach((column) => {
// console.log('Column: ', column, ' Index: ', columnIndices[column]);
const td = document.createElement('td');
td.innerText = row[column];
// Grab attribute from header
const header = columns[columnIndices[column]];
if(header.getAttribute('data-dataCol') == "FUNC:INLINE") {
try {
// Call data-ColHandler as a function
const handler = window[header.getAttribute('data-ColHandler')];
const result = handler(row);
row[column] = result;
} catch (e) {
console.error('Error in ColHandler: ', e);
}
}
if(header.getAttribute('data-type') == "bool") {
td.innerHTML = row[column] ? '<i class="bi bi-check"></i>' : '<i class="bi bi-x"></i>';
} else if(header.getAttribute('data-type') == "datetime"){
if(row[column] == null) {
td.innerHTML = "";
} else {
td.innerHTML = formatTimestamp(row[column]);
}
}
else {
td.innerHTML = row[column];
}
tr.appendChild(td);
});
// Add action fields
@ -539,6 +591,9 @@ function writeDataToTable(table, data, paginationPassOn) {
if(field.getAttribute('type') == 'submit') {
return;
}
if(field.getAttribute('data-edit-transfer') == 'disable') {
return;
}
field.value = data[field.getAttribute('name')];
});
form.closest('.modal').classList.add('is-active');
@ -556,9 +611,11 @@ function writeDataToTable(table, data, paginationPassOn) {
if(resp['status'] == 'DELETED') {
refreshTable(table);
updateSingeltonsByTableName(table.getAttribute('data-dataSource'));
createTemporaryNotification('Entry deleted successfully', 'is-success');
} else {
// Show error message
// TODO: Show error message
createTemporaryNotification('Error while deleting entry', 'is-danger');
}
}
break;
@ -605,6 +662,9 @@ document.addEventListener('DOMContentLoaded', () => {
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
const $target = $close.closest('.modal');
if($target.data && $target.data.dissmiss == "false") {
return;
}
$close.addEventListener('click', () => {
closeModal($target);
@ -618,3 +678,61 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
});
function createTemporaryNotification(message, type = 'is-success', timeout = 5000) {
const notification = document.createElement('div');
notification.classList.add('notification');
notification.classList.add(type);
notification.innerHTML = message;
notificationContainer.appendChild(notification);
setTimeout(() => {
$(notification).fadeOut(500);
}, timeout);
}
document.addEventListener('DOMContentLoaded', () => {
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
const $notification = $delete.parentNode;
$delete.addEventListener('click', () => {
$notification.parentNode.removeChild($notification);
});
});
});
function setCookie(name, value, days) {
let expires = "";
if(days) {
let date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + value + expires + "; path=/";
}
function getCookie(name) {
let value = "; " + document.cookie;
let parts = value.split("; " + name + "=");
if(parts.length == 2) {
return parts.pop().split(";").shift();
}
}
function eraseCookie(name) {
document.cookie = name + '=; Max-Age=-99999999;';
}
function errorIfAnyUndefined(inp) {
console.log(inp)
for(var i = 0; i < inp.length; i++) {
if(inp[i] == undefined) {
console.error("Missing element!")
createTemporaryNotification("Beim Laden der Seite ist ein Fehler aufgetreten", "is-danger", 90000)
}
}
}
function formatTimestamp(timestamp) {
const date = new Date(timestamp);
return date.toLocaleString();
}

View File

@ -0,0 +1,195 @@
let uploadFileInput = document.getElementById('imgUpload');
let fileName = document.getElementById('fileName');
let imgUploadForm = document.getElementById('imgUploadForm');
let scannerField = document.getElementById('scannerField');
let btn_restock = document.getElementById('btn_restock');
let btn_save_2 = document.getElementById('btn_save_2');
let form_gtin = document.getElementById('form_gtin');
let modal_stage_1 = document.getElementById('modal-stage-1');
let modal_stage_2 = document.getElementById('modal-stage-2');
let modal_stage_3 = document.getElementById('modal-stage-3');
let modal_stage_2_result = document.getElementById("stage-2-result");
let modal_stage_2_amount = document.getElementById("stage-2-amount");
let globalData;
waitingForScan = false;
let currentRestockProduct = null;
function handleImagePresence(row) {
// Check if /api/v1/image?id=row&check returns true
// Needs to be sync
let isThere = false;
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/v1/image?id=${row.id}&check=true`, false);
xhr.send();
if (xhr.status === 200) {
try {
isThere = JSON.parse(xhr.responseText);
} catch (error) {
console.error(error);
isThere = false;
}
}
let pretty = isThere ? '<i class="bi bi-check"></i>' : '<i class="bi bi-x"></i></a>';
const template = `<a href="/api/v1/image?id=${row.id}" target="_blank">${pretty}</a> <i class="bi bi-dot"></i> <button class="btn btn-primary" onclick="uploadImage(${row.id})"><i class="bi bi-upload"></i></button>`;
return template;
}
function uploadImage(id) {
// Open a file picker
uploadFileInput.click();
// // Open a modal to upload an image
// // Use a form
// const modal = document.getElementById('imageModal');
// modal.style.display = 'block';
imgUploadForm.action = `/api/v1/image?id=${id}`;
}
function silentFormSubmit() {
// Submit the form silently (without reloading the page or redirecting)
// Grab the form and do a POST request (dont forget to prevent default)
const xhr = new XMLHttpRequest();
xhr.open('POST', imgUploadForm.action, true);
xhr.send(new FormData(imgUploadForm));
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
//location.reload();
createTemporaryNotification('Bild hochgeladen', 'is-success');
// Close the modal
document.getElementById('imageModal').style.display = "none";
// Empty the input
uploadFileInput.value = '';
}
};
}
function enableScanner() {
waitingForScan = true;
scannerField.focus();
}
uploadFileInput.addEventListener('change', function() {
fileName.innerHTML = this.files[0].name;
silentFormSubmit();
setTimeout(() => {
refreshTableByName('products');
}, 1000);
});
scannerField.style.fontSize = '1px';
scannerField.style.height = '1px';
scannerField.style.width = '1px';
scannerField.style.opacity = '0';
scannerField.style.position = 'relative';
// Make sure text fields is always centerd vertically
window.addEventListener('scroll', function(event) {
if(!waitingForScan) {
return;
}
scannerField.y = document.documentElement.scrollTop + 20;
scannerField.style.top = document.documentElement.scrollTop + 20 + "px";
});
setInterval(() => {
if(!waitingForScan) {
return;
}
scannerField.focus();
}, 1000);
btn_restock.addEventListener('click', function() {
modal_stage_1.classList.remove('is-hidden');
modal_stage_2.classList.add('is-hidden');
modal_stage_3.classList.add('is-hidden');
waitingForScan = true;
});
// Handle barcode scanner input
scannerField.addEventListener('keydown', async function(event) {
if(event.key != 'Enter') {
return;
}
let barcode = scannerField.value;
console.log('Barcode scanned:', barcode);
scannerField.value = "";
// createTemporaryNotification(`Barcode ${barcode} gescannt`, 'is-info');
waitingForScan = false;
// Check if barcode is in the database
let product = globalData.find(p => p.gtin == barcode);
if(product) {
console.log('Product found:', product);
currentRestockProduct = product;
modal_stage_2_amount.innerHTML = "Aktuelle Menge: " + product.stock;
modal_stage_1.classList.add('is-hidden');
modal_stage_2.classList.remove('is-hidden');
modal_stage_3.classList.add('is-hidden');
createTemporaryNotification(`<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} gefunden`, 'is-success');
} else {
modal_stage_1.classList.add('is-hidden');
modal_stage_2.classList.add('is-hidden');
modal_stage_3.classList.remove('is-hidden');
form_gtin.value = barcode;
}
// modal_stage_2_result.innerHTML = product ? `<i class="bi bi-check"></i> Produkt gefunden: ${product.name}` : `<i class="bi bi-x"></i> Produkt nicht gefunden`;
// let product = globalData.find(p => p.gtin == barcode);
// if(product) {
// let event = new Event('click');
// createTemporaryNotification(`<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} gefunden`, 'is-success');
// document.getElementById(`product_${product.id}`).dispatchEvent(event);
// } else {
// createTemporaryNotification( `<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} nicht gefunden`, 'is-danger');
// }
});
function restock(amount) {
currentRestockProduct.stock += amount;
modal_stage_2_amount.innerHTML = "Aktuelle Menge: " + currentRestockProduct.stock;
}
function applyStock() {
let result = _api.patch('products', {
"id": currentRestockProduct.id,
"stock": currentRestockProduct.stock
})
if(result) {
createTemporaryNotification('Bestand erfolgreich aktualisiert', 'is-success');
modal_stage_2.classList.add('is-hidden');
modal_stage_1.classList.remove('is-hidden');
modal_stage_3.classList.add('is-hidden');
enableScanner();
} else {
createTemporaryNotification('Fehler beim Aktualisieren des Bestands', 'is-danger');
}
}
document.addEventListener('DOMContentLoaded', async function() {
let data = await returnTableDataByTableName('products');
console.info(`Found ${data.count} products`);
const result = data.result;
globalData = result;
});
// btn_save_2.addEventListener('click', async function() {
// // Assume submission is valid
// // Get the form data
// // reload table
// // close modal
// });

View File

@ -0,0 +1,2 @@
let elm_table_users = document.getElementById('table_users');

82
static/pages/payup.js Normal file
View File

@ -0,0 +1,82 @@
const tableContent = document.querySelector('.table-content');
// HTML Elements
const isEmptyAlert = document.getElementById("noBalance");
const tableDiv = document.getElementById("balanceSheet");
const payTable = document.getElementById("payTable");
const tableCnt = document.getElementById("table-content");
const tableSum = document.getElementById("table-sum");
const modal_sum = document.getElementById("ModalSum");
const confirmModal = document.getElementById("confirmModal");
const btn_paynow = document.getElementById("paynow");
const btn_confirm = document.getElementById("confirmCheckout");
const btn_logout = document.getElementById("logout");
const table_old = document.getElementById("alltransactions");
errorIfAnyUndefined([isEmptyAlert, tableDiv, payTable, tableCnt, tableSum, modal_sum])
// Current user
let cookieUser = getCookie('user');
if(cookieUser == undefined) {
createTemporaryNotification('Fehler: Nutzer nicht angemeldet.', 'is-danger');
window.location.href = '/user_select';
}
table_old.setAttribute('data-filters', `{"user_id": ${cookieUser}}`);
refreshTable(table_old);
console.log("Table refreshed");
let transactionIds = [];
// Request outstanding transactions by user
async function pullData() {
let data = await _api.get("transaction?user_id=" + parseInt(cookieUser) + "&paid=false");
console.log(data)
if(data.count == 0) {
isEmptyAlert.classList.remove("is-hidden");
tableDiv.classList.add("is-hidden");
return;
}
// Write data to table
const result = data.result;
let priceSum = 0;
for(var i = 0; i < data.count; i++) {
const row = result[i];
const newRow = tableCnt.insertRow();
newRow.id = `row_${row.id}`;
newRow.innerHTML = `
<td>${formatTimestamp(row.createdAt)}</td>
<td>${parseFloat(row.total).toFixed(2)} €</td>
`;
priceSum += parseFloat(row.total);
transactionIds.push(row.id);
}
tableSum.innerText = priceSum.toFixed(2) + " €";
modal_sum.innerText = priceSum.toFixed(2) + " €";
}
btn_paynow.onclick = () => {
confirmModal.classList.add("is-active");
}
btn_confirm.onclick = () => {
for(let i = 0; i < transactionIds.length; i++) {
let res = _api.patch(`transaction`, {paid: true, id: transactionIds[i], user_id: parseInt(cookieUser)});
console.log(res);
if(res == -1 || res == undefined) {
createTemporaryNotification('Fehler: Zahlung fehlgeschlagen.', 'is-danger');
return;
}
}
createTemporaryNotification('Zahlung erfolgreich.', 'is-success');
setTimeout(() => {
window.location.href = '/user_select';
}, 1000);
}
btn_logout.onclick = () => {
eraseCookie('user');
window.location.href = '/user_select';
}
pullData()

View File

@ -0,0 +1,230 @@
console.log('product_select.js loaded');
// Get containers
let mainSelectionDiv = document.getElementById('mainSelect');
let checkoutTable = document.getElementById('selectedProducts');
let sumField = document.getElementById('TableSum');
let toCheckoutButton = document.getElementById('checkout');
let confirmCartButton = document.getElementById('confirmCheckout');
let loadingModal = document.getElementById('loadingModal');
let scannerField = document.getElementById('scannerField');
const baseStruct = document.getElementById("baseStruct");
let globalData;
let shoppingCart = [];
toCheckoutButton.addEventListener('click', finalizeTransaction);
confirmCartButton.addEventListener('click', confirmedCart);
// Get user from url (and cookie)
let userFCookie = getCookie('user');
let userFUrl = new URLSearchParams(window.location.search).get('user');
if(userFCookie != userFUrl) {
createTemporaryNotification('Fehler: User nicht korrekt gesetzt!', 'is-danger');
window.location.href = '/user_select';
}
// On load
document.addEventListener('DOMContentLoaded', async function() {
let data = await returnTableDataByTableName('products');
console.info(`Found ${data.count} products`);
const result = data.result;
globalData = result;
for(let i = 0; i < result.length; i++) {
let product = result[i];
if(product.visible && product.stock > 0) {
let newDiv = baseStruct.cloneNode(true);
newDiv.id = `product_${product.id}`;
newDiv.style.display = 'block';
newDiv.querySelector('.product_name').innerText = product.name;
newDiv.querySelector('.product_description').innerText = product.description || "";
let price = parseFloat(product.price).toFixed(2);
newDiv.querySelector('.product_price').innerText = price + " €";
newDiv.querySelector('.product_ean').innerText = product.gtin;
newDiv.querySelector('.product_image').src = "/api/v1/image?id=" + product.id;
newDiv.querySelector('.product_image').alt = product.name;
newDiv.addEventListener('click', selectProductEvent);
mainSelectionDiv.appendChild(newDiv);
}
}
});
function canIAddProduct(product, shoppingCart) {
let stock = product.stock;
let count = shoppingCart.filter(p => p.id == product.id).length;
return count < stock;
}
function selectProductEvent(e) {
console.log('selectProductEvent', e);
let id = e.currentTarget.id.split('_')[1];
let product = globalData.find(p => p.id == id);
if(!canIAddProduct(product, shoppingCart)) {
createTemporaryNotification('Nicht genug Lagerbestand mehr vorhanden!', 'is-danger');
return;
}
let price = parseFloat(product.price).toFixed(2);
let row = checkoutTable.insertRow();
row.id = `product_${product.id}`;
let cell1 = row.insertCell(0); // Name
let cell2 = row.insertCell(1); // Price
let cell3 = row.insertCell(2); // Actions
shoppingCart.push(product);
cell1.innerText = product.name;
cell2.innerText = price + " €";
let deleteButton = document.createElement('button');
deleteButton.innerHTML = '<i class="bi bi-trash"></i>';
deleteButton.onclick = deleteProductEvent;
deleteButton.className = 'button is-danger';
deleteButton.style.color = 'white';
cell3.appendChild(deleteButton);
sumField.innerText = calculateSum(shoppingCart);
}
function calculateSum(cart) {
let sum = 0;
for(let i = 0; i < cart.length; i++) {
sum += parseFloat(cart[i].price);
}
return sum.toFixed(2) + " €";
}
function deleteProductEvent(e) {
let row = e.target.parentElement.parentElement;
// Check if icon was clicked instead of button
if(row.tagName != 'TR') {
row = e.target.parentElement.parentElement.parentElement;
}
let id = row.id.split('_')[1];
let product = shoppingCart.find(p => p.id == id);
let index = shoppingCart.indexOf(product);
shoppingCart.splice(index, 1);
row.remove();
sumField.innerText = calculateSum(shoppingCart);
}
function finalizeTransaction() {
if(shoppingCart.length == 0) {
return;
}
// Show confirmation dialog (id-> checkoutModal)
let modal = document.getElementById('checkoutModal');
modal.classList.add('is-active');
let modalContent = document.getElementById('modalContent');
// Grab table in modal
let modalTable = document.getElementById('selectedProductsModal');
modalTable.innerHTML = "";
for(let i = 0; i < shoppingCart.length; i++) {
let product = shoppingCart[i];
let row = modalTable.insertRow();
let cell1 = row.insertCell(0); // Name
let cell2 = row.insertCell(1); // Price
cell1.innerText = product.name;
cell2.innerText = parseFloat(product.price).toFixed(2) + " €";
}
let modalSum = document.getElementById('ModalSum');
modalSum.innerText = calculateSum(shoppingCart);
}
function confirmedCart() {
// Close modal
let modal = document.getElementById('checkoutModal');
modal.classList.remove('is-active');
// Show loading modal
loadingModal.classList.add('is-active');
// Send data to server
// alert('NYI: Send data to server. This demo ends here.');
let listOfIds = shoppingCart.map(p => p.id);
let data = {
products: listOfIds,
user_id: getCookie('user')
};
// Send data to server
fetch('/api/v1/transaction', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(async (response) => {
let json = await response.json();
if(response.ok) {
createTemporaryNotification('<i class="bi bi-check-lg"></i> Erfolgreich abgeschlossen', 'is-success');
setTimeout(() => {
window.location.href = '/user_select';
}, 1000);
} else {
createTemporaryNotification('Fehler: ' + json.error, 'is-danger');
}
loadingModal.classList.remove('is-active');
}).catch((error) => {
createTemporaryNotification('Fehler: ' + error, 'is-danger');
loadingModal.classList.remove('is-active');
});
}
// Handle barcode scanner
// Force the cursor to the scanner field
scannerField.focus();
// Do so in an interval
setInterval(() => {
scannerField.focus();
}, 1000);
// Make it tiny
scannerField.style.fontSize = '1px';
scannerField.style.height = '1px';
scannerField.style.width = '1px';
scannerField.style.opacity = '0';
scannerField.style.position = 'relative';
// Handle barcode scanner input
scannerField.addEventListener('keydown', async function(event) {
if(event.key != 'Enter') {
return;
}
let barcode = scannerField.value;
console.log('Barcode scanned:', barcode);
scannerField.value = "";
// createTemporaryNotification(`Barcode ${barcode} gescannt`, 'is-link');
let product = globalData.find(p => p.gtin == barcode);
if(product) {
let event = new Event('click');
createTemporaryNotification(`<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} gefunden`, 'is-success', 2000);
document.getElementById(`product_${product.id}`).dispatchEvent(event);
} else {
createTemporaryNotification( `<i class="bi bi-upc-scan"></i> Barcode scan: GTIN ${barcode} nicht gefunden`, 'is-danger');
}
});
// Make sure text fields is always centerd vertically
window.addEventListener('scroll', function(event) {
scannerField.y = document.documentElement.scrollTop + 20;
scannerField.style.top = document.documentElement.scrollTop + 20 + "px";
});

155
static/pages/user_select.js Normal file
View File

@ -0,0 +1,155 @@
console.log('user_select.js loaded');
// Get containers
let mainSelectionDiv = document.getElementById('mainSelect');
let pinPadModal = document.getElementById('pinPadModal');
let numpad = document.getElementById('numpad');
let pinInput1 = document.getElementById('pinInput1');
let pinInput2 = document.getElementById('pinInput2');
let pinInput3 = document.getElementById('pinInput3');
let pinInput4 = document.getElementById('pinInput4');
let pinError = document.getElementById('pinError');
let lastActivity = new Date().getTime();
let lastActivityTimeout = 1000 * 60 * 5; // 5 minutes
let currentUser = null;
document.addEventListener('click', function() {
lastActivity = new Date().getTime();
});
setInterval(function() {
let now = new Date().getTime();
if(now - lastActivity > lastActivityTimeout) {
window.location.href = '/';
}
}, 1000);
// Attach event listeners to all numpad buttons
let numpadButtons = numpad.getElementsByTagName('button');
for(let i = 0; i < numpadButtons.length; i++) {
let button = numpadButtons[i];
button.addEventListener('click', handleNumberClick);
}
// On keyboard input for pinInputX jump to next input field
pinInput1.addEventListener('input', function() {
if(pinInput1.value) {
pinInput2.focus();
// Update pinValue
pinValue = pinInput1.value;
}
});
pinInput2.addEventListener('input', function() {
if(pinInput2.value) {
pinInput3.focus();
pinValue = pinInput1.value + pinInput2.value;
}
}
);
pinInput3.addEventListener('input', function() {
if(pinInput3.value) {
pinInput4.focus();
pinValue = pinInput1.value + pinInput2.value + pinInput3.value;
}
});
pinInput4.addEventListener('input', function() {
if(pinInput4.value) {
pinValue = pinInput1.value + pinInput2.value + pinInput3.value + pinInput4.value;
validatePin();
}
});
let baseStruct = document.getElementById("baseStruct");
let globalData;
let pinValue = "";
// On load
document.addEventListener('DOMContentLoaded', async function() {
let data = await returnTableDataByTableName('user');
console.info(`Found ${data.count} users`);
const result = data.result;
globalData = result;
for(let i = 0; i < result.length; i++) {
let user = result[i];
let userDiv = baseStruct.cloneNode(true);
userDiv.id = `user_${user.id}`;
userDiv.innerHTML = user.name;
userDiv.addEventListener('click', handleUserClick);
mainSelectionDiv.appendChild(userDiv);
}
});
function handleUserClick(e) {
let userDiv = e.target;
let userId = userDiv.id.split('_')[1];
console.log(`Clicked on user with id ${userId}`);
let user = globalData.find(u => u.id == userId);
currentUser = user;
console.log(user);
if(user.code) {
pinPadModal.classList.add('is-active');
// Automatically focus on the first input field
pinInput1.focus();
} else {
pinValue = "0000";
validatePin();
}
}
function validatePin() {
let pin = pinValue;
let userId = currentUser.id;
console.log(`Validating pin ${pinValue} for user ${userId}`);
_api.get("/user/codecheck?code=" + pinValue + "&id=" + userId).then((response) => {
if(response) {
console.log("Pin is correct");
pinPadModal.classList.remove('is-active');
// Write a cookie
document.cookie = `user=${userId}`;
document.cookie = `name=${currentUser.name}`;
window.location.href = `/product_select?user=${userId}`;
} else {
console.log("Pin is incorrect");
pinValue = "";
updatePinFields();
createTemporaryNotification('Fehlerhafte PIN Eingabe!', 'is-danger');
}
});
}
function handleNumberClick(e) {
let number = e.target.innerHTML;
if(number == '<i class="bi bi-backspace-fill"></i>' || e.target.classList.contains('bi-backspace-fill')) {
pinValue = pinValue.slice(0, -1);
updatePinFields();
return;
}
if(number == '<i class="bi bi-check"></i>' || e.target.classList.contains('bi-check')) {
validatePin();
return;
}
if(pinValue.length >= 4) {
return;
}
pinValue += number;
updatePinFields();
}
function updatePinFields() {
let pin = pinValue;
pinInput1.value = pin[0] || "";
pinInput2.value = pin[1] || "";
pinInput3.value = pin[2] || "";
pinInput4.value = pin[3] || "";
}

24
views/admin/dashboard.eta Normal file
View File

@ -0,0 +1,24 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<section class="section container" id="mainSelect">
<h1 class="title">Administration</h1>
<!-- Big buttons linking to the different admin pages (Produkte, Benutzer, Bericht) -->
<div class="columns is-centered">
<div class="column is-4">
<a href="/admin/products" class="button is-large is-fullwidth is-primary">Produkte</a>
</div>
<div class="column is-4">
<a href="/admin/users" class="button is-large is-fullwidth is-primary">Benutzer</a>
</div>
<div class="column is-4">
<a href="/admin/report" class="button is-large is-fullwidth is-primary">Berichte</a>
</div>
</div>
</section>
<%~ include("partials/footer.eta") %>
<!-- <script src="/static/pages/admin_.js"></script>-->
<%~ include("partials/base_foot.eta") %>

236
views/admin/products.eta Normal file
View File

@ -0,0 +1,236 @@
<%~ include("partials/base_head.eta", {"title": "Admin - Benutzer"}) %>
<%~ include("partials/nav.eta") %>
<input id="scannerField" type="text"/>
<section class="section container" id="mainSelect">
<h1 class="title">Produktverwaltung</h1>
<p class="heading buttons">
<button class="js-modal-trigger button" data-target="modal-js-example">
Neues Produkt anlegen
</button><button class="js-modal-trigger button" data-target="modal-restock" id="btn_restock">
Lager nachfüllen / Anpassen
</button><br></p>
<input class="input" type="text" data-searchTargetId="productTable" placeholder="Nach Produkt suchen.." />
<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="products" id="productTable" data-pageSize="10">
<thead>
<tr>
<th data-dataCol = "id">ID</th>
<th data-dataCol = "name">Name</th>
<th data-dataCol = "gtin">GTIN</th>
<th data-dataCol = "price">Preis</th>
<th data-dataCol = "stock">Lagermenge</th>
<th data-dataCol = "visible" data-type="bool">Sichtbarkeit</th>
<th data-dataCol = "FUNC:INLINE" data-ColHandler=handleImagePresence>Bild</th>
<th data-fnc="actions" data-actions="edit,delete">Aktionen</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="productTable">
<ul class="pagination-list">
</ul>
</nav>
</section>
<!-- Image upload modal -->
<div id="imageModal" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<form id="imgUploadForm" enctype="multipart/form-data" method="post" action="/api/v1/image">
<h2 class="title">Bild hochladen</h1>
<div class="file has-name">
<label class="file-label">
<input id="imgUpload" class="file-input" type="file" name="image" />
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-upload"></i>
</span>
<span class="file-label"> Datei wählen… </span>
</span>
<span class="file-name" id="fileName"></span>
</label>
</div>
<br>
</form>
<div class="control">
<input type="button" class="button is-link" value="Hochladen" onclick="silentFormSubmit()">
</div>
</div>
</div>
</div>
<!-- TODO: Mark required fields as required; add handling for validation -->
<div id="modal-js-example" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box entryPhase is-hidden">
<h2 class="title">Neuer Kontakt</h1>
<i class="bi bi-arrow-clockwise title"></i>
</div>
<div class="box entryPhase">
<form data-targetTable="products">
<h2 class="title">Neuer Benutzer</h1>
<div class="field">
<label class="label">Bezeichner</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="John Doe" value="" name="name">
<span class="icon is-small is-left">
<i class="bi bi-file-earmark-person-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">GTIN</label>
<div class="control has-icons-left">
<input class="input" type="number" placeholder="" value="" name="gtin">
<span class="icon is-small is-left">
<i class="bi bi-upc"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Lagermenge</label>
<div class="control has-icons-left">
<input class="input" type="number" placeholder="" value="" name="stock">
<span class="icon is-small is-left">
<i class="bi bi-archive-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Preis</label>
<div class="control has-icons-left">
<input class="input" type="number" placeholder="" value="" step=0.01 name="price">
<span class="icon is-small is-left">
<i class="bi bi-currency-euro"></i>
</span>
</div>
</div>
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" value="" name="visible">
In der Liste anzeigen</a>
</label>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<input type="submit" class="button is-link" value="Save" data-actionBtn="save">
</div>
<!--<div class="control">
<button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button>
</div>-->
</div>
</form>
</div>
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<div id="modal-restock" class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box" id="modal-stage-1">
<h2 class="title">Nachfüllen</h1>
<center><h1 class="title"><i class="bi bi-upc-scan"></i></h1></center>
Warten auf Scan....
</div>
<div class="box" id="modal-stage-2">
<h2 class="title">Scan erfolgreich - Produktmenge eingeben</h1>
<h3 class="subtitle" id="stage-2-amount">Aktuelle Menge: 0</h3>
<div class="buttons">
<button class="button is-info" onclick="restock(-1)">-1</button>
<button class="button is-info" onclick="restock(1)">+1</button>
<button class="button is-info" onclick="restock(6)">+6</button>
<button class="button is-info" onclick="restock(10)">+10</button>
<button class="button is-info" onclick="restock(12)">+12</button>
</div>
<button class="button is-success" onclick="applyStock()">Änderungen speichern</button>
<div id="stage-2-result"></div>
</div>
<div class="box" id="modal-stage-3">
<h2 class="title">Scan erfolgreich - Produkt erstellen</h1>
<form data-targetTable="products">
<div class="field">
<label class="label">Bezeichner</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="Schokolade" value="" name="name">
<span class="icon is-small is-left">
<i class="bi bi-file-earmark-person-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">GTIN</label>
<div class="control has-icons-left">
<input id="form_gtin" class="input" type="number" placeholder="" value="" name="gtin" readonly>
<span class="icon is-small is-left">
<i class="bi bi-upc"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Lagermenge</label>
<div class="control has-icons-left">
<input class="input" type="number" placeholder="" value="" name="stock">
<span class="icon is-small is-left">
<i class="bi bi-archive-fill"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Preis</label>
<div class="control has-icons-left">
<input class="input" type="number" placeholder="" value="" step=0.01 name="price">
<span class="icon is-small is-left">
<i class="bi bi-currency-euro"></i>
</span>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<input type="submit" class="button is-link" value="Save" data-actionBtn="save" data-extTable="productTable" id="btn_save_2">
</div>
<!--<div class="control">
<button type="button" class="button is-link is-light" data-actionBtn="cancel">Cancel</button>
</div>-->
</div>
</form>
</div>
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<script src="/static/pages/admin_products.js"></script>
<%~ include("partials/footer.eta") %>
<%~ include("partials/base_foot.eta") %>

36
views/admin/reports.eta Normal file
View File

@ -0,0 +1,36 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<section class="section container" id="mainSelect">
<h1 class="title">Berichte</h1>
<!-- Big buttons linking to the different admin pages (Produkte, Benutzer, Bericht) -->
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Benutzer</p>
<p class="title"><span data-dataSource="user" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Transaktionen</p>
<p class="title"><span data-dataSource="transaction" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Produkte</p>
<p class="title"><span data-dataSource="products" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
</nav>
<div class="columns is-centered">
</div>
</section>
<%~ include("partials/footer.eta") %>
<!-- <script src="/static/pages/admin_.js"></script>-->
<%~ include("partials/base_foot.eta") %>

View File

@ -1,27 +1,29 @@
<%~ include("partials/base_head.eta", {"title": "Kontakte"}) %>
<%~ include("partials/base_head.eta", {"title": "Admin - Benutzer"}) %>
<%~ include("partials/nav.eta") %>
<section class="hero is-primary" id="heroStatus">
<div class="hero-body">
<p class="title" data-tK="start-hero-header-welcome">Kontaktverwaltung</p>
<p class="subtitle" data-tK="start-hero-header-subtitle-default" id="heroExplainer">Erklärungstext</p>
</div>
</section>
<section class="section">
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">Kontakte</p>
<p class="title"><span data-dataSource="AlertContacts" data-dataAction="COUNT" class="is-skeleton">Load.</span></p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<section class="section container" id="mainSelect">
<h1 class="title">Benutzerverwaltung</h1>
<p class="heading"><button class="js-modal-trigger button" data-target="modal-js-example">
Neuen Konakt anlegen
Benutzer anlegen
</button></p>
</div>
</div>
<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="user" id="userTable" data-pageSize="10">
<thead>
<tr>
<th data-dataCol = "id">Id</th>
<th data-dataCol = "name">Name</th>
<th data-dataCol = "email">E-Mail</th>
<th data-dataCol = "code" data-type="bool" data-edit-transfer="disable">Code</th>
<th data-fnc="actions" data-actions="edit,delete">Aktionen</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="userTable">
<ul class="pagination-list">
</ul>
</nav>
</section>
@ -38,8 +40,8 @@
</div>
<div class="box entryPhase">
<form data-targetTable="AlertContacts">
<h2 class="title">Neuer Kontakt</h1>
<form data-targetTable="user">
<h2 class="title">Neuer Benutzer</h1>
<div class="field">
<label class="label">Name</label>
<div class="control has-icons-left">
@ -51,19 +53,19 @@
</div>
<div class="field">
<label class="label">Telefonummer</label>
<label class="label">E-Mail</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="+49 1234 5678912" value="" name="phone">
<input class="input" type="email" placeholder="test@example.org" value="" name="email">
<span class="icon is-small is-left">
<i class="bi bi-telephone-fill"></i>
<i class="bi bi-envelope"></i>
</span>
</div>
</div>
<div class="field">
<label class="label">Anmerkung</label>
<label class="label">Pin</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="" value="" name="comment">
<input class="input" type="text" placeholder="" value="" name="code">
<span class="icon is-small is-left">
<i class="bi bi-chat-fill"></i>
</span>
@ -89,31 +91,6 @@
</div>
<section class="section">
<h1 class="title" data-tK="start-recent-header">Kontaktübersicht</h1>
<input class="input" type="text" data-searchTargetId="contactTable" placeholder="Search..." />
<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="AlertContacts" id="contactTable" data-pageSize="5">
<thead>
<tr>
<th data-dataCol = "id"><abbr title="Position">Pos</abbr></th>
<th data-dataCol = "name">Name</th>
<th data-dataCol = "phone">Telefonnummer</th>
<th data-dataCol = "comment">Kommentar</th>
<th data-fnc="actions" data-actions="edit,delete">Aktionen</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="contactTable">
<ul class="pagination-list">
</ul>
</nav>
</section>
<%~ include("partials/footer.eta") %>
<script src="/static/pages/admin_users.js"></script>
<%~ include("partials/base_foot.eta") %>

View File

@ -1,5 +1,5 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<!--<%~ include("partials/nav.eta") %>-->
<section class="hero is-primary">
<div class="hero-body">

View File

@ -1,4 +1,2 @@
<script src="/static/apiWrapper.js"></script>
<script src="/static/pageDriver.js"></script>
</body>
</html>

View File

@ -1,9 +1,12 @@
<footer class="footer">
<div class="content has-text-centered">
<p>
<i class="bi bi-universal-access"></i>
<strong>HydrationHUB</strong> by <a href="https://pnh.fyi">[Project-name-here]</a>.<br>
<i class="bi bi-cup-straw"></i>
<strong>HydrationHUB</strong> by <a target="_blank" rel="noopener noreferrer" href="https://pnh.fyi" class="external-link">[Project-name-here]</a>.<br>
Running Version <span data-dataSource="version" data-dataAction="SPECIAL" class="is-skeleton">Load.</span>
</p>
</div>
</footer>
<script src="/static/apiWrapper.js"></script>
<script src="/static/pageDriver.js"></script>
<script src="/static/js/kiosk_mode.js"></script>

View File

@ -1,11 +1,10 @@
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item primary" href="/">
<svg id="logo" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="498.424" height="148.888" viewBox="0 0 131.875 39.393">
<path d="M118.368 51.177c-3.682 0-6.537.32-8.566.958-1.99.6-3.419 1.635-4.283 3.099-.827 1.466-1.239 3.533-1.239 6.2 0 2.03.356 3.7 1.07 5.016.714 1.315 1.916 2.46 3.607 3.438 1.728.939 4.17 1.878 7.326 2.817 2.517.752 4.452 1.466 5.805 2.142 1.39.64 2.386 1.352 2.987 2.142.601.79.9 1.747.9 2.874 0 1.24-.224 2.198-.675 2.874-.451.64-1.202 1.09-2.254 1.353-1.052.263-2.536.375-4.452.338-1.916-.038-3.4-.226-4.453-.564-1.051-.376-1.822-.977-2.31-1.804-.451-.826-.733-2.01-.845-3.55h-7.045c-.113 3.157.263 5.598 1.127 7.327.864 1.728 2.348 2.95 4.452 3.663 2.142.714 5.166 1.07 9.074 1.07 3.795 0 6.706-.318 8.735-.958 2.066-.638 3.532-1.728 4.396-3.268.864-1.54 1.296-3.72 1.296-6.538 0-2.254-.357-4.095-1.07-5.522-.715-1.466-1.917-2.706-3.608-3.72-1.653-1.015-4.02-2.01-7.1-2.987-2.518-.79-4.49-1.485-5.918-2.085-1.39-.6-2.404-1.202-3.043-1.804-.639-.6-.959-1.277-.959-2.028 0-1.165.207-2.049.62-2.649.414-.601 1.09-1.033 2.03-1.296.976-.263 2.366-.395 4.17-.395 1.728 0 3.061.15 4.001.45.977.264 1.672.734 2.085 1.41.451.638.733 1.578.846 2.818h7.157c.038-2.856-.376-5.054-1.24-6.594-.863-1.54-2.292-2.63-4.283-3.27-1.954-.637-4.734-.957-8.34-.957zm-67.058.12a24.388 24.388 0 0 0-11.954 3.114c-3.67 2.051-8.73 6.137-6.085 10.87.37.66.9 1.222 1.47 1.714 1.53 1.322 2.98.222 4.554-.458.975-.42 1.95-.842 2.922-1.268.433-.19 1.01-.331 1.328-.7.858-.99.494-2.994.05-4.094a27.22 27.22 0 0 1 3.651-1.24v30.828h7.214V58.968c1.05.182 2.439.43 3.266 1.04.387.285.113.91.075 1.298-.08.827-.027 1.816.345 2.58.307.632 1.16.785 1.765 1.009l4.564 1.703c.628.233 1.33.644 1.979.298 2.822-1.508 3.574-5.39 1.842-8.023-1.164-1.771-3.254-3.13-5.034-4.216-3.69-2.254-7.822-3.347-11.952-3.36zm-39.287.443L1.146 90.063h7.045l2.423-8.453h12.962l2.48 8.453h7.101L22.055 51.74H12.023zm67.628.001L68.773 90.063h7.045l2.423-8.453h12.964l2.48 8.453h7.1L89.683 51.74H79.65zm-62.668 6.537h.056l4.903 17.076h-9.637l4.678-17.076zm67.628 0h.056l4.903 17.076h-9.637l4.678-17.076z" style="display:inline;fill:current;fill-opacity:1;stroke:none;stroke-width:.408654;stroke-opacity:1" transform="translate(-1.146 -51.177)"/>
</svg>
<i class="bi bi-cup-straw"></i>
</a>
<a class="navbar-item primary is-hidden" id="nav_username" href="/">
<strong>Hey, <span id="nav_usernameContent"></span></strong>
</a>
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
@ -17,31 +16,83 @@
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item" href="/">Home</a>
<a class="navbar-item" href="/dbTest">API Integration <span class="tag is-info">Dev</span></a>
<a class="navbar-item" href="/contact">Kontakte <span class="tag is-primary">Neu!</span></a>
<!--<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">More</a>
<div class="navbar-dropdown">
<a class="navbar-item">About</a>
<a class="navbar-item is-selected">Jobs</a>
<a class="navbar-item">Contact</a>
<hr class="navbar-divider">
<a class="navbar-item">Report an issue</a>
</div>
</div>-->
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary">
<strong>Sign up</strong>
</a>
<a class="button is-light">Log in</a>
</div>
<div class="navbar-item" id="dynamic-navbar-buttons">
<!-- Buttons will be dynamically injected here -->
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarButtons = document.getElementById('dynamic-navbar-buttons');
const currentPath = window.location.pathname;
const queryParams = new URLSearchParams(window.location.search);
const buttonsConfig = {
'/user_select': [
{ text: '', icon: 'bi bi-gear', link: '/admin' },
{ text: '', icon: 'bi bi-house', link: '/user_select' }
],
'/product_select': [
{ text: 'Zur Abrechnung', link: '/pay_up' },
{ text: '', icon: 'bi bi-gear', link: '/admin' },
{ text: '', icon: 'bi bi-box-arrow-right', link: '/user_select' }
],
'/pay_up': [
{ text: '', icon: 'bi bi-gear', link: '/admin' },
{ text: '', icon: 'bi bi-box-arrow-right', link: '/user_select' }
],
'/admin': [
{ text: '', icon: 'bi bi-house', link: '/user_select' }
],
'/admin/products': [
{ text: '', icon: 'bi bi-arrow-return-left', link: '/admin' },
{ text: '', icon: 'bi bi-house', link: '/user_select' }
],
'/admin/users': [
{ text: '', icon: 'bi bi-arrow-return-left', link: '/admin' },
{ text: '', icon: 'bi bi-house', link: '/user_select' }
],
'/admin/report': [
{ text: '', icon: 'bi bi-arrow-return-left', link: '/admin' },
{ text: '', icon: 'bi bi-house', link: '/user_select' }
]
};
if (currentPath === '/product_select' && queryParams.has('user')) {
const username = document.cookie.split('; ').find(row => row.startsWith('name'))?.split('=')[1];
if (username) {
document.getElementById('nav_usernameContent').innerText = username; // Set greeting
document.getElementById('nav_username').classList.remove('is-hidden'); // Show greeting
}
}
const buttons = buttonsConfig[currentPath] || [];
buttons.forEach(button => {
const btn = document.createElement('button');
btn.className = 'button';
btn.onclick = () => window.location = button.link;
if (button.icon) {
const icon = document.createElement('i');
icon.className = button.icon;
btn.appendChild(icon);
}
if (button.text) {
btn.appendChild(document.createTextNode(button.text));
}
navbarButtons.appendChild(btn);
});
// Burger menu toggle
const burger = document.querySelector('.navbar-burger');
const menu = document.querySelector('.navbar-menu');
if (burger && menu) {
burger.addEventListener('click', () => {
burger.classList.toggle('is-active');
menu.classList.toggle('is-active');
});
}
});
</script>
</nav>

86
views/payup.eta Normal file
View File

@ -0,0 +1,86 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<section class="section container" id="mainSelect">
<h1 class="title">Abrechnung</h1>
<h2 class="subtitle">Ausstehend</h2>
<div class="notification is-info is-light is-hidden" id="noBalance">
Für diesen Benutzer stehen keine Transaktionen aus. <strong>Es gibt nichts zu bezahlen.</strong>
<br>
<button class="button is-info is-large" id="logout">Abmelden</button>
</div>
<div id="balanceSheet">
<table class="table is-striped is-hoverable" id="payTable">
<thead>
<tr>
<th>Austellungsdatum</th>
<th>Preis</th>
</tr>
</thead>
<tfoot>
<tr>
<th></th>
<th id="table-sum"></th>
</tr>
</tfoot>
<tbody id="table-content">
</tbody>
</table>
<button class="button is-success is-large" id="paynow">Jetzt bezahlen <i class="bi bi-wallet2"></i></button>
</div>
<details>
<summary>Alle Transaktionen</summary>
<table class="table is-striped is-fullwidth is-hoverable" data-dataSource="transaction" data-pageSize="10" data-filters='{"user_id":-1}' data-loadmode="manual" id="alltransactions">
<thead>
<tr>
<th data-dataCol = "id">Id</th>
<th data-dataCol = "total">Name</th>
<th data-dataCol = "paid" data-type="bool">Bezahlt</th>
<th data-dataCol = "createdAt" data-type="datetime">Ausgestellt am</th>
<th data-dataCol = "paidAt" data-type="datetime" data-order="DESC">Bezahlt am</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<nav class="pagination is-hidden" role="navigation" aria-label="pagination" data-targetTable="alltransactions">
<ul class="pagination-list">
</ul>
</nav>
</details>
</section>
<!-- Confirmation modal -->
<div class="modal" id="confirmModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Bezahlung bestätigen</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="content">
<p>
Wurde der Betrag in die Kasse eingezahlt?
</p>
<h2 class="title is-2" id="ModalSum"></h2>
</div>
</section>
<footer class="modal-card-foot buttons">
<button class="button is-success" id="confirmCheckout">Bestätigen</button>
<button class="button" id="cancelCheckout">Abbrechen</button>
</footer>
</div>
</div>
<%~ include("partials/footer.eta") %>
<script src="/static/pages/payup.js"></script>
<%~ include("partials/base_foot.eta") %>

121
views/product_select.eta Normal file
View File

@ -0,0 +1,121 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<input id="scannerField" type="text"/>
<section class="section main-content">
<div class="container">
<div class="columns">
<!-- Main content in the middle -->
<div class="column is-three-quarters columns is-multiline" id="mainSelect">
<!-- This will be populated by JS with the templated content from the hidden section -->
</div>
<!-- Empty sidebar on the right -->
<div class="column is-one-quarter">
<h2 class="title is-4">Ausgewählte Produkte</h2>
<table class="table">
<thead>
<tr>
<th><abbr title="Bezeichner">Bez.</abbr></th>
<th>Preis</th>
<th></th>
</tr>
</thead>
<tfoot>
<tr>
<th></th>
<th id="TableSum"></th>
<th></th>
</tr>
</tfoot>
<tbody id="selectedProducts">
</tbody>
</table>
<button class="button is-primary" id="checkout">Zur Kasse</button>
</div>
</div>
</div>
</section>
<hidden>
<!-- Base Button -->
<div class="column is-one-quarter" id="baseStruct">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img
src="https://bulma.io/assets/images/placeholders/1280x960.png"
alt="Placeholder image"
class="product_image"
/>
</figure>
</div>
<div class="card-content">
<p class="title is-4 product_name">CocaCola</p>
<p class="subtitle is-6 product_ean">0123456789</p>
<p class="product_description">Explainer</p>
</div>
<footer class="card-footer">
<p class="card-footer-item">
<span class="product_price"> 9.99€ </span>
</p>
</footer>
</div>
</div>
</hidden>
<!-- Confirmation modal -->
<div class="modal" id="checkoutModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Bestellung abschließen</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="content">
<p>
Sind Sie sicher, dass Sie die ausgewählten so Produkte bestellen möchten?
</p>
<table class="table">
<thead>
<tr>
<th>Bezeichner</th>
<th>Preis</th>
</tr>
</thead>
<tfoot>
<tr>
<th></th>
<th id="ModalSum"></th>
</tr>
</tfoot>
<tbody id="selectedProductsModal">
</tbody>
</table>
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-success" id="confirmCheckout">Bestellen</button>
<button class="button" id="cancelCheckout">Abbrechen</button>
</footer>
</div>
</div>
<!-- Loading modal -->
<div class="modal" id="loadingModal" data-dissmiss="false">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box">
<p>Bitte warten...</p>
<div class="loader"></div>
</div>
</div>
</div>
<%~ include("partials/footer.eta") %>
<script src="/static/pages/product_select.js"></script>
<%~ include("partials/base_foot.eta") %>

View File

@ -1,5 +1,4 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<link rel="stylesheet" href="/static/css/lockscreen.css">
@ -11,8 +10,12 @@
<div id="date"></div>
</div>
<script>
const apiKey = "<%= it.apikey %>";
</script>
<script src="/static/apiWrapper.js"></script>
<script src="/static/pageDriver.js"></script>
<script src="/static/js/lockscreenBgHandler.js"></script>
<script src="/static/js/kiosk_mode.js"></script>
<%~ include("partials/footer.eta") %>
<%~ include("partials/base_foot.eta") %>

97
views/user_select.eta Normal file
View File

@ -0,0 +1,97 @@
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
<%~ include("partials/nav.eta") %>
<section class="section buttons container is-fluid is-centered" id="mainSelect">
</section>
<hidden>
<!-- Base Button -->
<button class="button is-dark is-medium m-2" id="baseStruct">Username</button>
</hidden>
<div class="modal" id="pinPadModal">
<div class="modal-background"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Bitte PIN eingeben</p>
<div class="notification is-danger is-hidden" id="pinError">
<button class="delete"></button>
Ein fehlerhafter PIN wurde eingegeben.
</div>
</header>
<section class="modal-card-body">
<!--
<input class="input" type="password" placeholder="1234" id="pinInput" maxlength="4" />
-->
<!-- Use 4 fields for the pin, similar to a 2FA fields -->
<div class="field has-addons is-centered">
<div class="control">
<input class="input is-large has-text-centered" type="password" placeholder="1" id="pinInput1" maxlength="1" />
</div>
<div class="control">
<input class="input is-large has-text-centered" type="password" placeholder="2" id="pinInput2" maxlength="1" />
</div>
<div class="control">
<input class="input is-large has-text-centered" type="password" placeholder="3" id="pinInput3" maxlength="1" />
</div>
<div class="control">
<input class="input is-large has-text-centered" type="password" placeholder="4" id="pinInput4" maxlength="1" />
</div>
</div>
<section class="section buttons container is-centered are-centered" id="numpad">
<!-- Num pad with 3 keys per row -->
<div class="columns is-grouped">
<div class="column">
<button class="button is-link is-large m-2">1</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">2</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">3</button>
</div>
</div>
<div class="columns is-grouped">
<div class="column">
<button class="button is-link is-large m-2">4</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">5</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">6</button>
</div>
</div>
<div class="columns is-grouped">
<div class="column">
<button class="button is-link is-large m-2">7</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">8</button>
</div>
<div class="column">
<button class="button is-link is-large m-2">9</button>
</div>
</div>
<div class="columns is-grouped">
<div class="column">
<button class="button is-link is-large m-2">0</button>
</div>
<div class="column">
<button class="button is-warning is-large m-2"><i class="bi bi-backspace-fill"></i></button>
</div>
<div class="column">
<button class="button is-success is-large m-2"><i class="bi bi-check"></i></button>
</div>
</div>
</section>
</section>
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<%~ include("partials/footer.eta") %>
<script src="/static/pages/user_select.js"></script>
<%~ include("partials/base_foot.eta") %>