Compare commits
52 commits
feature/ad
...
master
Author | SHA1 | Date | |
---|---|---|---|
2430243028 | |||
d13e7f664f | |||
6d6418858b | |||
dd59408c82 | |||
36638fbf74 | |||
0b087a38bf | |||
dc3c065d1a | |||
7089d0397f | |||
dd23862e9d | |||
d92696e2ff | |||
c98b51c22a | |||
54d60744d0 | |||
f1e7c0b514 | |||
cd5c7d1569 | |||
019d0ae566 | |||
97785d17ad | |||
a2d4d89776 | |||
5aea42d33a | |||
fe2eabaffc | |||
212e0bbcc3 | |||
c70dba5ab3 | |||
a2994e50e6 | |||
8f544f224b | |||
4f80b7e895 | |||
9a092e9324 | |||
be3f71eb12 | |||
3e1437300f | |||
c94bf147f5 | |||
23aad9b667 | |||
c2207e337a | |||
4944890510 | |||
bc7a295a06 | |||
8a6a8bd1dc | |||
364ee76159 | |||
84bb25a4fa | |||
10399aa812 | |||
86ef3d989f | |||
b5247ea3ad | |||
cfb41b2b32 | |||
8c1878d4aa | |||
46d0ad8770 | |||
f255ffcd94 | |||
971b760792 | |||
64f7c7393f | |||
eab98a13b7 | |||
dda5a1ac7d | |||
6425af1ffb | |||
5ba74d198a | |||
4915be99b8 | |||
069da4fc5e | |||
ebdfa33c37 | |||
f172a2c338 |
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 398 KiB |
28
.drone.yml
|
@ -2,21 +2,35 @@ kind: pipeline
|
|||
name: default
|
||||
|
||||
steps:
|
||||
- name: install
|
||||
image: node:18-alpine
|
||||
- name: install-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- corepack pnpm@7.5.1 install
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.6 install
|
||||
|
||||
- name: build
|
||||
image: node:18-alpine
|
||||
- name: lint-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- corepack pnpm@7.5.1 build
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.6 lint
|
||||
|
||||
- name: version-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.6 generate-version
|
||||
|
||||
- name: build-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.6 build
|
||||
|
||||
- name: deploy
|
||||
image: plugins/docker
|
||||
settings:
|
||||
registry: registry.cliffbreak.de
|
||||
repo: registry.cliffbreak.de/kispi-core
|
||||
repo: registry.cliffbreak.de/hgoe-sas
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
|
|
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Paths
|
||||
WEBAPP="../webapp/dist"
|
9
.gitignore
vendored
|
@ -20,3 +20,12 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Pocketbase-Data
|
||||
pb_data
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
*.csv
|
||||
*.xlsx
|
20
.vscode/settings.json
vendored
|
@ -1,20 +1,32 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"appwrite",
|
||||
"camelcase",
|
||||
"checkin",
|
||||
"classname",
|
||||
"cliffbreak",
|
||||
"corepack",
|
||||
"daisyui",
|
||||
"esnext",
|
||||
"Gitea",
|
||||
"github",
|
||||
"godotenv",
|
||||
"heroicons",
|
||||
"kispi",
|
||||
"mopidy",
|
||||
"Mopidy",
|
||||
"hgoe",
|
||||
"HGOE",
|
||||
"middlewares",
|
||||
"pnpm",
|
||||
"pocketbase",
|
||||
"tailwindcss",
|
||||
"typegen",
|
||||
"vite",
|
||||
"vitejs",
|
||||
"vueuse"
|
||||
],
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"directory": "webapp",
|
||||
"changeProcessCWD": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -26,7 +26,7 @@ docs(changelog): update change log to beta.5
|
|||
style(webapp): reorder imports
|
||||
```
|
||||
```
|
||||
fix(appwrite): need to depend on latest rxjs and zone.js
|
||||
fix(server): need to depend on latest rxjs and zone.js
|
||||
|
||||
The version in our package.json gets copied to the one we publish, and users need the latest of these.
|
||||
```
|
||||
|
@ -53,7 +53,7 @@ The following is the list of supported scopes (more to come):
|
|||
|
||||
* **core** used for changes to the whole project
|
||||
* **webapp** used for changes made in the webapp
|
||||
* **appwrite** used for changes made in the Appwrite cloud functions
|
||||
* **server** used for changes made in the server
|
||||
* **changelog**: used for updating the release notes in CHANGELOG.md
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
|
||||
|
||||
|
|
26
Dockerfile
|
@ -1,7 +1,25 @@
|
|||
FROM steebchen/nginx-spa:stable
|
||||
## Build
|
||||
FROM golang:1.20-buster as build
|
||||
|
||||
COPY dist/ /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 80
|
||||
COPY server/go.mod ./
|
||||
COPY server/go.sum ./
|
||||
COPY server/pkg ./pkg
|
||||
RUN go mod download
|
||||
|
||||
CMD ["nginx"]
|
||||
COPY server/*.go ./
|
||||
|
||||
RUN CGO_ENABLED=1 go build -o /server
|
||||
|
||||
## Deploy
|
||||
FROM gcr.io/distroless/base-debian10
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=build /server /server
|
||||
COPY webapp/dist /webapp
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT ["/server", "serve", "--http=0.0.0.0:8090"]
|
62
README.md
|
@ -1,11 +1,11 @@
|
|||
<h1 align="center">🔥 Core - Kinderspielstadt Öhringen 🔥</h1>
|
||||
<h3 align="center">Verwaltung + Bank + Radio = ❤️</h1>
|
||||
<h1 align="center">🔥 Schule als Staat - Hohenlohe Gymnasium Öhringen 🔥</h1>
|
||||
<h3 align="center">User administration, user check and customs, banking system, employer portal and an admin portal</h1>
|
||||
<p align="center">
|
||||
<img height="600px" src=".docs/screenshot.png" alt="screenshot"/>
|
||||
</p>
|
||||
|
||||
This is our repository containing all required resources to run the "Core" WebApp from the "Kinderspielstadt Öhringen".
|
||||
This repository also contains all the required Appwrite cloud functions if any exist.
|
||||
This is our repository containing all required resources to run the "Core" WebApp from the "Hohenlohe Gymnasium Öhringen - Schule als Staat" project.
|
||||
This repository also contains all the required Pocketbase files to run the backend server.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
|
@ -15,32 +15,70 @@ See deployment for notes on how to deploy the project on a live system.
|
|||
### 🍽️ Prerequisites
|
||||
|
||||
`NodeJS (including PNPM)` is required to run this project.
|
||||
Also a hosted instance of `Appwrite` is required.
|
||||
Also `Go` is required to run the built in server.
|
||||
|
||||
### 📦 Installing
|
||||
|
||||
At first clone this repository to your local machine by using
|
||||
|
||||
```
|
||||
git clone https://git.cliffbreak.de/Kontast/Core.git
|
||||
git clone https://git.cliffbreak.de/SimGie/HGOE-SaS.git
|
||||
```
|
||||
|
||||
Change to the cloned repository
|
||||
|
||||
```
|
||||
cd Core
|
||||
cd HGOE-Sas
|
||||
```
|
||||
|
||||
To install all required packages run
|
||||
Copy the example environment file
|
||||
|
||||
```
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
|
||||
```
|
||||
|
||||
To install all required webapp packages run
|
||||
|
||||
```
|
||||
cd webapp && pnpm install
|
||||
```
|
||||
|
||||
To start the webapp in development mode run the following npm script
|
||||
|
||||
```
|
||||
pnpm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
To build the webapp run the following npm script
|
||||
|
||||
```
|
||||
pnpm build
|
||||
```
|
||||
|
||||
To install all required server packages run
|
||||
|
||||
```
|
||||
cd ../server && go mod download
|
||||
```
|
||||
|
||||
To generate unique VAPID keys run the following Go command copy this keys to the `.env` file
|
||||
|
||||
```
|
||||
go run main.go generate-vapid-keys
|
||||
```
|
||||
|
||||
To start the server run the following Go command
|
||||
|
||||
```
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
If you do changes to the database schema keep in mind to `Export collections` via the Pocketbase UI.
|
||||
Paste the exported JSON into the `pb_schema.json` file and run the following npm script to update the frontend schema types.
|
||||
|
||||
```
|
||||
cd ../webapp && pnpm typegen
|
||||
```
|
||||
|
||||
## 🧑💻 Configure Visual Studio Code
|
||||
|
@ -58,7 +96,9 @@ Please refer to our **[COMMIT_CONVENTION](COMMIT_CONVENTION.md)**
|
|||
* [PNPM](https://pnpm.io/) - Faster alternative to npm for managing dependencies
|
||||
* [Vue.js](https://vuejs.org/) - The Frontend Web Framework
|
||||
* [Vite](https://vitejs.dev/) - Used Frontend Tooling
|
||||
* [Appwrite](https://docs.mongodb.com/) - The hosted Backend used for this application
|
||||
* [Go](https://go.dev/) - The Backend Programming Language
|
||||
* [Pocketbase](https://pocketbase.io/) - The Backend Framework
|
||||
|
||||
|
||||
## 🤵 Authors
|
||||
|
||||
|
|
37
package.json
|
@ -1,37 +0,0 @@
|
|||
{
|
||||
"name": "kispi-core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"lint": "eslint -c .eslintrc.js src/",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^1.0.6",
|
||||
"@vueuse/core": "^8.9.2",
|
||||
"daisyui": "^2.19.0",
|
||||
"events": "^3.3.0",
|
||||
"mopidy": "^1.3.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.6",
|
||||
"@typescript-eslint/parser": "^5.30.6",
|
||||
"@vitejs/plugin-vue": "^2.3.3",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-vue": "^9.2.0",
|
||||
"eslint-plugin-vue-scoped-css": "^2.2.0",
|
||||
"postcss": "^8.4.14",
|
||||
"sass": "^1.53.0",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^2.9.14",
|
||||
"vue-eslint-parser": "^9.0.3",
|
||||
"vue-tsc": "^0.38.5"
|
||||
}
|
||||
}
|
2022
pnpm-lock.yaml
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 5.5 KiB |
|
@ -1,162 +0,0 @@
|
|||
<?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="1181.000000pt" height="1181.000000pt" viewBox="0 0 1181.000000 1181.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,1181.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M8929 10966 c-19 -7 -39 -24 -49 -42 -15 -30 -16 -266 -16 -3380 l1
|
||||
-3349 310 0 310 0 1 3335 c1 1959 -2 3350 -8 3372 -16 67 -39 73 -295 74 -133
|
||||
0 -235 -4 -254 -10z"/>
|
||||
<path d="M414 9835 l0 -855 81 0 80 0 0 445 c0 245 3 445 6 445 5 0 541 -544
|
||||
769 -781 l105 -109 117 0 117 0 -156 158 c-85 86 -290 293 -454 460 -164 166
|
||||
-299 306 -299 310 0 4 42 44 93 89 50 45 112 101 137 124 50 46 175 159 211
|
||||
190 13 11 42 37 64 58 22 20 111 101 198 179 l157 142 -112 0 -113 -1 -100
|
||||
-91 c-55 -50 -140 -128 -189 -172 -49 -45 -100 -90 -112 -101 -12 -11 -54 -49
|
||||
-94 -85 -40 -36 -82 -74 -94 -85 -13 -11 -60 -55 -107 -97 -110 -100 -131
|
||||
-118 -138 -118 -3 0 -6 168 -6 373 l0 372 -80 3 -81 3 0 -856z"/>
|
||||
<path d="M1825 9835 l0 -855 80 0 80 0 0 855 0 855 -80 0 -80 0 0 -855z"/>
|
||||
<path d="M2394 9835 l0 -855 81 0 80 0 0 745 c0 410 2 745 5 745 3 -1 18 -19
|
||||
33 -41 16 -23 132 -190 259 -373 127 -182 348 -499 490 -703 l260 -373 104 0
|
||||
104 0 0 855 0 855 -80 0 -80 0 -1 -37 c0 -21 0 -356 -1 -745 0 -390 -3 -708
|
||||
-7 -708 -3 0 -55 72 -116 160 -113 165 -169 244 -643 925 l-278 400 -105 3
|
||||
-105 3 0 -856z"/>
|
||||
<path d="M4192 9835 l-1 -857 362 5 c199 2 373 7 387 10 14 4 39 9 55 12 216
|
||||
42 424 183 527 359 202 343 155 818 -107 1079 -71 72 -126 110 -228 157 -164
|
||||
76 -252 88 -664 89 l-331 2 0 -856z m678 689 c316 -48 523 -219 596 -489 20
|
||||
-74 26 -258 12 -343 -24 -143 -84 -257 -184 -354 -102 -98 -214 -151 -419
|
||||
-194 -16 -3 -141 -8 -276 -11 l-245 -5 -1 706 0 707 216 -2 c119 -1 254 -8
|
||||
301 -15z"/>
|
||||
<path d="M5958 10683 c0 -5 -2 -389 -3 -855 l-1 -848 542 0 542 0 3 43 c2 23
|
||||
3 57 1 74 l-3 33 -462 2 -462 3 0 328 0 327 417 0 417 0 -1 73 -1 72 -416 5
|
||||
-416 5 -1 285 c0 157 2 291 4 298 3 9 101 12 443 12 344 0 439 3 440 13 1 15
|
||||
1 119 0 130 -1 4 -235 7 -521 7 -286 0 -521 -3 -522 -7z"/>
|
||||
<path d="M7325 9836 l0 -856 80 0 80 0 0 403 0 402 149 0 150 0 71 -120 c300
|
||||
-502 356 -595 378 -635 l25 -45 91 -3 c50 -1 91 -1 91 2 0 2 -20 37 -45 78
|
||||
-24 40 -57 96 -74 123 -16 28 -106 176 -200 330 l-170 280 86 22 c191 51 305
|
||||
157 337 315 26 126 6 254 -54 346 -63 98 -135 146 -275 183 -72 20 -113 22
|
||||
-400 26 l-320 5 0 -856z m630 692 c184 -39 289 -169 265 -325 -17 -106 -71
|
||||
-177 -166 -219 -82 -36 -162 -45 -374 -42 l-195 3 0 285 c-1 157 1 291 3 298
|
||||
6 16 391 16 467 0z"/>
|
||||
<path d="M9650 8955 l0 -1601 930 1 930 0 0 230 0 230 -150 -1 c-119 -1 -150
|
||||
2 -151 13 0 7 -1 105 -1 218 0 113 1 210 1 215 1 6 58 10 151 10 l150 0 0 228
|
||||
0 229 -137 -2 c-76 0 -145 1 -153 3 -13 3 -15 35 -13 225 1 122 2 224 2 227 1
|
||||
3 69 5 151 5 l150 -1 0 228 0 228 -32 1 c-18 0 -87 1 -153 2 l-119 2 -1 215
|
||||
c0 118 1 221 3 228 3 9 43 12 153 12 l149 0 0 228 0 229 -930 0 -930 0 0
|
||||
-1602z m991 476 c1 -3 2 -60 3 -126 l2 -120 126 0 127 0 -2 -230 -2 -230 -122
|
||||
1 c-82 0 -123 -3 -124 -10 -1 -6 -2 -63 -3 -126 l-1 -115 -227 -1 c-162 -1
|
||||
-228 2 -228 10 -1 6 -2 63 -3 126 l-2 115 -123 0 c-67 0 -123 2 -123 5 -1 3
|
||||
-2 105 -3 227 -1 148 2 224 9 226 5 2 57 3 115 2 58 -1 111 0 118 3 9 3 13 21
|
||||
12 56 -1 28 -1 83 -1 123 l1 71 225 0 c124 -1 226 -4 226 -7z"/>
|
||||
<path d="M8316 8691 c-26 -10 -30 -45 -8 -64 16 -15 20 -15 40 -1 24 17 29 43
|
||||
10 62 -13 13 -15 14 -42 3z"/>
|
||||
<path d="M7785 8654 c3 -35 7 -37 120 -78 55 -20 165 -60 244 -89 136 -51 142
|
||||
-55 115 -64 -130 -47 -425 -157 -451 -169 -30 -14 -35 -25 -24 -61 1 -2 31 9
|
||||
69 23 37 14 74 27 82 29 23 6 412 154 417 158 10 11 10 47 0 55 -7 5 -41 19
|
||||
-77 32 -36 13 -83 31 -105 40 -37 16 -104 41 -317 120 l-76 29 3 -25z"/>
|
||||
<path d="M995 8394 c-11 -2 -45 -9 -75 -15 -246 -49 -425 -214 -469 -436 -14
|
||||
-70 -7 -225 13 -294 38 -128 130 -235 264 -307 42 -22 177 -75 300 -116 239
|
||||
-82 281 -100 355 -153 89 -66 130 -150 129 -273 -1 -110 -41 -200 -125 -277
|
||||
-92 -84 -209 -123 -360 -119 -191 6 -333 78 -427 216 -13 19 -25 37 -26 38 -2
|
||||
3 -159 -96 -183 -114 -13 -10 91 -124 162 -177 78 -59 228 -118 341 -135 94
|
||||
-13 296 -7 366 12 316 85 490 348 441 666 -27 172 -122 291 -303 379 -57 28
|
||||
-146 62 -198 77 -324 89 -478 175 -529 294 -50 116 -44 257 15 357 72 124 202
|
||||
190 389 198 165 7 271 -31 375 -135 33 -33 62 -60 65 -60 3 0 41 26 85 59 l80
|
||||
59 -58 60 c-87 90 -181 145 -307 178 -58 15 -279 28 -320 18z"/>
|
||||
<path d="M2074 7311 l0 -1041 98 0 98 0 0 495 0 495 263 0 c276 1 307 3 399
|
||||
26 268 68 422 267 412 536 -6 164 -53 271 -163 371 -76 68 -160 105 -326 143
|
||||
-16 4 -199 9 -405 11 l-376 4 0 -1040z m751 844 c98 -19 152 -44 216 -103 82
|
||||
-75 104 -129 103 -257 0 -88 -3 -106 -26 -150 -34 -65 -89 -119 -152 -150 -93
|
||||
-45 -169 -55 -443 -55 l-253 0 0 365 0 366 243 -1 c158 -1 266 -6 312 -15z"/>
|
||||
<path d="M3685 7313 l0 -1038 98 -3 97 -3 0 1041 0 1040 -98 0 -97 0 0 -1037z"/>
|
||||
<path d="M4374 7310 l1 -1040 660 0 660 0 0 90 1 90 -563 0 -563 0 0 405 0
|
||||
405 505 0 505 0 0 90 0 90 -505 0 -505 0 0 365 0 365 333 0 c182 0 425 0 540
|
||||
0 l207 0 0 90 0 90 -638 0 -638 0 0 -1040z"/>
|
||||
<path d="M6045 7313 l0 -1038 583 -3 582 -2 0 90 0 90 -485 0 -485 0 0 950 0
|
||||
950 -98 0 -97 0 0 -1037z"/>
|
||||
<path d="M8307 8138 c-29 -22 -10 -68 28 -68 35 0 46 37 19 64 -19 19 -25 20
|
||||
-47 4z"/>
|
||||
<path d="M8319 7975 c-1 -3 -2 -74 -3 -157 l-1 -153 -112 0 -113 -1 0 143 0
|
||||
143 -25 0 -24 0 -3 -142 -3 -143 -97 0 c-59 0 -99 4 -99 10 -1 5 -3 69 -4 140
|
||||
-3 163 -2 154 -22 150 -10 -2 -20 -4 -23 -4 -3 -1 -5 -80 -5 -176 l0 -175 290
|
||||
0 290 0 -1 178 c-1 97 -2 180 -3 185 -1 8 -40 10 -42 2z"/>
|
||||
<path d="M7789 7267 c0 -7 -2 -20 -4 -29 -3 -16 15 -17 213 -17 120 0 235 0
|
||||
257 0 l40 -1 -50 -33 c-120 -80 -451 -312 -457 -321 -3 -5 -5 -21 -4 -35 l3
|
||||
-26 289 0 c308 0 293 -2 285 43 0 4 -113 7 -251 7 -184 0 -246 3 -237 11 6 6
|
||||
50 37 97 69 399 273 397 271 395 307 l-2 33 -286 2 c-211 1 -287 -2 -288 -10z"/>
|
||||
<path d="M8318 6703 c-3 -4 -4 -74 -4 -155 1 -94 -2 -148 -9 -149 -5 -1 -56
|
||||
-2 -112 -3 l-103 -1 0 142 0 143 -25 0 -25 0 0 -143 0 -143 -97 2 c-54 1 -100
|
||||
2 -103 3 -3 0 -5 64 -5 141 0 78 -2 145 -5 150 -3 5 -14 8 -25 6 -19 -3 -20
|
||||
-12 -20 -170 0 -92 2 -172 6 -177 3 -6 121 -8 290 -7 l284 3 0 180 c0 177 0
|
||||
180 -22 183 -11 2 -23 0 -25 -5z"/>
|
||||
<path d="M8055 6188 c-3 -7 -4 -51 -2 -98 2 -83 3 -85 26 -85 23 0 24 3 23 67
|
||||
l-1 66 74 4 c114 5 127 0 141 -50 19 -65 12 -179 -14 -229 -88 -168 -369 -164
|
||||
-457 7 -22 42 -28 144 -11 191 7 21 23 49 35 65 l22 28 -21 20 c-21 21 -21 21
|
||||
-36 1 -42 -55 -57 -101 -60 -181 -3 -71 0 -87 25 -140 38 -80 112 -144 190
|
||||
-162 60 -14 165 -6 219 18 70 30 133 101 157 175 22 70 16 186 -13 261 l-20
|
||||
49 -136 3 c-103 2 -138 -1 -141 -10z"/>
|
||||
<path d="M7786 5563 c-16 -40 -4 -42 250 -41 135 0 247 -2 249 -3 1 -2 -65
|
||||
-50 -149 -107 -330 -225 -349 -239 -352 -264 -5 -49 -5 -48 296 -46 273 3 283
|
||||
4 286 23 2 11 -1 22 -6 25 -5 3 -119 5 -254 5 -135 0 -246 1 -246 2 0 2 242
|
||||
170 442 307 63 43 67 50 59 105 0 5 -123 9 -286 8 -228 0 -285 -3 -289 -14z"/>
|
||||
<path d="M860 5324 c-113 -25 -231 -91 -297 -164 -33 -36 -91 -158 -98 -205
|
||||
-18 -121 -2 -236 47 -328 63 -118 165 -182 431 -271 268 -90 338 -131 389
|
||||
-229 32 -62 31 -193 -2 -262 -113 -232 -488 -277 -685 -81 -28 28 -58 61 -66
|
||||
74 l-14 22 -77 -51 -78 -51 20 -28 c64 -89 191 -173 320 -210 100 -29 278 -36
|
||||
375 -14 238 54 395 234 397 458 1 102 -9 153 -42 221 -59 123 -176 203 -390
|
||||
268 -300 92 -385 140 -441 249 -31 61 -34 183 -6 256 50 131 194 211 375 209
|
||||
126 -1 208 -35 293 -121 l46 -47 63 44 c35 24 65 48 67 53 6 17 -64 90 -120
|
||||
128 -90 59 -193 88 -327 92 -77 2 -136 -2 -180 -12z"/>
|
||||
<path d="M1620 5225 l0 -75 298 -2 297 -3 1 -795 0 -795 82 -3 82 -3 0 801 0
|
||||
800 300 0 300 0 0 75 0 75 -680 0 -680 0 0 -75z"/>
|
||||
<path d="M3381 4923 c-91 -208 -175 -400 -187 -428 -12 -27 -108 -248 -214
|
||||
-490 -105 -242 -194 -443 -197 -447 -2 -5 35 -8 84 -8 l88 0 97 228 96 227
|
||||
465 3 464 2 94 -230 94 -230 93 0 92 0 -16 38 c-8 20 -38 91 -66 157 -48 116
|
||||
-70 168 -89 210 -5 11 -46 110 -92 220 -46 110 -171 408 -278 662 l-194 463
|
||||
-85 0 -85 0 -164 -377z m444 -298 c43 -104 102 -248 132 -318 29 -71 53 -133
|
||||
53 -138 0 -5 -160 -9 -395 -9 -217 0 -395 3 -395 6 0 6 292 695 331 781 10 23
|
||||
19 44 19 46 0 3 12 31 26 63 l26 59 63 -150 c34 -82 97 -235 140 -340z"/>
|
||||
<path d="M4619 5278 c-2 -12 -4 -405 -3 -873 l1 -850 344 0 c401 0 447 4 589
|
||||
53 130 45 229 107 322 204 170 174 255 455 224 733 -22 195 -127 412 -253 526
|
||||
-39 35 -157 119 -167 119 -3 0 -29 11 -58 25 -47 21 -140 50 -233 71 -16 4
|
||||
-195 9 -396 11 l-366 5 -4 -24z m669 -140 c110 -17 188 -40 277 -83 150 -73
|
||||
244 -166 310 -310 45 -96 62 -183 63 -311 2 -326 -140 -548 -426 -664 -127
|
||||
-51 -204 -62 -479 -68 l-253 -4 0 727 0 726 226 -2 c124 -1 251 -6 282 -11z"/>
|
||||
<path d="M6200 5225 l0 -75 298 0 298 0 0 -797 0 -798 82 -3 82 -3 0 158 c0
|
||||
87 0 447 0 801 l0 642 300 0 300 0 0 75 0 75 -680 0 -680 0 0 -75z"/>
|
||||
<path d="M7785 4938 l-1 -28 292 0 291 0 -2 27 -1 28 -290 0 -289 0 0 -27z"/>
|
||||
<path d="M8235 4740 c-71 -43 -136 -79 -142 -80 -7 0 -13 8 -13 18 0 59 -69
|
||||
122 -133 122 -75 0 -130 -39 -148 -104 -12 -41 -20 -220 -11 -243 3 -10 71
|
||||
-13 293 -13 269 0 289 1 286 18 -5 35 -15 38 -146 37 l-131 -1 0 51 0 51 123
|
||||
72 c67 40 128 76 136 81 10 6 26 71 17 71 0 0 -59 -36 -131 -80z m-243 -11
|
||||
c32 -17 49 -76 46 -159 l-3 -75 -90 -1 c-49 0 -95 2 -101 3 -14 5 -11 147 4
|
||||
183 24 61 84 81 144 49z"/>
|
||||
<path d="M7790 4300 c-14 -9 -5 -43 13 -49 9 -3 66 -6 127 -5 l110 1 -2 -161
|
||||
-3 -161 -115 -1 c-146 -2 -136 0 -136 -29 l0 -25 291 0 292 0 -2 28 -1 27
|
||||
-137 0 -137 -1 0 162 0 161 128 1 c70 0 132 1 138 1 5 1 10 12 10 26 0 24 -3
|
||||
25 -60 26 -365 3 -509 3 -516 -1z"/>
|
||||
<path d="M8620 4053 c-58 -29 -60 -36 -60 -313 0 -184 4 -262 13 -280 27 -55
|
||||
26 -55 608 -55 497 0 537 2 563 18 15 10 31 29 36 43 13 33 13 505 0 538 -5
|
||||
14 -21 33 -36 43 -25 16 -67 18 -564 18 -396 0 -542 -3 -560 -12z"/>
|
||||
<path d="M7994 3760 c-32 -9 -70 -22 -84 -30 -39 -21 -91 -81 -114 -130 -23
|
||||
-52 -31 -176 -14 -218 31 -76 41 -92 79 -127 61 -55 120 -78 204 -78 83 -1
|
||||
109 4 162 31 108 56 164 173 148 312 -21 177 -200 290 -381 240z m202 -70 c93
|
||||
-46 141 -136 131 -246 -13 -137 -126 -223 -279 -212 -88 7 -150 43 -193 112
|
||||
-26 44 -30 58 -30 125 0 88 16 129 69 180 71 69 206 87 302 41z"/>
|
||||
<path d="M7646 3571 c-17 -18 -17 -20 3 -40 26 -26 33 -26 53 -3 11 12 13 24
|
||||
7 40 -10 27 -40 29 -63 3z"/>
|
||||
<path d="M7652 3418 c-16 -16 -15 -43 3 -58 32 -26 78 25 49 54 -19 19 -36 20
|
||||
-52 4z"/>
|
||||
<path d="M8719 3271 c-22 -7 -1217 -1209 -1233 -1238 -14 -28 -30 -93 -30
|
||||
-123 1 -27 16 -84 32 -115 8 -16 358 -373 777 -792 579 -580 772 -767 806
|
||||
-782 59 -27 149 -27 208 -1 34 16 230 206 807 783 628 628 767 771 785 812 27
|
||||
60 27 140 0 200 -14 32 -172 196 -628 651 l-608 609 -452 0 c-249 0 -458 -2
|
||||
-464 -4z m892 -895 c239 -238 441 -441 448 -450 11 -13 -44 -72 -435 -463
|
||||
l-449 -448 -442 442 c-244 244 -444 448 -445 454 -3 9 868 894 883 898 3 0
|
||||
201 -194 440 -433z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 10 KiB |
99
server/go.mod
Normal file
|
@ -0,0 +1,99 @@
|
|||
module cliffbreak.de/hgoe-sas-server
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
|
||||
github.com/pocketbase/pocketbase v0.16.8
|
||||
github.com/pterm/pterm v0.12.62
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.1.2 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.0.2 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.299 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/containerd/console v1.0.3 // indirect
|
||||
github.com/disintegration/imaging v1.6.2 // indirect
|
||||
github.com/domodwyer/mailyak/v3 v3.6.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.15.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/ganigeorgiev/fexpr v0.3.0 // indirect
|
||||
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/gookit/color v1.5.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/lithammer/fuzzysearch v1.1.8 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.14 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/pocketbase/dbx v1.10.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/cobra v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
gocloud.dev v0.30.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/image v0.9.0 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.12.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/text v0.11.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.11.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.130.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
|
||||
google.golang.org/grpc v1.56.2 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.41.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.14 // indirect
|
||||
modernc.org/libc v1.24.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.6.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.24.0 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
3398
server/go.sum
Normal file
40
server/main.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"cliffbreak.de/hgoe-sas-server/pkg/middlewares"
|
||||
"cliffbreak.de/hgoe-sas-server/pkg/utils"
|
||||
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pterm.Info.Println("Loading environment variables...")
|
||||
utils.LoadEnv()
|
||||
pterm.Info.Println("Starting PocketBase server...")
|
||||
app := pocketbase.New()
|
||||
|
||||
app.OnBeforeServe().Add(middlewares.ServeSPA(utils.GetEnv("WEBAPP", "./webapp")))
|
||||
|
||||
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/time",
|
||||
Handler: func(c echo.Context) error {
|
||||
return c.String(http.StatusOK, time.Now().Format(time.RFC3339))
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
pterm.Fatal.Println(err)
|
||||
}
|
||||
|
||||
}
|
458
server/pb_schema.json
Normal file
|
@ -0,0 +1,458 @@
|
|||
[
|
||||
{
|
||||
"id": "s854d2w72fvyl54",
|
||||
"name": "companies",
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "h5ogbj93",
|
||||
"name": "accountNumber",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cvcgnf4x",
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ujqd3vik",
|
||||
"name": "earlyShiftPayed",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "t1kakf3f",
|
||||
"name": "lateShiftPayed",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX `idx_eX1Vqqz` ON `companies` (`accountNumber`)"
|
||||
],
|
||||
"listRule": "",
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": "(@request.data.username = null && @request.data.email = null && @request.data.accountNumber = null && @request.data.created = null && @request.data.updated = null && @request.data.name = null && @request.data.emailVisibility = null && @request.data.verified = null) && id = @request.auth.id",
|
||||
"deleteRule": null,
|
||||
"options": {
|
||||
"allowEmailAuth": true,
|
||||
"allowOAuth2Auth": true,
|
||||
"allowUsernameAuth": true,
|
||||
"exceptEmailDomains": null,
|
||||
"manageRule": null,
|
||||
"minPasswordLength": 8,
|
||||
"onlyEmailDomains": null,
|
||||
"requireEmail": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "c3zz98wpbn7m6zw",
|
||||
"name": "accountsList",
|
||||
"type": "view",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "ne8rkvlr",
|
||||
"name": "accountNumber",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "paqieqwu",
|
||||
"name": "name",
|
||||
"type": "json",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "7sij0xcm",
|
||||
"name": "grade",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "txpycyit",
|
||||
"name": "lastCheckIn",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "og7zs0ox",
|
||||
"name": "balance",
|
||||
"type": "json",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {
|
||||
"query": "SELECT\n a.id AS id,\n a.accountNumber AS accountNumber,\n (a.firstName || ' ' || a.lastName) AS name,\n a.grade AS grade,\n a.lastCheckIn AS lastCheckIn,\n SUM(t.amount) AS balance\nFROM\n accounts AS a\nLEFT JOIN\n transactions AS t\nON\n a.id = t.account\nGROUP BY\n a.id\nORDER BY\n a.grade, a.lastName"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "t4gewf713jqhz3i",
|
||||
"name": "settings",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "z7pmr7wm",
|
||||
"name": "minWage",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7wzv9qum",
|
||||
"name": "incomeTax",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "3pgjcfai",
|
||||
"name": "incomeTaxRecipient",
|
||||
"type": "relation",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"collectionId": "s854d2w72fvyl54",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dawtvakx",
|
||||
"name": "maxWage",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": 0,
|
||||
"max": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ftzmu1na",
|
||||
"name": "radioUrl",
|
||||
"type": "url",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"exceptDomains": [],
|
||||
"onlyDomains": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "74ftooxenpeq14b",
|
||||
"name": "accounts",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "as1gvc1r",
|
||||
"name": "accountNumber",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vxeq20lk",
|
||||
"name": "firstName",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bsinpdk7",
|
||||
"name": "lastName",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4lw6gmtc",
|
||||
"name": "grade",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "syfivtki",
|
||||
"name": "lastCheckIn",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gwrzizpu",
|
||||
"name": "company",
|
||||
"type": "relation",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"collectionId": "s854d2w72fvyl54",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "rtvjzpxw",
|
||||
"name": "shift",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ggx1gyeq",
|
||||
"name": "wage",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE INDEX `idx_mPdvrAQ` ON `accounts` (`accountNumber`)"
|
||||
],
|
||||
"listRule": "",
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": "(@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.grade = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.data.shift = null && @request.data.wage = null) || (@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.auth.id = company.id)",
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "w1au07idupp27qv",
|
||||
"name": "bankers",
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {
|
||||
"allowEmailAuth": true,
|
||||
"allowOAuth2Auth": true,
|
||||
"allowUsernameAuth": true,
|
||||
"exceptEmailDomains": null,
|
||||
"manageRule": null,
|
||||
"minPasswordLength": 8,
|
||||
"onlyEmailDomains": null,
|
||||
"requireEmail": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "w85pgrtrmovf916",
|
||||
"name": "transactions",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "5aeh5giq",
|
||||
"name": "account",
|
||||
"type": "relation",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"collectionId": "74ftooxenpeq14b",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": [
|
||||
"firstName",
|
||||
"lastName"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2bzqyeuk",
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "zsx9i8w7",
|
||||
"name": "amount",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
|
||||
"viewRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
|
||||
"createRule": "(@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id) || (@request.auth.id != \"\" && @collection.companies.id ?= @request.auth.id && amount > 0)",
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "5msnfxat1sc2c1r",
|
||||
"name": "companyTransactions",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "7rnyupog",
|
||||
"name": "account",
|
||||
"type": "relation",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"collectionId": "s854d2w72fvyl54",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ruby9fp9",
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "4hiu46ib",
|
||||
"name": "amount",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": "",
|
||||
"viewRule": "",
|
||||
"createRule": "(@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id) || (@request.auth.id != \"\" && @collection.companies.id ?= @request.auth.id)",
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
}
|
||||
]
|
24
server/pkg/middlewares/serve-spa.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/pocketbase/pocketbase/apis"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
func ServeSPA(webAppPath string) func(e *core.ServeEvent) error {
|
||||
return func(e *core.ServeEvent) error {
|
||||
pterm.Info.Print("Serving SPA from ", webAppPath)
|
||||
subFs := echo.MustSubFS(e.Router.Filesystem, webAppPath)
|
||||
e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false))
|
||||
originalErrorHandler := e.Router.HTTPErrorHandler
|
||||
e.Router.HTTPErrorHandler = func(c echo.Context, err error) {
|
||||
if c.Path() == "/*" && err == echo.ErrNotFound {
|
||||
err = c.FileFS("index.html", subFs)
|
||||
}
|
||||
originalErrorHandler(c, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
33
server/pkg/utils/env-helper.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pterm/pterm"
|
||||
)
|
||||
|
||||
// GetEnv returns the value of the environment variable named by the key.
|
||||
// If the env key is not present, it returns the default value.
|
||||
func GetEnv(key, def string) string {
|
||||
if value, ok := os.LookupEnv(key); ok {
|
||||
return value
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func LoadEnv() {
|
||||
err := godotenv.Load("../.env")
|
||||
if err != nil {
|
||||
pterm.Info.Println("No .env file found. Proceeding...")
|
||||
}
|
||||
|
||||
// Add env variables without "VITE_" prefix.
|
||||
for _, e := range os.Environ() {
|
||||
pair := strings.Split(e, "=")
|
||||
if strings.HasPrefix(pair[0], "VITE_") {
|
||||
os.Setenv(pair[0][5:], pair[1])
|
||||
}
|
||||
}
|
||||
}
|
62
src/App.vue
|
@ -1,62 +0,0 @@
|
|||
<template>
|
||||
<MoleculeNavigationDrawer
|
||||
:drawer-id="drawerId"
|
||||
:navigation-entries="navigationEntries"
|
||||
>
|
||||
<RouterView />
|
||||
</MoleculeNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
IdentificationIcon,
|
||||
LibraryIcon,
|
||||
LogoutIcon,
|
||||
MusicNoteIcon,
|
||||
TrendingUpIcon,
|
||||
UserIcon,
|
||||
} from '@heroicons/vue/outline';
|
||||
import { INavigationEntry } from './interfaces/navigation-entry.interface';
|
||||
import MoleculeNavigationDrawer from './components/molecules/MoleculeNavigationDrawer.vue';
|
||||
|
||||
const drawerId = 'default-drawer';
|
||||
const navigationEntries: INavigationEntry[] = [
|
||||
{
|
||||
name: 'Stammdaten',
|
||||
icon: UserIcon,
|
||||
to: '/data',
|
||||
},
|
||||
{
|
||||
name: 'Einstempeln',
|
||||
icon: IdentificationIcon,
|
||||
to: '/checkin',
|
||||
},
|
||||
{
|
||||
name: 'Bank',
|
||||
icon: LibraryIcon,
|
||||
to: '/bank',
|
||||
},
|
||||
{
|
||||
name: 'Börse',
|
||||
icon: TrendingUpIcon,
|
||||
to: '/stocks',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Radio',
|
||||
icon: MusicNoteIcon,
|
||||
to: '/radio',
|
||||
},
|
||||
{
|
||||
name: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'Abmelden',
|
||||
icon: LogoutIcon,
|
||||
to: '/logout',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -1,20 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<AtomInput
|
||||
ref="input"
|
||||
type="number"
|
||||
class="text-right pr-16"
|
||||
step="1"
|
||||
/>
|
||||
<span class="text-sm opacity-50 absolute -ml-16 top-2/4 -mt-8 pointer-events-none">,00 Öro</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AtomInput from './AtomInput.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||
<template>
|
||||
<input
|
||||
ref="input"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
class="input input-bordered w-full"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
</style>
|
|
@ -1,18 +0,0 @@
|
|||
<template>
|
||||
<img
|
||||
v-if="isDark"
|
||||
src="../../assets/logo_white.png"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="../../assets/logo.png"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { isDark } from '../../utils/darkMode';
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,38 +0,0 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="modal-toggle"
|
||||
@change="$emit('open', {id, open: ($event.target as HTMLInputElement).checked})"
|
||||
/>
|
||||
<div class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">{{ title }}</h3>
|
||||
<p class="py-4"><slot /></p>
|
||||
<div class="modal-action">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['open']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,91 +0,0 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
title="Account hinzufügen"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<h4 class="text-md">Daten</h4>
|
||||
<div class="flex gap-4">
|
||||
<AtomInput
|
||||
v-model="firstName"
|
||||
placeholder="Vorname"
|
||||
/>
|
||||
<AtomInput placeholder="Nachname" />
|
||||
</div>
|
||||
<AtomInput
|
||||
placeholder="Kontonummer"
|
||||
type="number"
|
||||
/>
|
||||
<AtomInput
|
||||
placeholder="Geburtsdatum"
|
||||
type="number"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<AtomInput placeholder="Straße" />
|
||||
<AtomInput
|
||||
placeholder="Hnr."
|
||||
class="w-3/12"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<AtomInput
|
||||
placeholder="Postleitzahl"
|
||||
class="w-6/12"
|
||||
/>
|
||||
<AtomInput placeholder="Stadt" />
|
||||
</div>
|
||||
<h4 class="text-md">Kontakt</h4>
|
||||
<div class="flex gap-4">
|
||||
<AtomSelect
|
||||
placeholder="Label"
|
||||
:options="['Festnetz', 'Mobil', 'Arbeit']"
|
||||
class="w-4/12"
|
||||
/>
|
||||
<AtomInput placeholder="Telefonnummer" />
|
||||
</div>
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
ref="abortButton"
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
Speichern
|
||||
</button>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import AtomSelect from '../atoms/AtomSelect.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const firstName = ref('');
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
console.log('TODO: Submit');
|
||||
console.log(firstName.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,52 +0,0 @@
|
|||
<template>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full table-zebra">
|
||||
<thead class="sticky top-0">
|
||||
<tr>
|
||||
<th v-for="(_, key) of data[0]">{{ key }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entries in data">
|
||||
<td
|
||||
v-for="(entry, _, key) of entries"
|
||||
:class="key >= 6 ? 'text-center' : null"
|
||||
>
|
||||
<span v-if="key < 6">{{ entry }}</span>
|
||||
<button
|
||||
v-if="key == 6"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
>
|
||||
<DocumentTextIcon
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="key == 7"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
>
|
||||
<CurrencyDollarIcon
|
||||
class="h-6 w-6"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>import { PropType } from 'vue';
|
||||
import { CurrencyDollarIcon, DocumentTextIcon } from '@heroicons/vue/outline';
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Array as PropType<object[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,97 +0,0 @@
|
|||
<template>
|
||||
<div class="drawer drawer-mobile">
|
||||
<input
|
||||
:id="drawerId"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
/>
|
||||
<div class="drawer-content flex flex-col bg-base-300 h-screen">
|
||||
<div
|
||||
class="navbar shadow-lg bg-neutral-focus text-neutral-content lg:hidden sticky top-0 z-50 max-w-7xl place-self-center"
|
||||
>
|
||||
<div class="flex-none">
|
||||
<label
|
||||
:for="drawerId"
|
||||
class="btn btn-ghost rounded-btn lg:hidden"
|
||||
>
|
||||
<MenuIcon class="h-7 w-7" />
|
||||
</label>
|
||||
<label
|
||||
tabindex="0"
|
||||
class="btn btn-ghost avatar"
|
||||
>
|
||||
<AtomLogo class="w-12" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="drawer-side">
|
||||
<label
|
||||
:for="drawerId"
|
||||
class="drawer-overlay"
|
||||
/>
|
||||
<ul class="menu p-4 overflow-y-auto w-64">
|
||||
<li class="pointer-events-none">
|
||||
<a>
|
||||
<AtomLogo class="w-28" />
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-for="navigationEntry of navigationEntries"
|
||||
:class="getNavigationEntryClass(navigationEntry)"
|
||||
>
|
||||
<component
|
||||
:is="!navigationEntry.to || navigationEntry.disabled ? 'span' : 'router-link'"
|
||||
v-if="navigationEntry.name !== 'divider'"
|
||||
:to="navigationEntry.to"
|
||||
active-class="active"
|
||||
>
|
||||
<component
|
||||
:is="navigationEntry.icon"
|
||||
class="h-6 w-6"
|
||||
/>{{ navigationEntry.name }}
|
||||
</component>
|
||||
</li>
|
||||
<AtomSwap
|
||||
class="mt-auto place-self-end"
|
||||
:on-icon="SunIcon"
|
||||
:off-icon="MoonIcon"
|
||||
:checked="isDark"
|
||||
@change="toggleDark()"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { isDark, toggleDark } from '../../utils/darkMode';
|
||||
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
|
||||
import { MenuIcon, MoonIcon, SunIcon } from '@heroicons/vue/outline';
|
||||
import AtomLogo from '../atoms/AtomLogo.vue';
|
||||
import AtomSwap from '../atoms/AtomSwap.vue';
|
||||
|
||||
defineProps({
|
||||
drawerId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
navigationEntries: {
|
||||
type: Array as PropType<INavigationEntry[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function getNavigationEntryClass(navigationEntry: INavigationEntry) {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'menu-title': navigationEntry.name === 'divider',
|
||||
disabled: navigationEntry.disabled,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,11 +0,0 @@
|
|||
export class DateService {
|
||||
public static toString(date: Date): string {
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
<template>
|
||||
<div class="hero-content flex-col text-center lg:p-6 pt-0">
|
||||
<h1 class="text-8xl mt-5">{{ CurrencyService.toString(balance) }}</h1>
|
||||
<h3 class="text-5xl font-bold mt-10">Kontoinhaber: John Doe</h3>
|
||||
<h4 class="text-4xl mb-10">Kontonummer: 0000123456</h4>
|
||||
<MoleculeTransactionTable
|
||||
class="w-[42rem]"
|
||||
:balance="balance"
|
||||
:transactions="transactions"
|
||||
/>
|
||||
</div>
|
||||
<div class="sticky flex place-content-center lg:gap-32 gap-16 bg-base-100 bottom-0 w-100 p-5">
|
||||
<MoleculeInputModal
|
||||
:id="depositModalId"
|
||||
title="Einzahlung"
|
||||
action-label="Einzahlen"
|
||||
input-label="Einzuzahlender Betrag:"
|
||||
@submit="handleDeposit"
|
||||
/>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="depositModalId"
|
||||
>
|
||||
<ChevronUpIcon class="w-6 h-6 text-success" />
|
||||
Einzahlen
|
||||
</label>
|
||||
<MoleculeInputModal
|
||||
:id="salaryModalId"
|
||||
title="Gehalt"
|
||||
action-label="Gehalt hinzufügen"
|
||||
input-label="Gehalt Betrag:"
|
||||
@submit="handleSalary"
|
||||
/>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="salaryModalId"
|
||||
>
|
||||
<CurrencyDollarIcon class="w-6 h-6 text-warning" />
|
||||
Gehalt
|
||||
</label>
|
||||
<MoleculeInputModal
|
||||
:id="withdrawModalId"
|
||||
title="Auszahlung"
|
||||
action-label="Auszahlen"
|
||||
input-label="Auszuzahlender Betrag:"
|
||||
@submit="handleWithdraw"
|
||||
/>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="withdrawModalId"
|
||||
>
|
||||
<ChevronDownIcon class="w-6 h-6 text-error" />
|
||||
Auszahlen
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
||||
import { CurrencyService } from '../services/currency.service';
|
||||
import { CurrencyDollarIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/outline';
|
||||
import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue';
|
||||
import MoleculeInputModal from '../molecules/MoleculeInputModal.vue';
|
||||
import { onKeyStroke } from '@vueuse/core';
|
||||
|
||||
const depositModalId = 'deposit-modal';
|
||||
const salaryModalId = 'salary-modal';
|
||||
const withdrawModalId = 'withdraw-modal';
|
||||
const balance = 13500;
|
||||
const transactions: ITransaction[] = [
|
||||
{
|
||||
type: 'Kontoeröffnung',
|
||||
date: new Date(2022, 7, 12, 17, 30, 0),
|
||||
amount: 500,
|
||||
},
|
||||
{
|
||||
type: 'Bargeldeinzahlung',
|
||||
date: new Date(2022, 7, 12, 17, 30, 10),
|
||||
amount: 10000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Gehalt',
|
||||
date: new Date(2022, 7, 12, 17, 34, 10),
|
||||
amount: 5000,
|
||||
},
|
||||
{
|
||||
type: 'Bargeldauszahlung',
|
||||
date: new Date(2022, 7, 15, 18, 26, 0),
|
||||
amount: -2000,
|
||||
},
|
||||
].sort((a, b) => b.date.getTime() - a.date.getTime());
|
||||
|
||||
|
||||
|
||||
onKeyStroke('e', (e) => {
|
||||
e.preventDefault();
|
||||
console.log('e pressed');
|
||||
// TODO: open deposit modal
|
||||
});
|
||||
|
||||
function handleDeposit(amount: number) {
|
||||
console.log('Deposit:', amount);
|
||||
}
|
||||
|
||||
function handleSalary(amount: number) {
|
||||
console.log('Salary:', amount);
|
||||
}
|
||||
|
||||
function handleWithdraw(amount: number) {
|
||||
console.log('Withdraw:', amount);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,199 +0,0 @@
|
|||
<template>
|
||||
<div class="lg:p-6">
|
||||
<MoleculeDataTable :data="data" />
|
||||
<MoleculeAddAccountModal :id="addAccountModalId" />
|
||||
<label
|
||||
class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-8"
|
||||
:for="addAccountModalId"
|
||||
>
|
||||
<PlusIcon class="w-7 h-7" />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MoleculeDataTable from '../molecules/MoleculeDataTable.vue';
|
||||
import { PlusIcon } from '@heroicons/vue/outline';
|
||||
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
|
||||
|
||||
const addAccountModalId = 'add-account-modal';
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const data = [
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
{
|
||||
'Kontonummer': '0000123456',
|
||||
'Name': 'John Doe',
|
||||
'Geburtsdatum': '13.11.1998',
|
||||
'Kontostand': '150,00 Öro',
|
||||
'Adresse': 'Test-Straße 1, 74613 Öhringen',
|
||||
'Letzter': '12.07.2022 17:39',
|
||||
'Kontakt': null,
|
||||
'Trans.': null,
|
||||
},
|
||||
/* eslint-enable @typescript-eslint/naming-convention */
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,59 +0,0 @@
|
|||
<template>
|
||||
<button @click="getTrack()">GetTrack</button>
|
||||
<button @click="setPlayPause()">Play/Pause</button>
|
||||
<div>Currently Playing: {{ `${artist} - ${track}` }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Mopidy from 'mopidy';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
|
||||
const artist = ref('');
|
||||
const track = ref('');
|
||||
|
||||
const mopidy = new Mopidy({
|
||||
webSocketUrl: 'ws://localhost:6680/mopidy/ws/',
|
||||
});
|
||||
|
||||
mopidy.on('state', console.log);
|
||||
mopidy.on('event', console.log);
|
||||
|
||||
mopidy.on('state:online', () => {
|
||||
getTrack();
|
||||
});
|
||||
|
||||
mopidy.on('event:trackPlaybackStarted', (event) => {
|
||||
track.value = event.tl_track.track.name;
|
||||
artist.value = event.tl_track.track.artists.map((artist) => artist.name).join(', ');
|
||||
});
|
||||
|
||||
async function getTrack() {
|
||||
if(mopidy.playback) {
|
||||
const currentTrack = await mopidy.playback.getCurrentTrack();
|
||||
console.log(await mopidy.playback.getTimePosition());
|
||||
if(currentTrack) {
|
||||
track.value = currentTrack.name;
|
||||
artist.value = currentTrack.artists.map((artist) => artist.name).join(', ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setPlayPause() {
|
||||
const currentState = await mopidy.playback?.getState();
|
||||
if(currentState === 'playing') {
|
||||
await mopidy.playback?.pause();
|
||||
} else {
|
||||
await mopidy.playback?.resume();
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mopidy.close();
|
||||
mopidy.off();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -1,5 +0,0 @@
|
|||
export interface ITransaction {
|
||||
type: string;
|
||||
date: Date;
|
||||
amount: number;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
import * as VueRouter from 'vue-router';
|
||||
import DataView from '../components/views/DataView.vue';
|
||||
import CheckInView from '../components/views/CheckInView.vue';
|
||||
import BankView from '../components/views/BankView.vue';
|
||||
import StocksView from '../components/views/StocksView.vue';
|
||||
import RadioView from '../components/views/RadioView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/data',
|
||||
component: DataView,
|
||||
},
|
||||
{
|
||||
path: '/checkin',
|
||||
component: CheckInView,
|
||||
},
|
||||
{
|
||||
path: '/bank',
|
||||
component: BankView,
|
||||
},
|
||||
{
|
||||
path: '/stocks',
|
||||
component: StocksView,
|
||||
},
|
||||
{
|
||||
path: '/radio',
|
||||
component: RadioView,
|
||||
},
|
||||
];
|
||||
|
||||
const router = VueRouter.createRouter({
|
||||
history: VueRouter.createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('daisyui')],
|
||||
}
|
1
webapp/.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/types/pocketbase.types.ts
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue-scoped-css/vue3-recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
env: {
|
||||
|
@ -52,7 +53,7 @@ module.exports = {
|
|||
'asyncArrow': 'always',
|
||||
}],
|
||||
'space-in-parens': ['error', 'never'],
|
||||
'spaced-comment': ['error', 'always'],
|
||||
'spaced-comment': ['error', 'always', { 'markers': ['/'] }],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
@ -70,7 +71,8 @@ module.exports = {
|
|||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
'project': 'tsconfig.json',
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -138,6 +140,7 @@ module.exports = {
|
|||
'svg': 'always',
|
||||
'math': 'always',
|
||||
}],
|
||||
// 'tailwindcss/no-custom-classname': 'off',
|
||||
},
|
||||
},
|
||||
],
|
|
@ -8,10 +8,10 @@
|
|||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<meta name="msapplication-TileColor" content="#ffc40d">
|
||||
<meta name="msapplication-TileColor" content="#da532c">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Kinderspielstadt Öhringen</title>
|
||||
<title>Schule als Staat | Hohenlohe Gymnasium Öhringen</title>
|
||||
</head>
|
||||
|
||||
<body>
|
47
webapp/package.json
Normal file
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "hgoe-sas",
|
||||
"private": true,
|
||||
"author": "Simon Giesel",
|
||||
"version": "1.3.6",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"lint": "eslint -c .eslintrc.js src/",
|
||||
"preview": "vite preview",
|
||||
"typegen": "pocketbase-typegen --json ../server/pb_schema.json --out ./src/types/pocketbase.types.ts",
|
||||
"optimize-svg": "svgo -f ./src/assets/svg/ --config=./src/assets/svg/svgo.config.js",
|
||||
"generate-version": "node ./scripts/generate-version.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"daisyui": "^3.2.1",
|
||||
"date-fns": "^2.30.0",
|
||||
"pocketbase": "^0.15.3",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-tailwindcss": "^3.13.0",
|
||||
"eslint-plugin-vue": "^9.15.1",
|
||||
"eslint-plugin-vue-scoped-css": "^2.5.0",
|
||||
"pocketbase-typegen": "^1.1.11",
|
||||
"postcss": "^8.4.25",
|
||||
"sass": "^1.63.6",
|
||||
"svgo": "^3.0.2",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^4.4.3",
|
||||
"vue-eslint-parser": "^9.3.1",
|
||||
"vue-tsc": "^1.8.4"
|
||||
}
|
||||
}
|
3096
webapp/pnpm-lock.yaml
Normal file
BIN
webapp/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
webapp/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
webapp/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 18 KiB |
|
@ -3,7 +3,7 @@
|
|||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#b91d47</TileColor>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
webapp/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
webapp/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
webapp/public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
webapp/public/mstile-144x144.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
webapp/public/mstile-150x150.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
webapp/public/mstile-310x150.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
webapp/public/mstile-310x310.png
Normal file
After Width: | Height: | Size: 5 KiB |
BIN
webapp/public/mstile-70x70.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
2
webapp/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-Agent: *
|
||||
Disallow: /
|
94
webapp/public/safari-pinned-tab.svg
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?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="3407.000000pt" height="3407.000000pt" viewBox="0 0 3407.000000 3407.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,3407.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M4410 33513 c-380 -51 -730 -280 -929 -609 -138 -227 -304 -646 -428
|
||||
-1079 -35 -124 -98 -371 -106 -415 -3 -14 -10 -43 -16 -65 -15 -58 -98 -472
|
||||
-106 -529 -3 -27 -8 -52 -9 -55 -2 -3 -6 -28 -10 -56 -3 -27 -8 -61 -11 -75
|
||||
-3 -14 -10 -57 -16 -97 -5 -39 -12 -80 -14 -91 -2 -10 -6 -43 -10 -73 -3 -30
|
||||
-8 -65 -10 -79 -10 -70 -15 -109 -20 -145 -10 -82 -24 -212 -30 -275 -3 -36
|
||||
-8 -81 -10 -100 -2 -19 -7 -62 -10 -95 -3 -33 -7 -85 -10 -115 -12 -132 -15
|
||||
-170 -20 -255 -3 -49 -8 -108 -10 -130 -2 -22 -7 -98 -10 -170 -4 -71 -8 -161
|
||||
-10 -200 -26 -490 -26 -1706 0 -2155 2 -30 6 -122 10 -205 3 -82 8 -175 10
|
||||
-205 21 -356 32 -516 40 -620 9 -107 14 -176 20 -255 9 -116 13 -173 20 -235
|
||||
3 -30 8 -80 10 -110 3 -30 7 -77 10 -105 3 -27 7 -77 10 -110 12 -129 14 -151
|
||||
20 -195 5 -46 9 -88 20 -195 3 -33 8 -73 10 -90 2 -16 7 -57 10 -90 3 -33 10
|
||||
-94 15 -135 5 -41 12 -97 15 -125 3 -27 7 -66 10 -85 2 -19 7 -60 10 -90 3
|
||||
-30 8 -66 10 -80 2 -14 7 -49 10 -79 4 -30 8 -61 10 -70 1 -9 6 -43 10 -76 3
|
||||
-33 8 -76 11 -95 5 -41 23 -174 29 -210 3 -14 7 -50 11 -80 5 -55 7 -63 18
|
||||
-130 3 -19 8 -53 11 -75 3 -22 7 -53 10 -70 3 -16 7 -48 10 -70 3 -22 7 -53
|
||||
10 -70 3 -16 7 -48 10 -70 3 -22 12 -80 20 -130 8 -49 17 -108 20 -130 3 -22
|
||||
10 -65 15 -95 5 -30 11 -68 14 -85 10 -67 19 -121 22 -140 2 -11 6 -36 9 -55
|
||||
3 -19 7 -46 9 -60 2 -14 10 -59 16 -100 7 -41 14 -84 16 -95 4 -22 24 -138 29
|
||||
-170 6 -34 16 -97 21 -119 2 -11 18 -100 34 -196 31 -179 57 -318 90 -495 11
|
||||
-55 22 -115 25 -134 3 -19 7 -37 9 -40 2 -3 7 -28 10 -56 4 -27 11 -68 16 -90
|
||||
5 -22 12 -56 15 -75 3 -19 17 -91 31 -160 14 -69 28 -136 30 -150 3 -14 13
|
||||
-68 24 -120 20 -97 26 -128 35 -177 3 -16 10 -48 15 -73 13 -57 18 -82 30
|
||||
-145 6 -27 14 -68 19 -90 18 -84 43 -201 47 -224 3 -12 21 -91 40 -175 20 -83
|
||||
41 -178 49 -211 12 -55 74 -298 140 -550 14 -55 42 -154 61 -220 19 -66 37
|
||||
-127 39 -135 16 -68 180 -596 260 -835 68 -202 201 -582 245 -695 11 -30 27
|
||||
-71 34 -90 41 -110 109 -288 116 -305 5 -11 47 -114 94 -230 98 -243 277 -666
|
||||
331 -780 34 -73 95 -207 95 -210 0 -2 25 -57 56 -122 30 -65 79 -167 107 -228
|
||||
61 -130 433 -870 502 -1000 26 -49 70 -129 96 -177 27 -48 49 -89 49 -92 0 -5
|
||||
269 -490 320 -576 133 -227 186 -319 200 -345 16 -29 294 -488 369 -608 20
|
||||
-32 54 -87 76 -123 130 -210 627 -964 818 -1239 107 -156 301 -430 306 -435 4
|
||||
-3 26 -34 51 -70 24 -36 50 -72 57 -80 6 -8 74 -100 150 -205 161 -222 635
|
||||
-849 653 -865 3 -3 21 -25 40 -50 19 -25 40 -52 47 -60 7 -8 67 -85 135 -170
|
||||
67 -85 128 -162 135 -170 7 -8 47 -58 89 -110 85 -105 105 -130 226 -276 45
|
||||
-55 98 -119 116 -142 19 -23 42 -50 52 -62 10 -12 33 -39 52 -62 18 -23 38
|
||||
-46 43 -52 6 -6 35 -40 65 -76 30 -36 57 -67 60 -70 3 -3 27 -32 54 -65 28
|
||||
-33 68 -80 90 -105 23 -25 64 -72 92 -105 103 -120 136 -158 147 -170 7 -7 37
|
||||
-41 67 -76 74 -86 220 -250 239 -270 9 -9 41 -44 71 -79 30 -34 66 -75 80 -90
|
||||
72 -79 209 -228 235 -256 17 -17 47 -51 69 -75 43 -50 253 -273 396 -424 52
|
||||
-55 106 -112 120 -126 224 -237 866 -885 1149 -1158 131 -127 410 -394 461
|
||||
-441 19 -18 67 -62 106 -99 154 -145 372 -346 420 -388 12 -10 62 -55 112
|
||||
-101 50 -45 109 -99 133 -120 23 -20 54 -48 69 -62 15 -14 56 -50 91 -80 35
|
||||
-30 70 -62 79 -70 9 -8 47 -42 85 -75 39 -33 72 -63 75 -66 9 -9 411 -351 535
|
||||
-454 66 -55 125 -104 131 -110 6 -5 28 -23 49 -40 21 -16 66 -53 99 -81 81
|
||||
-67 432 -343 476 -374 19 -13 44 -33 55 -43 11 -10 55 -44 99 -75 43 -31 117
|
||||
-85 165 -119 396 -286 662 -386 1006 -377 71 2 141 5 155 8 79 14 97 17 112
|
||||
22 9 3 25 6 35 8 38 8 172 56 229 83 102 48 305 172 420 258 22 16 53 39 68
|
||||
50 105 75 526 399 656 506 19 16 82 67 140 115 116 95 125 102 325 269 77 64
|
||||
156 131 176 148 20 18 62 55 94 82 32 28 63 55 69 60 6 6 43 37 81 70 39 33
|
||||
75 64 81 70 6 5 39 35 74 65 35 30 74 64 86 75 12 11 55 49 95 85 40 36 77 70
|
||||
83 75 6 6 56 51 111 100 198 178 279 252 340 310 18 17 83 77 145 135 62 58
|
||||
127 119 145 135 653 619 1260 1225 1825 1825 149 158 277 295 292 313 20 22
|
||||
183 200 204 221 5 6 55 61 109 121 114 126 100 112 186 207 37 40 88 98 114
|
||||
128 26 30 74 84 105 120 31 36 108 124 170 195 62 72 125 144 139 160 14 17
|
||||
76 89 136 160 60 72 142 168 181 215 75 88 323 388 336 405 4 6 22 28 40 50
|
||||
26 31 163 202 245 305 7 8 50 62 96 120 200 252 769 1001 857 1130 8 12 32 45
|
||||
54 73 40 53 234 324 273 380 12 18 69 100 127 182 141 201 605 895 751 1125
|
||||
202 317 566 913 677 1110 14 25 60 104 102 177 42 73 76 137 76 143 0 5 5 10
|
||||
10 10 6 0 10 4 10 9 0 5 17 40 39 78 21 37 53 95 71 128 18 33 41 74 50 90 33
|
||||
59 179 332 244 455 79 150 423 840 496 995 28 61 85 184 127 275 41 91 86 189
|
||||
99 218 59 126 300 697 387 917 211 533 407 1083 552 1550 20 66 43 140 52 165
|
||||
28 85 186 640 219 770 87 344 164 661 178 733 2 12 16 75 30 139 31 143 40
|
||||
188 50 238 2 14 16 81 31 150 26 123 70 341 79 390 2 14 7 36 10 50 3 14 8 36
|
||||
10 50 3 14 12 61 21 105 9 44 18 95 21 114 3 18 8 43 10 55 4 19 26 131 39
|
||||
201 3 17 8 41 10 55 9 43 13 66 39 213 15 78 30 165 36 192 5 28 12 66 15 85
|
||||
3 19 8 42 10 50 3 8 9 44 14 80 5 36 22 133 36 215 15 83 28 159 30 170 1 11
|
||||
6 36 9 55 4 19 9 46 11 60 1 14 8 57 15 95 13 82 17 106 25 160 3 22 10 63 15
|
||||
90 5 28 15 84 21 125 16 116 28 195 54 365 8 50 17 110 20 135 6 41 20 140 30
|
||||
200 2 14 9 63 15 110 6 47 13 101 16 120 21 150 31 220 79 610 9 69 18 141 20
|
||||
160 2 19 7 58 10 85 3 28 8 66 10 85 3 19 7 60 10 90 6 63 15 143 20 180 2 14
|
||||
7 59 11 100 10 116 11 121 18 185 3 33 8 85 11 115 3 30 8 75 10 100 2 25 7
|
||||
72 10 105 3 33 7 83 10 111 7 97 16 209 20 244 3 19 7 76 10 125 4 50 8 110
|
||||
10 135 4 50 14 181 20 280 2 36 7 106 10 155 3 50 8 128 10 175 11 233 16 341
|
||||
20 400 19 327 26 1432 12 1850 -12 349 -24 590 -42 815 -2 33 -7 94 -10 135
|
||||
-3 41 -7 95 -10 120 -2 25 -7 74 -10 110 -5 65 -13 135 -20 200 -3 19 -7 62
|
||||
-10 95 -3 33 -7 69 -9 80 -2 11 -7 49 -11 85 -4 36 -9 74 -11 85 -2 11 -6 43
|
||||
-9 70 -3 28 -7 59 -9 70 -2 11 -9 61 -15 110 -7 50 -13 97 -15 105 -12 61 -18
|
||||
101 -37 214 -11 72 -22 132 -24 135 -2 3 -6 31 -10 61 -4 30 -9 58 -11 61 -1
|
||||
3 -17 74 -34 158 -34 165 -124 535 -160 652 -160 525 -301 869 -449 1094 -70
|
||||
107 -221 268 -306 326 -27 19 -55 39 -62 46 -7 6 -68 39 -135 72 -122 60 -246
|
||||
102 -343 116 -27 4 -61 9 -75 12 -14 3 -5675 6 -12581 7 -10052 1 -12574 -1
|
||||
-12649 -11z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6.4 KiB |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Kinderspielstadt Öhringen",
|
||||
"short_name": "KiSpi Öhringen",
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
|
@ -16,4 +16,4 @@
|
|||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
}
|
9
webapp/scripts/generate-version.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const fs = require('fs');
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
const version = `${packageJson.version}+${fs.readFileSync('../.git/refs/heads/master').toString().trim().substring(0, 7)}`;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Writing version ${version} to local .env file.`);
|
||||
|
||||
fs.writeFileSync('.env', `VITE_APP_VERSION=${version}`);
|
67
webapp/src/App.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<MoleculeNavigationDrawer
|
||||
:drawer-id="drawerId"
|
||||
:navigation-entries="navigationEntries"
|
||||
>
|
||||
<div class="flex min-h-screen flex-col items-stretch">
|
||||
<RouterView
|
||||
v-if="route.name != 'radio'"
|
||||
class="shrink-0 grow"
|
||||
/>
|
||||
<ViewRadio
|
||||
v-show="route.name == 'radio'"
|
||||
class="shrink-0 grow"
|
||||
/>
|
||||
</div>
|
||||
<AtomFooter class="shrink-0" />
|
||||
</MoleculeNavigationDrawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { INavigationEntry } from './interfaces/navigation-entry.interface';
|
||||
import { useRoute } from 'vue-router';
|
||||
import {
|
||||
IdentificationIcon,
|
||||
BuildingLibraryIcon,
|
||||
MusicalNoteIcon,
|
||||
AdjustmentsHorizontalIcon,
|
||||
BriefcaseIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import MoleculeNavigationDrawer from './components/molecules/MoleculeNavigationDrawer.vue';
|
||||
import ViewRadio from './components/views/ViewRadio.vue';
|
||||
import AtomFooter from './components/atoms/AtomFooter.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const drawerId = 'default-drawer';
|
||||
const navigationEntries: INavigationEntry[] = [
|
||||
{
|
||||
name: 'Arbeitgeberportal',
|
||||
icon: BriefcaseIcon,
|
||||
to: '/employer',
|
||||
},
|
||||
{
|
||||
name: 'Zoll',
|
||||
icon: IdentificationIcon,
|
||||
to: '/checkin',
|
||||
},
|
||||
{
|
||||
name: 'Bank',
|
||||
icon: BuildingLibraryIcon,
|
||||
to: '/bank',
|
||||
},
|
||||
{
|
||||
name: 'Radio',
|
||||
icon: MusicalNoteIcon,
|
||||
to: '/radio',
|
||||
},
|
||||
{
|
||||
name: 'Staatsportal',
|
||||
icon: AdjustmentsHorizontalIcon,
|
||||
to: '/settings',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
1
webapp/src/assets/svg/logo.svg
Normal file
After Width: | Height: | Size: 6.8 KiB |
6
webapp/src/assets/svg/svgo.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: [
|
||||
'preset-default',
|
||||
'prefixIds',
|
||||
],
|
||||
};
|
18
webapp/src/components/atoms/AtomCard.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div class="card bg-base-100 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title"><slot name="title" /></h2>
|
||||
<p>
|
||||
<slot />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
83
webapp/src/components/atoms/AtomCurrencyInput.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
v-model="modelValue"
|
||||
type="number"
|
||||
class="input w-full border-primary text-right"
|
||||
:class="{
|
||||
'pr-[5.5rem]': !perDay,
|
||||
'pr-[8rem]': perDay,
|
||||
'!border-error': error,
|
||||
}"
|
||||
step="1"
|
||||
:min="min"
|
||||
:max="max"
|
||||
@keyup="onKeyUp"
|
||||
/>
|
||||
<span
|
||||
class="pointer-events-none absolute top-2/4 text-[1rem] opacity-50"
|
||||
:class="{
|
||||
'-mt-3': !suffixMargin,
|
||||
'mt-[-0.625rem]': suffixMargin,
|
||||
'ml-[-5.5rem]': !perDay,
|
||||
'ml-[-8rem]': perDay,
|
||||
}"
|
||||
>,00 Batzen{{ perDay ? ' / Tag' : '' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const input = ref<InstanceType<typeof HTMLInputElement>>();
|
||||
const error = ref(false);
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
suffixMargin?: boolean,
|
||||
perDay?: boolean,
|
||||
min?: number,
|
||||
max?: number,
|
||||
value?: number,
|
||||
}>(), {
|
||||
suffixMargin: false,
|
||||
perHour: false,
|
||||
min: 1,
|
||||
max: 1_000_000,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
|
||||
function onKeyUp(event: KeyboardEvent) {
|
||||
error.value = false;
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
const value = parseInt((event.target as HTMLInputElement).value);
|
||||
|
||||
if(isNaN(value)) {
|
||||
error.value = true;
|
||||
} else if(value < props.min) {
|
||||
error.value = true;
|
||||
} else if(value > props.max) {
|
||||
error.value = true;
|
||||
} else {
|
||||
emit('change', value);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
if(input.value) {
|
||||
input.value.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [value: number],
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
reset,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
23
webapp/src/components/atoms/AtomFactorInput.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<AtomInput
|
||||
v-model="modelValue"
|
||||
type="number"
|
||||
class="pr-7 text-right"
|
||||
step="1"
|
||||
min="1"
|
||||
/>
|
||||
<span class="pointer-events-none absolute top-2/4 -ml-6 -mt-3 opacity-50">x</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AtomInput from './AtomInput.vue';
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
14
webapp/src/components/atoms/AtomFileInput.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<input
|
||||
type="file"
|
||||
class="file-input-bordered file-input-primary file-input"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
19
webapp/src/components/atoms/AtomFooter.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<footer class="footer footer-center bg-base-100 p-4 text-base-content">
|
||||
<div>
|
||||
<p>Version: {{ version }}</p>
|
||||
<p>Made with ❤️ by Simon Giesel</p>
|
||||
<p>Copyright © 2023 - All rights reserved</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const version = (import.meta as any).env.VITE_APP_VERSION as string ?? 'local+dev';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="min-h-full lg:p-6">
|
||||
<div class="hero min-h-full bg-base-200 rounded-lg">
|
||||
<div class="hero min-h-full rounded-lg bg-base-200">
|
||||
<div class="hero-content flex-col text-center">
|
||||
<h1 class="text-6xl font-bold">John Doe um 17:39:33</h1>
|
||||
<h1 class="text-6xl font-bold"><slot /></h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
39
webapp/src/components/atoms/AtomInput.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
class="input w-full border-primary"
|
||||
:class="{
|
||||
'!border-error': error,
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
error: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
92
webapp/src/components/atoms/AtomLegacyModal.vue
Normal file
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<input
|
||||
:id="id"
|
||||
type="checkbox"
|
||||
class="modal-toggle"
|
||||
@change="($event.target as HTMLInputElement).checked ? $emit('close') : null"
|
||||
/>
|
||||
<div class="modal">
|
||||
<form
|
||||
method="dialog"
|
||||
class="modal-box"
|
||||
:class="{
|
||||
'bg-base-300': darker,
|
||||
'w-auto': wAuto,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-if="closeButton"
|
||||
class="btn-ghost btn-sm btn-circle btn absolute right-2 top-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<h3
|
||||
v-if="title"
|
||||
class="text-lg font-bold"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="py-4"><slot /></p>
|
||||
<div class="modal-action">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
>
|
||||
<button>close</button>
|
||||
</form>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
darker: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wAuto: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
function show(): void {
|
||||
(document.getElementById(props.id) as HTMLInputElement).checked = true;
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
(document.getElementById(props.id) as HTMLInputElement).checked = false;
|
||||
}
|
||||
|
||||
function isOpen(): boolean {
|
||||
return (document.getElementById(props.id) as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
defineEmits(['close']);
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close,
|
||||
isOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
14
webapp/src/components/atoms/AtomLogo.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<img
|
||||
src="../../assets/svg/logo.svg"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// import { isDark } from '../../utils/darkMode';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
97
webapp/src/components/atoms/AtomModal.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
:id="id"
|
||||
class="modal"
|
||||
>
|
||||
<form
|
||||
method="dialog"
|
||||
class="modal-box"
|
||||
:class="{
|
||||
'bg-base-300': darker,
|
||||
'w-auto': wAuto,
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-if="closeButton"
|
||||
class="btn-ghost btn-sm btn-circle btn absolute right-2 top-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<h3
|
||||
v-if="title"
|
||||
class="text-lg font-bold"
|
||||
>
|
||||
{{ title }}
|
||||
</h3>
|
||||
<p class="py-4"><slot /></p>
|
||||
<div class="modal-action">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</form>
|
||||
<form
|
||||
method="dialog"
|
||||
class="modal-backdrop"
|
||||
>
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
darker: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
closeButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wAuto: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
function show(): void {
|
||||
(document.getElementById(props.id) as HTMLDialogElement)?.showModal();
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
(document.getElementById(props.id) as HTMLDialogElement)?.close();
|
||||
}
|
||||
|
||||
function isOpen(): boolean {
|
||||
return (document.getElementById(props.id) as HTMLDialogElement)?.open;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
(document.getElementById(props.id) as HTMLDialogElement).addEventListener('close', () => {
|
||||
emit('close');
|
||||
});
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close,
|
||||
isOpen,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
23
webapp/src/components/atoms/AtomPercentInput.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<AtomInput
|
||||
v-model="modelValue"
|
||||
type="number"
|
||||
class="pr-14 text-right"
|
||||
step="1"
|
||||
min="1"
|
||||
/>
|
||||
<span class="pointer-events-none absolute top-2/4 -ml-14 -mt-3 opacity-50">,00 %</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AtomInput from './AtomInput.vue';
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<select class="select select-bordered w-full">
|
||||
<select
|
||||
class="select-bordered select w-full"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
>
|
||||
<option
|
||||
disabled
|
||||
selected
|
||||
|
@ -22,7 +25,13 @@ defineProps({
|
|||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['update:modelValue']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
|
@ -20,11 +20,11 @@ import { FunctionalComponent, PropType } from 'vue';
|
|||
|
||||
defineProps({
|
||||
onIcon: {
|
||||
type: Object as PropType<FunctionalComponent>,
|
||||
type: Function as PropType<FunctionalComponent>,
|
||||
required: true,
|
||||
},
|
||||
offIcon: {
|
||||
type: Object as PropType<FunctionalComponent>,
|
||||
type: Function as PropType<FunctionalComponent>,
|
||||
required: true,
|
||||
},
|
||||
checked: {
|
16
webapp/src/components/atoms/AtomTextarea.vue
Normal file
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<textarea
|
||||
v-model="modelValue"
|
||||
class="textarea-primary textarea h-auto"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
44
webapp/src/components/molecules/MoleculeAuthDialog.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div class="hero min-h-full">
|
||||
<form
|
||||
class="hero-content mx-auto flex-col text-center"
|
||||
@submit.prevent="handleAuth"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="email"
|
||||
placeholder="Admin-Email"
|
||||
type="email"
|
||||
required
|
||||
/>
|
||||
<AtomInput
|
||||
v-model="password"
|
||||
placeholder="Passwort"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
<button class="btn-primary btn gap-2 self-end">
|
||||
<ArrowRightOnRectangleIcon class="h-6 w-6" />
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
|
||||
const email = ref('');
|
||||
const password = ref('');
|
||||
|
||||
async function handleAuth() {
|
||||
await AuthService.loginAsAdmin(email.value, password.value);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
63
webapp/src/components/molecules/MoleculeBankerAuthDialog.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="hero min-h-full">
|
||||
<form
|
||||
class="hero-content mx-auto flex-col text-center"
|
||||
@submit.prevent="handleAuth"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="username"
|
||||
placeholder="Nutzername"
|
||||
type="text"
|
||||
:error="error"
|
||||
required
|
||||
/>
|
||||
<AtomInput
|
||||
v-model="password"
|
||||
placeholder="Passwort"
|
||||
type="password"
|
||||
:error="error"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert alert-error flex w-[211px] items-center gap-2"
|
||||
>
|
||||
<XCircleIcon class="h-12 w-12" />
|
||||
<span class="text-base font-normal">Nutzername oder Passwort falsch.</span>
|
||||
</div>
|
||||
<button class="btn-primary btn gap-2 self-end">
|
||||
<ArrowRightOnRectangleIcon class="h-6 w-6" />
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { ArrowRightOnRectangleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const error = ref(false);
|
||||
|
||||
async function handleAuth() {
|
||||
try {
|
||||
await AuthService.loginAsBanker(username.value, password.value);
|
||||
} catch (e) {
|
||||
error.value = true;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
watch([username, password], () => {
|
||||
error.value = false;
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="flex place-items-center justify-between">
|
||||
<AtomInput
|
||||
v-model="accountNumber"
|
||||
class="w-4/12"
|
||||
@keyup.esc="modal?.close()"
|
||||
/>
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
Speichern
|
||||
</button>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
const accountNumber = ref('');
|
||||
const accountId = ref('');
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
function handleClose() {
|
||||
accountNumber.value = '';
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if(!accountNumber.value) {
|
||||
return;
|
||||
}
|
||||
emit('submit', {
|
||||
id: accountId.value,
|
||||
accountNumber: accountNumber.value,
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: (id: string) => {
|
||||
accountId.value = id;
|
||||
modal.value?.show();
|
||||
},
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
166
webapp/src/components/molecules/MoleculeDataTable.vue
Normal file
|
@ -0,0 +1,166 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="settings"
|
||||
class="rounded-lg"
|
||||
>
|
||||
<table class="table-pin-rows table-zebra table">
|
||||
<thead
|
||||
v-if="!hideHeader"
|
||||
class="uppercase"
|
||||
>
|
||||
<tr>
|
||||
<th v-for="header in tableHeaders">{{ header.title }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-base-100">
|
||||
<tr
|
||||
v-for="entry in data"
|
||||
@click="$emit('click', entry.id)"
|
||||
>
|
||||
<td
|
||||
v-for="header in tableHeaders"
|
||||
:class="{
|
||||
'text-center': [TableHeaderType.BUTTON_ACCOUNT, TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER].includes(header.type),
|
||||
}"
|
||||
>
|
||||
<span v-if="header.type === TableHeaderType.STRING">
|
||||
{{ entry[header.key] }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.DATE">
|
||||
{{ DateService.toShortString(entry[header.key]) }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.DATETIME">
|
||||
{{ entry[header.key] ? DateService.toString(parseISO(entry[header.key])) : 'N/A' }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.CURRENCY">
|
||||
{{ CurrencyService.toString(entry[header.key]) }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.SHIFT">
|
||||
<div class="dropdown-bottom dropdown">
|
||||
<label
|
||||
tabindex="0"
|
||||
class="btn bg-base-300 normal-case"
|
||||
>{{ entry[header.key] }}
|
||||
<ChevronDownIcon class="h-4 w-4" />
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content menu rounded-box z-[1] w-52 bg-base-100 p-2 shadow"
|
||||
>
|
||||
<li @click="$emit('selectShift', { id: entry.id, shift: Shifts.NONE }); removeFocus();"><a>{{
|
||||
Shifts.NONE
|
||||
}}</a></li>
|
||||
<li><a @click="$emit('selectShift', { id: entry.id, shift: Shifts.EARLY }); removeFocus();">{{
|
||||
Shifts.EARLY }}</a></li>
|
||||
<li><a @click="$emit('selectShift', { id: entry.id, shift: Shifts.LATE }); removeFocus();">{{
|
||||
Shifts.LATE
|
||||
}}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
<RouterLink
|
||||
v-if="header.type === TableHeaderType.BUTTON_ACCOUNT"
|
||||
:to="`/bank?accountNumber=${entry.accountNumber}`"
|
||||
class="btn-ghost btn-sm btn p-1"
|
||||
>
|
||||
<div
|
||||
class="tooltip normal-case"
|
||||
data-tip="Transaktionen anzeigen"
|
||||
>
|
||||
<CurrencyDollarIcon class="h-6 w-6" />
|
||||
</div>
|
||||
</RouterLink>
|
||||
<span
|
||||
v-if="header.type === TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER"
|
||||
class="btn-ghost btn-sm btn p-1"
|
||||
@click="$emit('changeAccountNumber', entry.id)"
|
||||
>
|
||||
<div
|
||||
class="tooltip normal-case"
|
||||
data-tip="Kontonummer ändern"
|
||||
>
|
||||
<CreditCardIcon class="h-6 w-6" />
|
||||
</div>
|
||||
</span>
|
||||
<span
|
||||
v-if="header.type === TableHeaderType.WAGE"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<AtomCurrencyInput
|
||||
v-model="entry.wage"
|
||||
suffix-margin
|
||||
per-day
|
||||
class="min-w-[11rem]"
|
||||
:min="settings.minWage / 100"
|
||||
:max="settings.maxWage / 100"
|
||||
@change="$emit('selectWage', { id: entry.id, wage: $event })"
|
||||
/>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, onMounted, ref } from 'vue';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { SettingsResponse } from '../../types/pocketbase.types';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { Shifts } from '../../enums/shift.enum';
|
||||
import { ChevronDownIcon, CreditCardIcon, CurrencyDollarIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
|
||||
|
||||
const settings = ref<SettingsResponse>();
|
||||
|
||||
defineProps({
|
||||
tableHeaders: {
|
||||
type: Array as PropType<{ title: string, key: string, type: TableHeaderType }[]>,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
// Explicit allow any types as this is a dynamic list of data
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Array as PropType<any[]>,
|
||||
required: true,
|
||||
},
|
||||
hideHeader: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
settings.value = await SettingsService.getSettings();
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
click: [id: string],
|
||||
selectShift: [{ id: string, shift: Shifts }],
|
||||
selectWage: [{ id: string, wage: number }],
|
||||
changeAccountNumber: [id: string],
|
||||
}>();
|
||||
|
||||
function removeFocus() {
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export enum TableHeaderType {
|
||||
STRING,
|
||||
DATE,
|
||||
CURRENCY,
|
||||
DATETIME,
|
||||
BUTTON_ACCOUNT,
|
||||
BUTTON_CHANGE_ACCOUNT_NUMBER,
|
||||
WAGE,
|
||||
SHIFT,
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
54
webapp/src/components/molecules/MoleculeErrorModal.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<AtomTextarea v-model="modelValue" />
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Schließen
|
||||
</label>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomTextarea from '../atoms/AtomTextarea.vue';
|
||||
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
73
webapp/src/components/molecules/MoleculeImportDataModal.vue
Normal file
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<AtomFileInput @change="file = ($event.target as HTMLInputElement)?.files?.[0]" />
|
||||
<p>Datei für den Import der Stammdaten im CSV-Format auswählen.<br /><b>! Achtung !</b> Die Datei muss eine Kopfzeile besitzen.</p>
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomFileInput from '../atoms/AtomFileInput.vue';
|
||||
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
const file = ref<File>();
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
actionLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
function handleSubmit() {
|
||||
if(!file.value) {
|
||||
return;
|
||||
}
|
||||
emit('submit', file.value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,32 +1,33 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
@open="handleOpen"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="flex justify-between place-items-center">
|
||||
<div class="flex place-items-center justify-between">
|
||||
<span>{{ inputLabel }}</span>
|
||||
<AtomCurrencyInput
|
||||
ref="input"
|
||||
v-model="amount"
|
||||
class="w-4/12"
|
||||
@keyup.enter="handleSubmit()"
|
||||
@keyup.esc="abortButton.click()"
|
||||
:max="1000"
|
||||
@keyup.esc="modal?.close()"
|
||||
/>
|
||||
</div>
|
||||
<template #action>
|
||||
<label
|
||||
ref="abortButton"
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</template>
|
||||
|
@ -34,13 +35,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
||||
const input = ref();
|
||||
const abortButton = ref();
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
const amount = ref();
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
|
@ -63,18 +64,22 @@ defineProps({
|
|||
|
||||
const emit = defineEmits(['submit']);
|
||||
|
||||
function handleOpen(event: {id: string, open: boolean}) {
|
||||
if(event.open) {
|
||||
// This is (currently) the only method to focus a input field inside a custom component.
|
||||
input.value.$refs.input.$refs.input.value = '';
|
||||
input.value.$refs.input.$refs.input.focus();
|
||||
}
|
||||
function handleClose() {
|
||||
amount.value = '';
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
emit('submit', input.value.$refs.input.$refs.input.value);
|
||||
abortButton.value.click();
|
||||
if(amount.value <= 0) {
|
||||
return;
|
||||
}
|
||||
emit('submit', amount.value);
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
86
webapp/src/components/molecules/MoleculeLoginModal.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>{{ description }}</p>
|
||||
<AtomInput
|
||||
v-model="modelValue"
|
||||
placeholder="Passwort"
|
||||
type="password"
|
||||
@keydown.prevent.enter="$emit('login')"
|
||||
/>
|
||||
<p
|
||||
v-if="error"
|
||||
class="alert alert-error"
|
||||
>
|
||||
{{ error }}
|
||||
</p>
|
||||
</div>
|
||||
<template #action>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Schließen
|
||||
</button>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click.prevent="$emit('login')"
|
||||
>
|
||||
<ArrowRightOnRectangleIcon class="h-6 w-6" />
|
||||
Login
|
||||
</button>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ArrowRightOnRectangleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomModal from '../atoms/AtomLegacyModal.vue';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
|
||||
/* global defineModel */
|
||||
const modelValue = defineModel();
|
||||
|
||||
defineEmits<{
|
||||
login: [],
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
157
webapp/src/components/molecules/MoleculeNavigationDrawer.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div
|
||||
class="drawer"
|
||||
:class="{
|
||||
'lg:drawer-open': !hideNavigation,
|
||||
}"
|
||||
>
|
||||
<input
|
||||
:id="drawerId"
|
||||
ref="drawerCheckbox"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
/>
|
||||
<div
|
||||
class="drawer-content flex flex-col bg-base-300"
|
||||
:class="{'lg:pl-64': !hideNavigation}"
|
||||
>
|
||||
<div
|
||||
class="navbar top-0 place-self-center shadow-lg"
|
||||
:class="{
|
||||
'lg:hidden': !hideNavigation,
|
||||
}"
|
||||
>
|
||||
<div class="flex-none">
|
||||
<label
|
||||
:for="drawerId"
|
||||
class="btn-ghost rounded-btn btn"
|
||||
:class="{
|
||||
'lg:hidden': !hideNavigation,
|
||||
}"
|
||||
>
|
||||
<Bars3Icon class="h-7 w-7" />
|
||||
</label>
|
||||
<AtomLogo class="h-10 px-4" />
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="drawer-side !fixed z-50">
|
||||
<label
|
||||
:for="drawerId"
|
||||
class="drawer-overlay"
|
||||
/>
|
||||
<ul class="menu h-full w-64 overflow-y-auto bg-base-100 p-4">
|
||||
<li class="pointer-events-none">
|
||||
<AtomLogo class="w-full" />
|
||||
</li>
|
||||
<template v-if="isAdmin">
|
||||
<li class="pointer-events-none rounded-md bg-warning uppercase text-warning-content">
|
||||
<span>
|
||||
<ExclamationTriangleIcon class="h-6 w-6" />
|
||||
Admin-Modus
|
||||
<ExclamationTriangleIcon class="h-6 w-6" />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span @click="AuthService.logout()">
|
||||
<ArrowRightOnRectangleIcon class="h-6 w-6" />
|
||||
Abmelden
|
||||
</span>
|
||||
</li>
|
||||
<li class="menu-title !p-0" />
|
||||
</template>
|
||||
<template v-if="isBanker">
|
||||
<li class="pointer-events-none rounded-md bg-info text-warning-content">
|
||||
<span>
|
||||
Angemeldet als Banker
|
||||
<BuildingLibraryIcon class="h-6 w-6" />
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span @click="AuthService.logout()">
|
||||
<ArrowRightOnRectangleIcon class="h-6 w-6" />
|
||||
Abmelden
|
||||
</span>
|
||||
</li>
|
||||
<li class="menu-title !p-0" />
|
||||
</template>
|
||||
<li
|
||||
v-for="navigationEntry of navigationEntries"
|
||||
:class="getNavigationEntryClass(navigationEntry)"
|
||||
>
|
||||
<component
|
||||
:is="!navigationEntry.to || navigationEntry.disabled ? 'span' : 'router-link'"
|
||||
v-if="navigationEntry.name !== 'divider'"
|
||||
:to="navigationEntry.to"
|
||||
active-class="active"
|
||||
>
|
||||
<component
|
||||
:is="navigationEntry.icon"
|
||||
class="h-6 w-6"
|
||||
/>{{ navigationEntry.name }}
|
||||
</component>
|
||||
</li>
|
||||
<AtomSwap
|
||||
class="mt-auto place-self-end"
|
||||
:on-icon="SunIcon"
|
||||
:off-icon="MoonIcon"
|
||||
:checked="isDark"
|
||||
@change="toggleDark()"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType, onMounted, ref } from 'vue';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { isDark, toggleDark } from '../../utils/darkMode';
|
||||
import { ArrowRightOnRectangleIcon, Bars3Icon, BuildingLibraryIcon, ExclamationTriangleIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomLogo from '../atoms/AtomLogo.vue';
|
||||
import AtomSwap from '../atoms/AtomSwap.vue';
|
||||
|
||||
const isAdmin = ref(false);
|
||||
const isBanker = ref(false);
|
||||
const hideNavigation = ref(false);
|
||||
const router = useRouter();
|
||||
const drawerCheckbox = ref();
|
||||
|
||||
defineProps({
|
||||
drawerId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
navigationEntries: {
|
||||
type: Array as PropType<INavigationEntry[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
function getNavigationEntryClass(navigationEntry: INavigationEntry) {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
'menu-title !p-0': navigationEntry.name === 'divider',
|
||||
disabled: navigationEntry.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
useEventBus<boolean>('isAdmin').on(state => (isAdmin.value = state));
|
||||
useEventBus<boolean>('isBanker').on(state => (isBanker.value = state));
|
||||
useEventBus<boolean>('hideNavigation').on(state => (hideNavigation.value = state));
|
||||
|
||||
onMounted(() => {
|
||||
isAdmin.value = AuthService.isAdmin();
|
||||
isBanker.value = AuthService.isBanker();
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
drawerCheckbox.value.checked = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<table class="table-zebra table overflow-hidden rounded-lg">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th />
|
||||
<th class="normal-case text-lg text-right">{{ CurrencyService.toString(balance) }}</th>
|
||||
<th class="text-right text-lg normal-case text-base-content">{{ CurrencyService.toString(balance) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody class="bg-base-100">
|
||||
<tr v-for="transaction in transactions">
|
||||
<td>
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="font-bold">{{ transaction.type }}</div>
|
||||
<div>{{ DateService.toString(transaction.date) }}</div>
|
||||
<div class="font-bold">{{ transaction.label }}</div>
|
||||
<div>{{ DateService.toString(parseISO(transaction.created)) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -35,13 +35,14 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
||||
import { CurrencyService } from '../services/currency.service';
|
||||
import { DateService } from '../services/date.service';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { TransactionsResponse } from '../../types/pocketbase.types';
|
||||
|
||||
defineProps({
|
||||
transactions: {
|
||||
type: Array as PropType<ITransaction[]>,
|
||||
type: Array as PropType<TransactionsResponse[]>,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
61
webapp/src/components/molecules/MoleculeVerifyModal.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
ref="modal"
|
||||
:title="title"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<slot />
|
||||
</div>
|
||||
<template #action>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
:for="id"
|
||||
@click="modal?.close()"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click.prevent="$emit('confirm')"
|
||||
>
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
Bestätigen
|
||||
</button>
|
||||
</template>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomModal from '../atoms/AtomLegacyModal.vue';
|
||||
|
||||
const modal = ref<InstanceType<typeof AtomModal>>();
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: () => modal.value?.show(),
|
||||
close: () => modal.value?.close(),
|
||||
isOpen: () => modal.value?.isOpen(),
|
||||
});
|
||||
|
||||
defineEmits<{
|
||||
confirm: [],
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
37
webapp/src/components/organisms/OrganismAuthWrapper.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<MoleculeAuthDialog v-if="!isAdmin" />
|
||||
<div v-else>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import MoleculeAuthDialog from '../molecules/MoleculeAuthDialog.vue';
|
||||
|
||||
const isAdmin = ref(false);
|
||||
|
||||
useEventBus<boolean>('isAdmin').on((state: boolean) => {
|
||||
isAdmin.value = state;
|
||||
if(isAdmin.value) {
|
||||
emit('authChange', true);
|
||||
} else {
|
||||
emit('authChange', false);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
isAdmin.value = AuthService.isAdmin();
|
||||
if(isAdmin.value) {
|
||||
emit('authChange', true);
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['authChange']);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
#Stocks
|
||||
<iframe
|
||||
src="/_"
|
||||
frameborder="0"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
291
webapp/src/components/views/ViewBank.vue
Normal file
|
@ -0,0 +1,291 @@
|
|||
<template>
|
||||
<MoleculeBankerAuthDialog
|
||||
v-if="!isBanker"
|
||||
/>
|
||||
<div v-else>
|
||||
<div
|
||||
v-if="account || company"
|
||||
class="hero-content flex-col pt-0 text-center lg:p-6"
|
||||
>
|
||||
<h1 class="mt-5 text-8xl">{{ CurrencyService.toString(getBalance()) }}</h1>
|
||||
<h3
|
||||
v-if="account"
|
||||
class="mt-10 text-5xl font-bold"
|
||||
>
|
||||
Kontoinhaber: {{ `${account.firstName} ${account.lastName}` }}
|
||||
</h3>
|
||||
<h3
|
||||
v-if="company && company.id == settings?.incomeTaxRecipient"
|
||||
class="mt-10 text-5xl font-bold"
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<AtomLogo class="h-14 w-14" />
|
||||
{{ company.name }}
|
||||
</div>
|
||||
</h3>
|
||||
<h3
|
||||
v-else-if="company"
|
||||
class="mt-10 text-5xl font-bold"
|
||||
>
|
||||
Firma: {{ company.name }}
|
||||
</h3>
|
||||
<h4 class="mb-10 text-4xl">Kontonummer: {{ (account ? account : company ? company : null)?.accountNumber }}</h4>
|
||||
<MoleculeTransactionTable
|
||||
v-if="transactions"
|
||||
class="mb-24 max-w-[42rem]"
|
||||
:balance="getBalance()"
|
||||
:transactions="transactions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="account || company"
|
||||
class="fixed bottom-0 flex w-full place-content-center gap-16 bg-base-100 p-5 lg:w-[calc(100%-16rem)] lg:gap-32"
|
||||
>
|
||||
<MoleculeInputModal
|
||||
id="deposit-modal"
|
||||
ref="depositModal"
|
||||
title="Einzahlung"
|
||||
action-label="Einzahlen"
|
||||
input-label="Einzuzahlender Betrag"
|
||||
@submit="handleDeposit"
|
||||
/>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="() => depositModal?.show()"
|
||||
>
|
||||
<ChevronUpIcon class="h-6 w-6 text-success" />
|
||||
Einzahlen
|
||||
</button>
|
||||
<MoleculeInputModal
|
||||
id="withdraw-modal"
|
||||
ref="withdrawModal"
|
||||
title="Auszahlung"
|
||||
action-label="Auszahlen"
|
||||
input-label="Auszuzahlender Betrag"
|
||||
@submit="handleWithdraw"
|
||||
/>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="() => withdrawModal?.show()"
|
||||
>
|
||||
<ChevronDownIcon class="h-6 w-6 text-error" />
|
||||
Auszahlen
|
||||
</button>
|
||||
</div>
|
||||
<AtomHeroText
|
||||
v-else
|
||||
class="h-full"
|
||||
>
|
||||
Bitte Karte scannen...
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert alert-error mt-6 shadow-lg"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
<span class="text-base font-normal"><b>Fehler: </b>{{ error }}</span>
|
||||
</div>
|
||||
</AtomHeroText>
|
||||
<div
|
||||
v-if="depositError"
|
||||
class="toast z-10"
|
||||
>
|
||||
<div class="alert alert-error">
|
||||
<span>Transaktion nicht möglich. Konto ist nicht ausreichend gedeckt.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { onKeyStroke, promiseTimeout, useEventBus } from '@vueuse/core';
|
||||
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
|
||||
import { AccountsResponse, CompaniesResponse, SettingsResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { AccountType, BankService, TransactionType } from '../../services/bank.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import { CompanyService } from '../../services/company.service';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { ChevronDownIcon, ChevronUpIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomHeroText from '../atoms/AtomHeroText.vue';
|
||||
import MoleculeInputModal from '../molecules/MoleculeInputModal.vue';
|
||||
import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue';
|
||||
import AtomLogo from '../atoms/AtomLogo.vue';
|
||||
import MoleculeBankerAuthDialog from '../molecules/MoleculeBankerAuthDialog.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const subscription = ref<UnsubscribeFunc>();
|
||||
const companySubscription = ref<UnsubscribeFunc>();
|
||||
|
||||
const isBanker = ref(false);
|
||||
const account = ref<AccountsResponse|null>();
|
||||
const company = ref<CompaniesResponse|null>();
|
||||
const transactions = ref<TransactionsResponse[]>();
|
||||
const depositModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
||||
const withdrawModal = ref<InstanceType<typeof MoleculeInputModal>>();
|
||||
const accountId = ref<string>('');
|
||||
const inputBuffer = ref<string>('');
|
||||
const error = ref('');
|
||||
const depositError = ref(false);
|
||||
const settings = ref<SettingsResponse>();
|
||||
|
||||
if(router.currentRoute.value.query.accountNumber) {
|
||||
accountId.value = router.currentRoute.value.query.accountNumber as string;
|
||||
getAccount();
|
||||
router.replace({ path: '/bank' });
|
||||
}
|
||||
|
||||
onKeyStroke(['e', 'a'], (e) => {
|
||||
if(!isBanker.value) { return; }
|
||||
e.preventDefault();
|
||||
if(depositModal.value?.isOpen() || withdrawModal.value?.isOpen()) {
|
||||
return;
|
||||
}
|
||||
switch (e.key) {
|
||||
case 'e':
|
||||
depositModal.value?.show();
|
||||
break;
|
||||
case 'a':
|
||||
withdrawModal.value?.show();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], (e) => {
|
||||
if(!isBanker.value) { return; }
|
||||
if(depositModal.value?.isOpen() || withdrawModal.value?.isOpen()) {
|
||||
return;
|
||||
}
|
||||
error.value = '';
|
||||
e.preventDefault();
|
||||
if(e.key === 'Enter') {
|
||||
accountId.value = inputBuffer.value;
|
||||
inputBuffer.value = '';
|
||||
getAccount();
|
||||
} else {
|
||||
inputBuffer.value += e.key;
|
||||
}
|
||||
});
|
||||
|
||||
async function getAccount() {
|
||||
account.value = null;
|
||||
transactions.value = [];
|
||||
try {
|
||||
account.value = await AccountService.getAccount(accountId.value);
|
||||
transactions.value = await BankService.getTransactions(account.value.id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
try {
|
||||
company.value = await CompanyService.getCompanyByAccountNumber(accountId.value);
|
||||
transactions.value = await BankService.getCompanyTransactions(company.value.id);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if(e.status == 404) {
|
||||
error.value = 'Der Account wurde nicht gefunden.';
|
||||
await promiseTimeout(4000);
|
||||
error.value = '';
|
||||
} else {
|
||||
error.value = 'Ein unerwarteter Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.';
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getBalance(): number {
|
||||
return transactions.value?.reduce((acc, transaction) => acc + transaction.amount, 0) ?? 0;
|
||||
}
|
||||
|
||||
async function handleDeposit(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(amountNumber > 1000) { return; }
|
||||
if(!isNaN(amountNumber) && account.value) {
|
||||
await BankService.addTransaction(account.value.id, amountNumber, TransactionType.DEPOSIT);
|
||||
} else if(!isNaN(amountNumber) && company.value) {
|
||||
await BankService.addTransaction(company.value.id, amountNumber, TransactionType.DEPOSIT, AccountType.COMPANY);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithdraw(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(amountNumber > 1000) { return; }
|
||||
if(amountNumber * 100 <= getBalance()) {
|
||||
if(!isNaN(amountNumber) && account.value) {
|
||||
// TODO: add max amount to input field and display custom error message
|
||||
await BankService.addTransaction(account.value.id, -amountNumber, TransactionType.WITHDRAW);
|
||||
} else if(!isNaN(amountNumber) && company.value) {
|
||||
await BankService.addTransaction(company.value.id, -amountNumber, TransactionType.WITHDRAW, AccountType.COMPANY);
|
||||
}
|
||||
} else {
|
||||
depositError.value = true;
|
||||
const timeout = setTimeout(() => {
|
||||
depositError.value = false;
|
||||
clearTimeout(timeout);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRealtimeUpdates(transactionSubscription: RecordSubscription<TransactionsResponse>) {
|
||||
if(
|
||||
transactions.value &&
|
||||
(transactionSubscription.record.account === account.value?.id || transactionSubscription.record.account === company.value?.id)
|
||||
) {
|
||||
switch (transactionSubscription.action) {
|
||||
case 'create':
|
||||
transactions.value.unshift(transactionSubscription.record);
|
||||
break;
|
||||
case 'update':
|
||||
transactions.value = transactions.value.map((transaction) => {
|
||||
if(transaction.id == transactionSubscription.record.id) {
|
||||
transaction = transactionSubscription.record;
|
||||
}
|
||||
return transaction;
|
||||
});
|
||||
break;
|
||||
case 'delete':
|
||||
transactions.value = transactions.value.filter((transaction) => transaction.id !== transactionSubscription.record.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isBanker.value = AuthService.isBanker();
|
||||
isBanker.value = AuthService.isAdmin();
|
||||
});
|
||||
|
||||
useEventBus<boolean>('isBanker').on(state => (isBanker.value = state));
|
||||
useEventBus<boolean>('isAdmin').on(state => (isBanker.value = state));
|
||||
|
||||
onUnmounted(destroyRealtime);
|
||||
|
||||
async function init() {
|
||||
if(!subscription.value) {
|
||||
subscription.value = await BankService.subscribeToTransactionChanges(handleRealtimeUpdates);
|
||||
}
|
||||
if(!companySubscription.value) {
|
||||
companySubscription.value = await BankService.subscribeToCompanyTransactionChanges(handleRealtimeUpdates);
|
||||
}
|
||||
settings.value = await SettingsService.getSettings();
|
||||
}
|
||||
|
||||
function destroyRealtime() {
|
||||
subscription.value?.();
|
||||
companySubscription.value?.();
|
||||
}
|
||||
|
||||
watch(isBanker, async (newValue) => {
|
||||
if(newValue) {
|
||||
await init();
|
||||
} else {
|
||||
destroyRealtime();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
59
webapp/src/components/views/ViewCheckIn.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<AtomHeroText>
|
||||
{{ message }}
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert alert-error mt-6 shadow-lg"
|
||||
>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
<span class="text-base font-normal"><b>Fehler:</b> {{ error }}</span>
|
||||
</div>
|
||||
</AtomHeroText>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomHeroText from '../atoms/AtomHeroText.vue';
|
||||
|
||||
const DEFAULT_MESSAGE = 'Bitte Karte scannen...';
|
||||
const accountId = ref<string>('');
|
||||
const message = ref<string>(DEFAULT_MESSAGE);
|
||||
const error = ref('');
|
||||
|
||||
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], async (e) => {
|
||||
e.preventDefault();
|
||||
error.value = '';
|
||||
if(e.key === 'Enter') {
|
||||
try {
|
||||
const account = await AccountService.checkIn(accountId.value);
|
||||
accountId.value = '';
|
||||
message.value = `${account.firstName} ${account.lastName} um ${DateService.toTime(parseISO(account.lastCheckIn))}`;
|
||||
await promiseTimeout(5000);
|
||||
message.value = DEFAULT_MESSAGE;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if(e.status == 404) {
|
||||
accountId.value = '';
|
||||
error.value = 'Der Account wurde nicht gefunden.';
|
||||
await promiseTimeout(3000);
|
||||
error.value = '';
|
||||
} else {
|
||||
error.value = 'Ein unerwarteter Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.';
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
accountId.value += e.key;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
117
webapp/src/components/views/ViewCompaniesData.vue
Normal file
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<MoleculeAuthDialog v-if="!isAdmin" />
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full flex-col p-6"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Nach Firmenname suchen"
|
||||
class="mb-4"
|
||||
/>
|
||||
<MoleculeDataTable
|
||||
v-if="companies && changeAccountNumberModal"
|
||||
:table-headers="tableHeaders"
|
||||
:data="companies"
|
||||
@change-account-number="changeAccountNumberModal.show($event);"
|
||||
/>
|
||||
<MoleculeChanceAccountNumberModal
|
||||
id="changeAccountNumber"
|
||||
ref="changeAccountNumberModal"
|
||||
title="Kontonummer ändern"
|
||||
@submit="changeAccountNumber"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
import { UnsubscribeFunc } from 'pocketbase';
|
||||
import { CompaniesResponse } from '../../types/pocketbase.types';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
import MoleculeAuthDialog from '../molecules/MoleculeAuthDialog.vue';
|
||||
import MoleculeChanceAccountNumberModal from '../molecules/MoleculeChanceAccountNumberModal.vue';
|
||||
import { CompanyService } from '../../services/company.service';
|
||||
|
||||
const isAdmin = ref(false);
|
||||
const companiesSubscription = ref<UnsubscribeFunc>();
|
||||
const changeAccountNumberModal = ref<InstanceType<typeof MoleculeChanceAccountNumberModal>>();
|
||||
|
||||
const companies = ref<CompaniesResponse[]>([]);
|
||||
const initCompanies = ref<CompaniesResponse[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: 'Kontonummer',
|
||||
key: 'accountNumber',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Trans.',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_ACCOUNT,
|
||||
},
|
||||
{
|
||||
title: 'Knr. änd.',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER,
|
||||
},
|
||||
];
|
||||
|
||||
useEventBus<boolean>('isAdmin').on(state => {
|
||||
isAdmin.value = state;
|
||||
if(isAdmin.value) {
|
||||
getData();
|
||||
}
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
initCompanies.value = await CompanyService.getCompanies();
|
||||
companies.value = initCompanies.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
searchQuery,
|
||||
(value) => {
|
||||
search(value);
|
||||
},
|
||||
);
|
||||
|
||||
function search(value: string) {
|
||||
if(initCompanies.value) {
|
||||
companies.value = initCompanies.value.filter((company) =>
|
||||
company.name?.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function changeAccountNumber(account: {id: string, accountNumber: string}) {
|
||||
CompanyService.updateAccountNumber(account.id, account.accountNumber);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isAdmin.value = AuthService.isAdmin();
|
||||
if(isAdmin.value) {
|
||||
getData();
|
||||
}
|
||||
companiesSubscription.value = await CompanyService.subscribeToCompanyChanges(async () => {
|
||||
await getData();
|
||||
search(searchQuery.value);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
companiesSubscription.value?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
140
webapp/src/components/views/ViewData.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<MoleculeAuthDialog v-if="!isAdmin" />
|
||||
<div
|
||||
v-else
|
||||
class="flex h-full flex-col p-6"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Nach Vor-/Nachname oder Klasse suchen"
|
||||
class="mb-4"
|
||||
/>
|
||||
<MoleculeDataTable
|
||||
v-if="accounts && changeAccountNumberModal"
|
||||
:table-headers="tableHeaders"
|
||||
:data="accounts"
|
||||
@change-account-number="changeAccountNumberModal.show($event);"
|
||||
/>
|
||||
<MoleculeChanceAccountNumberModal
|
||||
id="changeAccountNumber"
|
||||
ref="changeAccountNumberModal"
|
||||
title="Kontonummer ändern"
|
||||
@submit="changeAccountNumber"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
import { UnsubscribeFunc } from 'pocketbase';
|
||||
import { BankService } from '../../services/bank.service';
|
||||
import { AccountsListResponse } from '../../types/pocketbase.types';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
import MoleculeAuthDialog from '../molecules/MoleculeAuthDialog.vue';
|
||||
import MoleculeChanceAccountNumberModal from '../molecules/MoleculeChanceAccountNumberModal.vue';
|
||||
|
||||
const isAdmin = ref(false);
|
||||
const accountsSubscription = ref<UnsubscribeFunc>();
|
||||
const transactionsSubscription = ref<UnsubscribeFunc>();
|
||||
const changeAccountNumberModal = ref<InstanceType<typeof MoleculeChanceAccountNumberModal>>();
|
||||
|
||||
const accounts = ref<AccountsListResponse<number, string>[]>([]);
|
||||
const initAccounts = ref<AccountsListResponse<number, string>[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: 'Kontonummer',
|
||||
key: 'accountNumber',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Klasse',
|
||||
key: 'grade',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Letzter Check-In',
|
||||
key: 'lastCheckIn',
|
||||
type: TableHeaderType.DATETIME,
|
||||
},
|
||||
{
|
||||
title: 'Kontostand',
|
||||
key: 'balance',
|
||||
type: TableHeaderType.CURRENCY,
|
||||
},
|
||||
{
|
||||
title: 'Trans.',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_ACCOUNT,
|
||||
},
|
||||
{
|
||||
title: 'Knr. änd.',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER,
|
||||
},
|
||||
];
|
||||
|
||||
useEventBus<boolean>('isAdmin').on(state => {
|
||||
isAdmin.value = state;
|
||||
if(isAdmin.value) {
|
||||
getData();
|
||||
}
|
||||
});
|
||||
|
||||
async function getData() {
|
||||
initAccounts.value = await AccountService.getAccounts();
|
||||
accounts.value = initAccounts.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
searchQuery,
|
||||
(value) => {
|
||||
search(value);
|
||||
},
|
||||
);
|
||||
|
||||
function search(value: string) {
|
||||
if(initAccounts.value) {
|
||||
accounts.value = initAccounts.value.filter((account) =>
|
||||
account.name?.toLocaleLowerCase().includes(value.toLocaleLowerCase())
|
||||
|| account.grade?.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function changeAccountNumber(account: {id: string, accountNumber: string}) {
|
||||
AccountService.updateAccountNumber(account.id, account.accountNumber);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isAdmin.value = AuthService.isAdmin();
|
||||
if(isAdmin.value) {
|
||||
getData();
|
||||
}
|
||||
accountsSubscription.value = await AccountService.subscribeToAccountChanges(async () => {
|
||||
await getData();
|
||||
search(searchQuery.value);
|
||||
});
|
||||
transactionsSubscription.value = await BankService.subscribeToTransactionChanges(async () => {
|
||||
await getData();
|
||||
search(searchQuery.value);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
accountsSubscription.value?.();
|
||||
transactionsSubscription.value?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
364
webapp/src/components/views/ViewEmployer.vue
Normal file
|
@ -0,0 +1,364 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="!isAuthenticated"
|
||||
class="flex min-h-full flex-col p-6"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Firmenname eingeben"
|
||||
class="mb-4"
|
||||
/>
|
||||
<MoleculeDataTable
|
||||
v-if="companies"
|
||||
class="cursor-pointer"
|
||||
:table-headers="companyTableHeaders"
|
||||
:data="companies"
|
||||
hide-header
|
||||
@click="showLoginDialog"
|
||||
/>
|
||||
<MoleculeLoginModal
|
||||
id="login-modal"
|
||||
ref="loginModal"
|
||||
v-model="password"
|
||||
:title="selectedCompany?.name ?? ''"
|
||||
description="Bitte gib dein Passwort ein, um das Firmenportal anzuzeigen."
|
||||
:error="error"
|
||||
@login="login"
|
||||
@close="password = ''; error = ''"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="company"
|
||||
class="flex min-h-full flex-col p-6"
|
||||
>
|
||||
<div class="mb-4 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-4xl">{{ company.name }}</h1>
|
||||
<button
|
||||
class="btn-primary btn w-48"
|
||||
@click="logout()"
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon class="h-6 w-6" />
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
<!-- <div class="flex items-center justify-between">
|
||||
<h2 class="text-2xl">Kontostand: {{ CurrencyService.toString(getBalance()) }}</h2>
|
||||
<button class="btn-primary btn-disabled btn w-48">
|
||||
<BanknotesIcon class="h-6 w-6" />
|
||||
Transaktionen
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="stats shadow">
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Kontostand</div>
|
||||
<div class="stat-value">{{ CurrencyService.toString(getBalance(), false) }}</div>
|
||||
<div class="stat-desc">Batzen</div>
|
||||
</div>
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Arbeitnehmer</div>
|
||||
<div class="stat-value">{{ accounts.filter(account => (account.shift != Shifts.EARLY && account.shift != Shifts.LATE)).length }}</div>
|
||||
<div class="stat-desc">ohne Schicht</div>
|
||||
</div>
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Arbeitnehmer</div>
|
||||
<div class="stat-value">{{ accounts.filter(account => account.shift == Shifts.EARLY).length }}</div>
|
||||
<div class="stat-desc">in Frühschicht</div>
|
||||
</div>
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Arbeitnehmer</div>
|
||||
<div class="stat-value">{{ accounts.filter(account => account.shift == Shifts.LATE).length }}</div>
|
||||
<div class="stat-desc">in Spätschicht</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="settings"
|
||||
class="stats shadow"
|
||||
>
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Aktueller Mindestlohn</div>
|
||||
<div class="stat-value">{{ CurrencyService.toString(settings.minWage, false) }} / Tag</div>
|
||||
<div class="stat-desc">Batzen</div>
|
||||
</div>
|
||||
<div class="stat place-items-center">
|
||||
<div class="stat-title">Aktueller Maximallohn</div>
|
||||
<div class="stat-value">{{ CurrencyService.toString(settings.maxWage, false) }} / Tag</div>
|
||||
<div class="stat-desc">Batzen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="company"
|
||||
class="flex items-center justify-center gap-4"
|
||||
>
|
||||
<button
|
||||
class="btn-primary btn"
|
||||
:class="{'btn-disabled': (getBalance() < getTotalCostsByShifts(Shifts.EARLY) || DateService.isToday(parseISO(company.earlyShiftPayed)))}"
|
||||
@click="wageType = Shifts.EARLY; verifyModal?.show()"
|
||||
>
|
||||
<SunIcon class="h-6 w-6" />
|
||||
Frühschicht bezahlen ({{ CurrencyService.toString(getTotalCostsByShifts(Shifts.EARLY)) }})
|
||||
</button>
|
||||
<button
|
||||
class="btn-primary btn"
|
||||
:class="{'btn-disabled': (getBalance() < getTotalCostsByShifts(Shifts.LATE) || DateService.isToday(parseISO(company.lateShiftPayed)))}"
|
||||
@click="wageType = Shifts.LATE; verifyModal?.show()"
|
||||
>
|
||||
<MoonIcon class="h-6 w-6" />
|
||||
Spätschicht bezahlen ({{ CurrencyService.toString(getTotalCostsByShifts(Shifts.LATE)) }})
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
v-if="DateService.isToday(parseISO(company.earlyShiftPayed))"
|
||||
class="alert alert-info"
|
||||
>
|
||||
<InformationCircleIcon class="h-6 w-6" />
|
||||
Die Frühschicht wurde heute bereits bezahlt.
|
||||
</div>
|
||||
<div
|
||||
v-if="DateService.isToday(parseISO(company.lateShiftPayed))"
|
||||
class="alert alert-info"
|
||||
>
|
||||
<InformationCircleIcon class="h-6 w-6" />
|
||||
Die Spätschicht wurde heute bereits bezahlt.
|
||||
</div>
|
||||
<div
|
||||
v-if="getBalance() < getTotalCostsByShifts(Shifts.EARLY)"
|
||||
class="alert alert-warning"
|
||||
>
|
||||
<ExclamationTriangleIcon class="h-6 w-6" />
|
||||
Konto nicht ausreichend gedeckt um die Frühschicht zu bezahlen.
|
||||
</div>
|
||||
<div
|
||||
v-if="getBalance() < getTotalCostsByShifts(Shifts.LATE)"
|
||||
class="alert alert-warning"
|
||||
>
|
||||
<ExclamationTriangleIcon class="h-6 w-6" />
|
||||
Konto nicht ausreichend gedeckt um die Spätschicht zu bezahlen.
|
||||
</div>
|
||||
</div>
|
||||
<MoleculeDataTable
|
||||
v-if="accounts"
|
||||
:table-headers="accountTableHeaders"
|
||||
:data="accounts"
|
||||
@select-shift="selectShift"
|
||||
@select-wage="updateWage"
|
||||
/>
|
||||
</div>
|
||||
<MoleculeVerifyModal
|
||||
id="verify-modal"
|
||||
ref="verifyModal"
|
||||
title="Lohnauszahlung bestätigen"
|
||||
@confirm="confirmWage"
|
||||
>
|
||||
<div class="flex flex-col gap-1">
|
||||
<p>Lohn wirklich für die {{ wageType == Shifts.EARLY ? 'Frühschicht' : 'Spätschicht' }} auszahlen?</p>
|
||||
<p>
|
||||
Das Konto wird mit <b>{{
|
||||
CurrencyService.toString(
|
||||
wageType == Shifts.EARLY ?
|
||||
getTotalCostsByShifts(Shifts.EARLY) : getTotalCostsByShifts(Shifts.LATE)
|
||||
)
|
||||
}}</b> belastet.<br />
|
||||
Dies beinhaltet die Lohnsteuer von {{ settings?.incomeTax }}% ({{
|
||||
CurrencyService.toString(
|
||||
wageType == Shifts.EARLY ?
|
||||
getTotalCostsByShifts(Shifts.EARLY) * (settings?.incomeTax ?? 0) / 100 :
|
||||
getTotalCostsByShifts(Shifts.LATE) * (settings?.incomeTax ?? 0) / 100
|
||||
)
|
||||
}}).
|
||||
</p>
|
||||
<p><b>Hinweis:</b> Diese Aktion kann nur 1x am Tag ausgeführt werden.</p>
|
||||
</div>
|
||||
</MoleculeVerifyModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { AccountsResponse, CompaniesResponse, SettingsResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { BankService } from '../../services/bank.service';
|
||||
import { CompanyService } from '../../services/company.service';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
import { Shifts } from '../../enums/shift.enum';
|
||||
import { ArrowLeftOnRectangleIcon,
|
||||
ExclamationTriangleIcon,
|
||||
InformationCircleIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
import MoleculeLoginModal from '../molecules/MoleculeLoginModal.vue';
|
||||
import MoleculeVerifyModal from '../molecules/MoleculeVerifyModal.vue';
|
||||
|
||||
const loginModal = ref<InstanceType<typeof MoleculeLoginModal>>();
|
||||
const searchQuery = ref('');
|
||||
const password = ref('');
|
||||
const companies = ref<CompaniesResponse[]>([]);
|
||||
const initCompanies = ref<CompaniesResponse[]>([]);
|
||||
const selectedCompany = ref<CompaniesResponse>();
|
||||
const company = ref<CompaniesResponse>();
|
||||
const accounts = ref<AccountsResponse[]>([]);
|
||||
const transactions = ref<TransactionsResponse[]>([]);
|
||||
const isAuthenticated = ref(false);
|
||||
const accountsSubscription = ref<UnsubscribeFunc>();
|
||||
const settings = ref<SettingsResponse>();
|
||||
const error = ref('');
|
||||
const verifyModal = ref<InstanceType<typeof MoleculeVerifyModal>>();
|
||||
const wageType = ref<Shifts|undefined>();
|
||||
|
||||
const companyTableHeaders = [
|
||||
{
|
||||
title: '',
|
||||
key: 'name',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
];
|
||||
|
||||
const accountTableHeaders = [
|
||||
{
|
||||
title: 'Vorname',
|
||||
key: 'firstName',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Nachname',
|
||||
key: 'lastName',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Schicht',
|
||||
key: 'shift',
|
||||
type: TableHeaderType.SHIFT,
|
||||
},
|
||||
{
|
||||
title: 'Lohn',
|
||||
key: 'wage',
|
||||
type: TableHeaderType.WAGE,
|
||||
},
|
||||
];
|
||||
|
||||
async function getData() {
|
||||
initCompanies.value = await CompanyService.getCompanies();
|
||||
companies.value = initCompanies.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
searchQuery,
|
||||
(value) => {
|
||||
search(value);
|
||||
},
|
||||
);
|
||||
|
||||
function search(value: string): void {
|
||||
if(initCompanies.value) {
|
||||
companies.value = initCompanies.value.filter((company) =>
|
||||
company.name?.toLocaleLowerCase().includes(value.toLocaleLowerCase()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isAuthenticated.value = CompanyService.isAuthenticated();
|
||||
settings.value = await SettingsService.getSettings();
|
||||
if(isAuthenticated.value) {
|
||||
init();
|
||||
} else {
|
||||
getData();
|
||||
}
|
||||
});
|
||||
|
||||
async function init() {
|
||||
company.value = await CompanyService.getCompany();
|
||||
accounts.value = await AccountService.getAccountsByCompanyId(company.value.id);
|
||||
transactions.value = await BankService.getCompanyTransactions(company.value.id);
|
||||
accounts.value.forEach((account) => {
|
||||
account.wage = account.wage / 100;
|
||||
});
|
||||
accountsSubscription.value = await AccountService.subscribeToAccountChanges(async (data: RecordSubscription<AccountsResponse>) => {
|
||||
if(data.action === 'update') {
|
||||
accounts.value = accounts.value.map((account) => {
|
||||
if(account.id === data.record.id) {
|
||||
data.record.wage = data.record.wage / 100;
|
||||
return data.record;
|
||||
}
|
||||
return account;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getBalance(): number {
|
||||
return transactions.value?.reduce((acc, transaction) => acc + transaction.amount, 0) ?? 0;
|
||||
}
|
||||
|
||||
function getTotalCostsByShifts(shift: Shifts): number {
|
||||
return accounts.value?.filter(
|
||||
(account) => account.shift === shift).reduce((acc, account) => acc + account.wage * 100, 0,
|
||||
) ?? 0;
|
||||
}
|
||||
|
||||
function showLoginDialog(id: string): void {
|
||||
selectedCompany.value = companies.value.find((company) => company.id === id);
|
||||
loginModal.value?.show();
|
||||
}
|
||||
|
||||
async function login(): Promise<void> {
|
||||
if(selectedCompany.value) {
|
||||
try {
|
||||
await CompanyService.login(selectedCompany.value?.username, password.value);
|
||||
} catch {
|
||||
error.value = 'Fehler beim Login. Bitte Passwort überprüfen.';
|
||||
return;
|
||||
}
|
||||
isAuthenticated.value = true;
|
||||
await init();
|
||||
loginModal.value?.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function selectShift(shiftChange: {id: string, shift: Shifts}): Promise<void> {
|
||||
await AccountService.updateShift(shiftChange.id, shiftChange.shift);
|
||||
}
|
||||
|
||||
async function updateWage(wageChange: {id: string, wage: number}): Promise<void> {
|
||||
if(wageChange.wage * 100 < (settings.value?.minWage ?? 0)) {
|
||||
return;
|
||||
}
|
||||
if(wageChange.wage * 100 > (settings.value?.maxWage ?? 0)) {
|
||||
return;
|
||||
}
|
||||
await AccountService.updateWage(wageChange.id, wageChange.wage);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
CompanyService.logout();
|
||||
isAuthenticated.value = false;
|
||||
selectedCompany.value = undefined;
|
||||
company.value = undefined;
|
||||
accounts.value = [];
|
||||
getData();
|
||||
}
|
||||
|
||||
async function confirmWage() {
|
||||
verifyModal.value?.close();
|
||||
if(wageType.value) {
|
||||
await CompanyService.payWage(wageType.value);
|
||||
}
|
||||
wageType.value = undefined;
|
||||
init();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
accountsSubscription.value?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
26
webapp/src/components/views/ViewRadio.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<iframe
|
||||
v-if="settings?.radioUrl"
|
||||
:src="settings?.radioUrl"
|
||||
frameborder="0"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
<span v-else>Fehler: Keine Radio URL festgelegt.</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { SettingsResponse } from '../../types/pocketbase.types';
|
||||
import { SettingsService } from '../../services/settings.service';
|
||||
|
||||
const settings = ref<SettingsResponse>();
|
||||
|
||||
onMounted(async () => {
|
||||
settings.value = await SettingsService.getSettings();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|