Compare commits
45 Commits
5a68622b7a
...
master
Author | SHA1 | Date | |
---|---|---|---|
2eb80e0da9 | |||
dbcdce5296 | |||
7482c329ed | |||
aafaf4dd9e | |||
7b08d6e03f | |||
fe5cbabd46 | |||
141f75717b | |||
c38be00f73 | |||
ccbcb94449 | |||
86b9595665 | |||
c6e441dc26 | |||
c89eb37361 | |||
5cfd8b2319 | |||
d44900435f | |||
1e4ebc2a3c | |||
5ce521c8a7 | |||
ef16f045f7 | |||
bf561f8c7f | |||
5da8060857 | |||
b7d12d18d4 | |||
16ee092b35 | |||
366f3297da | |||
475690ca2b | |||
03fec1ebd7 | |||
8cd011fc01 | |||
9066397cd4 | |||
cf7bd8da9c | |||
14cf8af14b | |||
d491033c29 | |||
fd7d1ffd47 | |||
fe04ad9ce3 | |||
bd43f03507 | |||
fa7f3004fa | |||
42f6e0b22d | |||
18d3646315 | |||
3bf0e2fdd5 | |||
2cbde4e344 | |||
66f0bebc9d | |||
50ad684ad3 | |||
0233196276 | |||
551f72f3e0 | |||
5ab2df351c | |||
bca596fa75 | |||
f1ddaa1cc2 | |||
3706ed07d2 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,3 +4,6 @@ dist/
|
||||
.env
|
||||
config.json
|
||||
.vsls.json
|
||||
|
||||
images/*.png
|
||||
!images/default.png
|
||||
|
BIN
images/default.png
Normal file
BIN
images/default.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 345 KiB |
711
package-lock.json
generated
711
package-lock.json
generated
@ -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": {
|
||||
|
20
package.json
20
package.json
@ -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",
|
||||
|
@ -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?
|
||||
|
@ -1,16 +1,30 @@
|
||||
import log from './log.js';
|
||||
import ConfigManager from '../libs/configManager.js';
|
||||
import __path from './path.js';
|
||||
import _ from 'lodash';
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
|
||||
|
||||
export default config;
|
||||
|
@ -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 });
|
||||
@ -45,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
|
||||
// );
|
||||
|
23
src/handlers/validation.ts
Normal file
23
src/handlers/validation.ts
Normal 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;
|
@ -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}" }`);
|
||||
}
|
||||
|
||||
|
17
src/index.ts
17
src/index.ts
@ -11,7 +11,7 @@ import config from './handlers/config.js';
|
||||
// Express & more
|
||||
import express from 'express';
|
||||
import helmet from 'helmet';
|
||||
//import fileUpload from 'express-fileupload';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import bodyParser from 'body-parser';
|
||||
import { Eta, Options } from 'eta';
|
||||
|
||||
@ -61,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 : []
|
||||
}
|
||||
@ -78,19 +78,20 @@ 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!');
|
||||
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
|
||||
|
@ -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;
|
||||
|
142
src/routes/api/v1/image/image.ts
Normal file
142
src/routes/api/v1/image/image.ts
Normal 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 };
|
37
src/routes/api/v1/image/image_schema.ts
Normal file
37
src/routes/api/v1/image/image_schema.ts
Normal 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 };
|
@ -1,18 +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 user_codecheck_route from './user_codecheck.js';
|
||||
import user_codecheck_schema from './user_codecheck_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.js';
|
||||
import products_schema from './products_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 });
|
||||
@ -27,6 +32,16 @@ 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);
|
||||
@ -37,7 +52,13 @@ 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;
|
||||
|
@ -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
|
||||
@ -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,
|
265
src/routes/api/v1/transaction/transaction.ts
Normal file
265
src/routes/api/v1/transaction/transaction.ts
Normal 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 };
|
59
src/routes/api/v1/transaction/transaction_schema.ts
Normal file
59
src/routes/api/v1/transaction/transaction_schema.ts
Normal 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 };
|
@ -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 !== '';
|
||||
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 !== '';
|
||||
//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,6 +97,7 @@ async function post(req: Request, res: Response) {
|
||||
.create({
|
||||
data: {
|
||||
name: value.name,
|
||||
email: value.email,
|
||||
code: value.code === '0000' ? null : value.code
|
||||
},
|
||||
select: {
|
||||
@ -125,6 +128,7 @@ async function patch(req: Request, res: Response) {
|
||||
},
|
||||
data: {
|
||||
name: value.name,
|
||||
email: value.email,
|
||||
code: value.code === '0000' ? null : value.code
|
||||
},
|
||||
select: {
|
@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
|
||||
import log from '../../../handlers/log.js';
|
||||
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
|
||||
@ -24,9 +24,10 @@ async function get(req: Request, res: Response) {
|
||||
.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 === undefined || value.code === result?.code);
|
||||
res.status(200).json(result?.code === '' || result?.code === null || result?.code === undefined || result?.code === value.code);
|
||||
//log.api?.debug(result, result?.code);
|
||||
});
|
||||
}
|
@ -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({
|
7
src/routes/frontend/admin/dashboard.ts
Normal file
7
src/routes/frontend/admin/dashboard.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("admin/dashboard")
|
||||
}
|
||||
|
||||
export default { get };
|
17
src/routes/frontend/admin/index.ts
Normal file
17
src/routes/frontend/admin/index.ts
Normal 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;
|
7
src/routes/frontend/admin/products.ts
Normal file
7
src/routes/frontend/admin/products.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("admin/products")
|
||||
}
|
||||
|
||||
export default { get };
|
7
src/routes/frontend/admin/report.ts
Normal file
7
src/routes/frontend/admin/report.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("admin/reports")
|
||||
}
|
||||
|
||||
export default { get };
|
7
src/routes/frontend/admin/users.ts
Normal file
7
src/routes/frontend/admin/users.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("admin/users")
|
||||
}
|
||||
|
||||
export default { get };
|
@ -2,17 +2,23 @@ import express from 'express';
|
||||
import config from '../../handlers/config.js';
|
||||
|
||||
// Route imports
|
||||
import screensaver_Route from './screensaver.js';
|
||||
import user_select_Route from './user_select.js';
|
||||
import product_select_Route from './product_select.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('/').get(screensaver_Route.get);
|
||||
Router.route('/user_select').get(user_select_Route.get);
|
||||
Router.route('/product_select').get(product_select_Route.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.use('/admin', adminRouter);
|
||||
|
||||
config.global.devmode && Router.route('/test').get(test_Route.get);
|
||||
|
||||
|
7
src/routes/frontend/pay_up.ts
Normal file
7
src/routes/frontend/pay_up.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("payup")
|
||||
}
|
||||
|
||||
export default { get };
|
@ -1,7 +1,8 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import config from '../../handlers/config.js';
|
||||
|
||||
function get(req: Request, res: Response) {
|
||||
res.render("screensaver")
|
||||
res.render("screensaver", { apikey: config.global.galleryApiKey })
|
||||
}
|
||||
|
||||
export default { get };
|
||||
|
@ -4,8 +4,8 @@ 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 });
|
||||
|
||||
@ -16,8 +16,8 @@ Router.use('/libs/jquery', express.static(path.join(__path, 'node_modules', 'jqu
|
||||
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) {
|
||||
|
@ -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);
|
||||
@ -94,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;
|
||||
@ -166,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";
|
||||
@ -181,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);
|
||||
@ -190,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);
|
||||
@ -207,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');
|
||||
|
||||
@ -214,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');
|
||||
|
||||
|
@ -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;
|
||||
}
|
13
static/js/kiosk_mode.js
Normal file
13
static/js/kiosk_mode.js
Normal 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);
|
||||
});
|
@ -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);
|
||||
|
@ -1,3 +1,10 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
@ -5,3 +12,16 @@ body {
|
||||
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 |
@ -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);
|
||||
@ -620,12 +680,59 @@ 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();
|
||||
}
|
195
static/pages/admin_products.js
Normal file
195
static/pages/admin_products.js
Normal 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
|
||||
// });
|
2
static/pages/admin_users.js
Normal file
2
static/pages/admin_users.js
Normal file
@ -0,0 +1,2 @@
|
||||
let elm_table_users = document.getElementById('table_users');
|
||||
|
82
static/pages/payup.js
Normal file
82
static/pages/payup.js
Normal 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()
|
@ -2,16 +2,229 @@ 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', function() {
|
||||
let data = await returnTableDataByTableName('product');
|
||||
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";
|
||||
});
|
@ -12,8 +12,22 @@ 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++) {
|
||||
@ -81,10 +95,14 @@ function handleUserClick(e) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,12 +114,15 @@ function validatePin() {
|
||||
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();
|
||||
pinError.classList.remove('is-hidden');
|
||||
createTemporaryNotification('Fehlerhafte PIN Eingabe!', 'is-danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
24
views/admin/dashboard.eta
Normal file
24
views/admin/dashboard.eta
Normal 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
236
views/admin/products.eta
Normal 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
36
views/admin/reports.eta
Normal 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") %>
|
96
views/admin/users.eta
Normal file
96
views/admin/users.eta
Normal file
@ -0,0 +1,96 @@
|
||||
<%~ include("partials/base_head.eta", {"title": "Admin - Benutzer"}) %>
|
||||
<%~ include("partials/nav.eta") %>
|
||||
|
||||
<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">
|
||||
Benutzer anlegen
|
||||
</button></p>
|
||||
<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>
|
||||
|
||||
|
||||
<!-- 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="user">
|
||||
<h2 class="title">Neuer Benutzer</h1>
|
||||
<div class="field">
|
||||
<label class="label">Name</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">E-Mail</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="email" placeholder="test@example.org" value="" name="email">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Pin</label>
|
||||
<div class="control has-icons-left">
|
||||
<input class="input" type="text" placeholder="" value="" name="code">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="bi bi-chat-fill"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<%~ include("partials/footer.eta") %>
|
||||
<script src="/static/pages/admin_users.js"></script>
|
||||
<%~ include("partials/base_foot.eta") %>
|
@ -2,10 +2,11 @@
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<i class="bi bi-cup-straw"></i>
|
||||
<strong>HydrationHUB</strong> by <a target="_blank" rel="noopener noreferrer" href="https://pnh.fyi">[Project-name-here]</a>.<br>
|
||||
<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>
|
||||
|
@ -2,8 +2,9 @@
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item primary" href="/">
|
||||
<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">
|
||||
@ -15,33 +16,83 @@
|
||||
</div>
|
||||
|
||||
<div id="navbarBasicExample" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item" href="/">Screensaver</a>
|
||||
<a class="navbar-item" href="/user_select">user_select</a>
|
||||
<a class="navbar-item" href="/product_select">product_select</a>
|
||||
<a class="navbar-item" href="/test">Test <span class="tag is-info">Dev</span></a>
|
||||
<div class="navbar-end">
|
||||
<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);
|
||||
|
||||
<!--<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>
|
||||
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' }
|
||||
]
|
||||
};
|
||||
|
||||
<% /* <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>
|
||||
</div> */ %>
|
||||
</div>
|
||||
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
86
views/payup.eta
Normal 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") %>
|
@ -1,38 +1,121 @@
|
||||
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
|
||||
<%~ include("partials/nav.eta") %>
|
||||
|
||||
<section class="section">
|
||||
<div class="container columns" id="mainSelect">
|
||||
<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" id="baseStruct">
|
||||
<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">CocaCola lol</p>
|
||||
<p class="subtitle is-6">0123456789</p>
|
||||
|
||||
<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> 9.99€ </span>
|
||||
<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") %>
|
@ -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") %>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<%~ include("partials/base_head.eta", {"title": "Dashboard"}) %>
|
||||
<%~ include("partials/nav.eta") %>
|
||||
|
||||
<section class="section buttons container" id="mainSelect">
|
||||
<section class="section buttons container is-fluid is-centered" id="mainSelect">
|
||||
|
||||
</section>
|
||||
<hidden>
|
||||
<!-- Base Button -->
|
||||
<button class="button is-link is-large m-2" id="baseStruct">Username</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>
|
||||
|
Reference in New Issue
Block a user