1
0
Fork 0
forked from Kispi/Core

Compare commits

..

1 commit

Author SHA1 Message Date
8dbf99050b feat(webapp): inital mopidy testing 2022-07-18 12:12:30 +02:00
121 changed files with 3203 additions and 10847 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 155 KiB

View file

@ -2,35 +2,21 @@ kind: pipeline
name: default
steps:
- name: install-webapp
image: node:lts-alpine
- name: install
image: node:18-alpine
commands:
- cd webapp
- corepack pnpm@8.6.6 install
- corepack pnpm@7.5.1 install
- name: lint-webapp
image: node:lts-alpine
- name: build
image: node:18-alpine
commands:
- cd webapp
- corepack pnpm@8.6.6 lint
- name: version-webapp
image: node:lts-alpine
commands:
- cd webapp
- corepack pnpm@8.6.6 generate-version
- name: build-webapp
image: node:lts-alpine
commands:
- cd webapp
- corepack pnpm@8.6.6 build
- corepack pnpm@7.5.1 build
- name: deploy
image: plugins/docker
settings:
registry: registry.cliffbreak.de
repo: registry.cliffbreak.de/hgoe-sas
repo: registry.cliffbreak.de/kispi-core
username:
from_secret: docker_username
password:

View file

@ -1,2 +0,0 @@
# Paths
WEBAPP="../webapp/dist"

View file

@ -4,7 +4,6 @@ module.exports = {
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:vue-scoped-css/vue3-recommended',
'plugin:tailwindcss/recommended',
],
parser: 'vue-eslint-parser',
env: {
@ -53,7 +52,7 @@ module.exports = {
'asyncArrow': 'always',
}],
'space-in-parens': ['error', 'never'],
'spaced-comment': ['error', 'always', { 'markers': ['/'] }],
'spaced-comment': ['error', 'always'],
},
overrides: [
{
@ -71,8 +70,7 @@ module.exports = {
parserOptions: {
parser: '@typescript-eslint/parser',
sourceType: 'module',
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
'project': 'tsconfig.json',
extraFileExtensions: ['.vue'],
},
plugins: [
@ -140,7 +138,6 @@ module.exports = {
'svg': 'always',
'math': 'always',
}],
// 'tailwindcss/no-custom-classname': 'off',
},
},
],

9
.gitignore vendored
View file

@ -20,12 +20,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Pocketbase-Data
pb_data
# Environment variables
.env
*.csv
*.xlsx

20
.vscode/settings.json vendored
View file

@ -1,32 +1,20 @@
{
"cSpell.words": [
"appwrite",
"camelcase",
"checkin",
"classname",
"cliffbreak",
"corepack",
"daisyui",
"esnext",
"Gitea",
"github",
"godotenv",
"heroicons",
"hgoe",
"HGOE",
"middlewares",
"kispi",
"mopidy",
"Mopidy",
"pnpm",
"pocketbase",
"tailwindcss",
"typegen",
"vite",
"vitejs",
"vueuse"
],
"eslint.format.enable": true,
"eslint.workingDirectories": [
{
"directory": "webapp",
"changeProcessCWD": true
}
]
}

View file

