feat(core): migrate to pocketbase and update ui
20
.drone.yml
|
@ -2,15 +2,23 @@ kind: pipeline
|
|||
name: default
|
||||
|
||||
steps:
|
||||
- name: install
|
||||
image: node:18-alpine
|
||||
- name: install-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- corepack pnpm@7.5.1 install
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.0 install
|
||||
|
||||
- name: build
|
||||
image: node:18-alpine
|
||||
- name: lint-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- corepack pnpm@7.5.1 build
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.0 lint
|
||||
|
||||
- name: build-webapp
|
||||
image: node:lts-alpine
|
||||
commands:
|
||||
- cd webapp
|
||||
- corepack pnpm@8.6.0 build
|
||||
|
||||
- name: deploy
|
||||
image: plugins/docker
|
||||
|
|
6
.gitignore
vendored
|
@ -20,3 +20,9 @@ dist-ssr
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Pocketbase-Data
|
||||
pb_data
|
||||
|
||||
# Environment variables
|
||||
.env
|
13
.vscode/settings.json
vendored
|
@ -1,18 +1,29 @@
|
|||
{
|
||||
"cSpell.words": [
|
||||
"appwrite",
|
||||
"camelcase",
|
||||
"checkin",
|
||||
"classname",
|
||||
"cliffbreak",
|
||||
"corepack",
|
||||
"daisyui",
|
||||
"esnext",
|
||||
"Gitea",
|
||||
"godotenv",
|
||||
"heroicons",
|
||||
"kispi",
|
||||
"pnpm",
|
||||
"pocketbase",
|
||||
"tailwindcss",
|
||||
"typegen",
|
||||
"vite",
|
||||
"vitejs",
|
||||
"vueuse"
|
||||
],
|
||||
"eslint.format.enable": true,
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"directory": "webapp",
|
||||
"changeProcessCWD": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -26,7 +26,7 @@ docs(changelog): update change log to beta.5
|
|||
style(webapp): reorder imports
|
||||
```
|
||||
```
|
||||
fix(appwrite): need to depend on latest rxjs and zone.js
|
||||
fix(server): need to depend on latest rxjs and zone.js
|
||||
|
||||
The version in our package.json gets copied to the one we publish, and users need the latest of these.
|
||||
```
|
||||
|
@ -53,7 +53,7 @@ The following is the list of supported scopes (more to come):
|
|||
|
||||
* **core** used for changes to the whole project
|
||||
* **webapp** used for changes made in the webapp
|
||||
* **appwrite** used for changes made in the Appwrite cloud functions
|
||||
* **server** used for changes made in the server
|
||||
* **changelog**: used for updating the release notes in CHANGELOG.md
|
||||
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
|
||||
|
||||
|
|
26
Dockerfile
|
@ -1,7 +1,25 @@
|
|||
FROM steebchen/nginx-spa:stable
|
||||
## Build
|
||||
FROM golang:1.20-buster as build
|
||||
|
||||
COPY dist/ /app
|
||||
WORKDIR /app
|
||||
|
||||
EXPOSE 80
|
||||
COPY server/go.mod ./
|
||||
COPY server/go.sum ./
|
||||
COPY server/pkg ./pkg
|
||||
RUN go mod download
|
||||
|
||||
CMD ["nginx"]
|
||||
COPY server/*.go ./
|
||||
|
||||
RUN CGO_ENABLED=1 go build -o /server
|
||||
|
||||
## Deploy
|
||||
FROM gcr.io/distroless/base-debian10
|
||||
|
||||
WORKDIR /
|
||||
|
||||
COPY --from=build /server /server
|
||||
COPY webapp/dist /webapp
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
ENTRYPOINT ["/server", "serve", "--http=0.0.0.0:8090"]
|
52
README.md
|
@ -5,7 +5,7 @@
|
|||
</p>
|
||||
|
||||
This is our repository containing all required resources to run the "Core" WebApp from the "Kinderspielstadt Öhringen".
|
||||
This repository also contains all the required Appwrite cloud functions if any exist.
|
||||
This repository also contains all the required Pocketbase files to run the backend server.
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
|
@ -15,7 +15,7 @@ See deployment for notes on how to deploy the project on a live system.
|
|||
### 🍽️ Prerequisites
|
||||
|
||||
`NodeJS (including PNPM)` is required to run this project.
|
||||
Also a hosted instance of `Appwrite` is required.
|
||||
Also `Go` is required to run the built in server.
|
||||
|
||||
### 📦 Installing
|
||||
|
||||
|
@ -31,16 +31,54 @@ Change to the cloned repository
|
|||
cd Core
|
||||
```
|
||||
|
||||
To install all required packages run
|
||||
Copy the example environment file
|
||||
|
||||
```
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
|
||||
```
|
||||
|
||||
To install all required webapp packages run
|
||||
|
||||
```
|
||||
cd webapp && pnpm install
|
||||
```
|
||||
|
||||
To start the webapp in development mode run the following npm script
|
||||
|
||||
```
|
||||
pnpm run dev
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
To build the webapp run the following npm script
|
||||
|
||||
```
|
||||
pnpm build
|
||||
```
|
||||
|
||||
To install all required server packages run
|
||||
|
||||
```
|
||||
cd ../server && go mod download
|
||||
```
|
||||
|
||||
To generate unique VAPID keys run the following Go command copy this keys to the `.env` file
|
||||
|
||||
```
|
||||
go run main.go generate-vapid-keys
|
||||
```
|
||||
|
||||
To start the server run the following Go command
|
||||
|
||||
```
|
||||
go run main.go serve
|
||||
```
|
||||
|
||||
If you do changes to the database schema keep in mind to `Export collections` via the Pocketbase UI.
|
||||
Paste the exported JSON into the `pb_schema.json` file and run the following npm script to update the frontend schema types.
|
||||
|
||||
```
|
||||
cd ../webapp && pnpm typegen
|
||||
```
|
||||
|
||||
## 🧑💻 Configure Visual Studio Code
|
||||
|
@ -58,7 +96,9 @@ Please refer to our **[COMMIT_CONVENTION](COMMIT_CONVENTION.md)**
|
|||
* [PNPM](https://pnpm.io/) - Faster alternative to npm for managing dependencies
|
||||
* [Vue.js](https://vuejs.org/) - The Frontend Web Framework
|
||||
* [Vite](https://vitejs.dev/) - Used Frontend Tooling
|
||||
* [Appwrite](https://docs.mongodb.com/) - The hosted Backend used for this application
|
||||
* [Go](https://go.dev/) - The Backend Programming Language
|
||||
* [Pocketbase](https://pocketbase.io/) - The Backend Framework
|
||||
|
||||
|
||||
## 🤵 Authors
|
||||
|
||||
|
|
36
package.json
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"name": "kispi-core",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"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",
|
||||
"appwrite": "^9.0.1",
|
||||
"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"
|
||||
}
|
||||
}
|
2070
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 |
101
server/go.mod
Normal file
|
@ -0,0 +1,101 @@
|
|||
module kinderspielstadt.de/core-server
|
||||
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/pocketbase/pocketbase v0.16.3
|
||||
github.com/pterm/pterm v0.12.62
|
||||
)
|
||||
|
||||
require (
|
||||
atomicgo.dev/cursor v0.1.1 // indirect
|
||||
atomicgo.dev/keyboard v0.2.9 // indirect
|
||||
atomicgo.dev/schedule v0.0.2 // indirect
|
||||
cloud.google.com/go v0.110.0 // indirect
|
||||
cloud.google.com/go/iam v0.13.0 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.3.6 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.273 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.18.0 // 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.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // 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.28 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // 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.10.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/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198 // 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.29.0 // indirect
|
||||
golang.org/x/crypto v0.9.0 // indirect
|
||||
golang.org/x/image v0.7.0 // indirect
|
||||
golang.org/x/mod v0.10.0 // indirect
|
||||
golang.org/x/net v0.10.0 // indirect
|
||||
golang.org/x/oauth2 v0.8.0 // indirect
|
||||
golang.org/x/sys v0.8.0 // indirect
|
||||
golang.org/x/term v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.9.2 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/api v0.125.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
|
||||
google.golang.org/grpc v1.55.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
lukechampine.com/uint128 v1.3.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.22.6 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.5.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/sqlite v1.22.1 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
)
|
2984
server/go.sum
Normal file
19
server/main.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pterm/pterm"
|
||||
"kinderspielstadt.de/core-server/pkg/utils"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pterm.Info.Println("Loading environment variables...")
|
||||
utils.LoadEnv()
|
||||
pterm.Info.Println("Starting PocketBase server...")
|
||||
app := pocketbase.New()
|
||||
|
||||
if err := app.Start(); err != nil {
|
||||
pterm.Fatal.Println(err)
|
||||
}
|
||||
|
||||
}
|
303
server/pb_schema.json
Normal file
|
@ -0,0 +1,303 @@
|
|||
[
|
||||
{
|
||||
"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": "",
|
||||
"viewRule": "",
|
||||
"createRule": "",
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "p0gixtx6jwria0d",
|
||||
"name": "accountsData",
|
||||
"type": "base",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "6ubgbgeq",
|
||||
"name": "birthday",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "r8rs5y2b",
|
||||
"name": "email",
|
||||
"type": "email",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"exceptDomains": null,
|
||||
"onlyDomains": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "uu9474bx",
|
||||
"name": "firstNameParent",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "xmqazgjl",
|
||||
"name": "lastNameParent",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "atylx3pk",
|
||||
"name": "street",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "1ocem7g7",
|
||||
"name": "zipCode",
|
||||
"type": "number",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "73do7uq3",
|
||||
"name": "city",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "ktjkhpmz",
|
||||
"name": "phone",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"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": true,
|
||||
"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": "syfivtki",
|
||||
"name": "lastCheckIn",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pkoynx7h",
|
||||
"name": "personalData",
|
||||
"type": "relation",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"collectionId": "p0gixtx6jwria0d",
|
||||
"cascadeDelete": false,
|
||||
"minSelect": null,
|
||||
"maxSelect": 1,
|
||||
"displayFields": [
|
||||
"firstNameParent",
|
||||
"lastNameParent",
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
"CREATE UNIQUE 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.created = null && @request.data.updated = null",
|
||||
"deleteRule": null,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "c3zz98wpbn7m6zw",
|
||||
"name": "accountsList",
|
||||
"type": "view",
|
||||
"system": false,
|
||||
"schema": [
|
||||
{
|
||||
"id": "gw7zo5sb",
|
||||
"name": "accountNumber",
|
||||
"type": "text",
|
||||
"system": false,
|
||||
"required": true,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "44jwqwg4",
|
||||
"name": "name",
|
||||
"type": "json",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "dwqo9wuf",
|
||||
"name": "lastCheckIn",
|
||||
"type": "date",
|
||||
"system": false,
|
||||
"required": false,
|
||||
"options": {
|
||||
"min": "",
|
||||
"max": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "etoowqoa",
|
||||
"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.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"
|
||||
}
|
||||
}
|
||||
]
|
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])
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 57 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -1,53 +0,0 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
:title="`Kontaktdaten von ${name}`"
|
||||
darker
|
||||
close-button
|
||||
>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<tbody>
|
||||
<tr v-for="entry of contact">
|
||||
<td>{{ JSON.parse(entry).label }}</td>
|
||||
<td>{{ JSON.parse(entry).phone }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-if="contact.length == 0"
|
||||
class="alert alert-info shadow-lg"
|
||||
>
|
||||
<div>
|
||||
<InformationCircleIcon class="h-6 w-6" />
|
||||
<span>Keine Kontaktdaten vorhanden.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { InformationCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
||||
defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,66 +0,0 @@
|
|||
import { Databases, Models, Query } from 'appwrite';
|
||||
import { AppwriteService } from './appwrite.service';
|
||||
import { IAccount } from '../../interfaces/account.interface';
|
||||
|
||||
const DATABASE_ID = '62dfd5787755e7ed9fcd';
|
||||
const ACCOUNTS_COLLECTION_ID = '62dfd6ca06197f9baa86';
|
||||
const sdk = AppwriteService.getSDK();
|
||||
const database = new Databases(sdk, DATABASE_ID);
|
||||
|
||||
export const AccountService = {
|
||||
async getAccount(accountNumber: string): Promise<IAccount & Models.Document> {
|
||||
const accountDocument = await database.listDocuments<IAccount & Models.Document>(
|
||||
ACCOUNTS_COLLECTION_ID, [Query.equal('accountNumber', accountNumber)], 1);
|
||||
return accountDocument.documents[0];
|
||||
},
|
||||
async updateBalance(accountNumber: string, balance: number): Promise<IAccount & Models.Document> {
|
||||
const account = await this.getAccount(accountNumber);
|
||||
await database.updateDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, account.$id, {
|
||||
balance: account.balance + balance,
|
||||
});
|
||||
return account;
|
||||
},
|
||||
async getAllAccounts(): Promise<IAccount[]> {
|
||||
let accountDocuments: Models.DocumentList<IAccount & Models.Document>,
|
||||
offset = 0;
|
||||
const accounts: IAccount[] = [];
|
||||
do {
|
||||
accountDocuments = await database.listDocuments<IAccount & Models.Document>(
|
||||
ACCOUNTS_COLLECTION_ID, [], 100, offset);
|
||||
accounts.push(...accountDocuments.documents.map(account => ({
|
||||
accountNumber: account.accountNumber,
|
||||
firstName: account.firstName,
|
||||
lastName: account.lastName,
|
||||
balance: account.balance,
|
||||
transactions: [],
|
||||
lastCheckIn: account.lastCheckIn,
|
||||
})));
|
||||
offset += 100;
|
||||
} while(accountDocuments.total > offset);
|
||||
return accounts;
|
||||
},
|
||||
async checkIn(accountNumber: string): Promise<IAccount> {
|
||||
const account = await this.getAccount(accountNumber);
|
||||
if(!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
const accountDocument = await database.updateDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, account.$id, {
|
||||
lastCheckIn: Math.floor(+new Date() / 1000),
|
||||
});
|
||||
return accountDocument;
|
||||
},
|
||||
async addAccount(account: IAccount): Promise<IAccount> {
|
||||
const accountDocument = await database.createDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, 'unique()', account);
|
||||
return accountDocument;
|
||||
},
|
||||
async migrateAccount(oldAccountNumber: string, newAccountNumber: string): Promise<IAccount> {
|
||||
const account = await this.getAccount(oldAccountNumber);
|
||||
if(!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
await database.updateDocument<IAccount & Models.Document>(ACCOUNTS_COLLECTION_ID, account.$id, {
|
||||
accountNumber: newAccountNumber,
|
||||
});
|
||||
return account;
|
||||
},
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
import { Client, Account } from 'appwrite';
|
||||
|
||||
const sdk = new Client();
|
||||
|
||||
sdk
|
||||
.setEndpoint('http://192.168.1.115/v1')
|
||||
.setProject('62dfc94a1637dc23acbc');
|
||||
|
||||
export const AppwriteService = {
|
||||
getSDK(): Client {
|
||||
// TODO Authenticate
|
||||
const account = new Account(sdk);
|
||||
account.createAnonymousSession();
|
||||
return sdk;
|
||||
},
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
import { Databases, Models, Query } from 'appwrite';
|
||||
import { AccountService } from './account.service';
|
||||
import { AppwriteService } from './appwrite.service';
|
||||
import { IAccount } from '../../interfaces/account.interface';
|
||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
||||
|
||||
const DATABASE_ID = '62dfd5787755e7ed9fcd';
|
||||
const TRANSACTIONS_COLLECTION_ID = '62dfd81b7671727b27cd';
|
||||
const DEPOSIT_LABEL = 'Bargeldeinzahlung';
|
||||
const SALARY_LABEL = 'Gehalt';
|
||||
const WITHDRAW_LABEL = 'Bargeldauszahlung';
|
||||
const OPEN_ACCOUNT_LABEL = 'Kontoeröffnung';
|
||||
const sdk = AppwriteService.getSDK();
|
||||
const database = new Databases(sdk, DATABASE_ID);
|
||||
|
||||
export const BankService = {
|
||||
async getAccountDetails(accountNumber: string): Promise<IAccount> {
|
||||
const account = await AccountService.getAccount(accountNumber);
|
||||
if(!account) {
|
||||
throw new Error('Account not found');
|
||||
}
|
||||
const transactionDocuments = await database.listDocuments<ITransaction & Models.Document>(
|
||||
TRANSACTIONS_COLLECTION_ID, [Query.equal('accountNumber', accountNumber)], 100, 0, undefined, undefined, ['$createdAt'], ['DESC']);
|
||||
return {
|
||||
accountNumber: account.accountNumber,
|
||||
firstName: account.firstName,
|
||||
lastName: account.lastName,
|
||||
balance: account.balance,
|
||||
transactions: transactionDocuments.documents.map(transaction => ({
|
||||
label: transaction.label,
|
||||
amount: transaction.amount,
|
||||
date: new Date(transaction.$createdAt * 1000),
|
||||
})),
|
||||
lastCheckIn: account.lastCheckIn,
|
||||
};
|
||||
},
|
||||
async addTransaction(accountNumber: string, amount: number, type: TransactionType): Promise<ITransaction> {
|
||||
let label = '';
|
||||
switch (type) {
|
||||
case TransactionType.DEPOSIT:
|
||||
label = DEPOSIT_LABEL;
|
||||
break;
|
||||
case TransactionType.SALARY:
|
||||
label = SALARY_LABEL;
|
||||
break;
|
||||
case TransactionType.WITHDRAW:
|
||||
label = WITHDRAW_LABEL;
|
||||
break;
|
||||
case TransactionType.OPEN_ACCOUNT:
|
||||
label = OPEN_ACCOUNT_LABEL;
|
||||
break;
|
||||
}
|
||||
const transactionDocument = await database.createDocument<ITransaction & Models.Document>(TRANSACTIONS_COLLECTION_ID, 'unique()', {
|
||||
accountNumber: accountNumber,
|
||||
label,
|
||||
amount: (type === TransactionType.WITHDRAW ? -amount : amount),
|
||||
});
|
||||
await AccountService.updateBalance(accountNumber, type === TransactionType.WITHDRAW ? -amount : amount);
|
||||
return { label: transactionDocument.label, amount: transactionDocument.amount, date: new Date(transactionDocument.$createdAt * 1000) };
|
||||
},
|
||||
async migrateTransactions(oldAccountNumber: string, newAccountNumber: string): Promise<boolean> {
|
||||
const transactionDocuments = await database.listDocuments<ITransaction & Models.Document>(
|
||||
TRANSACTIONS_COLLECTION_ID, [Query.equal('accountNumber', oldAccountNumber)], 100, 0, undefined, undefined, ['$createdAt'], ['DESC']);
|
||||
transactionDocuments.documents.forEach(async transaction => {
|
||||
await database.updateDocument(TRANSACTIONS_COLLECTION_ID, transaction.$id, {
|
||||
accountNumber: newAccountNumber,
|
||||
});
|
||||
});
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT,
|
||||
SALARY,
|
||||
WITHDRAW,
|
||||
OPEN_ACCOUNT,
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import { Databases, Models } from 'appwrite';
|
||||
import { AccountService } from './account.service';
|
||||
import { AppwriteService } from './appwrite.service';
|
||||
import { IAccountData } from '../../interfaces/account-data.interface';
|
||||
import { IPersonalInformation } from '../../interfaces/personal-information.interface';
|
||||
import { ICreateAccount } from '../../interfaces/create-account.interface';
|
||||
import { IAccount } from '../../interfaces/account.interface';
|
||||
import { BankService, TransactionType } from './bank.service';
|
||||
|
||||
const OPEN_ACCOUNT_BALANCE = 5;
|
||||
const DATABASE_ID = '62dfd5787755e7ed9fcd';
|
||||
const PERSONAL_INFORMATION_COLLECTION_ID = '62e004abb0cdae53b483';
|
||||
const sdk = AppwriteService.getSDK();
|
||||
const database = new Databases(sdk, DATABASE_ID);
|
||||
|
||||
export const DataService = {
|
||||
async getAllData(): Promise<IAccountData[]> {
|
||||
const accounts = (await AccountService.getAllAccounts()) as IAccountData[];
|
||||
accounts.forEach(account => {
|
||||
account.name = `${account.firstName} ${account.lastName}`;
|
||||
});
|
||||
// let offset = 0,
|
||||
// personalInformationDocuments: Models.DocumentList<IPersonalInformation & Models.Document>;
|
||||
// do {
|
||||
// personalInformationDocuments = await database.listDocuments<IPersonalInformation & Models.Document>(
|
||||
// PERSONAL_INFORMATION_COLLECTION_ID, [], 100, offset);
|
||||
// personalInformationDocuments.documents.forEach(personalInformation => {
|
||||
// const account = accounts.find(account => account.accountNumber === personalInformation.accountNumber);
|
||||
// if(account) {
|
||||
// account.name = `${account.firstName} ${account.lastName}`;
|
||||
// account.birthday = personalInformation.birthday;
|
||||
// account.address = personalInformation.address;
|
||||
// account.contact = personalInformation.contact;
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// offset += 100;
|
||||
// } while(personalInformationDocuments.total > offset);
|
||||
return accounts;
|
||||
},
|
||||
async addAccount(account: ICreateAccount): Promise<IAccountData> {
|
||||
if(await AccountService.getAccount(account.accountNumber)) {
|
||||
throw new Error('Account already exists');
|
||||
}
|
||||
const accountDocument = await AccountService.addAccount({
|
||||
accountNumber: account.accountNumber,
|
||||
firstName: account.firstName,
|
||||
lastName: account.lastName,
|
||||
balance: 0,
|
||||
} as IAccount) as IAccountData;
|
||||
await BankService.addTransaction(account.accountNumber, OPEN_ACCOUNT_BALANCE, TransactionType.OPEN_ACCOUNT);
|
||||
const personalInformation = await database.createDocument<IAccountData & Models.Document>(
|
||||
PERSONAL_INFORMATION_COLLECTION_ID, 'unique()', {
|
||||
accountNumber: account.accountNumber,
|
||||
birthday: account.birthday,
|
||||
address: account.address,
|
||||
contact: account.contact,
|
||||
} as IPersonalInformation);
|
||||
if(accountDocument) {
|
||||
accountDocument.name = `${accountDocument.firstName} ${accountDocument.lastName}`;
|
||||
accountDocument.birthday = personalInformation.birthday;
|
||||
accountDocument.address = personalInformation.address;
|
||||
}
|
||||
// manually edit balance as the TransactionService updates the value after a transaction was created
|
||||
accountDocument.balance = OPEN_ACCOUNT_BALANCE;
|
||||
return accountDocument;
|
||||
},
|
||||
async migrateAccount(oldAccountNumber: string, newAccountNumber: string): Promise<boolean> {
|
||||
await AccountService.migrateAccount(oldAccountNumber, newAccountNumber);
|
||||
await BankService.migrateTransactions(oldAccountNumber, newAccountNumber);
|
||||
return true;
|
||||
},
|
||||
};
|
|
@ -1,44 +0,0 @@
|
|||
export class DateService {
|
||||
public static toString(date: Date | number): string {
|
||||
if(typeof date === 'number') {
|
||||
date = new Date(date * 1000);
|
||||
}
|
||||
if(!date) {
|
||||
return 'n/a';
|
||||
}
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
public static toShortString(date: Date | number): string {
|
||||
if(typeof date === 'number') {
|
||||
date = new Date(date * 1000);
|
||||
}
|
||||
if(!date) {
|
||||
return 'n/a';
|
||||
}
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
public static toTime(date: Date | number): string {
|
||||
if(typeof date === 'number') {
|
||||
date = new Date(date * 1000);
|
||||
}
|
||||
if(!date) {
|
||||
return 'n/a';
|
||||
}
|
||||
return date.toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="accountDetails"
|
||||
class="hero-content flex-col text-center lg:p-6 pt-0"
|
||||
>
|
||||
<h1 class="text-8xl mt-5">{{ CurrencyService.toString(accountDetails.balance) }}</h1>
|
||||
<h3 class="text-5xl font-bold mt-10">Kontoinhaber: {{ `${accountDetails.firstName} ${accountDetails.lastName}` }}</h3>
|
||||
<h4 class="text-4xl mb-10">Kontonummer: {{ accountDetails.accountNumber }}</h4>
|
||||
<MoleculeTransactionTable
|
||||
class="w-[42rem] mb-24"
|
||||
:balance="accountDetails.balance"
|
||||
:transactions="accountDetails.transactions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="accountDetails"
|
||||
class="fixed flex place-content-center lg:gap-32 gap-16 bg-base-100 bottom-0 lg:w-[calc(100%-16rem)] p-5"
|
||||
>
|
||||
<MoleculeInputModal
|
||||
:id="depositModalId"
|
||||
title="Einzahlung"
|
||||
action-label="Einzahlen"
|
||||
input-label="Einzuzahlender Betrag:"
|
||||
@submit="handleDeposit"
|
||||
@open="depositModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="depositModalLabel"
|
||||
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"
|
||||
@open="salaryModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="salaryModalLabel"
|
||||
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"
|
||||
@open="withdrawModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="withdrawModalLabel"
|
||||
class="btn gap-2"
|
||||
:for="withdrawModalId"
|
||||
>
|
||||
<ChevronDownIcon class="w-6 h-6 text-error" />
|
||||
Auszahlen
|
||||
</label>
|
||||
</div>
|
||||
<AtomHeroText v-else>
|
||||
Bitte Karte scannen...
|
||||
<div
|
||||
v-if="accountNotFound"
|
||||
class="alert alert-error shadow-lg mt-6"
|
||||
>
|
||||
<div>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<span class="text-base font-normal">Fehler: Der Account wurde nicht gefunden.</span>
|
||||
</div>
|
||||
</div>
|
||||
</AtomHeroText>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
||||
import { BankService, TransactionType } from '../services/bank.service';
|
||||
import { CurrencyService } from '../services/currency.service';
|
||||
import { IAccount } from '../../interfaces/account.interface';
|
||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
||||
import { CurrencyDollarIcon, ChevronDownIcon, ChevronUpIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import AtomHeroText from '../atoms/AtomHeroText.vue';
|
||||
import MoleculeInputModal from '../molecules/MoleculeInputModal.vue';
|
||||
import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const accountDetails = ref<IAccount>();
|
||||
const depositModalLabel = ref<HTMLLabelElement>();
|
||||
const salaryModalLabel = ref<HTMLLabelElement>();
|
||||
const withdrawModalLabel = ref<HTMLLabelElement>();
|
||||
const accountId = ref<string>('');
|
||||
const inputBuffer = ref<string>('');
|
||||
const depositModalOpen = ref<boolean>(false);
|
||||
const salaryModalOpen = ref<boolean>(false);
|
||||
const withdrawModalOpen = ref<boolean>(false);
|
||||
const accountNotFound = ref<boolean>(false);
|
||||
|
||||
const depositModalId = 'deposit-modal';
|
||||
const salaryModalId = 'salary-modal';
|
||||
const withdrawModalId = 'withdraw-modal';
|
||||
|
||||
if(route.query.accountNumber) {
|
||||
accountId.value = route.query.accountNumber as string;
|
||||
getAccountDetails();
|
||||
router.replace({ path: '/bank' });
|
||||
}
|
||||
|
||||
onKeyStroke(['e', 'g', 'a'], (e) => {
|
||||
e.preventDefault();
|
||||
switch (e.key) {
|
||||
case 'e':
|
||||
depositModalLabel.value?.click();
|
||||
break;
|
||||
case 'g':
|
||||
salaryModalLabel.value?.click();
|
||||
break;
|
||||
case 'a':
|
||||
withdrawModalLabel.value?.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], (e) => {
|
||||
if(depositModalOpen.value || salaryModalOpen.value || withdrawModalOpen.value) {
|
||||
return;
|
||||
}
|
||||
accountNotFound.value = false;
|
||||
e.preventDefault();
|
||||
if(e.key === 'Enter') {
|
||||
accountId.value = inputBuffer.value;
|
||||
inputBuffer.value = '';
|
||||
getAccountDetails();
|
||||
} else {
|
||||
inputBuffer.value += e.key;
|
||||
}
|
||||
});
|
||||
|
||||
async function getAccountDetails() {
|
||||
try {
|
||||
accountDetails.value = await BankService.getAccountDetails(accountId.value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if(error.message == 'Account not found') {
|
||||
accountNotFound.value = true;
|
||||
await promiseTimeout(3000);
|
||||
accountNotFound.value = false;
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeposit(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(!isNaN(amountNumber)) {
|
||||
updateTransactions(await BankService.addTransaction(accountId.value, amountNumber, TransactionType.DEPOSIT));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSalary(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(!isNaN(amountNumber)) {
|
||||
updateTransactions(await BankService.addTransaction(accountId.value, amountNumber, TransactionType.SALARY));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithdraw(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(!isNaN(amountNumber)) {
|
||||
updateTransactions(await BankService.addTransaction(accountId.value, amountNumber, TransactionType.WITHDRAW));
|
||||
}
|
||||
}
|
||||
|
||||
function updateTransactions(transaction: ITransaction) {
|
||||
if(accountDetails.value) {
|
||||
accountDetails.value.balance += transaction.amount;
|
||||
accountDetails.value.transactions.unshift(transaction);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,154 +0,0 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="!pinCorrect"
|
||||
class="hero min-h-full"
|
||||
>
|
||||
<div class="hero-content flex-col text-center">
|
||||
<AtomInput
|
||||
placeholder="Bitte PIN eingeben"
|
||||
type="password"
|
||||
@input="inputEvent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-show="pinCorrect"
|
||||
class="lg:p-6"
|
||||
>
|
||||
<AtomInput
|
||||
placeholder="Nach Vor-/Nachname suchen"
|
||||
class="mb-4"
|
||||
@input="filterEvent"
|
||||
/>
|
||||
<MoleculeDataTable
|
||||
v-if="data"
|
||||
:table-headers="tableHeaders"
|
||||
:data="data"
|
||||
:contact-modal-id="contactModalId"
|
||||
@open-contact-modal="openContactModal"
|
||||
/>
|
||||
<MoleculeContactModal
|
||||
:id="contactModalId"
|
||||
:name="contactName"
|
||||
:contact="contact"
|
||||
/>
|
||||
<MoleculeMigrateAccountModal
|
||||
:id="migrateAccountModalId"
|
||||
/>
|
||||
<MoleculeAddAccountModal
|
||||
:id="addAccountModalId"
|
||||
@submit="addAccount"
|
||||
/>
|
||||
<label
|
||||
class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-28"
|
||||
:for="migrateAccountModalId"
|
||||
>
|
||||
<SupportIcon class="w-7 h-7" />
|
||||
</label>
|
||||
<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 { onMounted, ref } from 'vue';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { IAccountData } from '../../interfaces/account-data.interface';
|
||||
import { PlusIcon, SupportIcon } from '@heroicons/vue/outline';
|
||||
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
|
||||
import MoleculeContactModal from '../molecules/MoleculeContactModal.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import MoleculeMigrateAccountModal from '../molecules/MoleculeMigrateAccountModal.vue';
|
||||
|
||||
const ACCESS_PIN_KEY = '1337';
|
||||
const pinCorrect = ref(false);
|
||||
const data = ref<IAccountData[]>([]);
|
||||
const initialData = ref<IAccountData[]>([]);
|
||||
const contactName = ref('');
|
||||
const contact = ref<string[]>([]);
|
||||
const contactModalId = 'contact-modal';
|
||||
const addAccountModalId = 'add-account-modal';
|
||||
const migrateAccountModalId = 'migrate-account-modal';
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: 'Kontonummer',
|
||||
key: 'accountNumber',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
// {
|
||||
// title: 'Geburtsdatum',
|
||||
// key: 'birthday',
|
||||
// type: TableHeaderType.STRING,
|
||||
// },
|
||||
{
|
||||
title: 'Kontostand',
|
||||
key: 'balance',
|
||||
type: TableHeaderType.CURRENCY,
|
||||
},
|
||||
// {
|
||||
// title: 'Adresse',
|
||||
// key: 'address',
|
||||
// type: TableHeaderType.STRING,
|
||||
// },
|
||||
{
|
||||
title: 'Letzter Login',
|
||||
key: 'lastCheckIn',
|
||||
type: TableHeaderType.DATETIME,
|
||||
},
|
||||
// {
|
||||
// title: 'Kontakt',
|
||||
// key: '',
|
||||
// type: TableHeaderType.BUTTON_CONTACT,
|
||||
// },
|
||||
{
|
||||
title: 'Trans.',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_ACCOUNT,
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(async () => {
|
||||
data.value = await DataService.getAllData();
|
||||
initialData.value = data.value;
|
||||
});
|
||||
|
||||
function openContactModal(data: { name: string, contact: string[] }) {
|
||||
contactName.value = data.name;
|
||||
contact.value = data.contact;
|
||||
}
|
||||
|
||||
function addAccount(account: IAccountData) {
|
||||
data.value.push(account);
|
||||
initialData.value = data.value;
|
||||
}
|
||||
|
||||
function inputEvent(event: Event) {
|
||||
if((event.target as HTMLInputElement).value == ACCESS_PIN_KEY) {
|
||||
pinCorrect.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
function filterEvent(event: Event) {
|
||||
const input = (event.target as HTMLInputElement).value;
|
||||
data.value = initialData.value;
|
||||
if(input === '') {
|
||||
return;
|
||||
}
|
||||
data.value = data.value.filter((account: IAccountData) => {
|
||||
return `${account.firstName} ${account.lastName}`.toLocaleLowerCase().includes(input.toLocaleLowerCase());
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
|
@ -1,8 +0,0 @@
|
|||
import { IAccount } from './account.interface';
|
||||
|
||||
export interface IAccountData extends IAccount {
|
||||
name: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { ITransaction } from './transaction.interface';
|
||||
|
||||
export interface IAccount {
|
||||
accountNumber: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
balance: number;
|
||||
transactions: ITransaction[];
|
||||
lastCheckIn: number;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
export interface ICreateAccount {
|
||||
accountNumber: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export interface IPersonalInformation {
|
||||
accountNumber: string;
|
||||
birthday: string;
|
||||
address: string;
|
||||
contact: string[];
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export interface ITransaction {
|
||||
label: string;
|
||||
amount: number;
|
||||
date: Date;
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('daisyui')],
|
||||
}
|
1
webapp/.eslintignore
Normal file
|
@ -0,0 +1 @@
|
|||
src/types/pocketbase.types.ts
|
|
@ -4,6 +4,7 @@ module.exports = {
|
|||
'eslint:recommended',
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:vue-scoped-css/vue3-recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
],
|
||||
parser: 'vue-eslint-parser',
|
||||
env: {
|
||||
|
@ -70,7 +71,8 @@ module.exports = {
|
|||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
sourceType: 'module',
|
||||
'project': 'tsconfig.json',
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
plugins: [
|
||||
|
@ -138,6 +140,7 @@ module.exports = {
|
|||
'svg': 'always',
|
||||
'math': 'always',
|
||||
}],
|
||||
'tailwindcss/no-custom-classname': 'off',
|
||||
},
|
||||
},
|
||||
],
|
42
webapp/package.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "kispi-core",
|
||||
"private": true,
|
||||
"author": "Simon Giesel",
|
||||
"version": "0.0.0",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/vue": "^2.0.18",
|
||||
"@vueuse/core": "^10.1.2",
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"daisyui": "^2.52.0",
|
||||
"pocketbase": "^0.15.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "4.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/canvas-confetti": "^1.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
"@typescript-eslint/parser": "^5.59.8",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-plugin-tailwindcss": "^3.12.1",
|
||||
"eslint-plugin-vue": "^9.14.1",
|
||||
"eslint-plugin-vue-scoped-css": "^2.4.0",
|
||||
"pocketbase-typegen": "^1.1.9",
|
||||
"postcss": "^8.4.24",
|
||||
"sass": "^1.62.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vue-eslint-parser": "^9.3.0",
|
||||
"vue-tsc": "^1.6.5"
|
||||
}
|
||||
}
|
2987
webapp/pnpm-lock.yaml
Normal file
BIN
webapp/public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
webapp/public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
webapp/public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
|
@ -3,7 +3,7 @@
|
|||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#b91d47</TileColor>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
webapp/public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 739 B |
BIN
webapp/public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 1.1 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: 4 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: /
|
1074
webapp/public/safari-pinned-tab.svg
Normal file
After Width: | Height: | Size: 77 KiB |
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Kinderspielstadt Öhringen",
|
||||
"short_name": "KiSpi Öhringen",
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
|
@ -16,4 +16,4 @@
|
|||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
}
|
|
@ -11,12 +11,12 @@
|
|||
import { INavigationEntry } from './interfaces/navigation-entry.interface';
|
||||
import {
|
||||
IdentificationIcon,
|
||||
LibraryIcon,
|
||||
LogoutIcon,
|
||||
MusicNoteIcon,
|
||||
TrendingUpIcon,
|
||||
BuildingLibraryIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
MusicalNoteIcon,
|
||||
ArrowTrendingUpIcon,
|
||||
UserIcon,
|
||||
} from '@heroicons/vue/outline';
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import MoleculeNavigationDrawer from './components/molecules/MoleculeNavigationDrawer.vue';
|
||||
|
||||
const drawerId = 'default-drawer';
|
||||
|
@ -33,28 +33,29 @@ const navigationEntries: INavigationEntry[] = [
|
|||
},
|
||||
{
|
||||
name: 'Bank',
|
||||
icon: LibraryIcon,
|
||||
icon: BuildingLibraryIcon,
|
||||
to: '/bank',
|
||||
},
|
||||
{
|
||||
name: 'Börse',
|
||||
icon: TrendingUpIcon,
|
||||
icon: ArrowTrendingUpIcon,
|
||||
to: '/stocks',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: 'Radio',
|
||||
icon: MusicNoteIcon,
|
||||
icon: MusicalNoteIcon,
|
||||
to: '/radio',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'Abmelden',
|
||||
icon: LogoutIcon,
|
||||
to: '/logout',
|
||||
},
|
||||
// {
|
||||
// name: 'divider',
|
||||
// },
|
||||
// {
|
||||
// name: 'Abmelden',
|
||||
// icon: ArrowRightOnRectangleIcon,
|
||||
// to: '/logout',
|
||||
// },
|
||||
];
|
||||
</script>
|
||||
|
1
webapp/src/assets/logo_black.svg
Normal file
After Width: | Height: | Size: 33 KiB |
1
webapp/src/assets/logo_white.svg
Normal file
After Width: | Height: | Size: 32 KiB |
|
@ -3,12 +3,12 @@
|
|||
<AtomInput
|
||||
ref="input"
|
||||
type="number"
|
||||
class="text-right pr-16"
|
||||
class="pr-16 text-right"
|
||||
step="1"
|
||||
min="1"
|
||||
max="100"
|
||||
/>
|
||||
<span class="text-sm opacity-50 absolute -ml-16 top-2/4 -mt-8 pointer-events-none">,00 Öro</span>
|
||||
<span class="pointer-events-none absolute top-2/4 -ml-16 -mt-8 text-sm opacity-50">,00 Öro</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="min-h-full lg:p-6">
|
||||
<div class="hero min-h-full bg-base-200 rounded-lg">
|
||||
<div class="hero min-h-full rounded-lg bg-base-200">
|
||||
<div class="hero-content flex-col text-center">
|
||||
<h1 class="text-6xl font-bold"><slot /></h1>
|
||||
</div>
|
|
@ -2,9 +2,10 @@
|
|||
<input
|
||||
ref="input"
|
||||
:type="type"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
class="input input-bordered w-full"
|
||||
class="input-bordered input w-full"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</template>
|
||||
|
@ -15,6 +16,10 @@ defineProps({
|
|||
type: String,
|
||||
default: 'text',
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<img
|
||||
v-if="isDark"
|
||||
src="../../assets/logo_white.png"
|
||||
src="../../assets/logo_white.svg"
|
||||
class="w-48 scale-[1.4]"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="../../assets/logo.png"
|
||||
src="../../assets/logo_black.svg"
|
||||
class="w-48 scale-[1.4]"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -11,15 +11,22 @@
|
|||
class="modal cursor-pointer"
|
||||
>
|
||||
<label
|
||||
:class="`modal-box${darker ? ' bg-base-300' : ''}`"
|
||||
class="modal-box"
|
||||
:class="{
|
||||
'bg-base-300': darker,
|
||||
'w-auto': wAuto,
|
||||
}"
|
||||
for=""
|
||||
>
|
||||
<label
|
||||
v-if="closeButton"
|
||||
:for="id"
|
||||
class="btn btn-sm btn-circle absolute right-2 top-2"
|
||||
class="btn-sm btn-circle btn sticky left-full top-2"
|
||||
>✕</label>
|
||||
<h3 class="font-bold text-lg">{{ title }}</h3>
|
||||
<h3
|
||||
v-if="title"
|
||||
class="text-lg font-bold"
|
||||
>{{ title }}</h3>
|
||||
<p class="py-4"><slot /></p>
|
||||
<div class="modal-action">
|
||||
<slot name="action" />
|
||||
|
@ -37,7 +44,7 @@ defineProps({
|
|||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
darker: {
|
||||
type: Boolean,
|
||||
|
@ -47,6 +54,10 @@ defineProps({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
wAuto: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
defineEmits(['open']);
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
class="select-bordered select w-full"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
>
|
||||
<option
|
|
@ -20,11 +20,11 @@ import { FunctionalComponent, PropType } from 'vue';
|
|||
|
||||
defineProps({
|
||||
onIcon: {
|
||||
type: Object as PropType<FunctionalComponent>,
|
||||
type: Function as PropType<FunctionalComponent>,
|
||||
required: true,
|
||||
},
|
||||
offIcon: {
|
||||
type: Object as PropType<FunctionalComponent>,
|
||||
type: Function as PropType<FunctionalComponent>,
|
||||
required: true,
|
||||
},
|
||||
checked: {
|
|
@ -71,14 +71,14 @@
|
|||
:for="id"
|
||||
class="btn gap-2"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
Speichern
|
||||
</button>
|
||||
</template>
|
||||
|
@ -87,8 +87,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
import AtomSelect from '../atoms/AtomSelect.vue';
|
||||
|
@ -114,43 +113,44 @@ defineProps({
|
|||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const account = await DataService.addAccount(
|
||||
{
|
||||
accountNumber: accountNumber.value,
|
||||
firstName: firstName.value,
|
||||
lastName: lastName.value,
|
||||
birthday: birthday.value,
|
||||
address: `${street.value} ${houseNumber.value}, ${zipCode.value} ${city.value}`,
|
||||
contact: contactLabel.value.map((label, index) => JSON.stringify({
|
||||
label,
|
||||
phone: contactPhone.value[index],
|
||||
})),
|
||||
},
|
||||
);
|
||||
emit('submit', account);
|
||||
firstName.value = '';
|
||||
lastName.value = '';
|
||||
accountNumber.value = '';
|
||||
birthday.value = '';
|
||||
street.value = '';
|
||||
houseNumber.value = '';
|
||||
zipCode.value = '';
|
||||
city.value = '';
|
||||
contactLabel.value = [];
|
||||
contactPhone.value = [];
|
||||
contactCount.value = 1;
|
||||
abortButton.value.click();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if(error.message == 'Account already exists') {
|
||||
alert('Konto existiert bereits');
|
||||
} else {
|
||||
alert('Fehler beim Speichern');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
alert('TODO: Implement handleSubmit');
|
||||
// try {
|
||||
// const account = await DataService.addAccount(
|
||||
// {
|
||||
// accountNumber: accountNumber.value,
|
||||
// firstName: firstName.value,
|
||||
// lastName: lastName.value,
|
||||
// birthday: birthday.value,
|
||||
// address: `${street.value} ${houseNumber.value}, ${zipCode.value} ${city.value}`,
|
||||
// contact: contactLabel.value.map((label, index) => JSON.stringify({
|
||||
// label,
|
||||
// phone: contactPhone.value[index],
|
||||
// })),
|
||||
// },
|
||||
// );
|
||||
// emit('submit', account);
|
||||
// firstName.value = '';
|
||||
// lastName.value = '';
|
||||
// accountNumber.value = '';
|
||||
// birthday.value = '';
|
||||
// street.value = '';
|
||||
// houseNumber.value = '';
|
||||
// zipCode.value = '';
|
||||
// city.value = '';
|
||||
// contactLabel.value = [];
|
||||
// contactPhone.value = [];
|
||||
// contactCount.value = 1;
|
||||
// abortButton.value.click();
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// } catch (error: any) {
|
||||
// if(error.message == 'Account already exists') {
|
||||
// alert('Konto existiert bereits');
|
||||
// } else {
|
||||
// alert('Fehler beim Speichern');
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(error);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
function handleContactChange(index: number) {
|
44
webapp/src/components/molecules/MoleculeAuthDialog.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<div class="hero min-h-full">
|
||||
<form
|
||||
class="hero-content 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 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>
|
120
webapp/src/components/molecules/MoleculeContactModal.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<AtomModal
|
||||
:id="id"
|
||||
close-button
|
||||
w-auto
|
||||
>
|
||||
<div class="-my-16 overflow-x-auto">
|
||||
<template v-if="accountDetails">
|
||||
<div class="stats stats-vertical">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Name des Kindes</div>
|
||||
<div class="stat-value">{{ accountDetails.firstName }} {{ accountDetails.lastName }}</div>
|
||||
<div class="stat-desc">Vorname Nachname</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Kontonummer</div>
|
||||
<div class="stat-value">{{ accountDetails.accountNumber }}</div>
|
||||
<div class="stat-desc">ID: {{ accountDetails.id }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Letzter Check-In</div>
|
||||
<div class="stat-value">{{ accountDetails.lastCheckIn ? DateService.toTime(new Date(accountDetails.lastCheckIn)) : 'N/A' }}</div>
|
||||
<div
|
||||
v-if="accountDetails.lastCheckIn"
|
||||
class="stat-desc"
|
||||
>
|
||||
{{ DateService.toShortString(new Date(accountDetails.lastCheckIn)) }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="accountDetails.expand?.personalData">
|
||||
<div class="stat">
|
||||
<div class="stat-title">Geburtstag</div>
|
||||
<div class="stat-value">{{ DateService.toShortString(new Date(accountDetails.expand.personalData.birthday)) }}</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Name der Eltern</div>
|
||||
<div class="stat-value">
|
||||
{{ accountDetails.expand.personalData.firstNameParent }}
|
||||
{{ accountDetails.expand.personalData.lastNameParent }}
|
||||
</div>
|
||||
<div class="stat-desc">Vorname Nachname</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Adresse</div>
|
||||
<div class="stat-value">{{ accountDetails.expand.personalData.street }}</div>
|
||||
<div class="stat-desc">{{ accountDetails.expand.personalData.zipCode }} {{ accountDetails.expand.personalData.city }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat cursor-pointer"
|
||||
@click="truncateEmail = !truncateEmail"
|
||||
>
|
||||
<div class="stat-title">E-Mail-Adresse</div>
|
||||
<div
|
||||
class="stat-value"
|
||||
:class="{'truncate': truncateEmail}"
|
||||
>
|
||||
{{ accountDetails.expand.personalData.email }}
|
||||
</div>
|
||||
<div class="stat-desc">Um die E-Mail-Adresse komplett anzuzeigen, bitte drücken und scrollen.</div>
|
||||
</div>
|
||||
<div
|
||||
class="stat cursor-pointer"
|
||||
@click="truncatePhone = !truncatePhone"
|
||||
>
|
||||
<div class="stat-title">Telefonnummer</div>
|
||||
<div
|
||||
class="stat-value"
|
||||
:class="{'truncate': truncatePhone}"
|
||||
>
|
||||
{{ accountDetails.expand.personalData.phone }}
|
||||
</div>
|
||||
<div class="stat-desc">Um die Telefonnummer komplett anzuzeigen, bitte drücken und scrollen.</div>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
<div class="stat">
|
||||
<div class="stat-title">Keine Persönlichen Daten vorhanden</div>
|
||||
<div class="stat-desc">Dies ist vermutlich ein technischer Fehler.</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</AtomModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { AccountsDataResponse, AccountsResponse } from '../../types/pocketbase.types';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
||||
const accountDetails = ref<AccountsResponse<{personalData: AccountsDataResponse}>>();
|
||||
const truncateEmail = ref(true);
|
||||
const truncatePhone = ref(true);
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contactId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
watch(() => props.contactId, async (contactId) => {
|
||||
if(contactId) {
|
||||
accountDetails.value = await AccountService.getAccountDetails(contactId);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full table-zebra">
|
||||
<table class="table-zebra table w-full">
|
||||
<thead class="sticky top-0">
|
||||
<tr>
|
||||
<th v-for="header in tableHeaders">{{ header.title }}</th>
|
||||
|
@ -19,7 +19,7 @@
|
|||
{{ DateService.toShortString(entry[header.key]) }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.DATETIME">
|
||||
{{ DateService.toString(entry[header.key]) }}
|
||||
{{ entry[header.key] ? DateService.toString(new Date(entry[header.key])): 'N/A' }}
|
||||
</span>
|
||||
<span v-else-if="header.type === TableHeaderType.CURRENCY">
|
||||
{{ CurrencyService.toString(entry[header.key]) }}
|
||||
|
@ -27,15 +27,15 @@
|
|||
<label
|
||||
v-if="header.type === TableHeaderType.BUTTON_CONTACT"
|
||||
:for="contactModalId"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
@click="$emit('open-contact-modal', { name: entry.name, contact: entry.contact })"
|
||||
class="btn-ghost btn-sm btn p-1"
|
||||
@click="$emit('open-contact-modal', entry.id)"
|
||||
>
|
||||
<DocumentTextIcon class="h-6 w-6" />
|
||||
</label>
|
||||
<RouterLink
|
||||
v-if="header.type === TableHeaderType.BUTTON_ACCOUNT"
|
||||
:to="`/bank?accountNumber=${entry.accountNumber}`"
|
||||
class="btn btn-sm btn-ghost p-1"
|
||||
class="btn-ghost btn-sm btn p-1"
|
||||
>
|
||||
<CurrencyDollarIcon class="h-6 w-6" />
|
||||
</RouterLink>
|
||||
|
@ -48,9 +48,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { CurrencyService } from '../services/currency.service';
|
||||
import { DateService } from '../services/date.service';
|
||||
import { CurrencyDollarIcon, DocumentTextIcon } from '@heroicons/vue/outline';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { CurrencyDollarIcon, DocumentTextIcon } from '@heroicons/vue/24/outline';
|
||||
|
||||
defineProps({
|
||||
tableHeaders: {
|
|
@ -4,7 +4,7 @@
|
|||
:title="title"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="flex justify-between place-items-center">
|
||||
<div class="flex place-items-center justify-between">
|
||||
<span>{{ inputLabel }}</span>
|
||||
<AtomCurrencyInput
|
||||
ref="input"
|
||||
|
@ -19,14 +19,14 @@
|
|||
class="btn gap-2"
|
||||
:for="id"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit()"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
{{ actionLabel }}
|
||||
</button>
|
||||
</template>
|
||||
|
@ -35,7 +35,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
|
@ -21,14 +21,14 @@
|
|||
:for="id"
|
||||
class="btn gap-2"
|
||||
>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
Abbrechen
|
||||
</label>
|
||||
<button
|
||||
class="btn gap-2"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
<CheckCircleIcon class="w-6 h-6" />
|
||||
<CheckCircleIcon class="h-6 w-6" />
|
||||
Speichern
|
||||
</button>
|
||||
</template>
|
||||
|
@ -37,8 +37,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
|
||||
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import AtomModal from '../atoms/AtomModal.vue';
|
||||
|
||||
|
@ -54,17 +53,18 @@ defineProps({
|
|||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
const isSuccess = await DataService.migrateAccount(oldAccountNumber.value, newAccountNumber.value);
|
||||
emit('submit', isSuccess);
|
||||
abortButton.value.click();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
alert('Fehler beim Migrieren');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
alert('TODO: Implement handleSubmit');
|
||||
// try {
|
||||
// const isSuccess = await DataService.migrateAccount(oldAccountNumber.value, newAccountNumber.value);
|
||||
// emit('submit', isSuccess);
|
||||
// abortButton.value.click();
|
||||
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// } catch (error: any) {
|
||||
// alert('Fehler beim Migrieren');
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.error(error);
|
||||
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
const emit = defineEmits(['submit']);
|
|
@ -1,24 +1,24 @@
|
|||
<template>
|
||||
<div class="drawer drawer-mobile">
|
||||
<div class="drawer-mobile drawer">
|
||||
<input
|
||||
:id="drawerId"
|
||||
type="checkbox"
|
||||
class="drawer-toggle"
|
||||
/>
|
||||
<div class="drawer-content flex flex-col bg-base-300 h-screen">
|
||||
<div class="drawer-content flex h-screen flex-col bg-base-300">
|
||||
<div
|
||||
class="navbar shadow-lg bg-neutral-focus text-neutral-content lg:hidden sticky top-0 z-50 max-w-7xl place-self-center"
|
||||
class="navbar sticky top-0 z-50 max-w-7xl place-self-center bg-neutral-focus text-neutral-content shadow-lg lg:hidden"
|
||||
>
|
||||
<div class="flex-none">
|
||||
<label
|
||||
:for="drawerId"
|
||||
class="btn btn-ghost rounded-btn lg:hidden"
|
||||
class="btn-ghost rounded-btn btn lg:hidden"
|
||||
>
|
||||
<MenuIcon class="h-7 w-7" />
|
||||
<Bars3Icon class="h-7 w-7" />
|
||||
</label>
|
||||
<label
|
||||
tabindex="0"
|
||||
class="btn btn-ghost avatar"
|
||||
class="btn-ghost avatar btn"
|
||||
>
|
||||
<AtomLogo class="w-12" />
|
||||
</label>
|
||||
|
@ -31,12 +31,28 @@
|
|||
:for="drawerId"
|
||||
class="drawer-overlay"
|
||||
/>
|
||||
<ul class="menu p-4 overflow-y-auto w-64 bg-base-100">
|
||||
<ul class="menu w-64 overflow-y-auto bg-base-100 p-4">
|
||||
<li class="pointer-events-none">
|
||||
<a>
|
||||
<AtomLogo class="w-28" />
|
||||
</a>
|
||||
</li>
|
||||
<template v-if="isAuthenticated">
|
||||
<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" />
|
||||
</template>
|
||||
<li
|
||||
v-for="navigationEntry of navigationEntries"
|
||||
:class="getNavigationEntryClass(navigationEntry)"
|
||||
|
@ -66,12 +82,16 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { PropType, onMounted, ref } from 'vue';
|
||||
import { isDark, toggleDark } from '../../utils/darkMode';
|
||||
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
|
||||
import { MenuIcon, MoonIcon, SunIcon } from '@heroicons/vue/outline';
|
||||
import { ArrowRightOnRectangleIcon, Bars3Icon, ExclamationTriangleIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomLogo from '../atoms/AtomLogo.vue';
|
||||
import AtomSwap from '../atoms/AtomSwap.vue';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
const isAuthenticated = ref(false);
|
||||
|
||||
defineProps({
|
||||
drawerId: {
|
||||
|
@ -91,7 +111,12 @@ function getNavigationEntryClass(navigationEntry: INavigationEntry) {
|
|||
disabled: navigationEntry.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
useEventBus<boolean>('isAuthenticated').on(state => (isAuthenticated.value = state));
|
||||
|
||||
onMounted(() => {
|
||||
isAuthenticated.value = AuthService.isAuthenticated();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<table class="table table-zebra">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th class="normal-case text-lg text-right">{{ CurrencyService.toString(balance) }}</th>
|
||||
<th class="text-right text-lg normal-case">{{ CurrencyService.toString(balance) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<div class="flex items-center">
|
||||
<div>
|
||||
<div class="font-bold">{{ transaction.label }}</div>
|
||||
<div>{{ DateService.toString(transaction.date) }}</div>
|
||||
<div>{{ DateService.toString(new Date(transaction.created)) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -35,13 +35,13 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue';
|
||||
import { CurrencyService } from '../services/currency.service';
|
||||
import { DateService } from '../services/date.service';
|
||||
import { ITransaction } from '../../interfaces/transaction.interface';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { DateService } from '../../services/date.service';
|
||||
import { TransactionsResponse } from '../../types/pocketbase.types';
|
||||
|
||||
defineProps({
|
||||
transactions: {
|
||||
type: Array as PropType<ITransaction[]>,
|
||||
type: Array as PropType<TransactionsResponse[]>,
|
||||
required: true,
|
||||
},
|
||||
balance: {
|
227
webapp/src/components/views/BankView.vue
Normal file
|
@ -0,0 +1,227 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="account"
|
||||
class="hero-content flex-col pt-0 text-center lg:p-6"
|
||||
>
|
||||
<h1 class="mt-5 text-8xl">{{ CurrencyService.toString(getBalance()) }}</h1>
|
||||
<h3 class="mt-10 text-5xl font-bold">Kontoinhaber: {{ `${account.firstName} ${account.lastName}` }}</h3>
|
||||
<h4 class="mb-10 text-4xl">Kontonummer: {{ account.accountNumber }}</h4>
|
||||
<MoleculeTransactionTable
|
||||
v-if="transactions"
|
||||
class="mb-24 w-[42rem]"
|
||||
:balance="getBalance()"
|
||||
:transactions="transactions"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="account"
|
||||
class="fixed bottom-0 flex place-content-center gap-16 bg-base-100 p-5 lg:w-[calc(100%-16rem)] lg:gap-32"
|
||||
>
|
||||
<MoleculeInputModal
|
||||
:id="depositModalId"
|
||||
title="Einzahlung"
|
||||
action-label="Einzahlen"
|
||||
input-label="Einzuzahlender Betrag:"
|
||||
@submit="handleDeposit"
|
||||
@open="depositModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="depositModalLabel"
|
||||
class="btn gap-2"
|
||||
:for="depositModalId"
|
||||
>
|
||||
<ChevronUpIcon class="h-6 w-6 text-success" />
|
||||
Einzahlen
|
||||
</label>
|
||||
<MoleculeInputModal
|
||||
:id="salaryModalId"
|
||||
title="Gehalt"
|
||||
action-label="Gehalt hinzufügen"
|
||||
input-label="Gehalt Betrag:"
|
||||
@submit="handleSalary"
|
||||
@open="salaryModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="salaryModalLabel"
|
||||
class="btn gap-2"
|
||||
:for="salaryModalId"
|
||||
>
|
||||
<CurrencyDollarIcon class="h-6 w-6 text-warning" />
|
||||
Gehalt
|
||||
</label>
|
||||
<MoleculeInputModal
|
||||
:id="withdrawModalId"
|
||||
title="Auszahlung"
|
||||
action-label="Auszahlen"
|
||||
input-label="Auszuzahlender Betrag:"
|
||||
@submit="handleWithdraw"
|
||||
@open="withdrawModalOpen = $event.open"
|
||||
/>
|
||||
<label
|
||||
ref="withdrawModalLabel"
|
||||
class="btn gap-2"
|
||||
:for="withdrawModalId"
|
||||
>
|
||||
<ChevronDownIcon class="h-6 w-6 text-error" />
|
||||
Auszahlen
|
||||
</label>
|
||||
</div>
|
||||
<AtomHeroText v-else>
|
||||
Bitte Karte scannen...
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert alert-error mt-6 shadow-lg"
|
||||
>
|
||||
<div>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
<span class="text-base font-normal"><b>Fehler: </b>{{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</AtomHeroText>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
||||
import { UnsubscribeFunc } from 'pocketbase';
|
||||
import { AccountsResponse, TransactionsResponse } from '../../types/pocketbase.types';
|
||||
import { AccountService } from '../../services/account.service';
|
||||
import { BankService, TransactionType } from '../../services/bank.service';
|
||||
import { CurrencyService } from '../../services/currency.service';
|
||||
import { CurrencyDollarIcon, 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';
|
||||
|
||||
const router = useRouter();
|
||||
const subscription = ref<UnsubscribeFunc>();
|
||||
|
||||
const account = ref<AccountsResponse|null>();
|
||||
const transactions = ref<TransactionsResponse[]>();
|
||||
const depositModalLabel = ref<HTMLLabelElement>();
|
||||
const salaryModalLabel = ref<HTMLLabelElement>();
|
||||
const withdrawModalLabel = ref<HTMLLabelElement>();
|
||||
const accountId = ref<string>('');
|
||||
const inputBuffer = ref<string>('');
|
||||
const depositModalOpen = ref<boolean>(false);
|
||||
const salaryModalOpen = ref<boolean>(false);
|
||||
const withdrawModalOpen = ref<boolean>(false);
|
||||
const error = ref('');
|
||||
|
||||
const depositModalId = 'deposit-modal';
|
||||
const salaryModalId = 'salary-modal';
|
||||
const withdrawModalId = 'withdraw-modal';
|
||||
|
||||
if(router.currentRoute.value.query.accountNumber) {
|
||||
accountId.value = router.currentRoute.value.query.accountNumber as string;
|
||||
getAccount();
|
||||
router.replace({ path: '/bank' });
|
||||
}
|
||||
|
||||
onKeyStroke(['e', 'g', 'a'], (e) => {
|
||||
e.preventDefault();
|
||||
switch (e.key) {
|
||||
case 'e':
|
||||
depositModalLabel.value?.click();
|
||||
break;
|
||||
case 'g':
|
||||
salaryModalLabel.value?.click();
|
||||
break;
|
||||
case 'a':
|
||||
withdrawModalLabel.value?.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], (e) => {
|
||||
if(depositModalOpen.value || salaryModalOpen.value || withdrawModalOpen.value) {
|
||||
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) {
|
||||
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(!isNaN(amountNumber) && account.value) {
|
||||
await BankService.addTransaction(account.value.id, amountNumber, TransactionType.DEPOSIT);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSalary(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(!isNaN(amountNumber) && account.value) {
|
||||
await BankService.addTransaction(account.value.id, amountNumber, TransactionType.SALARY);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleWithdraw(amount: string) {
|
||||
const amountNumber = parseInt(amount);
|
||||
if(!isNaN(amountNumber) && account.value) {
|
||||
await BankService.addTransaction(account.value.id, -amountNumber, TransactionType.WITHDRAW);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
subscription.value = await BankService.subscribeToTransactionChanges((transactionSubscription) => {
|
||||
if(transactions.value && transactionSubscription.record.account === account.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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
subscription.value?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
|
@ -2,12 +2,12 @@
|
|||
<AtomHeroText>
|
||||
{{ message }}
|
||||
<div
|
||||
v-if="accountNotFound"
|
||||
class="alert alert-error shadow-lg mt-6"
|
||||
v-if="error"
|
||||
class="alert alert-error mt-6 shadow-lg"
|
||||
>
|
||||
<div>
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
<span class="text-base font-normal">Fehler: Der Account wurde nicht gefunden.</span>
|
||||
<XCircleIcon class="h-6 w-6" />
|
||||
<span class="text-base font-normal"><b>Fehler:</b> {{ error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</AtomHeroText>
|
||||
|
@ -16,35 +16,37 @@
|
|||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { onKeyStroke, promiseTimeout } from '@vueuse/core';
|
||||
import { AccountService } from '../services/account.service';
|
||||
import { DateService } from '../services/date.service';
|
||||
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 accountNotFound = ref<boolean>(false);
|
||||
const error = ref('');
|
||||
|
||||
onKeyStroke(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Enter'], async (e) => {
|
||||
e.preventDefault();
|
||||
accountNotFound.value = false;
|
||||
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(account.lastCheckIn)}`;
|
||||
message.value = `${account.firstName} ${account.lastName} um ${DateService.toTime(new Date(account.lastCheckIn))}`;
|
||||
await promiseTimeout(5000);
|
||||
message.value = DEFAULT_MESSAGE;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
if(error.message == 'Account not found') {
|
||||
} catch (e: any) {
|
||||
if(e.status == 404) {
|
||||
accountId.value = '';
|
||||
accountNotFound.value = true;
|
||||
error.value = 'Der Account wurde nicht gefunden.';
|
||||
await promiseTimeout(3000);
|
||||
accountNotFound.value = false;
|
||||
error.value = '';
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
error.value = 'Ein unerwarteter Fehler ist aufgetreten. Bitte kontaktiere einen Administrator.';
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
} else {
|
165
webapp/src/components/views/DataView.vue
Normal file
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<MoleculeAuthDialog v-if="!isAuthenticated" />
|
||||
<div
|
||||
v-else
|
||||
class="lg:p-6"
|
||||
>
|
||||
<AtomInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Nach Vor-/Nachname suchen"
|
||||
class="mb-4"
|
||||
/>
|
||||
<MoleculeDataTable
|
||||
v-if="accounts"
|
||||
:table-headers="tableHeaders"
|
||||
:data="accounts"
|
||||
:contact-modal-id="contactModalId"
|
||||
@open-contact-modal="openContactModal"
|
||||
/>
|
||||
<MoleculeContactModal
|
||||
:id="contactModalId"
|
||||
:contact-id="contactId"
|
||||
/>
|
||||
<!-- <MoleculeMigrateAccountModal
|
||||
:id="migrateAccountModalId"
|
||||
/>
|
||||
<MoleculeAddAccountModal
|
||||
:id="addAccountModalId"
|
||||
@submit="addAccount"
|
||||
/> -->
|
||||
<label
|
||||
class="btn-primary btn-circle btn-lg btn fixed bottom-8 right-28 shadow-2xl"
|
||||
:for="migrateAccountModalId"
|
||||
>
|
||||
<LifebuoyIcon class="h-7 w-7" />
|
||||
</label>
|
||||
<label
|
||||
class="btn-primary btn-circle btn-lg btn fixed bottom-8 right-8 shadow-2xl"
|
||||
:for="addAccountModalId"
|
||||
>
|
||||
<PlusIcon class="h-7 w-7" />
|
||||
</label>
|
||||
</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 { LifebuoyIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
||||
import AtomInput from '../atoms/AtomInput.vue';
|
||||
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
|
||||
import MoleculeContactModal from '../molecules/MoleculeContactModal.vue';
|
||||
import MoleculeDataTable, { TableHeaderType } from '../molecules/MoleculeDataTable.vue';
|
||||
import MoleculeMigrateAccountModal from '../molecules/MoleculeMigrateAccountModal.vue';
|
||||
import MoleculeAuthDialog from '../molecules/MoleculeAuthDialog.vue';
|
||||
|
||||
const isAuthenticated = ref(false);
|
||||
const accountsSubscription = ref<UnsubscribeFunc>();
|
||||
const transactionsSubscription = ref<UnsubscribeFunc>();
|
||||
|
||||
const accounts = ref<AccountsListResponse<number, string>[]>([]);
|
||||
const initAccounts = ref<AccountsListResponse<number, string>[]>([]);
|
||||
const searchQuery = ref('');
|
||||
const contactId = ref('');
|
||||
const contactModalId = 'contact-modal';
|
||||
const addAccountModalId = 'add-account-modal';
|
||||
const migrateAccountModalId = 'migrate-account-modal';
|
||||
const tableHeaders = [
|
||||
{
|
||||
title: 'Kontonummer',
|
||||
key: 'accountNumber',
|
||||
type: TableHeaderType.STRING,
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
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: 'Pers. Daten',
|
||||
key: '',
|
||||
type: TableHeaderType.BUTTON_CONTACT,
|
||||
},
|
||||
];
|
||||
|
||||
function openContactModal(id: string) {
|
||||
contactId.value = id;
|
||||
}
|
||||
|
||||
// function addAccount(account: IAccountData) {
|
||||
// data.value.push(account);
|
||||
// initialData.value = data.value;
|
||||
// }
|
||||
|
||||
useEventBus<boolean>('isAuthenticated').on(state => {
|
||||
isAuthenticated.value = state;
|
||||
if(isAuthenticated.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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
isAuthenticated.value = AuthService.isAuthenticated();
|
||||
if(isAuthenticated.value) {
|
||||
getData();
|
||||
}
|
||||
accountsSubscription.value = await AccountService.subscribeToAccountChanges(async () => {
|
||||
console.log('accounts changed');
|
||||
await getData();
|
||||
search(searchQuery.value);
|
||||
});
|
||||
transactionsSubscription.value = await BankService.subscribeToTransactionChanges(async () => {
|
||||
console.log('transactions changed');
|
||||
await getData();
|
||||
search(searchQuery.value);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
accountsSubscription.value?.();
|
||||
transactionsSubscription.value?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
0
src/env.d.ts → webapp/src/env.d.ts
vendored
30
webapp/src/services/account.service.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
|
||||
import { AccountsDataResponse, AccountsListResponse, AccountsResponse, Collections } from '../types/pocketbase.types';
|
||||
import { PocketbaseService } from './pocketbase.service';
|
||||
|
||||
const COLLECTION = PocketbaseService.getApi().collection(Collections.Accounts);
|
||||
const VIEW = PocketbaseService.getApi().collection(Collections.AccountsList);
|
||||
|
||||
export class AccountService {
|
||||
public static async getAccount(accountNumber: string): Promise<AccountsResponse> {
|
||||
return COLLECTION.getFirstListItem(`accountNumber = "${accountNumber}"`);
|
||||
}
|
||||
public static async checkIn(accountNumber: string): Promise<AccountsResponse> {
|
||||
const account = await this.getAccount(accountNumber);
|
||||
return COLLECTION.update(account.id, { lastCheckIn: new Date() });
|
||||
}
|
||||
public static async getAccounts(): Promise<AccountsListResponse<number, string>[]> {
|
||||
return VIEW.getFullList();
|
||||
}
|
||||
public static getAccountDetails(id: string): Promise<AccountsResponse<{personalData: AccountsDataResponse}>> {
|
||||
return COLLECTION.getOne(id, { expand: 'personalData' });
|
||||
}
|
||||
public static async subscribeToAccountChanges(
|
||||
callback: (data: RecordSubscription<AccountsResponse>)=> void,
|
||||
): Promise<UnsubscribeFunc> {
|
||||
return await COLLECTION.subscribe<AccountsResponse>('*', (data) => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
33
webapp/src/services/auth.service.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Admin } from 'pocketbase';
|
||||
import { PocketbaseService } from './pocketbase.service';
|
||||
import { useEventBus } from '@vueuse/core';
|
||||
|
||||
const API = PocketbaseService.getApi();
|
||||
|
||||
export class AuthService {
|
||||
public static isAuthenticated(): boolean {
|
||||
if(API.authStore.isValid) {
|
||||
return true;
|
||||
}
|
||||
this.logout();
|
||||
return false;
|
||||
}
|
||||
public static getAdmin(): Admin {
|
||||
if(this.isAuthenticated()) {
|
||||
return API.authStore.model as Admin;
|
||||
}
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
public static async loginAsAdmin(email: string, password: string): Promise<Admin> {
|
||||
const response = await API.admins.authWithPassword(email, password);
|
||||
if(response) {
|
||||
useEventBus<boolean>('isAuthenticated').emit(true);
|
||||
return response.admin;
|
||||
}
|
||||
throw new Error('Invalid credentials');
|
||||
}
|
||||
public static logout(): void {
|
||||
API.authStore.clear();
|
||||
useEventBus<boolean>('isAuthenticated').emit(false);
|
||||
}
|
||||
}
|
46
webapp/src/services/bank.service.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { RecordSubscription, UnsubscribeFunc } from 'pocketbase';
|
||||
import { Collections, TransactionsResponse } from '../types/pocketbase.types';
|
||||
import { PocketbaseService } from './pocketbase.service';
|
||||
|
||||
const COLLECTION = PocketbaseService.getApi().collection(Collections.Transactions);
|
||||
|
||||
const DEPOSIT_LABEL = 'Bargeldeinzahlung';
|
||||
const SALARY_LABEL = 'Gehalt';
|
||||
const WITHDRAW_LABEL = 'Bargeldauszahlung';
|
||||
const OPEN_ACCOUNT_LABEL = 'Kontoeröffnung';
|
||||
|
||||
export class BankService {
|
||||
public static async getTransactions(accountId: string): Promise<TransactionsResponse[]> {
|
||||
return COLLECTION.getFullList({ filter: `account = "${accountId}"`, sort: '-created' });
|
||||
}
|
||||
public static async addTransaction(accountId: string, amount: number, type: TransactionType): Promise<TransactionsResponse> {
|
||||
let label = '';
|
||||
switch (type) {
|
||||
case TransactionType.DEPOSIT:
|
||||
label = DEPOSIT_LABEL;
|
||||
break;
|
||||
case TransactionType.SALARY:
|
||||
label = SALARY_LABEL;
|
||||
break;
|
||||
case TransactionType.WITHDRAW:
|
||||
label = WITHDRAW_LABEL;
|
||||
break;
|
||||
case TransactionType.OPEN_ACCOUNT:
|
||||
label = OPEN_ACCOUNT_LABEL;
|
||||
break;
|
||||
}
|
||||
return COLLECTION.create({ account: accountId, label, amount: amount * 100 });
|
||||
}
|
||||
public static async subscribeToTransactionChanges(callback: (data: RecordSubscription<TransactionsResponse>)=> void): Promise<UnsubscribeFunc> {
|
||||
return await COLLECTION.subscribe<TransactionsResponse>('*', (data) => {
|
||||
callback(data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT,
|
||||
SALARY,
|
||||
WITHDRAW,
|
||||
OPEN_ACCOUNT,
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
export class CurrencyService {
|
||||
public static toString(value: number): string {
|
||||
return `${value.toLocaleString('de-DE', {
|
||||
return `${(value / 100).toLocaleString('de-DE', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} Öro`;
|
||||
}
|
||||
public static toSignedString(value: number): string {
|
||||
return `${value > 0 ? '+' : ''}${value.toLocaleString('de', {
|
||||
return `${(value / 100) > 0 ? '+' : ''}${(value / 100).toLocaleString('de', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} Öro`;
|
26
webapp/src/services/date.service.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
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',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
public static toShortString(date: Date): string {
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
});
|
||||
}
|
||||
public static toTime(date: Date): string {
|
||||
return date.toLocaleTimeString('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
13
webapp/src/services/pocketbase.service.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import PocketBase from 'pocketbase';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pb = new PocketBase((import.meta as any).env.MODE === 'development' ? 'http://127.0.0.1:8090' : '/');
|
||||
|
||||
export class PocketbaseService {
|
||||
public static getApi() {
|
||||
return pb;
|
||||
}
|
||||
public static getHealth() {
|
||||
return pb.health.check();
|
||||
}
|
||||
}
|
88
webapp/src/types/pocketbase.types.ts
Normal file
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* This file was @generated using pocketbase-typegen
|
||||
*/
|
||||
|
||||
export enum Collections {
|
||||
Accounts = "accounts",
|
||||
AccountsData = "accountsData",
|
||||
AccountsList = "accountsList",
|
||||
Transactions = "transactions",
|
||||
}
|
||||
|
||||
// Alias types for improved usability
|
||||
export type IsoDateString = string
|
||||
export type RecordIdString = string
|
||||
export type HTMLString = string
|
||||
|
||||
// System fields
|
||||
export type BaseSystemFields<T = never> = {
|
||||
id: RecordIdString
|
||||
created: IsoDateString
|
||||
updated: IsoDateString
|
||||
collectionId: string
|
||||
collectionName: Collections
|
||||
expand?: T
|
||||
}
|
||||
|
||||
export type AuthSystemFields<T = never> = {
|
||||
email: string
|
||||
emailVisibility: boolean
|
||||
username: string
|
||||
verified: boolean
|
||||
} & BaseSystemFields<T>
|
||||
|
||||
// Record types for each collection
|
||||
|
||||
export type AccountsRecord = {
|
||||
accountNumber: string
|
||||
firstName: string
|
||||
lastName: string
|
||||
lastCheckIn?: IsoDateString
|
||||
personalData: RecordIdString
|
||||
}
|
||||
|
||||
export type AccountsDataRecord = {
|
||||
birthday: IsoDateString
|
||||
email: string
|
||||
firstNameParent: string
|
||||
lastNameParent: string
|
||||
street: string
|
||||
zipCode: number
|
||||
city: string
|
||||
phone: string
|
||||
}
|
||||
|
||||
export type AccountsListRecord<Tbalance = unknown, Tname = unknown> = {
|
||||
accountNumber: string
|
||||
name?: null | Tname
|
||||
lastCheckIn?: IsoDateString
|
||||
balance?: null | Tbalance
|
||||
}
|
||||
|
||||
export type TransactionsRecord = {
|
||||
account: RecordIdString
|
||||
label: string
|
||||
amount?: number
|
||||
}
|
||||
|
||||
// Response types include system fields and match responses from the PocketBase API
|
||||
export type AccountsResponse<Texpand = unknown> = Required<AccountsRecord> & BaseSystemFields<Texpand>
|
||||
export type AccountsDataResponse = Required<AccountsDataRecord> & BaseSystemFields
|
||||
export type AccountsListResponse<Tbalance = unknown, Tname = unknown> = Required<AccountsListRecord<Tbalance, Tname>> & BaseSystemFields
|
||||
export type TransactionsResponse<Texpand = unknown> = Required<TransactionsRecord> & BaseSystemFields<Texpand>
|
||||
|
||||
// Types containing all Records and Responses, useful for creating typing helper functions
|
||||
|
||||
export type CollectionRecords = {
|
||||
accounts: AccountsRecord
|
||||
accountsData: AccountsDataRecord
|
||||
accountsList: AccountsListRecord
|
||||
transactions: TransactionsRecord
|
||||
}
|
||||
|
||||
export type CollectionResponses = {
|
||||
accounts: AccountsResponse
|
||||
accountsData: AccountsDataResponse
|
||||
accountsList: AccountsListResponse
|
||||
transactions: TransactionsResponse
|
||||
}
|
41
webapp/tailwind.config.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
module.exports = {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('daisyui')],
|
||||
daisyui: {
|
||||
themes: [
|
||||
'light',
|
||||
'night',
|
||||
{
|
||||
// light: {
|
||||
// 'primary': '#EE8039',
|
||||
// 'secondary': '#BFB0D6',
|
||||
// 'accent': '#F9F6F0',
|
||||
// 'neutral': '#1E293B',
|
||||
// 'base-100': '#F9F6F0',
|
||||
// 'info': '#CBE0DE',
|
||||
// 'success': '#9DC278',
|
||||
// 'warning': '#F6B64B',
|
||||
// 'error': '#DD513C',
|
||||
// },
|
||||
// night: {
|
||||
// 'primary': '#EE8039',
|
||||
// 'secondary': '#BFB0D6',
|
||||
// 'accent': '#F9F6F0',
|
||||
// 'neutral': '#1E293B',
|
||||
// 'base-100': '#4E4E4E',
|
||||
// 'info': '#CBE0DE',
|
||||
// 'success': '#9DC278',
|
||||
// 'warning': '#F6B64B',
|
||||
// 'error': '#DD513C',
|
||||
// },
|
||||
},
|
||||
],
|
||||
darkTheme: 'night',
|
||||
},
|
||||
};
|