Rename frontend to web
2
web/.dockerignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
src/services/openapi/
|
||||
node_modules/
|
24
web/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
.DS_Store
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/src/services/openapi/
|
35
web/README.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# web
|
||||
|
||||
## Project setup
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compiles and hot-reloads for development
|
||||
|
||||
```
|
||||
npm run serve
|
||||
```
|
||||
|
||||
### Compiles and minifies for production
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Run your unit tests
|
||||
|
||||
```
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
### Lints and fixes files
|
||||
|
||||
```
|
||||
npm run lint
|
||||
```
|
||||
|
||||
### Customize configuration
|
||||
|
||||
See [Configuration Reference](https://cli.vuejs.org/config/).
|
3
web/babel.config.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ["@vue/cli-plugin-babel/preset"],
|
||||
};
|
98
web/package.json
Normal file
|
@ -0,0 +1,98 @@
|
|||
{
|
||||
"name": "web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"types:openapi": "openapi -i ../docs/swagger.json -o src/services/openapi",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.1",
|
||||
"@vuelidate/core": "^2.0.0",
|
||||
"@vuelidate/validators": "^2.0.0",
|
||||
"core-js": "^3.8.3",
|
||||
"moment": "^2.29.3",
|
||||
"primeflex": "^3.3.0",
|
||||
"primeicons": "^6.0.1",
|
||||
"primevue": "^3.23.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.8.0",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"@vue/cli-plugin-babel": "~5.0.0",
|
||||
"@vue/cli-plugin-eslint": "~5.0.0",
|
||||
"@vue/cli-plugin-router": "~5.0.0",
|
||||
"@vue/cli-plugin-typescript": "~5.0.0",
|
||||
"@vue/cli-plugin-unit-jest": "~5.0.0",
|
||||
"@vue/cli-plugin-vuex": "~5.0.0",
|
||||
"@vue/cli-service": "~5.0.0",
|
||||
"@vue/compiler-dom": "^3.0.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/server-renderer": "^3.0.1",
|
||||
"@vue/test-utils": "^2.0.0-0",
|
||||
"@vue/vue3-jest": "^29.2.4",
|
||||
"babel-jest": "^29.5.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"jest": "^29.5.0",
|
||||
"less": "^4.0.0",
|
||||
"less-loader": "^11.1.3",
|
||||
"openapi-typescript": "^6.2.8",
|
||||
"openapi-typescript-codegen": "^0.24.0",
|
||||
"prettier": "^2.4.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": ">=3.3.1 <5.1.0",
|
||||
"webpack": "^5.0.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"rules": {},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"**/__tests__/*.{j,t}s?(x)",
|
||||
"**/tests/unit/**/*.spec.{j,t}s?(x)"
|
||||
],
|
||||
"env": {
|
||||
"jest": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 160
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead",
|
||||
"not ie 11"
|
||||
],
|
||||
"jest": {
|
||||
"preset": "@vue/cli-plugin-unit-jest/presets/typescript-and-babel"
|
||||
}
|
||||
}
|
BIN
web/public/favicon/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
web/public/favicon/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 178 KiB |
BIN
web/public/favicon/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 32 KiB |
9
web/public/favicon/browserconfig.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
web/public/favicon/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
web/public/favicon/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
web/public/favicon/favicon.ico
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
web/public/favicon/mstile-150x150.png
Normal file
After Width: | Height: | Size: 19 KiB |
153
web/public/favicon/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="750.000000pt" height="750.000000pt" viewBox="0 0 750.000000 750.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,750.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1137 7112 c-16 -18 -103 -205 -117 -253 -9 -30 4 -59 26 -59 8 0 33
|
||||
9 56 21 22 11 44 17 48 12 4 -4 12 -33 19 -63 10 -48 42 -115 83 -171 15 -21
|
||||
-18 -99 -196 -459 -148 -298 -181 -381 -173 -438 3 -20 21 -122 41 -225 41
|
||||
-212 56 -383 56 -630 l0 -169 -85 -176 c-48 -97 -99 -193 -114 -212 -15 -19
|
||||
-36 -51 -46 -70 -27 -53 -133 -151 -202 -187 -77 -42 -138 -44 -215 -8 -64 30
|
||||
-99 32 -123 5 -25 -28 -13 -82 27 -115 48 -41 236 -131 298 -144 63 -12 112
|
||||
-4 162 27 92 57 162 92 169 85 11 -11 37 -328 49 -603 27 -623 35 -718 66
|
||||
-813 9 -27 12 -52 8 -57 -5 -4 -63 -22 -129 -40 -129 -34 -339 -112 -367 -138
|
||||
-22 -18 -23 -45 -3 -103 8 -24 16 -59 18 -78 6 -59 22 -60 154 -7 293 115 659
|
||||
181 1147 206 199 10 344 28 400 50 14 5 28 20 32 35 7 27 33 33 78 15 39 -14
|
||||
54 -3 59 43 2 23 16 123 31 222 14 99 31 223 37 275 21 190 76 428 148 640 98
|
||||
290 105 321 91 392 -6 32 -8 63 -5 68 10 17 -23 67 -52 78 -15 6 -53 16 -85
|
||||
22 -71 14 -128 52 -209 139 -68 73 -109 130 -164 223 -45 77 -114 158 -136
|
||||
158 -20 0 -56 -43 -160 -190 -100 -144 -245 -291 -322 -330 -33 -16 -71 -30
|
||||
-83 -30 -25 0 -49 -28 -59 -70 -4 -16 7 -67 30 -140 103 -324 199 -953 181
|
||||
-1183 -7 -94 -35 -164 -68 -171 -13 -3 -70 -10 -128 -16 -58 -7 -150 -20 -205
|
||||
-30 -55 -10 -103 -16 -108 -13 -4 2 -3 13 3 23 5 10 10 28 10 39 0 11 10 36
|
||||
21 56 15 26 20 46 17 73 -3 20 -2 96 3 167 4 73 3 242 -3 385 -5 140 -11 368
|
||||
-12 505 -3 269 -5 281 -63 386 l-27 50 56 112 c94 188 230 488 306 677 62 152
|
||||
94 229 162 385 29 66 60 140 69 165 9 25 24 59 33 75 9 17 41 91 71 165 31 74
|
||||
76 180 102 235 25 55 50 109 54 120 18 46 86 179 155 305 78 143 183 279 270
|
||||
350 75 61 85 57 138 -57 61 -130 108 -288 108 -359 0 -14 13 -68 29 -122 16
|
||||
-53 31 -117 34 -142 3 -25 13 -65 22 -90 9 -25 27 -97 40 -160 27 -134 105
|
||||
-414 136 -491 12 -30 34 -90 49 -134 75 -219 250 -554 339 -648 40 -43 127
|
||||
-90 153 -82 55 15 158 59 158 68 0 5 -32 43 -70 85 -49 52 -83 100 -111 158
|
||||
-22 45 -44 84 -48 86 -14 5 -159 339 -195 450 -19 59 -40 137 -46 175 -7 37
|
||||
-24 118 -40 178 -15 61 -31 130 -35 155 -4 25 -19 99 -35 165 -16 66 -32 147
|
||||
-35 180 -3 33 -12 89 -20 125 -7 36 -16 84 -19 108 -9 61 -63 227 -96 292 -40
|
||||
77 -83 135 -171 228 -63 66 -84 82 -115 87 -22 3 -54 13 -73 21 -55 24 -120
|
||||
28 -189 10 -137 -35 -244 -137 -385 -366 -78 -127 -217 -398 -217 -423 0 -9
|
||||
-4 -18 -9 -21 -5 -3 -20 -34 -34 -68 -14 -35 -36 -88 -50 -118 -41 -92 -104
|
||||
-241 -238 -568 -142 -345 -146 -355 -158 -348 -4 3 -11 88 -14 188 -7 188 -32
|
||||
363 -60 431 -16 37 -16 39 7 75 52 79 202 357 302 556 106 212 119 253 89 287
|
||||
-9 9 -29 68 -46 130 -16 62 -42 136 -58 164 l-29 51 29 32 c16 18 29 37 29 42
|
||||
0 16 -60 49 -129 70 -78 25 -103 25 -124 2z m936 -2859 c56 -73 228 -243 292
|
||||
-288 69 -50 79 -67 71 -128 -4 -29 -34 -133 -67 -232 -54 -159 -65 -211 -104
|
||||
-445 -47 -288 -85 -594 -85 -677 0 -29 -3 -53 -6 -53 -3 0 -33 11 -67 24 -100
|
||||
38 -128 46 -171 46 -23 0 -57 3 -77 6 l-36 6 5 122 c5 123 -14 335 -43 493 -8
|
||||
43 -26 143 -40 223 -30 174 -72 334 -120 459 -40 107 -41 111 -18 111 10 0 49
|
||||
12 88 27 57 21 82 39 135 92 36 37 81 95 100 129 37 67 87 142 94 142 3 0 25
|
||||
-26 49 -57z"/>
|
||||
<path d="M1965 3159 c-9 -13 5 -149 17 -172 16 -30 51 -17 65 23 16 46 16 133
|
||||
1 148 -15 15 -74 16 -83 1z"/>
|
||||
<path d="M3685 6514 c-16 -2 -73 -9 -125 -15 -97 -11 -292 -49 -390 -75 -97
|
||||
-27 -180 -55 -180 -62 0 -32 37 -165 47 -169 7 -3 47 7 90 21 135 44 294 77
|
||||
523 107 119 15 478 12 620 -6 407 -51 792 -184 1123 -389 774 -478 1263 -1259
|
||||
1357 -2166 13 -127 13 -430 -1 -566 -51 -523 -247 -1025 -566 -1453 -119 -160
|
||||
-400 -439 -565 -562 -423 -315 -899 -498 -1426 -550 -149 -15 -510 -6 -652 16
|
||||
-645 100 -1214 398 -1657 869 -100 107 -270 331 -343 451 -56 92 -53 91 -146
|
||||
74 -79 -14 -94 -19 -94 -30 0 -5 27 -55 61 -110 431 -714 1136 -1225 1928
|
||||
-1398 241 -53 360 -65 646 -66 265 0 352 7 555 46 489 94 928 296 1320 608
|
||||
181 145 410 386 557 586 720 983 776 2345 140 3391 -137 226 -284 409 -489
|
||||
609 -386 378 -855 638 -1382 765 -237 58 -357 72 -651 75 -148 2 -283 1 -300
|
||||
-1z"/>
|
||||
<path d="M2203 5810 c-52 -11 -137 -51 -192 -90 -57 -42 -148 -183 -177 -277
|
||||
-16 -48 -25 -101 -26 -153 -1 -76 1 -82 40 -148 62 -104 123 -166 200 -204 57
|
||||
-28 77 -33 142 -33 95 0 135 19 216 101 96 99 143 207 159 364 8 77 -15 160
|
||||
-75 281 -48 95 -100 143 -169 158 -58 12 -59 12 -118 1z m-51 -192 c10 -29 -3
|
||||
-113 -19 -125 -29 -22 -103 -10 -103 17 0 11 100 120 110 120 4 0 10 -6 12
|
||||
-12z m196 -20 c5 -7 13 -41 17 -75 8 -69 3 -73 -50 -53 -21 8 -24 14 -19 37
|
||||
24 107 30 117 52 91z m-244 -276 c4 -7 -15 -102 -30 -151 -6 -18 -39 11 -60
|
||||
51 -17 34 -18 95 -1 101 20 9 85 8 91 -1z m241 -1 c8 -14 -31 -110 -56 -137
|
||||
-32 -34 -49 -31 -49 9 1 47 19 120 33 129 17 11 65 10 72 -1z"/>
|
||||
<path d="M5872 3183 c-5 -10 -15 -46 -21 -80 -7 -35 -30 -123 -52 -195 -98
|
||||
-327 -103 -473 -17 -514 25 -12 40 -13 60 -5 32 12 50 41 38 62 -8 12 -13 12
|
||||
-30 1 -32 -20 -57 -7 -63 33 -12 73 37 294 117 534 47 137 48 160 10 175 -25
|
||||
9 -33 7 -42 -11z"/>
|
||||
<path d="M2915 2988 c-85 -30 -261 -190 -340 -309 -96 -144 -128 -252 -89
|
||||
-307 20 -28 64 -30 64 -3 0 11 -4 23 -10 26 -5 3 -10 27 -10 53 1 133 225 398
|
||||
405 478 91 40 180 27 222 -32 49 -68 67 -203 38 -284 -24 -68 -97 -169 -191
|
||||
-263 -62 -61 -85 -90 -77 -98 45 -45 256 175 313 327 28 77 28 209 -1 265 -27
|
||||
54 -92 123 -133 143 -39 18 -144 20 -191 4z"/>
|
||||
<path d="M4585 2952 c-6 -4 -18 -32 -29 -62 -10 -30 -26 -73 -36 -95 -28 -62
|
||||
-120 -314 -120 -328 0 -44 -47 -136 -98 -195 -143 -164 -163 -176 -190 -120
|
||||
-23 48 25 205 77 252 20 18 21 18 30 -7 17 -45 49 -46 70 -1 35 77 -20 156
|
||||
-91 132 -52 -18 -118 -123 -152 -240 -9 -32 -20 -58 -24 -58 -4 0 -31 24 -60
|
||||
53 -52 53 -65 90 -41 114 18 18 2 43 -26 43 -35 0 -65 -33 -65 -73 0 -33 25
|
||||
-77 95 -167 51 -67 45 -103 -21 -116 -40 -7 -63 13 -69 61 -3 22 -10 44 -16
|
||||
50 -16 16 -39 -23 -39 -67 0 -75 89 -137 166 -116 30 7 86 60 96 89 3 8 17 3
|
||||
46 -16 33 -21 49 -26 75 -21 37 7 99 66 138 131 l24 40 3 -57 c2 -31 9 -61 16
|
||||
-67 22 -18 44 -2 50 37 9 52 102 234 142 279 41 47 43 33 12 -72 -20 -67 -21
|
||||
-80 -10 -122 16 -57 28 -73 55 -73 18 0 19 4 12 53 -12 83 -7 123 30 234 19
|
||||
57 35 112 35 123 0 27 -36 45 -64 32 -19 -8 -112 -123 -153 -188 -23 -36 -14
|
||||
16 13 81 14 33 41 101 61 150 19 50 46 114 59 143 13 29 24 56 24 61 0 5 7 14
|
||||
16 21 8 7 14 24 12 38 -3 27 -37 55 -53 44z"/>
|
||||
<path d="M5166 2904 c-24 -23 -18 -41 18 -61 47 -26 78 24 38 60 -23 21 -36
|
||||
21 -56 1z"/>
|
||||
<path d="M6054 2881 c-94 -57 -191 -270 -168 -368 27 -113 114 -159 210 -110
|
||||
72 37 140 123 151 192 8 47 -12 45 -42 -5 -34 -57 -99 -116 -149 -135 -47 -18
|
||||
-47 -18 -71 5 -20 19 -25 33 -25 72 0 27 7 64 15 84 13 33 16 34 53 28 57 -9
|
||||
96 15 128 79 36 69 38 132 5 158 -29 24 -67 24 -107 0z m30 -128 c-20 -40 -54
|
||||
-58 -54 -28 0 18 58 81 67 72 3 -3 -3 -23 -13 -44z"/>
|
||||
<path d="M3906 2858 c-9 -12 -16 -28 -16 -36 0 -20 -46 -106 -66 -123 -9 -8
|
||||
-51 -23 -93 -35 -68 -18 -76 -23 -79 -46 -5 -40 27 -53 72 -31 20 9 39 14 42
|
||||
10 4 -4 -16 -55 -43 -114 -28 -60 -65 -153 -82 -208 -28 -88 -39 -107 -87
|
||||
-159 -42 -46 -57 -57 -66 -48 -15 15 11 96 53 162 58 94 58 130 -2 130 -18 0
|
||||
-32 7 -39 19 -14 27 -62 36 -98 17 -64 -33 -210 -213 -253 -313 -36 -84 -21
|
||||
-151 39 -174 57 -22 99 4 204 130 l27 32 13 -27 c32 -65 80 -57 140 23 l42 57
|
||||
17 -41 c19 -45 39 -60 84 -65 45 -5 44 25 -1 51 l-37 22 6 72 c4 58 18 104 72
|
||||
241 39 97 80 182 96 201 27 30 34 33 103 37 48 3 85 11 102 23 26 17 27 18 9
|
||||
31 -11 8 -43 14 -75 14 -31 0 -59 3 -63 6 -3 4 5 23 18 43 29 41 32 83 9 105
|
||||
-21 21 -30 20 -48 -6z m-461 -558 c8 -12 -21 -62 -99 -172 -95 -135 -146 -179
|
||||
-146 -128 0 46 81 171 173 268 41 42 60 51 72 32z"/>
|
||||
<path d="M5572 2840 c-13 -6 -30 -27 -39 -47 -25 -60 -12 -129 41 -228 52 -96
|
||||
63 -165 26 -165 -26 0 -43 34 -50 100 -3 30 -9 55 -15 55 -5 0 -19 -16 -32
|
||||
-35 -56 -86 -71 -107 -115 -153 -29 -30 -54 -47 -65 -45 -29 5 -55 66 -50 117
|
||||
6 59 23 75 65 60 26 -9 37 -8 57 5 33 22 97 139 102 189 6 55 -24 97 -68 97
|
||||
-45 0 -127 -87 -172 -181 -30 -64 -41 -102 -63 -215 -5 -28 -74 -98 -86 -86
|
||||
-14 14 38 228 83 339 16 39 -1 66 -40 61 -22 -3 -26 -9 -37 -73 -15 -90 -44
|
||||
-195 -55 -195 -4 0 -10 26 -14 58 -20 183 -39 256 -70 268 -9 4 -17 5 -19 3
|
||||
-2 -2 5 -62 15 -134 20 -142 25 -299 9 -339 -20 -55 -63 -17 -74 66 -9 59 -34
|
||||
93 -63 84 -12 -4 -25 -17 -30 -29 -14 -38 -88 -167 -95 -167 -23 0 -6 56 65
|
||||
224 47 112 50 120 36 145 -10 17 -23 26 -39 26 -21 0 -25 -7 -36 -60 -7 -33
|
||||
-22 -81 -33 -107 -31 -73 -61 -181 -61 -222 0 -63 47 -108 95 -90 17 7 55 75
|
||||
74 137 7 20 16 37 21 37 5 0 15 -19 22 -42 18 -54 56 -83 110 -83 23 0 43 5
|
||||
45 11 2 6 9 19 16 28 11 16 15 16 41 2 26 -13 33 -13 57 -1 15 8 41 31 57 51
|
||||
l30 37 40 -42 c43 -45 80 -53 119 -26 23 16 110 127 120 151 9 25 19 15 34
|
||||
-33 19 -63 62 -90 114 -73 19 6 44 23 56 38 29 38 20 82 -42 202 -45 87 -50
|
||||
103 -47 151 3 50 5 54 28 54 36 0 39 37 4 64 -29 23 -38 24 -72 11z m-152
|
||||
-167 c0 -40 -69 -117 -87 -99 -9 8 10 47 42 84 30 36 45 41 45 15z"/>
|
||||
<path d="M2963 2683 c-7 -2 -18 -23 -24 -46 -6 -23 -30 -73 -54 -112 -23 -38
|
||||
-75 -130 -114 -203 -175 -321 -246 -531 -198 -579 28 -28 44 -9 49 59 6 76 72
|
||||
246 148 382 28 49 50 93 50 97 0 5 20 39 44 76 24 37 67 109 95 160 29 51 56
|
||||
95 61 98 16 10 11 34 -10 55 -21 21 -26 22 -47 13z"/>
|
||||
<path d="M3320 2661 c-5 -11 -10 -25 -10 -32 0 -6 -49 -94 -109 -194 -234
|
||||
-390 -288 -518 -247 -580 19 -28 47 -33 66 -10 10 13 10 19 -4 33 -9 10 -16
|
||||
28 -16 41 0 42 148 311 294 536 122 187 119 180 88 208 -25 22 -49 21 -62 -2z"/>
|
||||
<path d="M3396 2551 c-21 -24 -14 -57 15 -66 46 -15 65 44 23 71 -18 13 -23
|
||||
12 -38 -5z"/>
|
||||
<path d="M3499 2554 c-17 -20 -1 -48 31 -52 35 -6 46 13 26 42 -19 30 -38 33
|
||||
-57 10z"/>
|
||||
<path d="M5709 2096 c-2 -3 -42 -8 -89 -11 -97 -7 -121 -10 -585 -64 -424 -50
|
||||
-592 -72 -831 -111 -104 -17 -207 -33 -229 -36 -39 -5 -229 -38 -525 -90 -80
|
||||
-14 -185 -35 -235 -46 -49 -11 -117 -26 -150 -33 -33 -7 -87 -20 -120 -29 -32
|
||||
-9 -74 -16 -93 -16 -72 0 -94 -41 -45 -80 34 -26 66 -23 173 16 41 15 122 37
|
||||
180 50 164 36 336 72 435 89 50 9 126 23 170 31 44 8 105 19 135 24 112 17
|
||||
135 22 153 31 9 6 23 9 30 8 13 -3 71 5 197 26 47 8 121 19 165 25 44 6 100
|
||||
16 125 21 25 5 124 18 220 29 219 26 389 48 460 59 30 5 91 12 135 15 44 3
|
||||
132 12 195 20 63 8 144 17 180 21 63 6 157 21 193 31 9 3 15 10 12 15 -6 9
|
||||
-247 13 -256 5z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 10 KiB |
19
web/public/favicon/site.webmanifest
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
24
web/public/index.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= BASE_URL %>favicon/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="<%= BASE_URL %>favicon/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="<%= BASE_URL %>favicon/favicon-16x16.png" />
|
||||
<link rel="manifest" href="<%= BASE_URL %>favicon/site.webmanifest" />
|
||||
<link rel="mask-icon" href="<%= BASE_URL %>favicon/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1f2d40" />
|
||||
<title>Cafe Plätschwiesle</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
91
web/src/App.vue
Normal file
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<Toast style="width: 90vw" position="bottom-right" group="br" />
|
||||
<TheNavigation @logout="logout" />
|
||||
<div class="m-2">
|
||||
<router-view v-slot="{ Component }">
|
||||
<Transition mode="out-in">
|
||||
<component :is="Component" />
|
||||
</Transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import TheNavigation from "@/components/UI/TheNavigation.vue";
|
||||
import Toast from "primevue/toast";
|
||||
|
||||
export default defineComponent({
|
||||
name: "App",
|
||||
components: { TheNavigation, Toast },
|
||||
setup() {
|
||||
async function logout() {
|
||||
const response = await fetch("/auth/api/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const result = await response.json();
|
||||
result.status === "OK" && window.location.reload();
|
||||
}
|
||||
return { logout };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import "primevue/resources/themes/saga-blue/theme.css";
|
||||
@import "primevue/resources/themes/vela-blue/theme.css" screen and (prefers-color-scheme: dark);
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
img {
|
||||
content: url(@/assets/logos/logo.png);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
img {
|
||||
content: url(@/assets/logos/logo_white.png);
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "roboto";
|
||||
src: url(@/assets/fonts/roboto.ttf);
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.p-button.p-button-success:enabled:focus,
|
||||
.p-button.p-button-danger:enabled:focus,
|
||||
.p-button.p-button-success:enabled:active,
|
||||
.p-button.p-button-danger:enabled:active {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--surface-b);
|
||||
}
|
||||
html,
|
||||
body {
|
||||
font-size: 1.2em;
|
||||
font-family: "roboto", sans-serif;
|
||||
}
|
||||
.p-component {
|
||||
font-family: "roboto", sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-leave-active {
|
||||
transition: opacity 0.1s ease-out;
|
||||
}
|
||||
.v-enter-from,
|
||||
.v-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
BIN
web/src/assets/fonts/roboto.ttf
Normal file
BIN
web/src/assets/logos/logo.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
web/src/assets/logos/logo_white.png
Normal file
After Width: | Height: | Size: 25 KiB |
107
web/src/components/Bills/BillModal.vue
Normal file
|
@ -0,0 +1,107 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="flex flex-column align-items-center justify-content-center">
|
||||
<img alt="logo" class="mb-3" style="height: auto; width: 5rem" src="" />
|
||||
<div class="text-center text-sm">Plätschwiesen 2, 72622 Nürtingen<br />Baden-Württemberg</div>
|
||||
</div>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<div v-else>
|
||||
<div class="flex justify-content-between my-5">
|
||||
<div>{{ date }}</div>
|
||||
<div>|</div>
|
||||
<div class="mb-1">Tisch {{ bill.table_id }}</div>
|
||||
<div>|</div>
|
||||
<div>{{ time }}</div>
|
||||
</div>
|
||||
<div class="text-lg">
|
||||
<div v-for="billItem in billItems" :key="billItem.id" class="flex flex-column mb-1">
|
||||
<div class="flex align-items-center justify-content-between">
|
||||
<div class="entry white-space-nowrap overflow-hidden">{{ billItem.description }}</div>
|
||||
<div>{{ convertToEur(billItem.total) }}</div>
|
||||
</div>
|
||||
<div v-if="billItem.amount !== 1" class="ml-4 font-italic text-sm">{{ billItem.amount }} x {{ convertToEur(billItem.price) }}</div>
|
||||
</div>
|
||||
<div class="flex justify-content-end font-bold mt-5 mb-3">Total: {{ convertToEur(bill.total) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, PropType, ref } from "vue";
|
||||
import { BillsService, controller_Bill, controller_BillItem } from "@/services/openapi";
|
||||
import { convertToEur } from "@/utils";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
import moment from "moment";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BillModal",
|
||||
components: { WaveSpinner },
|
||||
props: { bill: { type: Object as PropType<controller_Bill>, required: true } },
|
||||
setup(props) {
|
||||
const isLoading = ref(true);
|
||||
const billItems = ref<controller_BillItem[]>();
|
||||
const date = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("DD.MM.YYYY"));
|
||||
const time = computed(() => props.bill.created_at && moment.unix(props.bill.created_at).format("HH:mm") + " Uhr");
|
||||
onMounted(() => {
|
||||
props.bill.id &&
|
||||
BillsService.getBillsItems(props.bill.id)
|
||||
.then((res) => {
|
||||
billItems.value = res;
|
||||
})
|
||||
.finally(() => (isLoading.value = false));
|
||||
});
|
||||
|
||||
return { convertToEur, isLoading, billItems, date, time };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
--bs-gutter-x: 0;
|
||||
--bs-gutter-y: 0;
|
||||
width: 100%;
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
}
|
||||
.entry:first-child:after {
|
||||
content: " . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
|
||||
}
|
||||
</style>
|
189
web/src/components/OrderItem/OrderItemList.vue
Normal file
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<ConfirmDialog></ConfirmDialog>
|
||||
<div class="p-card shadow-1 md:p-3">
|
||||
<DataTable :value="orderItems" dataKey="id" :filters="filters" responsiveLayout="scroll" stripedRows class="p-datatable-sm">
|
||||
<template #header>
|
||||
<div class="grid p-fluid align-items-center">
|
||||
<div class="col-9">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="Suchen" @keydown.esc="filters['global'].value = null" />
|
||||
<span v-if="filters['global'].value !== null" class="leftMiddle styling" @click="filters['global'].value = null">
|
||||
<i class="pi pi-times"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-3 text-right">
|
||||
<Button :disabled="isDisabled" icon="pi pi-plus" class="p-button-rounded" @click="modal = true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="description">
|
||||
<template #body="slotProps">
|
||||
<span class="white-space-nowrap">{{ slotProps.data.description }}</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="price" style="text-align: right">
|
||||
<template #body="slotProps">{{ convertToEur(slotProps.data.price) }}</template>
|
||||
</Column>
|
||||
<Column style="width: 3.5rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex align-items-center justify-content-end">
|
||||
<div
|
||||
class="mr-2"
|
||||
:style="{ color: isDisabled ? 'grey' : 'green', cursor: isDisabled ? 'default' : 'pointer' }"
|
||||
@click="editOrderItem(slotProps.data)"
|
||||
>
|
||||
<i class="pi pi-pencil"></i>
|
||||
</div>
|
||||
<div :style="{ color: isDisabled ? 'grey' : 'red', cursor: isDisabled ? 'default' : 'pointer' }" @click="confirmDeleteProduct(slotProps.data)">
|
||||
<i class="pi pi-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #empty><div class="mb-1">Keine Einträge</div></template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<Dialog position="top" v-model:visible="modal" :modal="true" :showHeader="false" @hide="resetModal" style="min-width: 50vw">
|
||||
<div class="p-fluid">
|
||||
<div class="field mt-5">
|
||||
<InputText :disabled="isDisabled" id="name" v-model.trim="orderItem.description" required="true" autofocus @keydown.enter="saveOrderItem" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<InputNumber
|
||||
:disabled="isDisabled"
|
||||
id="currency-germany"
|
||||
v-model="orderItem.price"
|
||||
mode="currency"
|
||||
currency="EUR"
|
||||
locale="de-DE"
|
||||
@keydown.enter="saveOrderItem"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-content-end">
|
||||
<Button :disabled="isDisabled" icon="pi pi-times" class="p-button-text p-button-rounded p-button-secondary mr-2" @click="resetModal" />
|
||||
<Button :loading="isDisabled" icon="pi pi-check" class="p-button-rounded p-button-success" @click="saveOrderItem" />
|
||||
</div>
|
||||
</Dialog>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, watch } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import { OrderItemsService, controller_OrderItem } from "@/services/openapi";
|
||||
import InputText from "primevue/inputtext";
|
||||
import { FilterMatchMode } from "primevue/api";
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import Button from "primevue/button";
|
||||
import { convertToEur, errorToast } from "@/utils";
|
||||
import Dialog from "primevue/dialog";
|
||||
import InputNumber from "primevue/inputnumber";
|
||||
import { useConfirm } from "primevue/useconfirm";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OrderItemList",
|
||||
// eslint-disable-next-line
|
||||
components: { BaseCard, InputText, DataTable, Column, Button, Dialog, InputNumber, ConfirmDialog },
|
||||
props: {
|
||||
orderItems: { type: Array as PropType<controller_OrderItem[]>, default: () => [] },
|
||||
emptyOrderItem: { type: Object as PropType<controller_OrderItem>, default: () => ({}) },
|
||||
title: { type: String, default: "" },
|
||||
},
|
||||
emits: ["orderItemChanged", "orderItemDeleted", "orderItemCreated"],
|
||||
setup(props, { emit }) {
|
||||
const isDisabled = ref(false);
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const modal = ref(false);
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
});
|
||||
const orderItem = ref<controller_OrderItem>({ ...props.emptyOrderItem });
|
||||
function editOrderItem(item: controller_OrderItem) {
|
||||
orderItem.value = { ...item };
|
||||
modal.value = true;
|
||||
}
|
||||
watch(props.emptyOrderItem, () => resetModal());
|
||||
|
||||
function saveOrderItem() {
|
||||
if (isDisabled.value) return;
|
||||
isDisabled.value = true;
|
||||
if (orderItem.value.id) {
|
||||
OrderItemsService.putOrdersItems(orderItem.value)
|
||||
.then((res) => emit("orderItemChanged", res))
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => resetModal());
|
||||
} else {
|
||||
OrderItemsService.postOrdersItems(orderItem.value)
|
||||
.then((res) => emit("orderItemCreated", res))
|
||||
.finally(() => resetModal());
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteProduct(item: controller_OrderItem) {
|
||||
if (isDisabled.value) return;
|
||||
confirm.require({
|
||||
message: item.description + " löschen?",
|
||||
header: "Achtung",
|
||||
position: "top",
|
||||
accept: () => {
|
||||
isDisabled.value = true;
|
||||
item.id &&
|
||||
OrderItemsService.deleteOrdersItems(item.id)
|
||||
.then(() => emit("orderItemDeleted", item))
|
||||
.finally(() => resetModal());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resetModal() {
|
||||
modal.value = false;
|
||||
orderItem.value = { ...props.emptyOrderItem };
|
||||
isDisabled.value = false;
|
||||
}
|
||||
|
||||
return { filters, convertToEur, editOrderItem, saveOrderItem, confirmDeleteProduct, modal, orderItem, resetModal, isDisabled };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.p-datatable .p-datatable-header,
|
||||
.p-datatable .p-datatable-footer {
|
||||
background: transparent !important;
|
||||
}
|
||||
.p-datatable .p-datatable-header,
|
||||
.p-datatable .p-datatable-footer,
|
||||
.p-datatable .p-datatable-tbody > tr,
|
||||
.p-datatable .p-datatable-tbody > tr > td {
|
||||
border-width: 0 !important;
|
||||
}
|
||||
.p-datatable-thead {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.styling {
|
||||
cursor: pointer;
|
||||
color: gray;
|
||||
border-radius: 50%;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
.leftMiddle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
53
web/src/components/Orders/OrderCard.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<SmallCard bgColor="d" :badgeTwo="badgeTwo">
|
||||
<template #description>{{ order.order_item.description }}</template>
|
||||
<template #badgeOne>{{ since }}</template>
|
||||
<template #badgeTwo>Tisch {{ order.table_id }}</template>
|
||||
<template #right>
|
||||
<div class="flex align-items-center">
|
||||
<Button v-if="!newOrder" :disabled="isDisabled" icon="pi pi-check" class="p-button-rounded p-button-success" @click="$emit('orderDone', order)" />
|
||||
<TheBadge v-else color="danger">NEU</TheBadge>
|
||||
</div>
|
||||
</template>
|
||||
</SmallCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
|
||||
import { controller_Order, controller_ItemType } from "@/services/openapi";
|
||||
import { convertToEur, getCurrentTimeSince, lessThan15SecondsAgo } from "@/utils";
|
||||
import Button from "primevue/button";
|
||||
import moment from "moment";
|
||||
import TheBadge from "@/components/UI/TheBadge.vue";
|
||||
import SmallCard from "@/components/UI/SmallCard.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OrderCard",
|
||||
// eslint-disable-next-line
|
||||
components: { SmallCard, TheBadge, Button },
|
||||
props: {
|
||||
order: { type: Object as PropType<controller_Order>, required: true },
|
||||
isDisabled: { type: Boolean, default: false },
|
||||
itemType: { type: Number as PropType<controller_ItemType>, required: false },
|
||||
},
|
||||
emits: ["orderDone"],
|
||||
setup(props) {
|
||||
moment.locale("de");
|
||||
// eslint-disable-next-line
|
||||
let ticker: any;
|
||||
const since = ref(getCurrentTimeSince(props.order.updated_at));
|
||||
const newOrder = ref(lessThan15SecondsAgo(props.order.updated_at));
|
||||
const badgeTwo = computed(() => props.itemType === controller_ItemType.ColdDrink);
|
||||
|
||||
onMounted(() => {
|
||||
ticker = setInterval(() => {
|
||||
since.value = getCurrentTimeSince(props.order.updated_at);
|
||||
newOrder.value === true && (newOrder.value = lessThan15SecondsAgo(props.order.updated_at));
|
||||
}, 1000);
|
||||
});
|
||||
onUnmounted(() => ticker && clearInterval(ticker));
|
||||
|
||||
return { convertToEur, since, newOrder, badgeTwo };
|
||||
},
|
||||
});
|
||||
</script>
|
60
web/src/components/Orders/OrderSection.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div v-if="orders.length !== 0">
|
||||
<BaseToolbar :icon="detailedItemTypeIcon(itemType)" :title="title || detailedItemTypeString(itemType)" btnIcon="check" @click="checkAllOpenOrders" />
|
||||
<div class="grid">
|
||||
<OrderCard
|
||||
v-for="order in orders"
|
||||
v-bind:key="order.id"
|
||||
:order="order"
|
||||
:isDisabled="isDisabled"
|
||||
:bigRight="true"
|
||||
@orderDone="(o) => orderDone(o)"
|
||||
:itemType="itemType"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, PropType, ref } from "vue";
|
||||
import { OrdersService, controller_Order, controller_ItemType } from "@/services/openapi";
|
||||
import { detailedItemTypeIcon, detailedItemTypeString, errorToast, lessThan15SecondsAgo } from "@/utils";
|
||||
import OrderCard from "@/components/Orders/OrderCard.vue";
|
||||
import BaseToolbar from "@/components/UI/BaseToolbar.vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { disabled } from "@/keys";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OrderSection",
|
||||
components: { OrderCard, BaseToolbar },
|
||||
props: {
|
||||
orders: { type: Object as PropType<controller_Order[]>, required: true },
|
||||
icon: { type: String, required: false },
|
||||
title: { type: String, required: false },
|
||||
itemType: { type: Number as PropType<controller_ItemType>, required: false },
|
||||
},
|
||||
emits: ["filterOrders"],
|
||||
setup(props, { emit }) {
|
||||
const toast = useToast();
|
||||
const isDisabled = inject(disabled, ref(false));
|
||||
const collapseOrders = ref(true);
|
||||
const collapseIcon = computed(() => (collapseOrders.value ? "chevron-down" : "chevron-up"));
|
||||
|
||||
function checkAllOpenOrders() {
|
||||
props.orders.forEach((order) => {
|
||||
if (!lessThan15SecondsAgo(order.updated_at)) orderDone(order);
|
||||
});
|
||||
}
|
||||
|
||||
function orderDone(order: controller_Order) {
|
||||
isDisabled.value = true;
|
||||
order.is_served = true;
|
||||
OrdersService.putOrders(order)
|
||||
.then(() => emit("filterOrders", order.id))
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => (isDisabled.value = false));
|
||||
}
|
||||
return { detailedItemTypeIcon, detailedItemTypeString, checkAllOpenOrders, orderDone, isDisabled, collapseIcon, collapseOrders };
|
||||
},
|
||||
});
|
||||
</script>
|
25
web/src/components/Tables/OrderAmountChange.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="flex align-items-center">
|
||||
<div @click="!isDisabled && $emit('decrementOrder')" :style="{ color: isDisabled ? 'grey' : 'red' }" style="cursor: pointer">
|
||||
<i class="pi pi-minus"></i>
|
||||
</div>
|
||||
<div class="mx-2 font-bold">{{ order.order_count }}</div>
|
||||
<div @click="!isDisabled && $emit('incrementOrder')" :style="{ color: isDisabled ? 'grey' : 'green' }" style="cursor: pointer">
|
||||
<i class="pi pi-plus"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from "vue";
|
||||
import { controller_Order } from "@/services/openapi";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OrderAmountChange",
|
||||
props: {
|
||||
order: { type: Object as PropType<controller_Order>, required: true },
|
||||
isDisabled: { type: Boolean, default: false },
|
||||
},
|
||||
emits: ["incrementOrder", "decrementOrder"],
|
||||
});
|
||||
</script>
|
48
web/src/components/Tables/OverviewPerType.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div>
|
||||
<BaseToolbar :title="generalItemTypeString(type)" :icon="generalItemTypeIcon(type)" @click="$emit('openModal', type)" btnIcon="plus" />
|
||||
<div class="grid">
|
||||
<TableOrderCard v-for="order in OrdersForType" v-bind:key="order.id" :order="order">
|
||||
<div class="flex align-items-end">
|
||||
<OrderAmountChange :order="order" :isDisabled="isLoading" @incrementOrder="incrementOrder(order)" @decrementOrder="decrementOrder(order)" />
|
||||
</div>
|
||||
</TableOrderCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, PropType, ref } from "vue";
|
||||
import { OrdersService, controller_Order } from "@/services/openapi";
|
||||
import { convertToEur, generalItemTypeString, generalItemTypeIcon } from "@/utils";
|
||||
import BaseToolbar from "@/components/UI/BaseToolbar.vue";
|
||||
import TableOrderCard from "@/components/Tables/TableOrderCard.vue";
|
||||
import OrderAmountChange from "@/components/Tables/OrderAmountChange.vue";
|
||||
import { loading } from "@/keys";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OverviewPerType",
|
||||
components: { TableOrderCard, BaseToolbar, OrderAmountChange },
|
||||
props: {
|
||||
orders: { type: Array as PropType<controller_Order[]>, default: () => [] },
|
||||
type: { type: Array as PropType<number[]>, required: true },
|
||||
},
|
||||
emits: ["openModal", "getData"],
|
||||
setup(props, { emit }) {
|
||||
const OrdersForType = computed(() => props.orders.filter((order) => props.type.includes(order.order_item.item_type)));
|
||||
const isLoading = inject(loading, ref(false));
|
||||
|
||||
function incrementOrder(order: controller_Order) {
|
||||
isLoading.value = true;
|
||||
OrdersService.postOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
|
||||
}
|
||||
|
||||
function decrementOrder(order: controller_Order) {
|
||||
isLoading.value = true;
|
||||
OrdersService.deleteOrders(order.order_item_id, order.table_id).finally(() => emit("getData"));
|
||||
}
|
||||
|
||||
return { OrdersForType, isLoading, convertToEur, incrementOrder, decrementOrder, generalItemTypeIcon, generalItemTypeString };
|
||||
},
|
||||
});
|
||||
</script>
|
48
web/src/components/Tables/TableCard.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<SmallCard bgColor="a" :to="'/tables/' + table.id">
|
||||
<template #description>Tisch {{ table.id }}</template>
|
||||
<template #badge>{{ since }}</template>
|
||||
<template #right>
|
||||
<div class="flex align-items-end">
|
||||
<TheBadge v-if="table.order_count" class="topRight">{{ table.order_count }}</TheBadge>
|
||||
<div v-if="table.total" class="font-bold">{{ convertToEur(table.total) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</SmallCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, PropType, ref } from "vue";
|
||||
import { controller_Table } from "@/services/openapi";
|
||||
import moment from "moment";
|
||||
import { convertToEur, getCurrentTimeSince } from "@/utils";
|
||||
import TheBadge from "@/components/UI/TheBadge.vue";
|
||||
import SmallCard from "@/components/UI/SmallCard.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TableCard",
|
||||
components: { TheBadge, SmallCard },
|
||||
props: { table: { type: Object as PropType<controller_Table>, required: true } },
|
||||
setup(props) {
|
||||
moment.locale("de");
|
||||
// eslint-disable-next-line
|
||||
let ticker: any;
|
||||
const since = ref(getCurrentTimeSince(props.table.updated_at));
|
||||
onMounted(() => {
|
||||
ticker = setInterval(() => {
|
||||
since.value = getCurrentTimeSince(props.table.updated_at);
|
||||
}, 1000);
|
||||
});
|
||||
onUnmounted(() => ticker && clearInterval(ticker));
|
||||
return { since, convertToEur };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.topRight {
|
||||
position: absolute;
|
||||
top: -0.2rem;
|
||||
right: 0.3rem;
|
||||
}
|
||||
</style>
|
30
web/src/components/Tables/TableOrderCard.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<SmallCard bgColor="d" :badgeTwo="order.total !== order.order_item.price">
|
||||
<template #description>{{ order.order_item.description }}</template>
|
||||
<template #badgeOne>{{ convertToEur(order.order_item.price) }}</template>
|
||||
<template #badgeTwo>{{ convertToEur(order.total) }}</template>
|
||||
<template #right>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</SmallCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, PropType } from "vue";
|
||||
import { controller_Order, controller_ItemType } from "@/services/openapi";
|
||||
import { convertToEur } from "@/utils";
|
||||
import SmallCard from "@/components/UI/SmallCard.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TableOrderCard",
|
||||
components: { SmallCard },
|
||||
props: {
|
||||
order: { type: Object as PropType<controller_Order>, required: true },
|
||||
},
|
||||
emits: ["decrementOrder", "incrementOrder"],
|
||||
setup(props) {
|
||||
const showTotal = computed(() => props.order.order_item.price !== props.order.total);
|
||||
return { convertToEur, controller_ItemType, showTotal };
|
||||
},
|
||||
});
|
||||
</script>
|
177
web/src/components/Tables/TableOverview.vue
Normal file
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="initialLoading" />
|
||||
<div v-else>
|
||||
<OverviewPerType :type="[controller_ItemType.Food]" :orders="orders" @getData="getData" @openModal="(t) => addBeverage(t)" />
|
||||
<OverviewPerType
|
||||
:type="[controller_ItemType.ColdDrink, controller_ItemType.HotDrink]"
|
||||
:orders="orders"
|
||||
@getData="getData"
|
||||
@openModal="(t) => addBeverage(t)"
|
||||
/>
|
||||
<div class="h-4rem"></div>
|
||||
|
||||
<BottomNavigation>
|
||||
<template #left>
|
||||
<router-link :to="{ name: 'Tables' }" class="no-underline">
|
||||
<Button :disabled="isLoading" icon="pi pi-arrow-left" class="p-button-rounded" />
|
||||
</router-link>
|
||||
</template>
|
||||
<template #middle>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<div class="text-sm">Tisch {{ table }}</div>
|
||||
<div class="font-bold">{{ convertToEur(total) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<router-link
|
||||
:style="{ cursor: isLoading || orders.length === 0 ? 'default' : 'pointer' }"
|
||||
:to="{ name: isLoading || orders.length === 0 ? 'TableDetail' : 'Checkout' }"
|
||||
class="no-underline"
|
||||
>
|
||||
<Button :disabled="isLoading || orders.length === 0" icon="pi pi-money-bill" class="p-button-danger p-button-rounded" />
|
||||
</router-link>
|
||||
</template>
|
||||
</BottomNavigation>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Sidebar v-model:visible="newOrderModal" :baseZIndex="10000" position="full">
|
||||
<div class="p-fluid">
|
||||
<Listbox
|
||||
v-model="selected"
|
||||
:options="options"
|
||||
:filter="true"
|
||||
optionLabel="description"
|
||||
dataKey="id"
|
||||
optionValue="id"
|
||||
listStyle="max-height:65vh"
|
||||
filterPlaceholder="Suchen"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-content-end mt-4">
|
||||
<Button :loading="isLoading" label="Speichern" icon="pi pi-check" class="p-button p-button-success mr-3" @click="postOrder" />
|
||||
</div>
|
||||
</Sidebar>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, provide, ref } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import { useStore } from "vuex";
|
||||
import { OrdersService, controller_Order, controller_OrderItem, controller_ItemType } from "@/services/openapi";
|
||||
import BottomNavigation from "@/components/UI/BottomNavigation.vue";
|
||||
import Button from "primevue/button";
|
||||
import { convertToEur } from "@/utils";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
import Sidebar from "primevue/sidebar";
|
||||
import Listbox from "primevue/listbox";
|
||||
import OverviewPerType from "@/components/Tables/OverviewPerType.vue";
|
||||
import { loading } from "@/keys";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TableOverview",
|
||||
// eslint-disable-next-line
|
||||
components: { OverviewPerType, WaveSpinner, BottomNavigation, BaseCard, Button, Sidebar, Listbox },
|
||||
props: { id: { type: String, default: "0" } },
|
||||
setup(props) {
|
||||
const initialLoading = ref(false);
|
||||
const isLoading = ref(false);
|
||||
provide(loading, isLoading);
|
||||
const newOrderModal = ref(false);
|
||||
const store = useStore();
|
||||
const selectedOrder = ref();
|
||||
const table = computed(() => parseInt(props.id));
|
||||
const total = ref(0);
|
||||
const orderItems = computed(() => store.getters.getOrderItems);
|
||||
const options = ref();
|
||||
const orders = ref<controller_Order[]>([]);
|
||||
|
||||
store.dispatch("getAllOrderItems");
|
||||
|
||||
getData(true);
|
||||
function getData(initial = false) {
|
||||
initial && (initialLoading.value = true);
|
||||
OrdersService.getOrders(table.value, true)
|
||||
.then((res) => (orders.value = res))
|
||||
.finally(() => {
|
||||
updateTotal();
|
||||
resetValues();
|
||||
});
|
||||
}
|
||||
|
||||
function resetValues() {
|
||||
newOrderModal.value = false;
|
||||
selectedOrder.value = undefined;
|
||||
isLoading.value = false;
|
||||
initialLoading.value = false;
|
||||
}
|
||||
|
||||
function updateTotal() {
|
||||
let temp = 0;
|
||||
orders.value.forEach((order) => (temp += order.total));
|
||||
total.value = temp;
|
||||
}
|
||||
|
||||
function addBeverage(itemType: controller_ItemType[]) {
|
||||
newOrderModal.value = true;
|
||||
options.value = [];
|
||||
itemType.forEach((type) => {
|
||||
options.value = options.value.concat(orderItems.value.get(type));
|
||||
});
|
||||
options.value.sort((a: controller_OrderItem, b: controller_OrderItem) => {
|
||||
const x = a.description.toLowerCase();
|
||||
const y = b.description.toLowerCase();
|
||||
if (x < y) return -1;
|
||||
if (x > y) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function postOrder() {
|
||||
isLoading.value = true;
|
||||
if (selectedOrder.value) {
|
||||
OrdersService.postOrders(selectedOrder.value, table.value).finally(() => getData());
|
||||
} else isLoading.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
initialLoading,
|
||||
isLoading,
|
||||
newOrderModal,
|
||||
selected: selectedOrder,
|
||||
options,
|
||||
table,
|
||||
total,
|
||||
convertToEur,
|
||||
addBeverage,
|
||||
controller_ItemType,
|
||||
postOrder,
|
||||
orders,
|
||||
getData,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.p-sidebar-content {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.p-listbox {
|
||||
border: 0 !important;
|
||||
}
|
||||
</style>
|
50
web/src/components/UI/BaseCard.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BaseCard",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
--bs-gutter-x: 0;
|
||||
--bs-gutter-y: 0;
|
||||
width: 100%;
|
||||
padding-right: calc(var(--bs-gutter-x) * 0.5);
|
||||
padding-left: calc(var(--bs-gutter-x) * 0.5);
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
@media (min-width: 576px) {
|
||||
.container {
|
||||
max-width: 540px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
max-width: 720px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.container {
|
||||
max-width: 960px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.container {
|
||||
max-width: 1140px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1400px) {
|
||||
.container {
|
||||
max-width: 1320px;
|
||||
}
|
||||
}
|
||||
</style>
|
17
web/src/components/UI/BaseItem.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div class="p-card p-2 shadow-1" :style="`color: var(--text-color); background-color: var(--surface-${bgColor})`" :class="`pr-${paddingRight}`">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BaseItem",
|
||||
props: {
|
||||
paddingRight: { type: String, default: "2" },
|
||||
bgColor: { type: String, default: "a" },
|
||||
},
|
||||
});
|
||||
</script>
|
43
web/src/components/UI/BaseToolbar.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<Toolbar class="border-0 shadow-1 my-2 p-2 bg-color">
|
||||
<template #start>
|
||||
<div class="flex align-items-center">
|
||||
<div class="font-bold text-2xl">{{ title }}</div>
|
||||
<div v-if="icon" class="ml-3"><font-awesome-icon :icon="icon" style="font-size: 1.5rem"></font-awesome-icon></div>
|
||||
</div>
|
||||
</template>
|
||||
<template #end>
|
||||
<Button v-if="btnIcon" :disabled="isLoading" :icon="'pi pi-' + btnIcon" class="p-button-success p-button-rounded" @click="$emit('click')" />
|
||||
</template>
|
||||
</Toolbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, inject, ref } from "vue";
|
||||
import Toolbar from "primevue/toolbar";
|
||||
import Button from "primevue/button";
|
||||
import { loading } from "@/keys";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BaseToolbar",
|
||||
// eslint-disable-next-line
|
||||
components: { Toolbar, Button },
|
||||
emits: ["click"],
|
||||
props: {
|
||||
title: { type: String, default: "" },
|
||||
icon: { type: String, default: "" },
|
||||
btnIcon: { type: String, default: "" },
|
||||
},
|
||||
setup() {
|
||||
const isLoading = inject(loading, ref(false));
|
||||
return { isLoading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-color {
|
||||
background-color: var(--surface-a);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
33
web/src/components/UI/BottomNavigation.vue
Normal file
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="fixed-bottom">
|
||||
<div class="flex justify-content-between align-items-center border-round-xs py-2 px-3 bg-color shadow-1">
|
||||
<div><slot name="left"></slot></div>
|
||||
<div><slot name="middle"></slot></div>
|
||||
<div><slot name="right"></slot></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BottomNavigation",
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fixed-bottom {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.bg-color {
|
||||
background-color: var(--surface-a);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
60
web/src/components/UI/SmallCard.vue
Normal file
|
@ -0,0 +1,60 @@
|
|||
<template>
|
||||
<div v-if="!to && !bigRight" class="col-12 lg:col-6">
|
||||
<BaseItem class="relative" :bgColor="bgColor">
|
||||
<div class="flex flex-column justify-content-between">
|
||||
<div class="white-space-nowrap font-bold overflow-hidden text-overflow-ellipsis"><slot name="description"></slot></div>
|
||||
<div class="flex justify-content-between">
|
||||
<div class="flex align-items-center mt-1">
|
||||
<TheBadge size="sm" color="info"><slot name="badgeOne"></slot></TheBadge>
|
||||
<TheBadge v-if="badgeTwo" size="sm" color="warning" class="ml-2"><slot name="badgeTwo"></slot></TheBadge>
|
||||
</div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</BaseItem>
|
||||
</div>
|
||||
<div v-else-if="bigRight" class="col-12 lg:col-6">
|
||||
<BaseItem class="relative" :bgColor="bgColor">
|
||||
<div class="flex justify-content-between">
|
||||
<div class="flex flex-column overflow-hidden">
|
||||
<div class="white-space-nowrap font-bold overflow-hidden text-overflow-ellipsis"><slot name="description"></slot></div>
|
||||
<div class="flex align-items-center mt-1">
|
||||
<TheBadge size="sm" color="info"><slot name="badgeOne"></slot></TheBadge>
|
||||
<TheBadge v-if="badgeTwo" size="sm" color="warning" class="ml-2"><slot name="badgeTwo"></slot></TheBadge>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</BaseItem>
|
||||
</div>
|
||||
<router-link v-else class="col-12 lg:col-6 no-underline" :to="to">
|
||||
<BaseItem class="relative" :bgColor="bgColor">
|
||||
<div class="flex justify-content-between overflow-hidden">
|
||||
<div class="flex flex-column align-items-start">
|
||||
<div class="white-space-nowrap overflow-hidden text-overflow-ellipsis font-bold"><slot name="description"></slot></div>
|
||||
<div class="flex align-items-center mt-1">
|
||||
<TheBadge size="sm" color="success"><slot name="badge"></slot></TheBadge>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="right"></slot>
|
||||
</div>
|
||||
</BaseItem>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import TheBadge from "@/components/UI/TheBadge.vue";
|
||||
import BaseItem from "@/components/UI/BaseItem.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SmallCard",
|
||||
components: { TheBadge, BaseItem },
|
||||
props: {
|
||||
bgColor: { type: String, default: "a" },
|
||||
to: { type: String, default: "" },
|
||||
badgeTwo: { type: Boolean, default: true },
|
||||
bigRight: { type: Boolean, default: false },
|
||||
},
|
||||
});
|
||||
</script>
|
36
web/src/components/UI/TheBadge.vue
Normal file
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="badge p-badge" :class="`p-badge-${color}`">
|
||||
<span :class="`text-${size}`"><slot></slot></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TheBadge",
|
||||
props: { color: { type: String, default: "primary" }, size: { type: String, default: "xs" } },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.badge {
|
||||
--bs-badge-padding-x: 0.5em;
|
||||
--bs-badge-padding-y: 0.3em;
|
||||
--bs-badge-font-size: 0.75em;
|
||||
--bs-badge-font-weight: 700;
|
||||
--bs-badge-border-radius: 0.375rem;
|
||||
display: inline-block;
|
||||
padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x);
|
||||
font-size: var(--bs-badge-font-size);
|
||||
font-weight: var(--bs-badge-font-weight);
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: var(--bs-badge-border-radius, 0);
|
||||
}
|
||||
.badge:empty {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
120
web/src/components/UI/TheNavigation.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<UserSettings />
|
||||
<Menubar :model="items" class="py-1 px-3 mb-3 shadow-1 border-0 bg-color">
|
||||
<template #start>
|
||||
<router-link class="no-underline" to="/tables"><img alt="logo" class="mr-2" width="50" height="50" src="@/assets/logos/logo.png" /></router-link>
|
||||
</template>
|
||||
<template #end>
|
||||
<div v-if="tablePath">
|
||||
<div v-if="editor">
|
||||
<Button v-if="tablesCount !== 0" :disabled="isLoading" icon="pi pi-minus" class="p-button-danger p-button-rounded mr-2" @click="removeTable" />
|
||||
<Button :disabled="isLoading" icon="pi pi-plus" class="p-button-success p-button-rounded" @click="addTable" />
|
||||
</div>
|
||||
</div>
|
||||
<router-link v-else :to="{ name: 'Tables' }" class="no-underline">
|
||||
<Button label="Tische" class="p-button-secondary" icon="pi pi-table" />
|
||||
</router-link>
|
||||
</template>
|
||||
</Menubar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, provide, ref } from "vue";
|
||||
import Menubar from "primevue/menubar";
|
||||
import { useStore } from "vuex";
|
||||
import Button from "primevue/button";
|
||||
import { useRoute } from "vue-router";
|
||||
import { TablesService, controller_ItemType } from "@/services/openapi";
|
||||
import { detailedItemTypeString, errorToast } from "@/utils";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { visible } from "@/keys";
|
||||
import UserSettings from "@/components/User/UserSettings.vue";
|
||||
import { MenuItem } from "primevue/menuitem";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TheNavigation",
|
||||
// eslint-disable-next-line
|
||||
components: { UserSettings, Menubar, Button },
|
||||
emits: ["logout"],
|
||||
setup(_, { emit }) {
|
||||
const toast = useToast();
|
||||
const store = useStore();
|
||||
const route = useRoute();
|
||||
const isLoading = ref(false);
|
||||
const tablesCount = computed(() => store.getters.getTablesCount);
|
||||
const tablePath = computed(() => route.path === "/tables");
|
||||
const user = computed<string>(() => store.getters.getUsername);
|
||||
const maker = computed(() => store.getters.getGroups.includes("make"));
|
||||
const editor = computed(() => store.getters.getGroups.includes("edit"));
|
||||
const accountant = computed(() => store.getters.getGroups.includes("account"));
|
||||
|
||||
const settingsVisible = ref(false);
|
||||
provide(visible, settingsVisible);
|
||||
|
||||
function removeTable() {
|
||||
isLoading.value = true;
|
||||
TablesService.deleteTables()
|
||||
.then(() => {
|
||||
store.dispatch("removeLastTable");
|
||||
})
|
||||
.catch((err) => {
|
||||
errorToast(toast, err.body.error);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
function addTable() {
|
||||
isLoading.value = true;
|
||||
TablesService.postTables()
|
||||
.then((res) => {
|
||||
store.dispatch("addTable", res);
|
||||
})
|
||||
.catch((err) => {
|
||||
errorToast(toast, err.body.error);
|
||||
})
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
const items = ref<MenuItem[]>([
|
||||
{ label: "Bestellungen", icon: "pi pi-fw pi-history", to: "/orders", visible: () => maker.value },
|
||||
{
|
||||
label: "Artikel",
|
||||
icon: "pi pi-fw pi-shopping-cart",
|
||||
items: [
|
||||
{ label: detailedItemTypeString(controller_ItemType.Food), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.Food },
|
||||
{ label: detailedItemTypeString(controller_ItemType.ColdDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.ColdDrink },
|
||||
{ label: detailedItemTypeString(controller_ItemType.HotDrink), icon: "pi pi-fw pi-shopping-cart", to: "/items/" + controller_ItemType.HotDrink },
|
||||
],
|
||||
visible: () => editor.value,
|
||||
},
|
||||
{ label: "Rechnungen", icon: "pi pi-fw pi-euro", to: "/bills", visible: () => accountant.value },
|
||||
{ separator: true },
|
||||
{
|
||||
label: user.value,
|
||||
icon: "pi pi-fw pi-user",
|
||||
items: [
|
||||
{
|
||||
label: "Einstellungen",
|
||||
icon: "pi pi-fw pi-cog",
|
||||
command: () => (settingsVisible.value = true),
|
||||
visible: () => maker.value,
|
||||
},
|
||||
{ label: "Abmelden", icon: "pi pi-fw pi-power-off", command: () => emit("logout") },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return { items, tablePath, removeTable, addTable, isLoading, tablesCount, editor };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bg-color {
|
||||
background-color: var(--surface-a);
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
87
web/src/components/UI/WaveSpinner.vue
Normal file
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<div v-if="isShowing" class="center">
|
||||
<div class="lds-ellipsis">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "WaveSpinner",
|
||||
setup() {
|
||||
const isShowing = ref(false);
|
||||
setTimeout(() => (isShowing.value = true), 200);
|
||||
return { isShowing };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.lds-ellipsis {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ellipsis div {
|
||||
position: absolute;
|
||||
top: 33px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-color);
|
||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
||||
}
|
||||
.lds-ellipsis div:nth-child(1) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis1 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(2) {
|
||||
left: 8px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(3) {
|
||||
left: 32px;
|
||||
animation: lds-ellipsis2 0.6s infinite;
|
||||
}
|
||||
.lds-ellipsis div:nth-child(4) {
|
||||
left: 56px;
|
||||
animation: lds-ellipsis3 0.6s infinite;
|
||||
}
|
||||
@keyframes lds-ellipsis1 {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis3 {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes lds-ellipsis2 {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
100% {
|
||||
transform: translate(24px, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
48
web/src/components/User/UserSettings.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<Sidebar v-model:visible="isVisible" :baseZIndex="10000" class="pl-3">
|
||||
<div class="font-bold mb-3">Bestellungen anzeigen:</div>
|
||||
<div class="flex align-items-center mb-3">
|
||||
<InputSwitch :disabled="isLoading" id="show_cold_drinks" v-model="user.show_cold_drinks" @change="updateUser()" />
|
||||
<label for="show_cold_drinks" class="ml-3">Kaltgetränke</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<InputSwitch :disabled="isLoading" id="show_hot_drinks" v-model="user.show_hot_drinks" @change="updateUser()" />
|
||||
<label for="show_hot_drinks" class="ml-3">Heiß/Eiskaffee & Speisen</label>
|
||||
</div>
|
||||
</Sidebar>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, inject, ref } from "vue";
|
||||
import Sidebar from "primevue/sidebar";
|
||||
import { visible } from "@/keys";
|
||||
import { controller_User, UsersService } from "@/services/openapi";
|
||||
import InputSwitch from "primevue/inputswitch";
|
||||
import { useStore } from "vuex";
|
||||
import { errorToast } from "@/utils";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
|
||||
export default defineComponent({
|
||||
name: "UserSettings",
|
||||
components: { Sidebar, InputSwitch },
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const toast = useToast();
|
||||
const isLoading = ref(false);
|
||||
const isVisible = inject(visible, ref(false));
|
||||
const user = computed<controller_User>(() => store.getters.getUser);
|
||||
|
||||
function updateUser() {
|
||||
isLoading.value = true;
|
||||
UsersService.putUsers(user.value)
|
||||
.then((res) => store.commit("setUser", res))
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
return { isVisible, user, updateUser, isLoading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
5
web/src/keys.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { InjectionKey, Ref } from "vue";
|
||||
|
||||
export const loading = Symbol() as InjectionKey<Ref<boolean>>;
|
||||
export const disabled = Symbol() as InjectionKey<Ref<boolean>>;
|
||||
export const visible = Symbol() as InjectionKey<Ref<boolean>>;
|
137
web/src/main.ts
Normal file
|
@ -0,0 +1,137 @@
|
|||
import { createApp } from "vue";
|
||||
import { OpenAPI, UsersService } from "@/services/openapi";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import store from "./store";
|
||||
import PrimeVue from "primevue/config";
|
||||
import ConfirmationService from "primevue/confirmationservice";
|
||||
import ToastService from "primevue/toastservice";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faMugHot } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faChampagneGlasses } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCheese } from "@fortawesome/free-solid-svg-icons";
|
||||
library.add(faMugHot, faChampagneGlasses, faCheese);
|
||||
|
||||
import "primevue/resources/primevue.min.css";
|
||||
import "primeicons/primeicons.css";
|
||||
import "primeflex/primeflex.css";
|
||||
|
||||
export const API_ENDPOINT_URL = window.origin + "/api";
|
||||
OpenAPI.BASE = API_ENDPOINT_URL;
|
||||
|
||||
async function getHealth() {
|
||||
const response = await fetch(API_ENDPOINT_URL + "/health");
|
||||
const groups = response.headers.get("remote-groups")?.split(",");
|
||||
store.commit("setGroups", groups);
|
||||
const user = await UsersService.getUsers(response.headers.get("remote-name") || "Benutzer");
|
||||
store.commit("setUser", user);
|
||||
}
|
||||
|
||||
getHealth().then(() => {
|
||||
const app = createApp(App);
|
||||
app.use(store);
|
||||
app.use(router);
|
||||
app.use(PrimeVue, {
|
||||
locale: {
|
||||
startsWith: "Beginnt mit",
|
||||
contains: "enthält",
|
||||
notContains: "enthält nicht",
|
||||
endsWith: "endet mit",
|
||||
equals: "entspricht",
|
||||
notEquals: "entspricht nicht",
|
||||
noFilter: "Kein Filter",
|
||||
lt: "Weniger als",
|
||||
lte: "Weniger als oder gleich viel",
|
||||
gt: "Mehr als",
|
||||
gte: "Mehr als oder gleich viel",
|
||||
dateIs: "Datum ist",
|
||||
dateIsNot: "Datum ist nicht",
|
||||
dateBefore: "Datum liegt vor",
|
||||
dateAfter: "Datum liegt nach",
|
||||
clear: "Löschen",
|
||||
apply: "Anwenden",
|
||||
matchAll: "Alle abgleichen",
|
||||
matchAny: "Mit jedem abgleichen",
|
||||
addRule: "Regel hinzufügen",
|
||||
removeRule: "Regel entfernen",
|
||||
accept: "Ja",
|
||||
reject: "Nein",
|
||||
choose: "Auswählen",
|
||||
upload: "Hochladen",
|
||||
cancel: "Abbrechen",
|
||||
dayNames: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"],
|
||||
dayNamesShort: ["Son", "Mon", "Die", "Mit", "Don", "Fre", "Sam"],
|
||||
dayNamesMin: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"],
|
||||
monthNames: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"],
|
||||
monthNamesShort: ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"],
|
||||
today: "Heute",
|
||||
weekHeader: "Wo",
|
||||
firstDayOfWeek: 1,
|
||||
dateFormat: "dd.mm.yy",
|
||||
weak: "Schwach",
|
||||
medium: "Medium",
|
||||
strong: "Stark",
|
||||
passwordPrompt: "Passwort eingeben",
|
||||
emptySearchMessage: "Keine Ergebnisse gefunden",
|
||||
emptyMessage: "Keine verfügbaren Optionen",
|
||||
aria: {
|
||||
trueLabel: "True",
|
||||
falseLabel: "False",
|
||||
nullLabel: "Nicht ausgewählt",
|
||||
star: "1 Stern",
|
||||
stars: "{star} Sterne",
|
||||
selectAll: "Alle Artikel ausgewählt",
|
||||
unselectAll: "Alle Artikel nicht ausgewählt",
|
||||
close: "Schließen",
|
||||
previous: "Vorherig",
|
||||
next: "Nächste",
|
||||
navigation: "Navigation",
|
||||
scrollTop: "Nach Oben scrollen",
|
||||
moveTop: "Nach oben bewegen",
|
||||
moveUp: "Aufsteigen",
|
||||
moveDown: "Absteigen",
|
||||
moveBottom: "Nach unten bewegen",
|
||||
moveToTarget: "Move to Target",
|
||||
moveToSource: "Move to Source",
|
||||
moveAllToTarget: "Move All to Target",
|
||||
moveAllToSource: "Move All to Source",
|
||||
pageLabel: "{page}",
|
||||
firstPageLabel: "Erste Seite",
|
||||
lastPageLabel: "Letzte Seite",
|
||||
nextPageLabel: "Nächste Seite",
|
||||
prevPageLabel: "Vorherige Seite",
|
||||
rowsPerPageLabel: "Reihen pro Seite",
|
||||
jumpToPageDropdownLabel: "Jump to Page Dropdown",
|
||||
jumpToPageInputLabel: "Jump to Page Input",
|
||||
selectRow: "Reihe ausgewählt",
|
||||
unselectRow: "Reihe abgewählt",
|
||||
expandRow: "Row Expanded",
|
||||
collapseRow: "Row Collapsed",
|
||||
showFilterMenu: "Show Filter Menu",
|
||||
hideFilterMenu: "Hide Filter Menu",
|
||||
filterOperator: "Filter Operator",
|
||||
filterConstraint: "Filter Constraint",
|
||||
editRow: "Row Edit",
|
||||
saveEdit: "Save Edit",
|
||||
cancelEdit: "Cancel Edit",
|
||||
listView: "List View",
|
||||
gridView: "Grid View",
|
||||
slide: "Slide",
|
||||
slideNumber: "{slideNumber}",
|
||||
zoomImage: "Zoom Image",
|
||||
zoomIn: "Zoom In",
|
||||
zoomOut: "Zoom Out",
|
||||
rotateRight: "Rotate Right",
|
||||
rotateLeft: "Rotate Left",
|
||||
},
|
||||
},
|
||||
});
|
||||
app.use(ConfirmationService);
|
||||
app.use(ToastService);
|
||||
app.component("font-awesome-icon", FontAwesomeIcon);
|
||||
|
||||
router.isReady().then(() => {
|
||||
app.mount("#app");
|
||||
});
|
||||
});
|
39
web/src/router/index.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import TableView from "@/views/Tables.vue";
|
||||
import ItemView from "@/views/Items.vue";
|
||||
import OrderView from "@/views/Orders.vue";
|
||||
import BillView from "@/views/Bills.vue";
|
||||
import CheckoutView from "@/views/Checkout.vue";
|
||||
import TableDetail from "@/components/Tables/TableOverview.vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{ path: "/tables", name: "Tables", component: TableView, meta: { needsAuth: true } },
|
||||
{ path: "/tables/:id", name: "TableDetail", props: true, component: TableDetail, meta: { needsAuth: true } },
|
||||
{ path: "/tables/:id/checkout", name: "Checkout", props: true, component: CheckoutView, meta: { needsAuth: true } },
|
||||
{ path: "/orders", name: "Orders", component: OrderView, meta: { needsAuth: true } },
|
||||
{ path: "/items/:id", name: "Items", props: true, component: ItemView, meta: { needsAuth: true } },
|
||||
{ path: "/bills", name: "Bills", component: BillView, meta: { needsAuth: true } },
|
||||
{ path: "/:pathMatch(.*)*", redirect: { name: "Tables" } },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
routes,
|
||||
history: createWebHistory(process.env.BASE_URL),
|
||||
});
|
||||
|
||||
router.beforeEach(async (to) => {
|
||||
const store = useStore();
|
||||
if (to.name === "Bills") {
|
||||
if (!store.getters.getGroups.includes("account")) return "/tables";
|
||||
}
|
||||
if (to.name === "Orders") {
|
||||
if (!store.getters.getGroups.includes("make")) return "/tables";
|
||||
}
|
||||
if (to.name === "Items") {
|
||||
if (!store.getters.getGroups.includes("edit")) return "/tables";
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export default router;
|
6
web/src/shims-vue.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable */
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
43
web/src/store/index.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { createStore } from "vuex";
|
||||
import tableStore from "@/store/tables";
|
||||
import orderItemStore from "@/store/orderItems";
|
||||
import { controller_User } from "@/services/openapi";
|
||||
|
||||
interface AppStateModel {
|
||||
user: controller_User;
|
||||
groups: string[];
|
||||
}
|
||||
export default createStore({
|
||||
state: {
|
||||
user: {
|
||||
username: "",
|
||||
show_cold_drinks: true,
|
||||
show_hot_drinks: true,
|
||||
},
|
||||
groups: [""],
|
||||
},
|
||||
getters: {
|
||||
getUser(state: AppStateModel) {
|
||||
return state.user;
|
||||
},
|
||||
getGroups(state: AppStateModel) {
|
||||
return state.groups;
|
||||
},
|
||||
getUsername(state: AppStateModel) {
|
||||
return state.user.username;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setUser(state: AppStateModel, _user: controller_User) {
|
||||
state.user = _user;
|
||||
},
|
||||
setGroups(state: AppStateModel, groups: string[]) {
|
||||
state.groups = groups;
|
||||
},
|
||||
},
|
||||
actions: {},
|
||||
modules: {
|
||||
tableStore,
|
||||
orderItemStore,
|
||||
},
|
||||
});
|
76
web/src/store/orderItems/index.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { OrderItemsService, controller_OrderItem, controller_ItemType } from "@/services/openapi";
|
||||
|
||||
interface AppStateModel {
|
||||
orderItems: Map<number, controller_OrderItem[]>;
|
||||
}
|
||||
|
||||
interface mutationOrderItems {
|
||||
orderItems: controller_OrderItem[];
|
||||
orderType: controller_ItemType;
|
||||
}
|
||||
|
||||
const orderItemStore = {
|
||||
state: () => ({
|
||||
orderItems: new Map<number, controller_OrderItem[]>(),
|
||||
}),
|
||||
getters: {
|
||||
getOrderItems(state: AppStateModel) {
|
||||
return state.orderItems;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setOrderItems(state: AppStateModel, payload: mutationOrderItems) {
|
||||
state.orderItems.set(payload.orderType, payload.orderItems);
|
||||
},
|
||||
pushOrderItem(state: AppStateModel, orderItem: controller_OrderItem) {
|
||||
const tempOrderItems = state.orderItems.get(orderItem.item_type);
|
||||
tempOrderItems && tempOrderItems.push(orderItem);
|
||||
},
|
||||
filterOrderItem(state: AppStateModel, orderItem: controller_OrderItem) {
|
||||
const tempOrderItems = state.orderItems.get(orderItem.item_type);
|
||||
tempOrderItems &&
|
||||
state.orderItems.set(
|
||||
orderItem.item_type,
|
||||
tempOrderItems.filter((origItem: controller_OrderItem) => origItem.id !== orderItem.id)
|
||||
);
|
||||
},
|
||||
putOrderItem(state: AppStateModel, orderItem: controller_OrderItem) {
|
||||
const tempOrderItems = state.orderItems.get(orderItem.item_type);
|
||||
tempOrderItems &&
|
||||
state.orderItems.set(
|
||||
orderItem.item_type,
|
||||
tempOrderItems.map((origItem: controller_OrderItem) => (origItem.id === orderItem.id ? orderItem : origItem))
|
||||
);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// eslint-disable-next-line
|
||||
async getAllOrderItems(context: any) {
|
||||
await context.dispatch("getOrderItems", controller_ItemType.Food);
|
||||
await context.dispatch("getOrderItems", controller_ItemType.ColdDrink);
|
||||
await context.dispatch("getOrderItems", controller_ItemType.HotDrink);
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
async getOrderItems(context: any, orderType: controller_ItemType) {
|
||||
const orderTypeArray = context.getters.getOrderItems;
|
||||
if (!orderTypeArray.get(orderType)) {
|
||||
const orderItems: controller_OrderItem[] | null = await OrderItemsService.getOrdersItems(orderType);
|
||||
context.commit("setOrderItems", { orderItems, orderType });
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
addOrderItem(context: any, orderItem: controller_OrderItem) {
|
||||
context.commit("pushOrderItem", orderItem);
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
deleteOrderItem(context: any, orderItem: controller_OrderItem) {
|
||||
context.commit("filterOrderItem", orderItem);
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
updateOrderItem(context: any, orderItem: controller_OrderItem) {
|
||||
context.commit("putOrderItem", orderItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default orderItemStore;
|
47
web/src/store/tables/index.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { controller_Table, TablesService } from "@/services/openapi";
|
||||
|
||||
interface AppStateModel {
|
||||
tables: controller_Table[] | null;
|
||||
}
|
||||
|
||||
const tableStore = {
|
||||
state: () => ({
|
||||
tables: null,
|
||||
}),
|
||||
getters: {
|
||||
getTables(state: AppStateModel) {
|
||||
return state.tables;
|
||||
},
|
||||
getTablesCount(state: AppStateModel) {
|
||||
return state.tables && state.tables.length;
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
setTables(state: AppStateModel, tables: controller_Table[]) {
|
||||
state.tables = tables;
|
||||
},
|
||||
popTables(state: AppStateModel) {
|
||||
state.tables && state.tables.pop();
|
||||
},
|
||||
pushTable(state: AppStateModel, table: controller_Table) {
|
||||
state.tables && state.tables.push(table);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// eslint-disable-next-line
|
||||
fetchTables(context: any) {
|
||||
context.commit("setTables", null);
|
||||
return TablesService.getTables();
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
removeLastTable(context: any) {
|
||||
context.commit("popTables");
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
addTable(context: any, table: controller_Table) {
|
||||
context.commit("pushTable", table);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default tableStore;
|
78
web/src/utils.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { controller_Bill, controller_Order, controller_ItemType } from "@/services/openapi";
|
||||
|
||||
export function convertToEur(value: number | undefined) {
|
||||
const temp: number = value ? value : 0;
|
||||
return temp.toLocaleString("de-DE", { style: "currency", currency: "EUR" });
|
||||
}
|
||||
|
||||
export function detailedItemTypeString(type: controller_ItemType | undefined) {
|
||||
switch (type) {
|
||||
case controller_ItemType.Food:
|
||||
return "Speisen";
|
||||
case controller_ItemType.ColdDrink:
|
||||
return "Kaltgetränke";
|
||||
default:
|
||||
return "Heiß/Eiskaffee";
|
||||
}
|
||||
}
|
||||
|
||||
export function generalItemTypeString(type: controller_ItemType[]) {
|
||||
if (type.includes(controller_ItemType.Food)) {
|
||||
return "Speisen";
|
||||
} else {
|
||||
return "Getränke";
|
||||
}
|
||||
}
|
||||
|
||||
export function detailedItemTypeIcon(type: controller_ItemType | undefined) {
|
||||
switch (type) {
|
||||
case controller_ItemType.Food:
|
||||
return "fa-cheese";
|
||||
case controller_ItemType.ColdDrink:
|
||||
return "fa-champagne-glasses";
|
||||
case controller_ItemType.HotDrink:
|
||||
return "fa-mug-hot";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function generalItemTypeIcon(type: controller_ItemType[]) {
|
||||
if (type.includes(controller_ItemType.Food)) {
|
||||
return "fa-cheese";
|
||||
} else {
|
||||
return "fa-champagne-glasses";
|
||||
}
|
||||
}
|
||||
|
||||
export interface WebSocketMsg {
|
||||
type: NotifierType;
|
||||
payload: controller_Order[];
|
||||
}
|
||||
|
||||
export enum NotifierType {
|
||||
Create,
|
||||
Delete,
|
||||
DeleteAll,
|
||||
}
|
||||
|
||||
import { ToastServiceMethods } from "primevue/toastservice";
|
||||
import moment from "moment";
|
||||
|
||||
const timeToLife = 3600;
|
||||
|
||||
export function errorToast(toast: ToastServiceMethods, message: string) {
|
||||
toast.removeAllGroups();
|
||||
toast.add({ severity: "error", summary: "Fehler", detail: message, group: "br", life: timeToLife });
|
||||
}
|
||||
|
||||
export function getCurrentTimeSince(updated_at: number | undefined) {
|
||||
return updated_at ? moment.unix(updated_at).fromNow() : "";
|
||||
}
|
||||
|
||||
export function lessThan15SecondsAgo(updated_at: number | undefined) {
|
||||
const updated = updated_at ? moment.unix(updated_at) : moment();
|
||||
return moment().diff(updated, "seconds") < 15;
|
||||
}
|
||||
|
||||
export const emptyBill: controller_Bill = { table_id: 0, total: 0 };
|
158
web/src/views/Bills.vue
Normal file
|
@ -0,0 +1,158 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<ConfirmDialog></ConfirmDialog>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<div v-else class="p-card shadow-1 md:p-3">
|
||||
<DataTable :value="bills" dataKey="id" :filters="filters" responsiveLayout="scroll" stripedRows class="p-datatable-sm">
|
||||
<template #header>
|
||||
<div class="grid p-fluid align-items-center">
|
||||
<div class="col-12 md:col-4">
|
||||
<Calendar id="basic" v-model="today" autocomplete="off" :inputStyle="{ 'text-align': 'center' }" :manualInput="false" />
|
||||
</div>
|
||||
<div class="col-12 md:col-8">
|
||||
<span class="p-input-icon-left">
|
||||
<i class="pi pi-search" />
|
||||
<InputText v-model="filters['global'].value" placeholder="Suchen" @keydown.esc="filters['global'].value = null" />
|
||||
<span v-if="filters['global'].value !== null" class="leftMiddle styling" @click="filters['global'].value = null">
|
||||
<i class="pi pi-times"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="table_id">
|
||||
<template #body="slotProps">
|
||||
<span class="white-space-nowrap">
|
||||
Tisch {{ slotProps.data.table_id }} <span class="text-sm">({{ time(slotProps.data.created_at) }})</span>
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="total" style="text-align: right">
|
||||
<template #body="slotProps">{{ convertToEur(slotProps.data.total) }}</template>
|
||||
</Column>
|
||||
<Column style="width: 3.5rem">
|
||||
<template #body="slotProps">
|
||||
<div class="flex align-items-center justify-content-end">
|
||||
<div class="mr-2" :style="{ color: isDisabled ? 'grey' : 'green' }" style="cursor: pointer" @click="openBill(slotProps.data.id)">
|
||||
<i class="pi pi-eye"></i>
|
||||
</div>
|
||||
<div :style="{ color: isDisabled ? 'grey' : 'red' }" style="cursor: pointer" @click="deleteBill(slotProps.data.id)">
|
||||
<i class="pi pi-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<template #empty><div class="mb-1">Keine Rechnungen</div></template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</Transition>
|
||||
<Sidebar v-model:visible="billModal" :baseZIndex="10000" position="full">
|
||||
<BillModal :bill="bill" />
|
||||
</Sidebar>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import Calendar from "primevue/calendar";
|
||||
import { BillsService, controller_Bill } from "@/services/openapi";
|
||||
import Sidebar from "primevue/sidebar";
|
||||
import BillModal from "@/components/Bills/BillModal.vue";
|
||||
import { convertToEur, emptyBill, errorToast } from "@/utils";
|
||||
import { FilterMatchMode } from "primevue/api";
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import InputText from "primevue/inputtext";
|
||||
import moment from "moment";
|
||||
import { useConfirm } from "primevue/useconfirm";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "BillView",
|
||||
components: { BillModal, BaseCard, Calendar, Sidebar, DataTable, Column, InputText, ConfirmDialog, WaveSpinner },
|
||||
setup() {
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const today = ref(new Date());
|
||||
const bills = ref<controller_Bill[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const isDisabled = ref(false);
|
||||
const billModal = ref(false);
|
||||
const bill = ref<controller_Bill>({ ...emptyBill });
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: FilterMatchMode.CONTAINS },
|
||||
});
|
||||
|
||||
getData();
|
||||
watch(today, () => getData());
|
||||
|
||||
function getData() {
|
||||
isLoading.value = true;
|
||||
BillsService.getBills(today.value.getFullYear(), today.value.getUTCMonth() + 1, today.value.getDate())
|
||||
.then((res) => (bills.value = res))
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openBill(billId: number) {
|
||||
if (isDisabled.value) return;
|
||||
const temp: controller_Bill | undefined = bills.value.find((bill) => bill.id === billId);
|
||||
temp && (bill.value = temp);
|
||||
billModal.value = true;
|
||||
}
|
||||
|
||||
function time(unixDate: number) {
|
||||
return moment.unix(unixDate).format("HH:mm") + " Uhr";
|
||||
}
|
||||
|
||||
function deleteBill(billId: number) {
|
||||
if (isDisabled.value) return;
|
||||
confirm.require({
|
||||
message: "Rechnung löschen?",
|
||||
header: "Rechnung",
|
||||
icon: "pi pi-info-circle",
|
||||
acceptClass: "p-button-danger",
|
||||
accept: () => {
|
||||
isDisabled.value = true;
|
||||
BillsService.deleteBills(billId)
|
||||
.then(() => (bills.value = bills.value.filter((bill) => bill.id !== billId)))
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => (isDisabled.value = false));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { convertToEur, openBill, deleteBill, today, bills, isLoading, isDisabled, filters, billModal, bill, time };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.styling {
|
||||
cursor: pointer;
|
||||
color: gray;
|
||||
border-radius: 50%;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
.leftMiddle {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
</style>
|
195
web/src/views/Checkout.vue
Normal file
|
@ -0,0 +1,195 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<ConfirmDialog></ConfirmDialog>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<div v-else>
|
||||
<BaseItem>
|
||||
<div class="field-checkbox mt-1">
|
||||
<Checkbox id="binary" v-model="checkAll" :binary="true" @click="checkAllClicked" />
|
||||
<label for="binary">Alle Auswählen</label>
|
||||
</div>
|
||||
<hr style="color: var(--text-color)" class="my-3" />
|
||||
<div v-for="order in orders" :key="order.id" class="field-checkbox">
|
||||
<Checkbox :id="order.id" name="order" :value="order.id" v-model="orderFilter" />
|
||||
<label :for="'' + order.id" class="w-full">
|
||||
<span class="flex justify-content-between">
|
||||
<span class="overflow-hidden text-overflow-ellipsis white-space-nowrap">{{ order.order_item.description }}</span>
|
||||
<span>{{ convertToEur(order.order_item.price) }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</BaseItem>
|
||||
<div class="h-4rem"></div>
|
||||
|
||||
<BottomNavigation>
|
||||
<template #left>
|
||||
<router-link :to="{ name: 'TableDetail' }" class="no-underline">
|
||||
<Button :disabled="applyFilterLoading" icon="pi pi-arrow-left" class="p-button-rounded" />
|
||||
</router-link>
|
||||
</template>
|
||||
<template #middle>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<div class="text-sm">Tisch {{ table }}</div>
|
||||
<div class="font-bold">{{ convertToEur(total) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #right>
|
||||
<Button
|
||||
:disabled="total === 0"
|
||||
:loading="applyFilterLoading"
|
||||
icon="pi pi-money-bill"
|
||||
class="p-button-danger p-button-rounded"
|
||||
@click="generateBill"
|
||||
/>
|
||||
</template>
|
||||
</BottomNavigation>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Sidebar v-model:visible="billModal" :baseZIndex="10000" position="full" @hide="billModalClosed">
|
||||
<BillModal :bill="bill" />
|
||||
</Sidebar>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref, watch } from "vue";
|
||||
import { BillsService, OrdersService, controller_Bill, controller_Order } from "@/services/openapi";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
import { convertToEur, emptyBill, errorToast } from "@/utils";
|
||||
import Button from "primevue/button";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import BottomNavigation from "@/components/UI/BottomNavigation.vue";
|
||||
import BaseItem from "@/components/UI/BaseItem.vue";
|
||||
import ConfirmDialog from "primevue/confirmdialog";
|
||||
import { useConfirm } from "primevue/useconfirm";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import Sidebar from "primevue/sidebar";
|
||||
import BillModal from "@/components/Bills/BillModal.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
export default defineComponent({
|
||||
name: "CheckoutView",
|
||||
// eslint-disable-next-line
|
||||
components: { BaseItem, BaseCard, WaveSpinner, Checkbox, Button, BottomNavigation, ConfirmDialog, Sidebar, BillModal },
|
||||
props: { id: { type: String, default: "0" } },
|
||||
setup(props) {
|
||||
const confirm = useConfirm();
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const table = computed(() => parseInt(props.id));
|
||||
const orders = ref<controller_Order[]>([]);
|
||||
const orderFilter = ref<number[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const applyFilterLoading = ref(false);
|
||||
const checkAll = ref(false);
|
||||
const total = ref(0);
|
||||
const bill = ref<controller_Bill>({ ...emptyBill });
|
||||
const billModal = ref(false);
|
||||
|
||||
function checkAllCheck() {
|
||||
if (orderFilter.value) checkAll.value = orderFilter.value.length === orders.value.length;
|
||||
}
|
||||
|
||||
function calculateTotal() {
|
||||
let temp = 0;
|
||||
orders.value.forEach((order) => {
|
||||
if (order.id && orderFilter.value.includes(order.id)) temp += order.order_item.price;
|
||||
});
|
||||
total.value = temp;
|
||||
}
|
||||
|
||||
watch(orderFilter, () => {
|
||||
checkAllCheck();
|
||||
calculateTotal();
|
||||
});
|
||||
|
||||
getData();
|
||||
function getData() {
|
||||
isLoading.value = true;
|
||||
OrdersService.getOrders(table.value, false)
|
||||
.then((res) => {
|
||||
orders.value = res;
|
||||
setAllOrdersSelected();
|
||||
checkAllCheck();
|
||||
})
|
||||
.finally(() => (isLoading.value = false));
|
||||
}
|
||||
|
||||
function setAllOrdersSelected() {
|
||||
const temp: number[] = [];
|
||||
orders.value.forEach((order) => order.id && temp.push(order.id));
|
||||
orderFilter.value = temp;
|
||||
}
|
||||
|
||||
function checkAllClicked() {
|
||||
if (orderFilter.value.length === orders.value.length) {
|
||||
orderFilter.value = [];
|
||||
} else {
|
||||
setAllOrdersSelected();
|
||||
}
|
||||
}
|
||||
|
||||
function generateBill() {
|
||||
applyFilterLoading.value = true;
|
||||
confirm.require({
|
||||
message: "Alle ausgewählte Bestellungen abrechnen?",
|
||||
header: "Abrechnen",
|
||||
icon: "pi pi-exclamation-triangle",
|
||||
acceptClass: "p-button-danger",
|
||||
rejectClass: "p-button-secondary",
|
||||
accept: () => {
|
||||
BillsService.postBills(table.value, orderFilter.value.toString())
|
||||
.then((res) => {
|
||||
bill.value = res;
|
||||
billModal.value = true;
|
||||
getData();
|
||||
})
|
||||
.catch((err) => errorToast(toast, err.body.error))
|
||||
.finally(() => (applyFilterLoading.value = false));
|
||||
},
|
||||
reject: () => {
|
||||
applyFilterLoading.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function billModalClosed() {
|
||||
if (orderFilter.value.length === 0) {
|
||||
router.push({ name: "Bills" });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
orders,
|
||||
orderFilter,
|
||||
checkAll,
|
||||
checkAllClicked,
|
||||
convertToEur,
|
||||
generateBill,
|
||||
isLoading,
|
||||
applyFilterLoading,
|
||||
total,
|
||||
table,
|
||||
bill,
|
||||
billModal,
|
||||
billModalClosed,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.field-checkbox:last-child {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
21
web/src/views/Empty.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<BaseItem bgColor="c">
|
||||
<div class="p-3 text-center">{{ message }}</div>
|
||||
</BaseItem>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import BaseItem from "@/components/UI/BaseItem.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "EmptyView",
|
||||
components: { BaseItem, BaseCard },
|
||||
props: { message: { type: String, default: "Bis jetzt noch nichts..." } },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style></style>
|
78
web/src/views/Items.vue
Normal file
|
@ -0,0 +1,78 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<OrderItemList
|
||||
v-else
|
||||
:orderItems="currentOrderItems"
|
||||
:emptyOrderItem="emptyOrderItem"
|
||||
@orderItemChanged="(item) => orderItemChanged(item)"
|
||||
@orderItemDeleted="(item) => orderItemDeleted(item)"
|
||||
@orderItemCreated="(item) => orderItemCreated(item)"
|
||||
/>
|
||||
</Transition>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, watch } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import { controller_OrderItem, controller_ItemType } from "@/services/openapi";
|
||||
import OrderItemList from "@/components/OrderItem/OrderItemList.vue";
|
||||
import { useStore } from "vuex";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ItemView",
|
||||
components: { OrderItemList, BaseCard, WaveSpinner },
|
||||
props: { id: { type: String, default: "0" } },
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
const isLoading = ref(true);
|
||||
const orderItems = computed(() => store.getters.getOrderItems);
|
||||
const currentOrderItems = ref();
|
||||
const emptyOrderItem = reactive<controller_OrderItem>({ description: "", item_type: 0, price: 0 });
|
||||
const intId = ref<controller_ItemType>(parseInt(props.id));
|
||||
|
||||
getData();
|
||||
async function getData() {
|
||||
isLoading.value = true;
|
||||
intId.value = parseInt(props.id);
|
||||
await store.dispatch("getOrderItems", intId.value);
|
||||
emptyOrderItem.item_type = intId.value;
|
||||
refreshMap();
|
||||
isLoading.value = false;
|
||||
}
|
||||
|
||||
function refreshMap() {
|
||||
currentOrderItems.value = orderItems.value.get(intId.value);
|
||||
}
|
||||
|
||||
watch(props, () => getData());
|
||||
|
||||
function orderItemChanged(item: controller_OrderItem) {
|
||||
store.dispatch("updateOrderItem", item);
|
||||
refreshMap();
|
||||
}
|
||||
function orderItemDeleted(item: controller_OrderItem) {
|
||||
store.dispatch("deleteOrderItem", item);
|
||||
refreshMap();
|
||||
}
|
||||
function orderItemCreated(item: controller_OrderItem) {
|
||||
store.dispatch("addOrderItem", item);
|
||||
refreshMap();
|
||||
}
|
||||
|
||||
return { currentOrderItems, orderItemChanged, orderItemDeleted, orderItemCreated, emptyOrderItem, isLoading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
140
web/src/views/Orders.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<EmptyView v-else-if="empty" message="Keine offenen Bestellungen" />
|
||||
<EmptyView v-else-if="!user.show_cold_drinks && !user.show_hot_drinks" message="Keine Bestellungen in den Einstellungen gewählt" />
|
||||
<div v-else>
|
||||
<template v-if="user.show_hot_drinks">
|
||||
<OrderSection
|
||||
v-for="[key, orders] in otherOrders"
|
||||
v-bind:key="key"
|
||||
:orders="orders"
|
||||
:title="'Tisch ' + orders[0].table_id"
|
||||
@filterOrders="(id) => filterOrder(id)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="user.show_cold_drinks">
|
||||
<OrderSection :orders="coldOrders" :itemType="controller_ItemType.ColdDrink" @filterOrders="(id) => filterOrder(id)" />
|
||||
</template>
|
||||
</div>
|
||||
</Transition>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, onUnmounted, provide, ref } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import { OrdersService, controller_Order, controller_ItemType, controller_User } from "@/services/openapi";
|
||||
import { detailedItemTypeIcon, detailedItemTypeString, NotifierType, WebSocketMsg } from "@/utils";
|
||||
import { API_ENDPOINT_URL } from "@/main";
|
||||
import EmptyView from "@/views/Empty.vue";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
import { disabled, loading } from "@/keys";
|
||||
import OrderSection from "@/components/Orders/OrderSection.vue";
|
||||
import { useStore } from "vuex";
|
||||
|
||||
export default defineComponent({
|
||||
name: "OrderView",
|
||||
components: { OrderSection, EmptyView, BaseCard, WaveSpinner },
|
||||
setup() {
|
||||
const store = useStore();
|
||||
const isLoading = ref(true);
|
||||
const isDisabled = ref(false);
|
||||
provide(disabled, isDisabled);
|
||||
provide(loading, isDisabled);
|
||||
const orders = ref<controller_Order[]>([]);
|
||||
const user = computed<controller_User>(() => store.getters.getUser);
|
||||
|
||||
const empty = computed(() => {
|
||||
return (
|
||||
(user.value.show_cold_drinks && user.value.show_hot_drinks && orders.value.length === 0) ||
|
||||
(user.value.show_cold_drinks && !user.value.show_hot_drinks && coldOrders.value.length === 0) ||
|
||||
(!user.value.show_cold_drinks && user.value.show_hot_drinks && otherOrders.value.size === 0)
|
||||
);
|
||||
});
|
||||
|
||||
const otherOrders = computed(() => {
|
||||
const temp = new Map<number, controller_Order[]>();
|
||||
orders.value.forEach((order) => {
|
||||
if (order.order_item.item_type === controller_ItemType.ColdDrink) return;
|
||||
const existing = temp.get(order.table_id);
|
||||
if (existing) {
|
||||
existing.push(order);
|
||||
} else {
|
||||
temp.set(order.table_id, [order]);
|
||||
}
|
||||
});
|
||||
return new Map([...temp.entries()].sort());
|
||||
});
|
||||
const coldOrders = computed(() => orders.value.filter((order) => order.order_item.item_type === controller_ItemType.ColdDrink));
|
||||
const sse = ref<EventSource | null>(null);
|
||||
|
||||
getData();
|
||||
function getData() {
|
||||
isLoading.value = true;
|
||||
OrdersService.getOrders()
|
||||
.then((res) => (orders.value = res))
|
||||
.finally(() => {
|
||||
isLoading.value = false;
|
||||
setupSSE();
|
||||
});
|
||||
}
|
||||
onUnmounted(() => sse.value && sse.value.close());
|
||||
addEventListener("beforeunload", () => {
|
||||
sse.value && sse.value.close();
|
||||
});
|
||||
|
||||
function filterOrder(id: number) {
|
||||
orders.value = orders.value.filter((old) => old.id !== id);
|
||||
}
|
||||
|
||||
function setupSSE() {
|
||||
sse.value = new EventSource(API_ENDPOINT_URL + "/orders/sse?stream=sse");
|
||||
sse.value.onmessage = (evt: Event) => {
|
||||
isDisabled.value = true;
|
||||
const messageEvent = evt as MessageEvent;
|
||||
const webSocketMsg: WebSocketMsg = JSON.parse(messageEvent.data);
|
||||
if (webSocketMsg.type === NotifierType.Create) {
|
||||
orders.value.push(webSocketMsg.payload[0]);
|
||||
} else if (webSocketMsg.type === NotifierType.Delete) {
|
||||
orders.value = orders.value.filter((o) => o.id !== webSocketMsg.payload[0].id);
|
||||
} else if (webSocketMsg.type === NotifierType.DeleteAll) {
|
||||
webSocketMsg.payload.forEach((obj) => {
|
||||
orders.value = orders.value.filter((o) => o.id !== obj.id);
|
||||
});
|
||||
}
|
||||
sortOrders();
|
||||
isDisabled.value = false;
|
||||
};
|
||||
}
|
||||
|
||||
function sortOrders() {
|
||||
orders.value.sort((a, b) => (a.updated_at && b.updated_at ? a.updated_at - b.updated_at : 0));
|
||||
}
|
||||
|
||||
return {
|
||||
orders,
|
||||
otherOrders,
|
||||
coldOrders,
|
||||
filterOrder,
|
||||
controller_ItemType,
|
||||
isLoading,
|
||||
isDisabled,
|
||||
detailedItemTypeString,
|
||||
detailedItemTypeIcon,
|
||||
user,
|
||||
empty,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
50
web/src/views/Tables.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<BaseCard>
|
||||
<Transition>
|
||||
<WaveSpinner v-if="isLoading" />
|
||||
<EmptyView v-else-if="tables && tables.length === 0" message="Keine Tische" />
|
||||
<div v-else class="grid">
|
||||
<TableCard v-for="table in tables" v-bind:key="table.id" :table="table" />
|
||||
</div>
|
||||
</Transition>
|
||||
</BaseCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, ref } from "vue";
|
||||
import BaseCard from "@/components/UI/BaseCard.vue";
|
||||
import { useStore } from "vuex";
|
||||
import TableCard from "@/components/Tables/TableCard.vue";
|
||||
import EmptyView from "@/views/Empty.vue";
|
||||
import WaveSpinner from "@/components/UI/WaveSpinner.vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TablesView",
|
||||
components: { WaveSpinner, EmptyView, TableCard, BaseCard },
|
||||
setup() {
|
||||
const isLoading = ref(false);
|
||||
const store = useStore();
|
||||
const tables = computed(() => store.getters.getTables);
|
||||
|
||||
getData();
|
||||
function getData() {
|
||||
isLoading.value = true;
|
||||
store.dispatch("fetchTables").then((res) => {
|
||||
store.commit("setTables", res);
|
||||
isLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
return { tables, isLoading };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-enter-active {
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
.v-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
12
web/tests/unit/example.spec.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { shallowMount } from "@vue/test-utils";
|
||||
import HelloWorld from "@/components/HelloWorld.vue";
|
||||
|
||||
describe("HelloWorld.vue", () => {
|
||||
it("renders props.msg when passed", () => {
|
||||
const msg = "new message";
|
||||
const wrapper = shallowMount(HelloWorld, {
|
||||
props: { msg },
|
||||
});
|
||||
expect(wrapper.text()).toMatch(msg);
|
||||
});
|
||||
});
|
23
web/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"useDefineForClassFields": true,
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["webpack-env", "jest"],
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "tests/**/*.ts", "tests/**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
7
web/vue.config.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const { defineConfig } = require("@vue/cli-service");
|
||||
module.exports = defineConfig({
|
||||
transpileDependencies: true,
|
||||
devServer: {
|
||||
allowedHosts: 'all'
|
||||
}
|
||||
});
|