@ -26,7 +26,7 @@ docs(changelog): update change log to beta.5
style(webapp): reorder imports
```
```
fix(server): need to depend on latest rxjs and zone.js
fix(appwrite): 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
* **server** used for changes made in the server
* **appwrite** used for changes made in the Appwrite cloud functions
* **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`)

View file

@ -1,25 +1,7 @@
## Build
FROM golang:1.20-buster as build
FROM steebchen/nginx-spa:stable
WORKDIR /app
COPY dist/ /app
COPY server/go.mod ./
COPY server/go.sum ./
COPY server/pkg ./pkg
RUN go mod download
EXPOSE 80
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"]
CMD ["nginx"]

View file

@ -1,11 +1,11 @@
<h1 align="center">🔥 Schule als Staat - Hohenlohe Gymnasium Öhringen 🔥</h1>
<h3 align="center">User administration, user check and customs, banking system, employer portal and an admin portal</h1>
<h1 align="center">🔥 Core - Kinderspielstadt Öhringen 🔥</h1>
<h3 align="center">Verwaltung + Bank + Radio = ❤️</h1>
<p align="center">
<img height="600px" src=".docs/screenshot.png" alt="screenshot"/>
</p>
This is our repository containing all required resources to run the "Core" WebApp from the "Hohenlohe Gymnasium Öhringen - Schule als Staat" project.
This repository also contains all the required Pocketbase files to run the backend server.
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.
## 🚀 Getting Started
@ -15,70 +15,32 @@ 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 `Go` is required to run the built in server.
Also a hosted instance of `Appwrite` is required.
### 📦 Installing
At first clone this repository to your local machine by using
```
git clone https://git.cliffbreak.de/SimGie/HGOE-SaS.git
git clone https://git.cliffbreak.de/Kontast/Core.git
```
Change to the cloned repository
```
cd HGOE-Sas
cd Core
```
Copy the example environment file
To install all required packages run
```
cp .env.example .env
```
To install all required webapp packages run
```
cd webapp && pnpm install
pnpm install
```
To start the webapp in development mode run the following npm script
```
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
pnpm run dev
```
## 🧑‍💻 Configure Visual Studio Code
@ -96,9 +58,7 @@ 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
* [Go](https://go.dev/) - The Backend Programming Language
* [Pocketbase](https://pocketbase.io/) - The Backend Framework
* [Appwrite](https://docs.mongodb.com/) - The hosted Backend used for this application
## 🤵 Authors

View file

@ -8,10 +8,10 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-TileColor" content="#ffc40d">
<meta name="theme-color" content="#ffffff">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Schule als Staat | Hohenlohe Gymnasium Öhringen</title>
<title>Kinderspielstadt Öhringen</title>
</head>
<body>

37
package.json Normal file
View file

@ -0,0 +1,37 @@
{
"name": "kispi-core",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"lint": "eslint -c .eslintrc.js src/",
"preview": "vite preview"
},
"dependencies": {
"@heroicons/vue": "^1.0.6",
"@vueuse/core": "^8.9.2",
"daisyui": "^2.19.0",
"events": "^3.3.0",
"mopidy": "^1.3.0",
"vue": "^3.2.37",
"vue-router": "4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.3",
"@typescript-eslint/eslint-plugin": "^5.30.6",
"@typescript-eslint/parser": "^5.30.6",
"@vitejs/plugin-vue": "^2.3.3",
"autoprefixer": "^10.4.7",
"eslint": "^8.19.0",
"eslint-plugin-vue": "^9.2.0",
"eslint-plugin-vue-scoped-css": "^2.2.0",
"postcss": "^8.4.14",
"sass": "^1.53.0",
"tailwindcss": "^3.1.6",
"typescript": "^4.7.4",
"vite": "^2.9.14",
"vue-eslint-parser": "^9.0.3",
"vue-tsc": "^0.38.5"
}
}

2022
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View file

@ -3,7 +3,7 @@
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
<TileColor>#b91d47</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -0,0 +1,162 @@
<?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>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,6 +1,6 @@
{
"name": "",
"short_name": "",
"name": "Kinderspielstadt Öhringen",
"short_name": "KiSpi Öhringen",
"icons": [
{
"src": "/android-chrome-192x192.png",

View file

@ -1,99 +0,0 @@
module cliffbreak.de/hgoe-sas-server
go 1.20
require (
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v5 v5.0.0-20220201181537-ed2888cfa198
github.com/pocketbase/pocketbase v0.16.8
github.com/pterm/pterm v0.12.62
)
require (
atomicgo.dev/cursor v0.1.2 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.0.2 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go v1.44.299 // indirect
github.com/aws/aws-sdk-go-v2 v1.18.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.27 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.26 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.71 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.34 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.26 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.29 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.3 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.36.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.2 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/domodwyer/mailyak/v3 v3.6.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/ganigeorgiev/fexpr v0.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/wire v0.5.0 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/gookit/color v1.5.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.17 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/pocketbase/dbx v1.10.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opencensus.io v0.24.0 // indirect
gocloud.dev v0.30.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/image v0.9.0 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.11.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.130.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.56.2 // indirect
google.golang.org/protobuf v1.31.0 // indirect
lukechampine.com/uint128 v1.3.0 // indirect
modernc.org/cc/v3 v3.41.0 // indirect
modernc.org/ccgo/v3 v3.16.14 // indirect
modernc.org/libc v1.24.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.6.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/sqlite v1.24.0 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.1.0 // indirect
)

File diff suppressed because it is too large Load diff

View file

@ -1,40 +0,0 @@
package main
import (
"net/http"
"time"
"cliffbreak.de/hgoe-sas-server/pkg/middlewares"
"cliffbreak.de/hgoe-sas-server/pkg/utils"
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase"
"github.com/pocketbase/pocketbase/core"
"github.com/pterm/pterm"
)
func main() {
pterm.Info.Println("Loading environment variables...")
utils.LoadEnv()
pterm.Info.Println("Starting PocketBase server...")
app := pocketbase.New()
app.OnBeforeServe().Add(middlewares.ServeSPA(utils.GetEnv("WEBAPP", "./webapp")))
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
e.Router.AddRoute(echo.Route{
Method: http.MethodGet,
Path: "/api/time",
Handler: func(c echo.Context) error {
return c.String(http.StatusOK, time.Now().Format(time.RFC3339))
},
})
return nil
})
if err := app.Start(); err != nil {
pterm.Fatal.Println(err)
}
}

View file

@ -1,458 +0,0 @@
[
{
"id": "s854d2w72fvyl54",
"name": "companies",
"type": "auth",
"system": false,
"schema": [
{
"id": "h5ogbj93",
"name": "accountNumber",
"type": "text",
"system": false,
"required": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "cvcgnf4x",
"name": "name",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "ujqd3vik",
"name": "earlyShiftPayed",
"type": "date",
"system": false,
"required": false,
"options": {
"min": "",
"max": ""
}
},
{
"id": "t1kakf3f",
"name": "lateShiftPayed",
"type": "date",
"system": false,
"required": false,
"options": {
"min": "",
"max": ""
}
}
],
"indexes": [
"CREATE INDEX `idx_eX1Vqqz` ON `companies` (`accountNumber`)"
],
"listRule": "",
"viewRule": null,
"createRule": null,
"updateRule": "(@request.data.username = null && @request.data.email = null && @request.data.accountNumber = null && @request.data.created = null && @request.data.updated = null && @request.data.name = null && @request.data.emailVisibility = null && @request.data.verified = null) && id = @request.auth.id",
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "c3zz98wpbn7m6zw",
"name": "accountsList",
"type": "view",
"system": false,
"schema": [
{
"id": "ne8rkvlr",
"name": "accountNumber",
"type": "text",
"system": false,
"required": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "paqieqwu",
"name": "name",
"type": "json",
"system": false,
"required": false,
"options": {}
},
{
"id": "7sij0xcm",
"name": "grade",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "txpycyit",
"name": "lastCheckIn",
"type": "date",
"system": false,
"required": false,
"options": {
"min": "",
"max": ""
}
},
{
"id": "og7zs0ox",
"name": "balance",
"type": "json",
"system": false,
"required": false,
"options": {}
}
],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"query": "SELECT\n a.id AS id,\n a.accountNumber AS accountNumber,\n (a.firstName || ' ' || a.lastName) AS name,\n a.grade AS grade,\n a.lastCheckIn AS lastCheckIn,\n SUM(t.amount) AS balance\nFROM\n accounts AS a\nLEFT JOIN\n transactions AS t\nON\n a.id = t.account\nGROUP BY\n a.id\nORDER BY\n a.grade, a.lastName"
}
},
{
"id": "t4gewf713jqhz3i",
"name": "settings",
"type": "base",
"system": false,
"schema": [
{
"id": "z7pmr7wm",
"name": "minWage",
"type": "number",
"system": false,
"required": true,
"options": {
"min": 0,
"max": null
}
},
{
"id": "7wzv9qum",
"name": "incomeTax",
"type": "number",
"system": false,
"required": true,
"options": {
"min": 0,
"max": null
}
},
{
"id": "3pgjcfai",
"name": "incomeTaxRecipient",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "s854d2w72fvyl54",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"id": "dawtvakx",
"name": "maxWage",
"type": "number",
"system": false,
"required": true,
"options": {
"min": 0,
"max": null
}
},
{
"id": "ftzmu1na",
"name": "radioUrl",
"type": "url",
"system": false,
"required": true,
"options": {
"exceptDomains": [],
"onlyDomains": []
}
}
],
"indexes": [],
"listRule": "",
"viewRule": "",
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "74ftooxenpeq14b",
"name": "accounts",
"type": "base",
"system": false,
"schema": [
{
"id": "as1gvc1r",
"name": "accountNumber",
"type": "text",
"system": false,
"required": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "vxeq20lk",
"name": "firstName",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "bsinpdk7",
"name": "lastName",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "4lw6gmtc",
"name": "grade",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "syfivtki",
"name": "lastCheckIn",
"type": "date",
"system": false,
"required": false,
"options": {
"min": "",
"max": ""
}
},
{
"id": "gwrzizpu",
"name": "company",
"type": "relation",
"system": false,
"required": false,
"options": {
"collectionId": "s854d2w72fvyl54",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"id": "rtvjzpxw",
"name": "shift",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "ggx1gyeq",
"name": "wage",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [
"CREATE INDEX `idx_mPdvrAQ` ON `accounts` (`accountNumber`)"
],
"listRule": "",
"viewRule": null,
"createRule": null,
"updateRule": "(@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.grade = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.data.shift = null && @request.data.wage = null) || (@request.data.id = null && @request.data.accountNumber = null && @request.data.firstName = null && @request.data.lastName = null && @request.data.created = null && @request.data.updated = null && @request.data.company = null && @request.auth.id = company.id)",
"deleteRule": null,
"options": {}
},
{
"id": "w1au07idupp27qv",
"name": "bankers",
"type": "auth",
"system": false,
"schema": [],
"indexes": [],
"listRule": null,
"viewRule": null,
"createRule": null,
"updateRule": null,
"deleteRule": null,
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": true,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
},
{
"id": "w85pgrtrmovf916",
"name": "transactions",
"type": "base",
"system": false,
"schema": [
{
"id": "5aeh5giq",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "74ftooxenpeq14b",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": [
"firstName",
"lastName"
]
}
},
{
"id": "2bzqyeuk",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "zsx9i8w7",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"viewRule": "@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id",
"createRule": "(@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id) || (@request.auth.id != \"\" && @collection.companies.id ?= @request.auth.id && amount > 0)",
"updateRule": null,
"deleteRule": null,
"options": {}
},
{
"id": "5msnfxat1sc2c1r",
"name": "companyTransactions",
"type": "base",
"system": false,
"schema": [
{
"id": "7rnyupog",
"name": "account",
"type": "relation",
"system": false,
"required": true,
"options": {
"collectionId": "s854d2w72fvyl54",
"cascadeDelete": false,
"minSelect": null,
"maxSelect": 1,
"displayFields": []
}
},
{
"id": "ruby9fp9",
"name": "label",
"type": "text",
"system": false,
"required": true,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "4hiu46ib",
"name": "amount",
"type": "number",
"system": false,
"required": false,
"options": {
"min": null,
"max": null
}
}
],
"indexes": [],
"listRule": "",
"viewRule": "",
"createRule": "(@request.auth.id != \"\" && @collection.bankers.id ?= @request.auth.id) || (@request.auth.id != \"\" && @collection.companies.id ?= @request.auth.id)",
"updateRule": null,
"deleteRule": null,
"options": {}
}
]

View file

@ -1,24 +0,0 @@
package middlewares
import (
"github.com/labstack/echo/v5"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"github.com/pterm/pterm"
)
func ServeSPA(webAppPath string) func(e *core.ServeEvent) error {
return func(e *core.ServeEvent) error {
pterm.Info.Print("Serving SPA from ", webAppPath)
subFs := echo.MustSubFS(e.Router.Filesystem, webAppPath)
e.Router.GET("/*", apis.StaticDirectoryHandler(subFs, false))
originalErrorHandler := e.Router.HTTPErrorHandler
e.Router.HTTPErrorHandler = func(c echo.Context, err error) {
if c.Path() == "/*" && err == echo.ErrNotFound {
err = c.FileFS("index.html", subFs)
}
originalErrorHandler(c, err)
}
return nil
}
}

View file

@ -1,33 +0,0 @@
package utils
import (
"os"
"strings"
"github.com/joho/godotenv"
"github.com/pterm/pterm"
)
// GetEnv returns the value of the environment variable named by the key.
// If the env key is not present, it returns the default value.
func GetEnv(key, def string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return def
}
func LoadEnv() {
err := godotenv.Load("../.env")
if err != nil {
pterm.Info.Println("No .env file found. Proceeding...")
}
// Add env variables without "VITE_" prefix.
for _, e := range os.Environ() {
pair := strings.Split(e, "=")
if strings.HasPrefix(pair[0], "VITE_") {
os.Setenv(pair[0][5:], pair[1])
}
}
}

62
src/App.vue Normal file
View file

@ -0,0 +1,62 @@
<template>
<MoleculeNavigationDrawer
:drawer-id="drawerId"
:navigation-entries="navigationEntries"
>
<RouterView />
</MoleculeNavigationDrawer>
</template>
<script setup lang="ts">
import {
IdentificationIcon,
LibraryIcon,
LogoutIcon,
MusicNoteIcon,
TrendingUpIcon,
UserIcon,
} from '@heroicons/vue/outline';
import { INavigationEntry } from './interfaces/navigation-entry.interface';
import MoleculeNavigationDrawer from './components/molecules/MoleculeNavigationDrawer.vue';
const drawerId = 'default-drawer';
const navigationEntries: INavigationEntry[] = [
{
name: 'Stammdaten',
icon: UserIcon,
to: '/data',
},
{
name: 'Einstempeln',
icon: IdentificationIcon,
to: '/checkin',
},
{
name: 'Bank',
icon: LibraryIcon,
to: '/bank',
},
{
name: 'Börse',
icon: TrendingUpIcon,
to: '/stocks',
disabled: true,
},
{
name: 'Radio',
icon: MusicNoteIcon,
to: '/radio',
},
{
name: 'divider',
},
{
name: 'Abmelden',
icon: LogoutIcon,
to: '/logout',
},
];
</script>
<style lang="scss" scoped>
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
src/assets/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View file

@ -0,0 +1,20 @@
<template>
<div>
<AtomInput
ref="input"
type="number"
class="text-right pr-16"
step="1"
/>
<span class="text-sm opacity-50 absolute -ml-16 top-2/4 -mt-8 pointer-events-none">,00 Öro</span>
</div>
</template>
<script lang="ts" setup>
import AtomInput from './AtomInput.vue';
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,40 @@
<template>
<input
ref="input"
:type="type"
:placeholder="placeholder"
:value="modelValue"
class="input input-bordered w-full"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
<script lang="ts" setup>
defineProps({
type: {
type: String,
default: 'text',
},
placeholder: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
});
defineEmits(['update:modelValue']);
</script>
<style lang="scss" scoped>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
</style>

View file

@ -0,0 +1,18 @@
<template>
<img
v-if="isDark"
src="../../assets/logo_white.png"
/>
<img
v-else
src="../../assets/logo.png"
/>
</template>
<script lang="ts" setup>
import { isDark } from '../../utils/darkMode';
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,38 @@
<template>
<Teleport to="body">
<input
:id="id"
type="checkbox"
class="modal-toggle"
@change="$emit('open', {id, open: ($event.target as HTMLInputElement).checked})"
/>
<div class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">{{ title }}</h3>
<p class="py-4"><slot /></p>
<div class="modal-action">
<slot name="action" />
</div>
</div>
</div>
</Teleport>
</template>
<script lang="ts" setup>
defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
});
defineEmits(['open']);
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,8 +1,5 @@
<template>
<select
class="select-bordered select w-full"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
>
<select class="select select-bordered w-full">
<option
disabled
selected
@ -25,13 +22,7 @@ defineProps({
type: Array as PropType<string[]>,
required: true,
},
modelValue: {
type: String,
default: '',
},
});
defineEmits(['update:modelValue']);
</script>
<style lang="scss" scoped>

View file

@ -20,11 +20,11 @@ import { FunctionalComponent, PropType } from 'vue';
defineProps({
onIcon: {
type: Function as PropType<FunctionalComponent>,
type: Object as PropType<FunctionalComponent>,
required: true,
},
offIcon: {
type: Function as PropType<FunctionalComponent>,
type: Object as PropType<FunctionalComponent>,
required: true,
},
checked: {

View file

@ -0,0 +1,91 @@
<template>
<AtomModal
:id="id"
title="Account hinzufügen"
>
<div class="flex flex-col gap-4">
<h4 class="text-md">Daten</h4>
<div class="flex gap-4">
<AtomInput
v-model="firstName"
placeholder="Vorname"
/>
<AtomInput placeholder="Nachname" />
</div>
<AtomInput
placeholder="Kontonummer"
type="number"
/>
<AtomInput
placeholder="Geburtsdatum"
type="number"
/>
<div class="flex gap-4">
<AtomInput placeholder="Straße" />
<AtomInput
placeholder="Hnr."
class="w-3/12"
/>
</div>
<div class="flex gap-4">
<AtomInput
placeholder="Postleitzahl"
class="w-6/12"
/>
<AtomInput placeholder="Stadt" />
</div>
<h4 class="text-md">Kontakt</h4>
<div class="flex gap-4">
<AtomSelect
placeholder="Label"
:options="['Festnetz', 'Mobil', 'Arbeit']"
class="w-4/12"
/>
<AtomInput placeholder="Telefonnummer" />
</div>
</div>
<template #action>
<label
ref="abortButton"
class="btn gap-2"
:for="id"
>
<XCircleIcon class="w-6 h-6" />
Abbrechen
</label>
<button
class="btn gap-2"
@click="handleSubmit()"
>
<CheckCircleIcon class="w-6 h-6" />
Speichern
</button>
</template>
</AtomModal>
</template>
<script lang="ts" setup>
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
import AtomModal from '../atoms/AtomModal.vue';
import AtomInput from '../atoms/AtomInput.vue';
import AtomSelect from '../atoms/AtomSelect.vue';
import { ref } from 'vue';
const firstName = ref('');
defineProps({
id: {
type: String,
required: true,
},
});
function handleSubmit() {
console.log('TODO: Submit');
console.log(firstName.value);
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,52 @@
<template>
<div class="overflow-x-auto">
<table class="table w-full table-zebra">
<thead class="sticky top-0">
<tr>
<th v-for="(_, key) of data[0]">{{ key }}</th>
</tr>
</thead>
<tbody>
<tr v-for="entries in data">
<td
v-for="(entry, _, key) of entries"
:class="key >= 6 ? 'text-center' : null"
>
<span v-if="key < 6">{{ entry }}</span>
<button
v-if="key == 6"
class="btn btn-sm btn-ghost p-1"
>
<DocumentTextIcon
class="h-6 w-6"
/>
</button>
<button
v-if="key == 7"
class="btn btn-sm btn-ghost p-1"
>
<CurrencyDollarIcon
class="h-6 w-6"
/>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>import { PropType } from 'vue';
import { CurrencyDollarIcon, DocumentTextIcon } from '@heroicons/vue/outline';
defineProps({
data: {
type: Array as PropType<object[]>,
required: true,
},
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,33 +1,32 @@
<template>
<AtomModal
:id="id"
ref="modal"
:title="title"
@close="handleClose"
@open="handleOpen"
>
<div class="flex place-items-center justify-between">
<div class="flex justify-between place-items-center">
<span>{{ inputLabel }}</span>
<AtomCurrencyInput
v-model="amount"
ref="input"
class="w-4/12"
:max="1000"
@keyup.esc="modal?.close()"
@keyup.enter="handleSubmit()"
@keyup.esc="abortButton.click()"
/>
</div>
<template #action>
<label
ref="abortButton"
class="btn gap-2"
:for="id"
@click="modal?.close()"
>
<XCircleIcon class="h-6 w-6" />
<XCircleIcon class="w-6 h-6" />
Abbrechen
</label>
<button
class="btn gap-2"
@click="handleSubmit()"
>
<CheckCircleIcon class="h-6 w-6" />
<CheckCircleIcon class="w-6 h-6" />
{{ actionLabel }}
</button>
</template>
@ -35,13 +34,13 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/outline';
import AtomModal from '../atoms/AtomModal.vue';
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
import { ref } from 'vue';
const modal = ref<InstanceType<typeof AtomModal>>();
const amount = ref();
const input = ref();
const abortButton = ref();
defineProps({
id: {
@ -64,22 +63,18 @@ defineProps({
const emit = defineEmits(['submit']);
function handleClose() {
amount.value = '';
function handleOpen(event: {id: string, open: boolean}) {
if(event.open) {
// This is (currently) the only method to focus a input field inside a custom component.
input.value.$refs.input.$refs.input.value = '';
input.value.$refs.input.$refs.input.focus();
}
}
function handleSubmit() {
if(amount.value <= 0) {
return;
emit('submit', input.value.$refs.input.$refs.input.value);
abortButton.value.click();
}
emit('submit', amount.value);
}
defineExpose({
show: () => modal.value?.show(),
close: () => modal.value?.close(),
isOpen: () => modal.value?.isOpen(),
});
</script>
<style lang="scss" scoped>

View file

@ -0,0 +1,97 @@
<template>
<div class="drawer drawer-mobile">
<input
:id="drawerId"
type="checkbox"
class="drawer-toggle"
/>
<div class="drawer-content flex flex-col bg-base-300 h-screen">
<div
class="navbar shadow-lg bg-neutral-focus text-neutral-content lg:hidden sticky top-0 z-50 max-w-7xl place-self-center"
>
<div class="flex-none">
<label
:for="drawerId"
class="btn btn-ghost rounded-btn lg:hidden"
>
<MenuIcon class="h-7 w-7" />
</label>
<label
tabindex="0"
class="btn btn-ghost avatar"
>
<AtomLogo class="w-12" />
</label>
</div>
</div>
<slot />
</div>
<div class="drawer-side">
<label
:for="drawerId"
class="drawer-overlay"
/>
<ul class="menu p-4 overflow-y-auto w-64">
<li class="pointer-events-none">
<a>
<AtomLogo class="w-28" />
</a>
</li>
<li
v-for="navigationEntry of navigationEntries"
:class="getNavigationEntryClass(navigationEntry)"
>
<component
:is="!navigationEntry.to || navigationEntry.disabled ? 'span' : 'router-link'"
v-if="navigationEntry.name !== 'divider'"
:to="navigationEntry.to"
active-class="active"
>
<component
:is="navigationEntry.icon"
class="h-6 w-6"
/>{{ navigationEntry.name }}
</component>
</li>
<AtomSwap
class="mt-auto place-self-end"
:on-icon="SunIcon"
:off-icon="MoonIcon"
:checked="isDark"
@change="toggleDark()"
/>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType } from 'vue';
import { isDark, toggleDark } from '../../utils/darkMode';
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
import { MenuIcon, MoonIcon, SunIcon } from '@heroicons/vue/outline';
import AtomLogo from '../atoms/AtomLogo.vue';
import AtomSwap from '../atoms/AtomSwap.vue';
defineProps({
drawerId: {
type: String,
required: true,
},
navigationEntries: {
type: Array as PropType<INavigationEntry[]>,
required: true,
},
});
function getNavigationEntryClass(navigationEntry: INavigationEntry) {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
'menu-title': navigationEntry.name === 'divider',
disabled: navigationEntry.disabled,
};
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,18 +1,18 @@
<template>
<table class="table-zebra table overflow-hidden rounded-lg">
<thead class="bg-base-200">
<table class="table table-zebra">
<thead>
<tr>
<th />
<th class="text-right text-lg normal-case text-base-content">{{ CurrencyService.toString(balance) }}</th>
<th class="normal-case text-lg text-right">{{ CurrencyService.toString(balance) }}</th>
</tr>
</thead>
<tbody class="bg-base-100">
<tbody>
<tr v-for="transaction in transactions">
<td>
<div class="flex items-center">
<div>
<div class="font-bold">{{ transaction.label }}</div>
<div>{{ DateService.toString(parseISO(transaction.created)) }}</div>
<div class="font-bold">{{ transaction.type }}</div>
<div>{{ DateService.toString(transaction.date) }}</div>
</div>
</div>
</td>
@ -35,14 +35,13 @@
<script lang="ts" setup>
import { PropType } from 'vue';
import { parseISO } from 'date-fns';
import { CurrencyService } from '../../services/currency.service';
import { DateService } from '../../services/date.service';
import { TransactionsResponse } from '../../types/pocketbase.types';
import { ITransaction } from '../../interfaces/transaction.interface';
import { CurrencyService } from '../services/currency.service';
import { DateService } from '../services/date.service';
defineProps({
transactions: {
type: Array as PropType<TransactionsResponse[]>,
type: Array as PropType<ITransaction[]>,
required: true,
},
balance: {

View file

@ -1,14 +1,14 @@
export class CurrencyService {
public static toString(value: number, suffix = true): string {
public static toString(value: number): string {
return `${(value / 100).toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}${suffix ? ' Batzen' : ''}`;
})} Öro`;
}
public static toSignedString(value: number): string {
return `${(value / 100) > 0 ? '+' : ''}${(value / 100).toLocaleString('de', {
return `${value > 0 ? '+' : ''}${(value / 100).toLocaleString('de', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})} Batzen`;
})} Öro`;
}
}

