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
|
name: default
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: install
|
- name: install-webapp
|
||||||
image: node:18-alpine
|
image: node:lts-alpine
|
||||||
commands:
|
commands:
|
||||||
- corepack pnpm@7.5.1 install
|
- cd webapp
|
||||||
|
- corepack pnpm@8.6.6 install
|
||||||
|
|
||||||
- name: build
|
- name: lint-webapp
|
||||||
image: node:18-alpine
|
image: node:lts-alpine
|
||||||
commands:
|
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
|
- name: deploy
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
settings:
|
settings:
|
||||||
registry: registry.cliffbreak.de
|
registry: registry.cliffbreak.de
|
||||||
repo: registry.cliffbreak.de/kispi-core
|
repo: registry.cliffbreak.de/hgoe-sas
|
||||||
username:
|
username:
|
||||||
from_secret: docker_username
|
from_secret: docker_username
|
||||||
password:
|
password:
|
||||||
|
|
2
.env.example
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Paths
|
||||||
|
WEBAPP="../webapp/dist"
|
9
.gitignore
vendored
|
@ -20,3 +20,12 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
# Pocketbase-Data
|
||||||
|
pb_data
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|
||||||
|
*.csv
|
||||||
|
*.xlsx
|
18
.vscode/settings.json
vendored
|
@ -1,18 +1,32 @@
|
||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"appwrite",
|
|
||||||
"camelcase",
|
"camelcase",
|
||||||
"checkin",
|
"checkin",
|
||||||
|
"classname",
|
||||||
"cliffbreak",
|
"cliffbreak",
|
||||||
"corepack",
|
"corepack",
|
||||||
"daisyui",
|
"daisyui",
|
||||||
|
"esnext",
|
||||||
"Gitea",
|
"Gitea",
|
||||||
|
"github",
|
||||||
|
"godotenv",
|
||||||
"heroicons",
|
"heroicons",
|
||||||
"kispi",
|
"hgoe",
|
||||||
|
"HGOE",
|
||||||
|
"middlewares",
|
||||||
"pnpm",
|
"pnpm",
|
||||||
|
"pocketbase",
|
||||||
"tailwindcss",
|
"tailwindcss",
|
||||||
|
"typegen",
|
||||||
"vite",
|
"vite",
|
||||||
"vitejs",
|
"vitejs",
|
||||||
"vueuse"
|
"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
|
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.
|
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
|
* **core** used for changes to the whole project
|
||||||
* **webapp** used for changes made in the webapp
|
* **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
|
* **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`)
|
* 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>
|
<h1 align="center">🔥 Schule als Staat - Hohenlohe Gymnasium Öhringen 🔥</h1>
|
||||||
<h3 align="center">Verwaltung + Bank + Radio = ❤️</h1>
|
<h3 align="center">User administration, user check and customs, banking system, employer portal and an admin portal</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img height="600px" src=".docs/screenshot.png" alt="screenshot"/>
|
<img height="600px" src=".docs/screenshot.png" alt="screenshot"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
This is our repository containing all required resources to run the "Core" WebApp from the "Kinderspielstadt Öhringen".
|
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 Appwrite cloud functions if any exist.
|
This repository also contains all the required Pocketbase files to run the backend server.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
@ -15,32 +15,70 @@ See deployment for notes on how to deploy the project on a live system.
|
||||||
### 🍽️ Prerequisites
|
### 🍽️ Prerequisites
|
||||||
|
|
||||||
`NodeJS (including PNPM)` is required to run this project.
|
`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
|
### 📦 Installing
|
||||||
|
|
||||||
At first clone this repository to your local machine by using
|
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
|
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
|
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
|
## 🧑💻 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
|
* [PNPM](https://pnpm.io/) - Faster alternative to npm for managing dependencies
|
||||||
* [Vue.js](https://vuejs.org/) - The Frontend Web Framework
|
* [Vue.js](https://vuejs.org/) - The Frontend Web Framework
|
||||||
* [Vite](https://vitejs.dev/) - Used Frontend Tooling
|
* [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
|
## 🤵 Authors
|
||||||
|
|
||||||
|
|
35
package.json
|
@ -1,35 +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",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
1981
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,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',
|
'eslint:recommended',
|
||||||
'plugin:vue/vue3-recommended',
|
'plugin:vue/vue3-recommended',
|
||||||
'plugin:vue-scoped-css/vue3-recommended',
|
'plugin:vue-scoped-css/vue3-recommended',
|
||||||
|
'plugin:tailwindcss/recommended',
|
||||||
],
|
],
|
||||||
parser: 'vue-eslint-parser',
|
parser: 'vue-eslint-parser',
|
||||||
env: {
|
env: {
|
||||||
|
@ -52,7 +53,7 @@ module.exports = {
|
||||||
'asyncArrow': 'always',
|
'asyncArrow': 'always',
|
||||||
}],
|
}],
|
||||||
'space-in-parens': ['error', 'never'],
|
'space-in-parens': ['error', 'never'],
|
||||||
'spaced-comment': ['error', 'always'],
|
'spaced-comment': ['error', 'always', { 'markers': ['/'] }],
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
|
@ -70,7 +71,8 @@ module.exports = {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
'project': 'tsconfig.json',
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
extraFileExtensions: ['.vue'],
|
extraFileExtensions: ['.vue'],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -138,6 +140,7 @@ module.exports = {
|
||||||
'svg': 'always',
|
'svg': 'always',
|
||||||
'math': '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="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest">
|
||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
|
<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="theme-color" content="#ffffff">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Kinderspielstadt Öhringen</title>
|
<title>Schule als Staat | Hohenlohe Gymnasium Öhringen</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
<msapplication>
|
||||||
<tile>
|
<tile>
|
||||||
<square150x150logo src="/mstile-150x150.png"/>
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
<TileColor>#b91d47</TileColor>
|
<TileColor>#da532c</TileColor>
|
||||||
</tile>
|
</tile>
|
||||||
</msapplication>
|
</msapplication>
|
||||||
</browserconfig>
|
</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",
|
"name": "",
|
||||||
"short_name": "KiSpi Öhringen",
|
"short_name": "",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-192x192.png",
|
"src": "/android-chrome-192x192.png",
|
||||||
|
@ -16,4 +16,4 @@
|
||||||
"theme_color": "#ffffff",
|
"theme_color": "#ffffff",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"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>
|
|
@ -1,5 +1,8 @@
|
||||||
<template>
|
<template>
|
||||||
#Stocks
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input-bordered file-input-primary file-input"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
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>
|
<template>
|
||||||
<div class="min-h-full lg:p-6">
|
<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">
|
<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>
|
</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>
|
<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
|
<option
|
||||||
disabled
|
disabled
|
||||||
selected
|
selected
|
||||||
|
@ -22,7 +25,13 @@ defineProps({
|
||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<string[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
defineEmits(['update:modelValue']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
|
@ -20,11 +20,11 @@ import { FunctionalComponent, PropType } from 'vue';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
onIcon: {
|
onIcon: {
|
||||||
type: Object as PropType<FunctionalComponent>,
|
type: Function as PropType<FunctionalComponent>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
offIcon: {
|
offIcon: {
|
||||||
type: Object as PropType<FunctionalComponent>,
|
type: Function as PropType<FunctionalComponent>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
checked: {
|
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>
|
<template>
|
||||||
<AtomModal
|
<AtomModal
|
||||||
:id="id"
|
:id="id"
|
||||||
|
ref="modal"
|
||||||
:title="title"
|
: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>
|
<span>{{ inputLabel }}</span>
|
||||||
<AtomCurrencyInput
|
<AtomCurrencyInput
|
||||||
ref="input"
|
v-model="amount"
|
||||||
class="w-4/12"
|
class="w-4/12"
|
||||||
@keyup.enter="handleSubmit()"
|
:max="1000"
|
||||||
@keyup.esc="abortButton.click()"
|
@keyup.esc="modal?.close()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template #action>
|
<template #action>
|
||||||
<label
|
<label
|
||||||
ref="abortButton"
|
|
||||||
class="btn gap-2"
|
class="btn gap-2"
|
||||||
:for="id"
|
:for="id"
|
||||||
|
@click="modal?.close()"
|
||||||
>
|
>
|
||||||
<XCircleIcon class="w-6 h-6" />
|
<XCircleIcon class="h-6 w-6" />
|
||||||
Abbrechen
|
Abbrechen
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn gap-2"
|
class="btn gap-2"
|
||||||
@click="handleSubmit()"
|
@click="handleSubmit()"
|
||||||
>
|
>
|
||||||
<CheckCircleIcon class="w-6 h-6" />
|
<CheckCircleIcon class="h-6 w-6" />
|
||||||
{{ actionLabel }}
|
{{ actionLabel }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
@ -34,13 +35,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { 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 modal = ref<InstanceType<typeof AtomModal>>();
|
||||||
const abortButton = ref();
|
const amount = ref();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
id: {
|
id: {
|
||||||
|
@ -63,18 +64,22 @@ defineProps({
|
||||||
|
|
||||||
const emit = defineEmits(['submit']);
|
const emit = defineEmits(['submit']);
|
||||||
|
|
||||||
function handleOpen(event: {id: string, open: boolean}) {
|
function handleClose() {
|
||||||
if(event.open) {
|
amount.value = '';
|
||||||
// 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 handleSubmit() {
|
function handleSubmit() {
|
||||||
emit('submit', input.value.$refs.input.$refs.input.value);
|
if(amount.value <= 0) {
|
||||||
abortButton.value.click();
|
return;
|
||||||
|
}
|
||||||
|
emit('submit', amount.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
show: () => modal.value?.show(),
|
||||||
|
close: () => modal.value?.close(),
|
||||||
|
isOpen: () => modal.value?.isOpen(),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<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>
|
<template>
|
||||||
<table class="table table-zebra">
|
<table class="table-zebra table overflow-hidden rounded-lg">
|
||||||
<thead>
|
<thead class="bg-base-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th />
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="bg-base-100">
|
||||||
<tr v-for="transaction in transactions">
|
<tr v-for="transaction in transactions">
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold">{{ transaction.type }}</div>
|
<div class="font-bold">{{ transaction.label }}</div>
|
||||||
<div>{{ DateService.toString(transaction.date) }}</div>
|
<div>{{ DateService.toString(parseISO(transaction.created)) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -35,13 +35,14 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { PropType } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
import { parseISO } from 'date-fns';
|
||||||
import { CurrencyService } from '../services/currency.service';
|
import { CurrencyService } from '../../services/currency.service';
|
||||||
import { DateService } from '../services/date.service';
|
import { DateService } from '../../services/date.service';
|
||||||
|
import { TransactionsResponse } from '../../types/pocketbase.types';
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
transactions: {
|
transactions: {
|
||||||
type: Array as PropType<ITransaction[]>,
|
type: Array as PropType<TransactionsResponse[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
balance: {
|
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>
|
<template>
|
||||||
#Radio
|
<iframe
|
||||||
|
src="/_"
|
||||||
|
frameborder="0"
|
||||||
|
class="h-full w-full"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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>
|
433
webapp/src/components/views/ViewStatePortal.vue
Normal file
|
@ -0,0 +1,433 @@
|
||||||
|
<template>
|
||||||
|
<div class="min-h-full w-full @container">
|
||||||
|
<OrganismAuthWrapper class="flex h-full flex-wrap justify-center gap-4 p-6 @[832px]:justify-normal">
|
||||||
|
<AtomCard class="w-96">
|
||||||
|
<template #title>
|
||||||
|
<WrenchScrewdriverIcon class="h-6 w-6" />Einstellungen
|
||||||
|
</template>
|
||||||
|
<div class="mt-4 flex h-full flex-col gap-4">
|
||||||
|
<div class="flex place-items-center justify-between">
|
||||||
|
<span>Mindestlohn</span>
|
||||||
|
<AtomCurrencyInput
|
||||||
|
v-model="minWage"
|
||||||
|
class="w-2/4"
|
||||||
|
per-day
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center justify-between">
|
||||||
|
<span>Lohnsteuer</span>
|
||||||
|
<AtomPercentInput
|
||||||
|
v-model="incomeTax"
|
||||||
|
class="w-2/4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center justify-between">
|
||||||
|
<span>Höchstlohn</span>
|
||||||
|
<AtomCurrencyInput
|
||||||
|
v-model="maxWage"
|
||||||
|
:min="minWage"
|
||||||
|
per-day
|
||||||
|
class="w-2/4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex place-items-center justify-between">
|
||||||
|
<span>Steuer-Empfänger-ID</span>
|
||||||
|
<AtomInput
|
||||||
|
v-model="incomeTaxRecipient"
|
||||||
|
class="!w-2/4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span>Radio URL</span>
|
||||||
|
<AtomInput v-model="radioUrl" />
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-primary btn mt-auto w-1/2 self-end"
|
||||||
|
@click="saveSettings()"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="settingsSaveSuccess"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span>Änderungen erfolgreich gespeichert.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="settingsSaveError"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-error">
|
||||||
|
<span>Ein Fehler ist aufgetreten. Bitte Eingabe oder Verbindung überprüfen.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AtomCard>
|
||||||
|
<AtomCard class="w-96">
|
||||||
|
<template #title>
|
||||||
|
<MagnifyingGlassIcon class="h-6 w-6" />Verwaltung
|
||||||
|
</template>
|
||||||
|
<div class="mt-4 flex h-full flex-col gap-4">
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="showMissingStudentsPrintWindow"
|
||||||
|
>
|
||||||
|
<QueueListIcon class="h-5 w-5" />
|
||||||
|
Fehlende Schüler
|
||||||
|
</button>
|
||||||
|
<RouterLink
|
||||||
|
class="btn"
|
||||||
|
:to="{ name: 'data' }"
|
||||||
|
>
|
||||||
|
<UsersIcon class="h-5 w-5" />
|
||||||
|
Stammdaten-Übersicht
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
class="btn"
|
||||||
|
:to="{ name: 'companies-data' }"
|
||||||
|
>
|
||||||
|
<BriefcaseIcon class="h-5 w-5" />
|
||||||
|
Firmen-Übersicht
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink
|
||||||
|
class="btn"
|
||||||
|
:to="{ name: 'admin' }"
|
||||||
|
>
|
||||||
|
<StarIcon class="h-5 w-5" />
|
||||||
|
Admin Oberfläche
|
||||||
|
</RouterLink>
|
||||||
|
<div
|
||||||
|
v-if="serverTime"
|
||||||
|
class="stats mt-auto bg-base-300"
|
||||||
|
>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-title">Server-Time</div>
|
||||||
|
<div class="stat-value">{{ DateService.toTime(serverTime) }}</div>
|
||||||
|
<div class="stat-desc">{{ DateService.toShortString(serverTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AtomCard>
|
||||||
|
<AtomCard class="w-96 border-2 border-error">
|
||||||
|
<template #title>
|
||||||
|
<div class="flex items-center gap-2 text-error"><ExclamationTriangleIcon class="h-6 w-6" />Danger Zone</div>
|
||||||
|
</template>
|
||||||
|
<div class="mt-4 flex h-full flex-col gap-4">
|
||||||
|
<MoleculeImportDataModal
|
||||||
|
id="import-companies-modal"
|
||||||
|
ref="importCompaniesModal"
|
||||||
|
title="Firmen importieren"
|
||||||
|
action-label="Importieren"
|
||||||
|
@submit="importCompanies"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="importCompaniesModal?.show()"
|
||||||
|
>
|
||||||
|
<BriefcaseIcon class="h-5 w-5" />
|
||||||
|
Jobs importieren
|
||||||
|
</button>
|
||||||
|
<MoleculeImportDataModal
|
||||||
|
id="import-companies-modal"
|
||||||
|
ref="importCompaniesModal"
|
||||||
|
title="Firmen importieren"
|
||||||
|
action-label="Importieren"
|
||||||
|
@submit="importCompanies"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="createdCompanies.length"
|
||||||
|
class="btn"
|
||||||
|
@click="showCreatedCompaniesPrintWindow"
|
||||||
|
>
|
||||||
|
<PrinterIcon class="h-5 w-5" />
|
||||||
|
Jobs & Passwörter drucken
|
||||||
|
</button>
|
||||||
|
<MoleculeImportDataModal
|
||||||
|
id="import-accounts-modal"
|
||||||
|
ref="importAccountsModal"
|
||||||
|
title="Stammdaten importieren"
|
||||||
|
action-label="Importieren"
|
||||||
|
@submit="importAccounts"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="importAccountsModal?.show()"
|
||||||
|
>
|
||||||
|
<TableCellsIcon class="h-5 w-5" />
|
||||||
|
Stammdaten importieren
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<span>Startkapital auszahlen:</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<AtomCurrencyInput
|
||||||
|
v-model="seedCapital"
|
||||||
|
class="w-1/2"
|
||||||
|
placeholder="Startkapital"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn grow"
|
||||||
|
@click="verifySeedCapitalModal?.show()"
|
||||||
|
>
|
||||||
|
Auszahlen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="importPending"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<span>Daten werden importiert. Bitte warten...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="importDone"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span>Daten wurden erfolgreich importiert.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="capitalPending"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<span>Startkapital wird überwiesen. Bitte warten...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="capitalDone"
|
||||||
|
class="toast z-10"
|
||||||
|
>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<span>Startkapital wurden erfolgreich überwiesen.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AtomCard>
|
||||||
|
<MoleculeErrorModal
|
||||||
|
id="error-modal"
|
||||||
|
ref="errorModal"
|
||||||
|
v-model="errorMsg"
|
||||||
|
title="Fehler beim Importieren bei folgenden Datensätzen"
|
||||||
|
/>
|
||||||
|
<MoleculeVerifyModal
|
||||||
|
id="verify-seed-capital-modal"
|
||||||
|
ref="verifySeedCapitalModal"
|
||||||
|
title="Startkapital auszahlen"
|
||||||
|
@confirm="paySeedCapital"
|
||||||
|
>
|
||||||
|
<p>Wirklich jedem Bürger ein Startkapital von {{ CurrencyService.toString((seedCapital ?? 0) * 100) }} auszahlen?</p>
|
||||||
|
</MoleculeVerifyModal>
|
||||||
|
</OrganismAuthWrapper>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { SettingsResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
||||||
|
import { AccountService } from '../../services/account.service';
|
||||||
|
import { CompanyService } from '../../services/company.service';
|
||||||
|
import { SettingsService } from '../../services/settings.service';
|
||||||
|
import { Shifts } from '../../enums/shift.enum';
|
||||||
|
import {
|
||||||
|
BriefcaseIcon,
|
||||||
|
ExclamationTriangleIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
PrinterIcon,
|
||||||
|
QueueListIcon,
|
||||||
|
StarIcon,
|
||||||
|
TableCellsIcon,
|
||||||
|
UsersIcon,
|
||||||
|
WrenchScrewdriverIcon,
|
||||||
|
} from '@heroicons/vue/24/outline';
|
||||||
|
import AtomCard from '../atoms/AtomCard.vue';
|
||||||
|
import AtomInput from '../atoms/AtomInput.vue';
|
||||||
|
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
|
||||||
|
import AtomPercentInput from '../atoms/AtomPercentInput.vue';
|
||||||
|
import OrganismAuthWrapper from '../organisms/OrganismAuthWrapper.vue';
|
||||||
|
import MoleculeImportDataModal from '../molecules/MoleculeImportDataModal.vue';
|
||||||
|
import MoleculeErrorModal from '../molecules/MoleculeErrorModal.vue';
|
||||||
|
import { DateService } from '../../services/date.service';
|
||||||
|
import MoleculeVerifyModal from '../molecules/MoleculeVerifyModal.vue';
|
||||||
|
import { CurrencyService } from '../../services/currency.service';
|
||||||
|
import { BankService } from '../../services/bank.service';
|
||||||
|
|
||||||
|
const importAccountsModal = ref<InstanceType<typeof MoleculeImportDataModal>>();
|
||||||
|
const importCompaniesModal = ref<InstanceType<typeof MoleculeImportDataModal>>();
|
||||||
|
const verifySeedCapitalModal = ref<InstanceType<typeof MoleculeVerifyModal>>();
|
||||||
|
const errorModal = ref<InstanceType<typeof MoleculeErrorModal>>();
|
||||||
|
const minWage = ref<number>();
|
||||||
|
const incomeTax = ref<number>();
|
||||||
|
const maxWage = ref<number>();
|
||||||
|
const incomeTaxRecipient = ref<string>();
|
||||||
|
const radioUrl = ref<string>();
|
||||||
|
const settings = ref<SettingsResponse>();
|
||||||
|
const settingsSaveSuccess = ref<boolean>(false);
|
||||||
|
const settingsSaveError = ref<boolean>(false);
|
||||||
|
const importPending = ref<boolean>(false);
|
||||||
|
const importDone = ref<boolean>(false);
|
||||||
|
const capitalPending = ref<boolean>(false);
|
||||||
|
const capitalDone = ref<boolean>(false);
|
||||||
|
const errorMsg = ref<string>();
|
||||||
|
const seedCapital = ref<number>();
|
||||||
|
const createdCompanies = ref<{ name: string, password: string }[]>([]);
|
||||||
|
const serverTime = ref<Date>();
|
||||||
|
|
||||||
|
async function importAccounts(file: File): Promise<void> {
|
||||||
|
const data = await file.text();
|
||||||
|
const promises = [];
|
||||||
|
const companies = await CompanyService.getCompanies();
|
||||||
|
const errors = [];
|
||||||
|
for(const line of data.split('\n').slice(1)) {
|
||||||
|
const [lastName, firstName, grade, job] = line.trim().split(';');
|
||||||
|
const company = companies.find((company) => company.name === (job.trim() ? job.trim() : 'Aufsicht'));
|
||||||
|
if(!company) {
|
||||||
|
errors.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
promises.push(AccountService.addAccount({
|
||||||
|
firstName: firstName.trim(),
|
||||||
|
lastName: lastName.trim(),
|
||||||
|
grade: grade.trim(),
|
||||||
|
company: company?.id,
|
||||||
|
shift: Shifts.NONE,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if(errors.length) {
|
||||||
|
errorMsg.value = errors.join('\n');
|
||||||
|
errorModal.value?.show();
|
||||||
|
}
|
||||||
|
importPending.value = true;
|
||||||
|
await Promise.all(promises);
|
||||||
|
importPending.value = false;
|
||||||
|
importDone.value = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
importDone.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importCompanies(file: File): Promise<void> {
|
||||||
|
const data = await file.text();
|
||||||
|
const promises = [];
|
||||||
|
const companies = [];
|
||||||
|
for(const line of data.split('\n').slice(1)) {
|
||||||
|
if(!line) { continue; }
|
||||||
|
const company = {
|
||||||
|
name: line.trim().split(';')[0],
|
||||||
|
password: window.crypto.getRandomValues(new BigUint64Array(1))[0].toString(36),
|
||||||
|
};
|
||||||
|
companies.push(company);
|
||||||
|
promises.push(CompanyService.addCompany(company));
|
||||||
|
}
|
||||||
|
importPending.value = true;
|
||||||
|
createdCompanies.value = companies;
|
||||||
|
await Promise.all(promises);
|
||||||
|
importPending.value = false;
|
||||||
|
importDone.value = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
importDone.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreatedCompaniesPrintWindow(): void {
|
||||||
|
const printWindow = window.open('', '', 'height=400,width=800');
|
||||||
|
if(!printWindow) {
|
||||||
|
alert('Druckansicht konnte nicht erstellt werden.');
|
||||||
|
} else {
|
||||||
|
printWindow.document.write('<html><head><title>Job-Übersicht</title></head><body>');
|
||||||
|
printWindow.document.write('<table style="border-collapse: collapse;">');
|
||||||
|
printWindow.document.write('<tbody>');
|
||||||
|
for(const company of createdCompanies.value) {
|
||||||
|
printWindow.document.write(`<tr><td style="padding: 1rem; border: 1px solid black">${company.name}</td>`);
|
||||||
|
printWindow.document.write(`<td style="padding: 1rem; border: 1px solid black">${company.password}</td></tr>`);
|
||||||
|
}
|
||||||
|
printWindow.document.write('</tbody>');
|
||||||
|
printWindow.document.write('</table>');
|
||||||
|
printWindow.document.write('</body></html>');
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.print();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showMissingStudentsPrintWindow(): Promise<void> {
|
||||||
|
const printWindow = window.open('', '', 'height=400,width=800');
|
||||||
|
const missingStudents = await AccountService.getMissingStudents();
|
||||||
|
if(!printWindow) {
|
||||||
|
alert('Druckansicht konnte nicht erstellt werden.');
|
||||||
|
} else {
|
||||||
|
printWindow.document.write('<html><head><title>Fehlende Schüler</title></head><body>');
|
||||||
|
printWindow.document.write(`<h1>Fehlende Schüler am ${DateService.toShortString(new Date())}</h1>`);
|
||||||
|
printWindow.document.write('<table style="border-collapse: collapse;">');
|
||||||
|
printWindow.document.write('<tbody>');
|
||||||
|
printWindow.document.write('<tr><th style="padding: 1rem; border: 1px solid black">Schüler</th>');
|
||||||
|
printWindow.document.write('<th style="padding: 1rem; border: 1px solid black">Klasse</th></tr>');
|
||||||
|
for(const student of missingStudents) {
|
||||||
|
printWindow.document.write(`<tr><td style="padding: 1rem; border: 1px solid black">${student.firstName} ${student.lastName}</td>`);
|
||||||
|
printWindow.document.write(`<td style="padding: 1rem; border: 1px solid black">${student.grade}</td></tr>`);
|
||||||
|
}
|
||||||
|
printWindow.document.write('</tbody>');
|
||||||
|
printWindow.document.write('</table>');
|
||||||
|
printWindow.document.write('</body></html>');
|
||||||
|
printWindow.document.close();
|
||||||
|
printWindow.print();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
settings.value = await SettingsService.getSettings();
|
||||||
|
minWage.value = settings.value.minWage / 100;
|
||||||
|
incomeTax.value = settings.value.incomeTax;
|
||||||
|
maxWage.value = settings.value.maxWage / 100;
|
||||||
|
incomeTaxRecipient.value = settings.value.incomeTaxRecipient;
|
||||||
|
radioUrl.value = settings.value.radioUrl;
|
||||||
|
serverTime.value = await DateService.getServerTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function saveSettings() {
|
||||||
|
if(minWage.value && incomeTax.value && maxWage.value && incomeTaxRecipient.value && radioUrl.value) {
|
||||||
|
settings.value = await SettingsService.setSettings({
|
||||||
|
minWage: minWage.value,
|
||||||
|
incomeTax: incomeTax.value,
|
||||||
|
maxWage: maxWage.value,
|
||||||
|
incomeTaxRecipient: incomeTaxRecipient.value,
|
||||||
|
radioUrl: radioUrl.value,
|
||||||
|
});
|
||||||
|
settingsSaveSuccess.value = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
settingsSaveSuccess.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
settingsSaveError.value = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
settingsSaveError.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function paySeedCapital() {
|
||||||
|
capitalPending.value = true;
|
||||||
|
verifySeedCapitalModal.value?.close();
|
||||||
|
const accounts = await AccountService.getAccounts();
|
||||||
|
const promises: Promise<TransactionsResponse>[] = [];
|
||||||
|
accounts.forEach((account) => {
|
||||||
|
promises.push(BankService.addTransaction(account.id, (seedCapital.value ?? 0), 'Startkapital'));
|
||||||
|
});
|
||||||
|
await Promise.all(promises);
|
||||||
|
capitalPending.value = false;
|
||||||
|
capitalDone.value = true;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
capitalDone.value = false;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|