Rename frontend to web

This commit is contained in:
Florian Hoss 2023-07-04 23:20:47 +02:00
parent f63210272d
commit c135c604f0
59 changed files with 13 additions and 7 deletions

2
web/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
src/services/openapi/
node_modules/

24
web/.gitignore vendored Normal file
View 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
View 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
View file

@ -0,0 +1,3 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};

98
web/package.json Normal file
View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View 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

View 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
View 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
View 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>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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,
},
});

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

View 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
View 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
View file

@ -0,0 +1,7 @@
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
allowedHosts: 'all'
}
});

9160
web/yarn.lock Normal file

File diff suppressed because it is too large Load diff