View file

@ -0,0 +1,11 @@
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',
});
}
}

View file

@ -0,0 +1,161 @@
<template>
<div class="hero-content flex-col text-center lg:p-6 pt-0">
<h1 class="text-8xl mt-5">{{ CurrencyService.toString(balance) }}</h1>
<h3 class="text-5xl font-bold mt-10">Kontoinhaber: John Doe</h3>
<h4 class="text-4xl mb-10">Kontonummer: 0000123456</h4>
<MoleculeTransactionTable
class="w-[42rem]"
:balance="balance"
:transactions="transactions"
/>
</div>
<div class="sticky flex place-content-center lg:gap-32 gap-16 bg-base-100 bottom-0 w-100 p-5">
<MoleculeInputModal
:id="depositModalId"
title="Einzahlung"
action-label="Einzahlen"
input-label="Einzuzahlender Betrag:"
@submit="handleDeposit"
/>
<label
class="btn gap-2"
:for="depositModalId"
>
<ChevronUpIcon class="w-6 h-6 text-success" />
Einzahlen
</label>
<MoleculeInputModal
:id="salaryModalId"
title="Gehalt"
action-label="Gehalt hinzufügen"
input-label="Gehalt Betrag:"
@submit="handleSalary"
/>
<label
class="btn gap-2"
:for="salaryModalId"
>
<CurrencyDollarIcon class="w-6 h-6 text-warning" />
Gehalt
</label>
<MoleculeInputModal
:id="withdrawModalId"
title="Auszahlung"
action-label="Auszahlen"
input-label="Auszuzahlender Betrag:"
@submit="handleWithdraw"
/>
<label
class="btn gap-2"
:for="withdrawModalId"
>
<ChevronDownIcon class="w-6 h-6 text-error" />
Auszahlen
</label>
</div>
</template>
<script lang="ts" setup>
import { ITransaction } from '../../interfaces/transaction.interface';
import { CurrencyService } from '../services/currency.service';
import { CurrencyDollarIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/outline';
import MoleculeTransactionTable from '../molecules/MoleculeTransactionTable.vue';
import MoleculeInputModal from '../molecules/MoleculeInputModal.vue';
import { onKeyStroke } from '@vueuse/core';
const depositModalId = 'deposit-modal';
const salaryModalId = 'salary-modal';
const withdrawModalId = 'withdraw-modal';
const balance = 13500;
const transactions: ITransaction[] = [
{
type: 'Kontoeröffnung',
date: new Date(2022, 7, 12, 17, 30, 0),
amount: 500,
},
{
type: 'Bargeldeinzahlung',
date: new Date(2022, 7, 12, 17, 30, 10),
amount: 10000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Gehalt',
date: new Date(2022, 7, 12, 17, 34, 10),
amount: 5000,
},
{
type: 'Bargeldauszahlung',
date: new Date(2022, 7, 15, 18, 26, 0),
amount: -2000,
},
].sort((a, b) => b.date.getTime() - a.date.getTime());
onKeyStroke('e', (e) => {
e.preventDefault();
console.log('e pressed');
// TODO: open deposit modal
});
function handleDeposit(amount: number) {
console.log('Deposit:', amount);
}
function handleSalary(amount: number) {
console.log('Salary:', amount);
}
function handleWithdraw(amount: number) {
console.log('Withdraw:', amount);
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,8 +1,8 @@
<template>
<div class="min-h-full lg:p-6">
<div class="hero min-h-full rounded-lg bg-base-200">
<div class="hero min-h-full bg-base-200 rounded-lg">
<div class="hero-content flex-col text-center">
<h1 class="text-6xl font-bold"><slot /></h1>
<h1 class="text-6xl font-bold">John Doe um 17:39:33</h1>
</div>
</div>
</div>

View file

@ -0,0 +1,199 @@
<template>
<div class="lg:p-6">
<MoleculeDataTable :data="data" />
<MoleculeAddAccountModal :id="addAccountModalId" />
<label
class="btn btn-primary btn-lg btn-circle shadow-2xl fixed bottom-8 right-8"
:for="addAccountModalId"
>
<PlusIcon class="w-7 h-7" />
</label>
</div>
</template>
<script lang="ts" setup>
import MoleculeDataTable from '../molecules/MoleculeDataTable.vue';
import { PlusIcon } from '@heroicons/vue/outline';
import MoleculeAddAccountModal from '../molecules/MoleculeAddAccountModal.vue';
const addAccountModalId = 'add-account-modal';
/* eslint-disable @typescript-eslint/naming-convention */
const data = [
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
{
'Kontonummer': '0000123456',
'Name': 'John Doe',
'Geburtsdatum': '13.11.1998',
'Kontostand': '150,00 Öro',
'Adresse': 'Test-Straße 1, 74613 Öhringen',
'Letzter': '12.07.2022 17:39',
'Kontakt': null,
'Trans.': null,
},
/* eslint-enable @typescript-eslint/naming-convention */
];
</script>
<style lang="scss" scoped>
</style>

View file

@ -0,0 +1,59 @@
<template>
<button @click="getTrack()">GetTrack</button>
<button @click="setPlayPause()">Play/Pause</button>
<div>Currently Playing: {{ `${artist} - ${track}` }}</div>
</template>
<script lang="ts" setup>
import Mopidy from 'mopidy';
import { onBeforeUnmount, ref } from 'vue';
const artist = ref('');
const track = ref('');
const mopidy = new Mopidy({
webSocketUrl: 'ws://localhost:6680/mopidy/ws/',
});
mopidy.on('state', console.log);
mopidy.on('event', console.log);
mopidy.on('state:online', () => {
getTrack();
});
mopidy.on('event:trackPlaybackStarted', (event) => {
track.value = event.tl_track.track.name;
artist.value = event.tl_track.track.artists.map((artist) => artist.name).join(', ');
});
async function getTrack() {
if(mopidy.playback) {
const currentTrack = await mopidy.playback.getCurrentTrack();
console.log(await mopidy.playback.getTimePosition());
if(currentTrack) {
track.value = currentTrack.name;
artist.value = currentTrack.artists.map((artist) => artist.name).join(', ');
}
}
}
async function setPlayPause() {
const currentState = await mopidy.playback?.getState();
if(currentState === 'playing') {
await mopidy.playback?.pause();
} else {
await mopidy.playback?.resume();
}
}
onBeforeUnmount(() => {
mopidy.close();
mopidy.off();
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,9 +1,5 @@
<template>
<iframe
src="/_"
frameborder="0"
class="h-full w-full"
/>
#Stocks
</template>
<script lang="ts" setup>

View file

3
src/index.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -0,0 +1,5 @@
export interface ITransaction {
type: string;
date: Date;
amount: number;
}

36
src/router/router.ts Normal file
View file

@ -0,0 +1,36 @@
import * as VueRouter from 'vue-router';
import DataView from '../components/views/DataView.vue';
import CheckInView from '../components/views/CheckInView.vue';
import BankView from '../components/views/BankView.vue';
import StocksView from '../components/views/StocksView.vue';
import RadioView from '../components/views/RadioView.vue';
const routes = [
{
path: '/data',
component: DataView,
},
{
path: '/checkin',
component: CheckInView,
},
{
path: '/bank',
component: BankView,
},
{
path: '/stocks',
component: StocksView,
},
{
path: '/radio',
component: RadioView,
},
];
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes,
});
export default router;

10
tailwind.config.js Normal file
View file

@ -0,0 +1,10 @@
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [require('@tailwindcss/typography'), require('daisyui')],
}

View file

@ -1,10 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
script: {
defineModel: true,
}
})]
plugins: [vue()]
})

View file

@ -1 +0,0 @@
src/types/pocketbase.types.ts

View file

@ -1,47 +0,0 @@
{
"name": "hgoe-sas",
"private": true,
"author": "Simon Giesel",
"version": "1.3.6",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"lint": "eslint -c .eslintrc.js src/",
"preview": "vite preview",
"typegen": "pocketbase-typegen --json ../server/pb_schema.json --out ./src/types/pocketbase.types.ts",
"optimize-svg": "svgo -f ./src/assets/svg/ --config=./src/assets/svg/svgo.config.js",
"generate-version": "node ./scripts/generate-version.js"
},
"dependencies": {
"@heroicons/vue": "^2.0.18",
"@vueuse/core": "^10.2.1",
"canvas-confetti": "^1.6.0",
"daisyui": "^3.2.1",
"date-fns": "^2.30.0",
"pocketbase": "^0.15.3",
"vue": "^3.3.4",
"vue-router": "4.2.4"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.9",
"@types/canvas-confetti": "^1.6.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"eslint-plugin-vue": "^9.15.1",
"eslint-plugin-vue-scoped-css": "^2.5.0",
"pocketbase-typegen": "^1.1.11",
"postcss": "^8.4.25",
"sass": "^1.63.6",
"svgo": "^3.0.2",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.6",
"vite": "^4.4.3",
"vue-eslint-parser": "^9.3.1",
"vue-tsc": "^1.8.4"
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,2 +0,0 @@
User-Agent: *
Disallow: /

View file

@ -1,94 +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="3407.000000pt" height="3407.000000pt" viewBox="0 0 3407.000000 3407.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,3407.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4410 33513 c-380 -51 -730 -280 -929 -609 -138 -227 -304 -646 -428
-1079 -35 -124 -98 -371 -106 -415 -3 -14 -10 -43 -16 -65 -15 -58 -98 -472
-106 -529 -3 -27 -8 -52 -9 -55 -2 -3 -6 -28 -10 -56 -3 -27 -8 -61 -11 -75
-3 -14 -10 -57 -16 -97 -5 -39 -12 -80 -14 -91 -2 -10 -6 -43 -10 -73 -3 -30
-8 -65 -10 -79 -10 -70 -15 -109 -20 -145 -10 -82 -24 -212 -30 -275 -3 -36
-8 -81 -10 -100 -2 -19 -7 -62 -10 -95 -3 -33 -7 -85 -10 -115 -12 -132 -15
-170 -20 -255 -3 -49 -8 -108 -10 -130 -2 -22 -7 -98 -10 -170 -4 -71 -8 -161
-10 -200 -26 -490 -26 -1706 0 -2155 2 -30 6 -122 10 -205 3 -82 8 -175 10
-205 21 -356 32 -516 40 -620 9 -107 14 -176 20 -255 9 -116 13 -173 20 -235
3 -30 8 -80 10 -110 3 -30 7 -77 10 -105 3 -27 7 -77 10 -110 12 -129 14 -151
20 -195 5 -46 9 -88 20 -195 3 -33 8 -73 10 -90 2 -16 7 -57 10 -90 3 -33 10
-94 15 -135 5 -41 12 -97 15 -125 3 -27 7 -66 10 -85 2 -19 7 -60 10 -90 3
-30 8 -66 10 -80 2 -14 7 -49 10 -79 4 -30 8 -61 10 -70 1 -9 6 -43 10 -76 3
-33 8 -76 11 -95 5 -41 23 -174 29 -210 3 -14 7 -50 11 -80 5 -55 7 -63 18
-130 3 -19 8 -53 11 -75 3 -22 7 -53 10 -70 3 -16 7 -48 10 -70 3 -22 7 -53
10 -70 3 -16 7 -48 10 -70 3 -22 12 -80 20 -130 8 -49 17 -108 20 -130 3 -22
10 -65 15 -95 5 -30 11 -68 14 -85 10 -67 19 -121 22 -140 2 -11 6 -36 9 -55
3 -19 7 -46 9 -60 2 -14 10 -59 16 -100 7 -41 14 -84 16 -95 4 -22 24 -138 29
-170 6 -34 16 -97 21 -119 2 -11 18 -100 34 -196 31 -179 57 -318 90 -495 11
-55 22 -115 25 -134 3 -19 7 -37 9 -40 2 -3 7 -28 10 -56 4 -27 11 -68 16 -90
5 -22 12 -56 15 -75 3 -19 17 -91 31 -160 14 -69 28 -136 30 -150 3 -14 13
-68 24 -120 20 -97 26 -128 35 -177 3 -16 10 -48 15 -73 13 -57 18 -82 30
-145 6 -27 14 -68 19 -90 18 -84 43 -201 47 -224 3 -12 21 -91 40 -175 20 -83
41 -178 49 -211 12 -55 74 -298 140 -550 14 -55 42 -154 61 -220 19 -66 37
-127 39 -135 16 -68 180 -596 260 -835 68 -202 201 -582 245 -695 11 -30 27
-71 34 -90 41 -110 109 -288 116 -305 5 -11 47 -114 94 -230 98 -243 277 -666
331 -780 34 -73 95 -207 95 -210 0 -2 25 -57 56 -122 30 -65 79 -167 107 -228
61 -130 433 -870 502 -1000 26 -49 70 -129 96 -177 27 -48 49 -89 49 -92 0 -5
269 -490 320 -576 133 -227 186 -319 200 -345 16 -29 294 -488 369 -608 20
-32 54 -87 76 -123 130 -210 627 -964 818 -1239 107 -156 301 -430 306 -435 4
-3 26 -34 51 -70 24 -36 50 -72 57 -80 6 -8 74 -100 150 -205 161 -222 635
-849 653 -865 3 -3 21 -25 40 -50 19 -25 40 -52 47 -60 7 -8 67 -85 135 -170
67 -85 128 -162 135 -170 7 -8 47 -58 89 -110 85 -105 105 -130 226 -276 45
-55 98 -119 116 -142 19 -23 42 -50 52 -62 10 -12 33 -39 52 -62 18 -23 38
-46 43 -52 6 -6 35 -40 65 -76 30 -36 57 -67 60 -70 3 -3 27 -32 54 -65 28
-33 68 -80 90 -105 23 -25 64 -72 92 -105 103 -120 136 -158 147 -170 7 -7 37
-41 67 -76 74 -86 220 -250 239 -270 9 -9 41 -44 71 -79 30 -34 66 -75 80 -90
72 -79 209 -228 235 -256 17 -17 47 -51 69 -75 43 -50 253 -273 396 -424 52
-55 106 -112 120 -126 224 -237 866 -885 1149 -1158 131 -127 410 -394 461
-441 19 -18 67 -62 106 -99 154 -145 372 -346 420 -388 12 -10 62 -55 112
-101 50 -45 109 -99 133 -120 23 -20 54 -48 69 -62 15 -14 56 -50 91 -80 35
-30 70 -62 79 -70 9 -8 47 -42 85 -75 39 -33 72 -63 75 -66 9 -9 411 -351 535
-454 66 -55 125 -104 131 -110 6 -5 28 -23 49 -40 21 -16 66 -53 99 -81 81
-67 432 -343 476 -374 19 -13 44 -33 55 -43 11 -10 55 -44 99 -75 43 -31 117
-85 165 -119 396 -286 662 -386 1006 -377 71 2 141 5 155 8 79 14 97 17 112
22 9 3 25 6 35 8 38 8 172 56 229 83 102 48 305 172 420 258 22 16 53 39 68
50 105 75 526 399 656 506 19 16 82 67 140 115 116 95 125 102 325 269 77 64
156 131 176 148 20 18 62 55 94 82 32 28 63 55 69 60 6 6 43 37 81 70 39 33
75 64 81 70 6 5 39 35 74 65 35 30 74 64 86 75 12 11 55 49 95 85 40 36 77 70
83 75 6 6 56 51 111 100 198 178 279 252 340 310 18 17 83 77 145 135 62 58
127 119 145 135 653 619 1260 1225 1825 1825 149 158 277 295 292 313 20 22
183 200 204 221 5 6 55 61 109 121 114 126 100 112 186 207 37 40 88 98 114
128 26 30 74 84 105 120 31 36 108 124 170 195 62 72 125 144 139 160 14 17
76 89 136 160 60 72 142 168 181 215 75 88 323 388 336 405 4 6 22 28 40 50
26 31 163 202 245 305 7 8 50 62 96 120 200 252 769 1001 857 1130 8 12 32 45
54 73 40 53 234 324 273 380 12 18 69 100 127 182 141 201 605 895 751 1125
202 317 566 913 677 1110 14 25 60 104 102 177 42 73 76 137 76 143 0 5 5 10
10 10 6 0 10 4 10 9 0 5 17 40 39 78 21 37 53 95 71 128 18 33 41 74 50 90 33
59 179 332 244 455 79 150 423 840 496 995 28 61 85 184 127 275 41 91 86 189
99 218 59 126 300 697 387 917 211 533 407 1083 552 1550 20 66 43 140 52 165
28 85 186 640 219 770 87 344 164 661 178 733 2 12 16 75 30 139 31 143 40
188 50 238 2 14 16 81 31 150 26 123 70 341 79 390 2 14 7 36 10 50 3 14 8 36
10 50 3 14 12 61 21 105 9 44 18 95 21 114 3 18 8 43 10 55 4 19 26 131 39
201 3 17 8 41 10 55 9 43 13 66 39 213 15 78 30 165 36 192 5 28 12 66 15 85
3 19 8 42 10 50 3 8 9 44 14 80 5 36 22 133 36 215 15 83 28 159 30 170 1 11
6 36 9 55 4 19 9 46 11 60 1 14 8 57 15 95 13 82 17 106 25 160 3 22 10 63 15
90 5 28 15 84 21 125 16 116 28 195 54 365 8 50 17 110 20 135 6 41 20 140 30
200 2 14 9 63 15 110 6 47 13 101 16 120 21 150 31 220 79 610 9 69 18 141 20
160 2 19 7 58 10 85 3 28 8 66 10 85 3 19 7 60 10 90 6 63 15 143 20 180 2 14
7 59 11 100 10 116 11 121 18 185 3 33 8 85 11 115 3 30 8 75 10 100 2 25 7
72 10 105 3 33 7 83 10 111 7 97 16 209 20 244 3 19 7 76 10 125 4 50 8 110
10 135 4 50 14 181 20 280 2 36 7 106 10 155 3 50 8 128 10 175 11 233 16 341
20 400 19 327 26 1432 12 1850 -12 349 -24 590 -42 815 -2 33 -7 94 -10 135
-3 41 -7 95 -10 120 -2 25 -7 74 -10 110 -5 65 -13 135 -20 200 -3 19 -7 62
-10 95 -3 33 -7 69 -9 80 -2 11 -7 49 -11 85 -4 36 -9 74 -11 85 -2 11 -6 43
-9 70 -3 28 -7 59 -9 70 -2 11 -9 61 -15 110 -7 50 -13 97 -15 105 -12 61 -18
101 -37 214 -11 72 -22 132 -24 135 -2 3 -6 31 -10 61 -4 30 -9 58 -11 61 -1
3 -17 74 -34 158 -34 165 -124 535 -160 652 -160 525 -301 869 -449 1094 -70
107 -221 268 -306 326 -27 19 -55 39 -62 46 -7 6 -68 39 -135 72 -122 60 -246
102 -343 116 -27 4 -61 9 -75 12 -14 3 -5675 6 -12581 7 -10052 1 -12574 -1
-12649 -11z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -1,9 +0,0 @@
const fs = require('fs');
const packageJson = require('../package.json');
const version = `${packageJson.version}+${fs.readFileSync('../.git/refs/heads/master').toString().trim().substring(0, 7)}`;
// eslint-disable-next-line no-console
console.log(`Writing version ${version} to local .env file.`);
fs.writeFileSync('.env', `VITE_APP_VERSION=${version}`);

View file

@ -1,67 +0,0 @@
<template>
<MoleculeNavigationDrawer
:drawer-id="drawerId"
:navigation-entries="navigationEntries"
>
<div class="flex min-h-screen flex-col items-stretch">
<RouterView
v-if="route.name != 'radio'"
class="shrink-0 grow"
/>
<ViewRadio
v-show="route.name == 'radio'"
class="shrink-0 grow"
/>
</div>
<AtomFooter class="shrink-0" />
</MoleculeNavigationDrawer>
</template>
<script setup lang="ts">
import { INavigationEntry } from './interfaces/navigation-entry.interface';
import { useRoute } from 'vue-router';
import {
IdentificationIcon,
BuildingLibraryIcon,
MusicalNoteIcon,
AdjustmentsHorizontalIcon,
BriefcaseIcon,
} from '@heroicons/vue/24/outline';
import MoleculeNavigationDrawer from './components/molecules/MoleculeNavigationDrawer.vue';
import ViewRadio from './components/views/ViewRadio.vue';
import AtomFooter from './components/atoms/AtomFooter.vue';
const route = useRoute();
const drawerId = 'default-drawer';
const navigationEntries: INavigationEntry[] = [
{
name: 'Arbeitgeberportal',
icon: BriefcaseIcon,
to: '/employer',
},
{
name: 'Zoll',
icon: IdentificationIcon,
to: '/checkin',
},
{
name: 'Bank',
icon: BuildingLibraryIcon,
to: '/bank',
},
{
name: 'Radio',
icon: MusicalNoteIcon,
to: '/radio',
},
{
name: 'Staatsportal',
icon: AdjustmentsHorizontalIcon,
to: '/settings',
},
];
</script>
<style lang="scss" scoped>
</style>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -1,6 +0,0 @@
module.exports = {
plugins: [
'preset-default',
'prefixIds',
],
};

View file

@ -1,18 +0,0 @@
<template>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title"><slot name="title" /></h2>
<p>
<slot />
</p>
</div>
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,83 +0,0 @@
<template>
<div class="relative">
<input
v-model="modelValue"
type="number"
class="input w-full border-primary text-right"
:class="{
'pr-[5.5rem]': !perDay,
'pr-[8rem]': perDay,
'!border-error': error,
}"
step="1"
:min="min"
:max="max"
@keyup="onKeyUp"
/>
<span
class="pointer-events-none absolute top-2/4 text-[1rem] opacity-50"
:class="{
'-mt-3': !suffixMargin,
'mt-[-0.625rem]': suffixMargin,
'ml-[-5.5rem]': !perDay,
'ml-[-8rem]': perDay,
}"
>,00 Batzen{{ perDay ? ' / Tag' : '' }}</span>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const input = ref<InstanceType<typeof HTMLInputElement>>();
const error = ref(false);
const props = withDefaults(defineProps<{
suffixMargin?: boolean,
perDay?: boolean,
min?: number,
max?: number,
value?: number,
}>(), {
suffixMargin: false,
perHour: false,
min: 1,
max: 1_000_000,
value: undefined,
});
/* global defineModel */
const modelValue = defineModel();
function onKeyUp(event: KeyboardEvent) {
error.value = false;
// eslint-disable-next-line vue/no-mutating-props
const value = parseInt((event.target as HTMLInputElement).value);
if(isNaN(value)) {
error.value = true;
} else if(value < props.min) {
error.value = true;
} else if(value > props.max) {
error.value = true;
} else {
emit('change', value);
}
}
function reset() {
if(input.value) {
input.value.value = '';
}
}
const emit = defineEmits<{
change: [value: number],
}>();
defineExpose({
reset,
});
</script>
<style lang="scss" scoped></style>

View file

@ -1,23 +0,0 @@
<template>
<div class="relative">
<AtomInput
v-model="modelValue"
type="number"
class="pr-7 text-right"
step="1"
min="1"
/>
<span class="pointer-events-none absolute top-2/4 -ml-6 -mt-3 opacity-50">x</span>
</div>
</template>
<script lang="ts" setup>
import AtomInput from './AtomInput.vue';
/* global defineModel */
const modelValue = defineModel();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,14 +0,0 @@
<template>
<input
type="file"
class="file-input-bordered file-input-primary file-input"
/>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,19 +0,0 @@
<template>
<footer class="footer footer-center bg-base-100 p-4 text-base-content">
<div>
<p>Version: {{ version }}</p>
<p>Made with by Simon Giesel</p>
<p>Copyright © 2023 - All rights reserved</p>
</div>
</footer>
</template>
<script lang="ts" setup>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const version = (import.meta as any).env.VITE_APP_VERSION as string ?? 'local+dev';
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,39 +0,0 @@
<template>
<input
v-model="modelValue"
:type="type"
:required="required"
:placeholder="placeholder"
class="input w-full border-primary"
:class="{
'!border-error': error,
}"
/>
</template>
<script lang="ts" setup>
defineProps({
type: {
type: String,
default: 'text',
},
required: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: '',
},
error: {
type: Boolean,
default: false,
},
});
/* global defineModel */
const modelValue = defineModel();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,92 +0,0 @@
<template>
<Teleport to="body">
<input
:id="id"
type="checkbox"
class="modal-toggle"
@change="($event.target as HTMLInputElement).checked ? $emit('close') : null"
/>
<div class="modal">
<form
method="dialog"
class="modal-box"
:class="{
'bg-base-300': darker,
'w-auto': wAuto,
}"
>
<button
v-if="closeButton"
class="btn-ghost btn-sm btn-circle btn absolute right-2 top-2"
>
</button>
<h3
v-if="title"
class="text-lg font-bold"
>
{{ title }}
</h3>
<p class="py-4"><slot /></p>
<div class="modal-action">
<slot name="action" />
</div>
</form>
<form
method="dialog"
class="modal-backdrop"
>
<button>close</button>
</form>
</div>
</Teleport>
</template>
<script lang="ts" setup>
const props = defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
darker: {
type: Boolean,
default: false,
},
closeButton: {
type: Boolean,
default: false,
},
wAuto: {
type: Boolean,
default: false,
},
});
function show(): void {
(document.getElementById(props.id) as HTMLInputElement).checked = true;
}
function close(): void {
(document.getElementById(props.id) as HTMLInputElement).checked = false;
}
function isOpen(): boolean {
return (document.getElementById(props.id) as HTMLInputElement).checked;
}
defineEmits(['close']);
defineExpose({
show,
close,
isOpen,
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,14 +0,0 @@
<template>
<img
src="../../assets/svg/logo.svg"
/>
</template>
<script lang="ts" setup>
// import { isDark } from '../../utils/darkMode';
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,97 +0,0 @@
<template>
<Teleport to="body">
<dialog
:id="id"
class="modal"
>
<form
method="dialog"
class="modal-box"
:class="{
'bg-base-300': darker,
'w-auto': wAuto,
}"
>
<button
v-if="closeButton"
class="btn-ghost btn-sm btn-circle btn absolute right-2 top-2"
>
</button>
<h3
v-if="title"
class="text-lg font-bold"
>
{{ title }}
</h3>
<p class="py-4"><slot /></p>
<div class="modal-action">
<slot name="action" />
</div>
</form>
<form
method="dialog"
class="modal-backdrop"
>
<button>close</button>
</form>
</dialog>
</Teleport>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
const props = defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
default: '',
},
darker: {
type: Boolean,
default: false,
},
closeButton: {
type: Boolean,
default: false,
},
wAuto: {
type: Boolean,
default: false,
},
});
function show(): void {
(document.getElementById(props.id) as HTMLDialogElement)?.showModal();
}
function close(): void {
(document.getElementById(props.id) as HTMLDialogElement)?.close();
}
function isOpen(): boolean {
return (document.getElementById(props.id) as HTMLDialogElement)?.open;
}
onMounted(() => {
(document.getElementById(props.id) as HTMLDialogElement).addEventListener('close', () => {
emit('close');
});
});
const emit = defineEmits(['close']);
defineExpose({
show,
close,
isOpen,
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,23 +0,0 @@
<template>
<div class="relative">
<AtomInput
v-model="modelValue"
type="number"
class="pr-14 text-right"
step="1"
min="1"
/>
<span class="pointer-events-none absolute top-2/4 -ml-14 -mt-3 opacity-50">,00 %</span>
</div>
</template>
<script lang="ts" setup>
import AtomInput from './AtomInput.vue';
/* global defineModel */
const modelValue = defineModel();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,16 +0,0 @@
<template>
<textarea
v-model="modelValue"
class="textarea-primary textarea h-auto"
/>
</template>
<script lang="ts" setup>
/* global defineModel */
const modelValue = defineModel();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,44 +0,0 @@
<template>
<div class="hero min-h-full">
<form
class="hero-content mx-auto flex-col text-center"
@submit.prevent="handleAuth"
>
<AtomInput
v-model="email"
placeholder="Admin-Email"
type="email"
required
/>
<AtomInput
v-model="password"
placeholder="Passwort"
type="password"
required
/>
<button class="btn-primary btn gap-2 self-end">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Login
</button>
</form>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { AuthService } from '../../services/auth.service';
import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline';
import AtomInput from '../atoms/AtomInput.vue';
const email = ref('');
const password = ref('');
async function handleAuth() {
await AuthService.loginAsAdmin(email.value, password.value);
}
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,63 +0,0 @@
<template>
<div class="hero min-h-full">
<form
class="hero-content mx-auto flex-col text-center"
@submit.prevent="handleAuth"
>
<AtomInput
v-model="username"
placeholder="Nutzername"
type="text"
:error="error"
required
/>
<AtomInput
v-model="password"
placeholder="Passwort"
type="password"
:error="error"
required
/>
<div
v-if="error"
class="alert alert-error flex w-[211px] items-center gap-2"
>
<XCircleIcon class="h-12 w-12" />
<span class="text-base font-normal">Nutzername oder Passwort falsch.</span>
</div>
<button class="btn-primary btn gap-2 self-end">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Login
</button>
</form>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { AuthService } from '../../services/auth.service';
import { ArrowRightOnRectangleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomInput from '../atoms/AtomInput.vue';
const username = ref('');
const password = ref('');
const error = ref(false);
async function handleAuth() {
try {
await AuthService.loginAsBanker(username.value, password.value);
} catch (e) {
error.value = true;
console.error(e);
}
}
watch([username, password], () => {
error.value = false;
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,84 +0,0 @@
<template>
<AtomModal
:id="id"
ref="modal"
:title="title"
@close="handleClose"
>
<div class="flex place-items-center justify-between">
<AtomInput
v-model="accountNumber"
class="w-4/12"
@keyup.esc="modal?.close()"
/>
</div>
<template #action>
<label
class="btn gap-2"
:for="id"
@click="modal?.close()"
>
<XCircleIcon class="h-6 w-6" />
Abbrechen
</label>
<button
class="btn gap-2"
@click="handleSubmit()"
>
<CheckCircleIcon class="h-6 w-6" />
Speichern
</button>
</template>
</AtomModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomModal from '../atoms/AtomModal.vue';
import AtomInput from '../atoms/AtomInput.vue';
const modal = ref<InstanceType<typeof AtomModal>>();
const accountNumber = ref('');
const accountId = ref('');
defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
});
const emit = defineEmits(['submit']);
function handleClose() {
accountNumber.value = '';
}
function handleSubmit() {
if(!accountNumber.value) {
return;
}
emit('submit', {
id: accountId.value,
accountNumber: accountNumber.value,
});
}
defineExpose({
show: (id: string) => {
accountId.value = id;
modal.value?.show();
},
close: () => modal.value?.close(),
isOpen: () => modal.value?.isOpen(),
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,166 +0,0 @@
<template>
<div
v-if="settings"
class="rounded-lg"
>
<table class="table-pin-rows table-zebra table">
<thead
v-if="!hideHeader"
class="uppercase"
>
<tr>
<th v-for="header in tableHeaders">{{ header.title }}</th>
</tr>
</thead>
<tbody class="bg-base-100">
<tr
v-for="entry in data"
@click="$emit('click', entry.id)"
>
<td
v-for="header in tableHeaders"
:class="{
'text-center': [TableHeaderType.BUTTON_ACCOUNT, TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER].includes(header.type),
}"
>
<span v-if="header.type === TableHeaderType.STRING">
{{ entry[header.key] }}
</span>
<span v-else-if="header.type === TableHeaderType.DATE">
{{ DateService.toShortString(entry[header.key]) }}
</span>
<span v-else-if="header.type === TableHeaderType.DATETIME">
{{ entry[header.key] ? DateService.toString(parseISO(entry[header.key])) : 'N/A' }}
</span>
<span v-else-if="header.type === TableHeaderType.CURRENCY">
{{ CurrencyService.toString(entry[header.key]) }}
</span>
<span v-else-if="header.type === TableHeaderType.SHIFT">
<div class="dropdown-bottom dropdown">
<label
tabindex="0"
class="btn bg-base-300 normal-case"
>{{ entry[header.key] }}
<ChevronDownIcon class="h-4 w-4" />
</label>
<ul
tabindex="0"
class="dropdown-content menu rounded-box z-[1] w-52 bg-base-100 p-2 shadow"
>
<li @click="$emit('selectShift', { id: entry.id, shift: Shifts.NONE }); removeFocus();"><a>{{
Shifts.NONE
}}</a></li>
<li><a @click="$emit('selectShift', { id: entry.id, shift: Shifts.EARLY }); removeFocus();">{{
Shifts.EARLY }}</a></li>
<li><a @click="$emit('selectShift', { id: entry.id, shift: Shifts.LATE }); removeFocus();">{{
Shifts.LATE
}}</a></li>
</ul>
</div>
</span>
<RouterLink
v-if="header.type === TableHeaderType.BUTTON_ACCOUNT"
:to="`/bank?accountNumber=${entry.accountNumber}`"
class="btn-ghost btn-sm btn p-1"
>
<div
class="tooltip normal-case"
data-tip="Transaktionen anzeigen"
>
<CurrencyDollarIcon class="h-6 w-6" />
</div>
</RouterLink>
<span
v-if="header.type === TableHeaderType.BUTTON_CHANGE_ACCOUNT_NUMBER"
class="btn-ghost btn-sm btn p-1"
@click="$emit('changeAccountNumber', entry.id)"
>
<div
class="tooltip normal-case"
data-tip="Kontonummer ändern"
>
<CreditCardIcon class="h-6 w-6" />
</div>
</span>
<span
v-if="header.type === TableHeaderType.WAGE"
class="flex items-center gap-2"
>
<AtomCurrencyInput
v-model="entry.wage"
suffix-margin
per-day
class="min-w-[11rem]"
:min="settings.minWage / 100"
:max="settings.maxWage / 100"
@change="$emit('selectWage', { id: entry.id, wage: $event })"
/>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>
import { PropType, onMounted, ref } from 'vue';
import { parseISO } from 'date-fns';
import { SettingsResponse } from '../../types/pocketbase.types';
import { DateService } from '../../services/date.service';
import { CurrencyService } from '../../services/currency.service';
import { SettingsService } from '../../services/settings.service';
import { Shifts } from '../../enums/shift.enum';
import { ChevronDownIcon, CreditCardIcon, CurrencyDollarIcon } from '@heroicons/vue/24/outline';
import AtomCurrencyInput from '../atoms/AtomCurrencyInput.vue';
const settings = ref<SettingsResponse>();
defineProps({
tableHeaders: {
type: Array as PropType<{ title: string, key: string, type: TableHeaderType }[]>,
required: true,
},
data: {
// Explicit allow any types as this is a dynamic list of data
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type: Array as PropType<any[]>,
required: true,
},
hideHeader: {
type: Boolean,
required: false,
default: false,
},
});
onMounted(async () => {
settings.value = await SettingsService.getSettings();
});
defineEmits<{
click: [id: string],
selectShift: [{ id: string, shift: Shifts }],
selectWage: [{ id: string, wage: number }],
changeAccountNumber: [id: string],
}>();
function removeFocus() {
(document.activeElement as HTMLElement).blur();
}
</script>
<script lang="ts">
export enum TableHeaderType {
STRING,
DATE,
CURRENCY,
DATETIME,
BUTTON_ACCOUNT,
BUTTON_CHANGE_ACCOUNT_NUMBER,
WAGE,
SHIFT,
}
</script>
<style lang="scss" scoped></style>

View file

@ -1,54 +0,0 @@
<template>
<AtomModal
:id="id"
ref="modal"
:title="title"
>
<div class="flex flex-col gap-2">
<AtomTextarea v-model="modelValue" />
</div>
<template #action>
<label
class="btn gap-2"
:for="id"
@click="modal?.close()"
>
<XCircleIcon class="h-6 w-6" />
Schließen
</label>
</template>
</AtomModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { XCircleIcon } from '@heroicons/vue/24/outline';
import AtomModal from '../atoms/AtomModal.vue';
import AtomTextarea from '../atoms/AtomTextarea.vue';
const modal = ref<InstanceType<typeof AtomModal>>();
defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
});
defineExpose({
show: () => modal.value?.show(),
close: () => modal.value?.close(),
isOpen: () => modal.value?.isOpen(),
});
/* global defineModel */
const modelValue = defineModel();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,73 +0,0 @@
<template>
<AtomModal
:id="id"
ref="modal"
:title="title"
>
<div class="flex flex-col gap-2">
<AtomFileInput @change="file = ($event.target as HTMLInputElement)?.files?.[0]" />
<p>Datei für den Import der Stammdaten im CSV-Format auswählen.<br /><b>! Achtung !</b> Die Datei muss eine Kopfzeile besitzen.</p>
</div>
<template #action>
<label
class="btn gap-2"
:for="id"
@click="modal?.close()"
>
<XCircleIcon class="h-6 w-6" />
Abbrechen
</label>
<button
class="btn gap-2"
@click="handleSubmit()"
>
<CheckCircleIcon class="h-6 w-6" />
{{ actionLabel }}
</button>
</template>
</AtomModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomModal from '../atoms/AtomModal.vue';
import AtomFileInput from '../atoms/AtomFileInput.vue';
const modal = ref<InstanceType<typeof AtomModal>>();
const file = ref<File>();
defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
actionLabel: {
type: String,
required: true,
},
});
const emit = defineEmits(['submit']);
function handleSubmit() {
if(!file.value) {
return;
}
emit('submit', file.value);
}
defineExpose({
show: () => modal.value?.show(),
close: () => modal.value?.close(),
isOpen: () => modal.value?.isOpen(),
});
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,86 +0,0 @@
<template>
<AtomModal
:id="id"
ref="modal"
:title="title"
>
<div class="flex flex-col gap-4">
<p>{{ description }}</p>
<AtomInput
v-model="modelValue"
placeholder="Passwort"
type="password"
@keydown.prevent.enter="$emit('login')"
/>
<p
v-if="error"
class="alert alert-error"
>
{{ error }}
</p>
</div>
<template #action>
<button
class="btn gap-2"
:for="id"
@click="modal?.close()"
>
<XCircleIcon class="h-6 w-6" />
Schließen
</button>
<button
class="btn gap-2"
:for="id"
@click.prevent="$emit('login')"
>
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Login
</button>
</template>
</AtomModal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ArrowRightOnRectangleIcon, XCircleIcon } from '@heroicons/vue/24/outline';
import AtomModal from '../atoms/AtomLegacyModal.vue';
import AtomInput from '../atoms/AtomInput.vue';
const modal = ref<InstanceType<typeof AtomModal>>();
defineProps({
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
error: {
type: String,
default: '',
},
});
defineExpose({
show: () => modal.value?.show(),
close: () => modal.value?.close(),
isOpen: () => modal.value?.isOpen(),
});
/* global defineModel */
const modelValue = defineModel();
defineEmits<{
login: [],
}>();
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,157 +0,0 @@
<template>
<div
class="drawer"
:class="{
'lg:drawer-open': !hideNavigation,
}"
>
<input
:id="drawerId"
ref="drawerCheckbox"
type="checkbox"
class="drawer-toggle"
/>
<div
class="drawer-content flex flex-col bg-base-300"
:class="{'lg:pl-64': !hideNavigation}"
>
<div
class="navbar top-0 place-self-center shadow-lg"
:class="{
'lg:hidden': !hideNavigation,
}"
>
<div class="flex-none">
<label
:for="drawerId"
class="btn-ghost rounded-btn btn"
:class="{
'lg:hidden': !hideNavigation,
}"
>
<Bars3Icon class="h-7 w-7" />
</label>
<AtomLogo class="h-10 px-4" />
</div>
</div>
<slot />
</div>
<div class="drawer-side !fixed z-50">
<label
:for="drawerId"
class="drawer-overlay"
/>
<ul class="menu h-full w-64 overflow-y-auto bg-base-100 p-4">
<li class="pointer-events-none">
<AtomLogo class="w-full" />
</li>
<template v-if="isAdmin">
<li class="pointer-events-none rounded-md bg-warning uppercase text-warning-content">
<span>
<ExclamationTriangleIcon class="h-6 w-6" />
Admin-Modus
<ExclamationTriangleIcon class="h-6 w-6" />
</span>
</li>
<li>
<span @click="AuthService.logout()">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Abmelden
</span>
</li>
<li class="menu-title !p-0" />
</template>
<template v-if="isBanker">
<li class="pointer-events-none rounded-md bg-info text-warning-content">
<span>
Angemeldet als Banker
<BuildingLibraryIcon class="h-6 w-6" />
</span>
</li>
<li>
<span @click="AuthService.logout()">
<ArrowRightOnRectangleIcon class="h-6 w-6" />
Abmelden
</span>
</li>
<li class="menu-title !p-0" />
</template>
<li
v-for="navigationEntry of navigationEntries"
:class="getNavigationEntryClass(navigationEntry)"
>
<component
:is="!navigationEntry.to || navigationEntry.disabled ? 'span' : 'router-link'"
v-if="navigationEntry.name !== 'divider'"
:to="navigationEntry.to"
active-class="active"
>
<component
:is="navigationEntry.icon"
class="h-6 w-6"
/>{{ navigationEntry.name }}
</component>
</li>
<AtomSwap
class="mt-auto place-self-end"
:on-icon="SunIcon"
:off-icon="MoonIcon"
:checked="isDark"
@change="toggleDark()"
/>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { PropType, onMounted, ref } from 'vue';
import { useEventBus } from '@vueuse/core';
import { useRouter } from 'vue-router';
import { INavigationEntry } from '../../interfaces/navigation-entry.interface';
import { AuthService } from '../../services/auth.service';
import { isDark, toggleDark } from '../../utils/darkMode';
import { ArrowRightOnRectangleIcon, Bars3Icon, BuildingLibraryIcon, ExclamationTriangleIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline';
import AtomLogo from '../atoms/AtomLogo.vue';
import AtomSwap from '../atoms/AtomSwap.vue';
const isAdmin = ref(false);
const isBanker = ref(false);
const hideNavigation = ref(false);
const router = useRouter();
const drawerCheckbox = ref();
defineProps({
drawerId: {
type: String,
required: true,
},
navigationEntries: {
type: Array as PropType<INavigationEntry[]>,
required: true,
},
});
function getNavigationEntryClass(navigationEntry: INavigationEntry) {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
'menu-title !p-0': navigationEntry.name === 'divider',
disabled: navigationEntry.disabled,
};
}
useEventBus<boolean>('isAdmin').on(state => (isAdmin.value = state));
useEventBus<boolean>('isBanker').on(state => (isBanker.value = state));
useEventBus<boolean>('hideNavigation').on(state => (hideNavigation.value = state));
onMounted(() => {
isAdmin.value = AuthService.isAdmin();
isBanker.value = AuthService.isBanker();
});
router.afterEach(() => {
drawerCheckbox.value.checked = false;
});
</script>
<style lang="scss" scoped></style>

Some files were not shown because too many files have changed in this diff Show more