Compare commits

..

No commits in common. "dev" and "feature/171-select_runner_by_id" have entirely different histories.

180 changed files with 15863 additions and 20491 deletions

6
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM mcr.microsoft.com/vscode/devcontainers/base:alpine-3.12
RUN apk update
RUN apk add --upgrade nodejs-current npm
RUN npm i -g yarn rimraf
RUN rimraf node_modules
RUN yarn set version berry

View File

@ -0,0 +1,20 @@
{
"name": "Node.js",
"build": {
"dockerfile": "Dockerfile"
},
"settings": {
"terminal.integrated.shell.linux": "/bin/sh"
},
"extensions": [
"dbaeumer.vscode-eslint",
"2gua.rainbow-brackets",
"christian-kohler.npm-intellisense",
"remimarsal.prettier-now",
"svelte.svelte-vscode",
"lokalise.i18n-ally",
"fivethree.vscode-svelte-snippets",
"voorjaar.windicss-intellisense"
],
"postCreateCommand": "yarn && yarn dev --open"
}

View File

@ -1,4 +1,2 @@
public/env.sample.js public/env.sample.js
.pnpm-store .pnpm-store
.yarn
.pnp.*

101
.drone.yml Normal file
View File

@ -0,0 +1,101 @@
---
kind: secret
name: docker_username
get:
path: odit-registry-builder
name: username
---
kind: secret
name: docker_password
get:
path: odit-registry-builder
name: password
---
kind: secret
name: git_ssh
get:
path: odit-git-bot
name: sshkey
---
kind: secret
name: npm_url
get:
path: odit-npm-cache
name: url
---
kind: pipeline
type: kubernetes
name: build:dev
steps:
- name: run full license export
depends_on: ["clone"]
image: registry.odit.services/hub/library/node:19.7.0-alpine3.16
commands:
- npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8
- pnpm i
- pnpm licenses:export
environment:
NPM_REGISTRY_URL:
from_secret: npm_url
- name: push new licenses file to repo
depends_on: ["run full license export"]
image: appleboy/drone-git-push
settings:
branch: dev
commit: true
commit_message: new license file version [CI SKIP]
author_email: bot@odit.services
remote: git@git.odit.services:lfk/frontend.git
ssh_key:
from_secret: git_ssh
- name: build dev
depends_on: ["clone"]
image: registry.odit.services/library/drone-kaniko
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- NPM_REGISTRY_URL:
from_secret: npm_url
repo: lfk/frontend
tags:
- dev
cache: true
registry: registry.odit.services
trigger:
branch:
- dev
event:
- push
---
kind: pipeline
type: kubernetes
name: build:tags
steps:
- name: build $DRONE_TAG
depends_on: ["clone"]
image: registry.odit.services/library/drone-kaniko
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
build_args:
- NPM_REGISTRY_URL:
from_secret: npm_url
repo: lfk/frontend
tags:
- "${DRONE_TAG}"
cache: true
registry: registry.odit.services
trigger:
event:
- tag

View File

@ -1,33 +0,0 @@
name: Build release images
on:
push:
tags:
- "*.*.*"
jobs:
build-container:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 19
- run: npm i -g pnpm@10.8 && pnpm i
- run: pnpm licenses:export
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.odit.services
username: ${{ vars.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
${{ vars.REGISTRY }}/lfk/frontend:${{ github.ref_name }}
platforms: linux/amd64,linux/arm64

4
.gitignore vendored
View File

@ -3,6 +3,4 @@ node_modules
public/env.js public/env.js
public/index.html public/index.html
/dist /dist
.pnpm-store .pnpm-store
.yarn
.pnp.*

View File

@ -1,12 +1,14 @@
{ {
"recommendations": [ "recommendations": [
"2gua.rainbow-brackets", "2gua.rainbow-brackets",
"christian-kohler.npm-intellisense", "christian-kohler.npm-intellisense",
"remimarsal.prettier-now", "remimarsal.prettier-now",
"svelte.svelte-vscode", "svelte.svelte-vscode",
"lokalise.i18n-ally", "lokalise.i18n-ally",
"fivethree.vscode-svelte-snippets", "fivethree.vscode-svelte-snippets",
"voorjaar.windicss-intellisense" "voorjaar.windicss-intellisense"
], ],
"unwantedRecommendations": ["antfu.i18n-ally"] "unwantedRecommendations": [
} "antfu.i18n-ally"
]
}

View File

@ -1,7 +1,7 @@
languageIds: languageIds:
- javascript - javascript
- svelte - svelte
- html - html
monopoly: false monopoly: false
refactorTemplates: refactorTemplates:
- "{$_('$1')}" - "{$_('$1')}"

View File

@ -1,5 +1,5 @@
{ {
"i18n-ally.localesPaths": "src/locales", "i18n-ally.localesPaths": "src/locales",
"i18n-ally.keystyle": "nested", "i18n-ally.keystyle": "nested",
"windicss.enableCodeFolding": false "windicss.enableCodeFolding": false,
} }

View File

@ -2,893 +2,9 @@
All notable changes to this project will be documented in this file. Dates are displayed in UTC. All notable changes to this project will be documented in this file. Dates are displayed in UTC.
#### [1.14.2](https://git.odit.services/lfk/frontend/compare/1.14.1...1.14.2)
- feat(GenerateRunnerCertificates): support skipping runners without scans [`5a7bc23`](https://git.odit.services/lfk/frontend/commit/5a7bc239d2f93ced9ebdc5b113fe27d0d8d3899c)
#### [1.14.1](https://git.odit.services/lfk/frontend/compare/1.14.0...1.14.1)
> 26 May 2025
- fix: ensure numeric values are parsed as integers in DocumentServer methods [`1b088b8`](https://git.odit.services/lfk/frontend/commit/1b088b87bf6e67796c2509d9c21f21833cb4df0f)
- chore(release): 1.14.1 [`661a698`](https://git.odit.services/lfk/frontend/commit/661a698fbaeb2432bec758ed632a520676ae86b2)
#### [1.14.0](https://git.odit.services/lfk/frontend/compare/1.13.5...1.14.0)
> 20 May 2025
- wip [`564a971`](https://git.odit.services/lfk/frontend/commit/564a971c63403af2e2eb550db814519576d62023)
- wip [`50b5e4e`](https://git.odit.services/lfk/frontend/commit/50b5e4e455ce705fc5ef7f3d069d88c9ff48a6af)
- wip [`2c91f46`](https://git.odit.services/lfk/frontend/commit/2c91f463758c8452561fbcc5dad8412edba8915d)
- wip [`1386b80`](https://git.odit.services/lfk/frontend/commit/1386b80d0c8569cf127f8235b3dd249c2775594a)
- wip [`6ef6dc0`](https://git.odit.services/lfk/frontend/commit/6ef6dc007837c237273a29ca489ef0cdb92f7c6c)
- wip [`3709881`](https://git.odit.services/lfk/frontend/commit/370988117683ab1fdc149a30f920cc6a66575c7a)
- chore(release): 1.14.0 [`d5fecd3`](https://git.odit.services/lfk/frontend/commit/d5fecd3f31916b80c305d76f37c4600f1d242eba)
- wip [`77413c7`](https://git.odit.services/lfk/frontend/commit/77413c7e5350a1d8643d2baf135b531235f78e64)
- wip [`0cb1193`](https://git.odit.services/lfk/frontend/commit/0cb1193269912b047abfacb6012463093c2adcfa)
- wip [`9ef3435`](https://git.odit.services/lfk/frontend/commit/9ef34359d8ac32674c28825b91b6aa2877e63552)
- wip [`a00af08`](https://git.odit.services/lfk/frontend/commit/a00af08b3f7c8278cfc54af6f593a9dcf4509ab4)
- wip [`286bd61`](https://git.odit.services/lfk/frontend/commit/286bd614976dcf8bcb14cffd092f23ef65393917)
- wip [`b89d4f2`](https://git.odit.services/lfk/frontend/commit/b89d4f248c5575548d77336832c64dc6e395efc3)
- inputElementID param [`4d79589`](https://git.odit.services/lfk/frontend/commit/4d79589903bb0726f6bcb2c0e5089a9e20f7db17)
- wip [`53f5fa3`](https://git.odit.services/lfk/frontend/commit/53f5fa3988e81215e17e41b7dd92e9ddf897610a)
- wip [`444b1f5`](https://git.odit.services/lfk/frontend/commit/444b1f537016b303a57fcaaac4468a749fe4f33c)
- disable autocomplete [`72e5425`](https://git.odit.services/lfk/frontend/commit/72e5425c0847102b0ed3f88abe17dc22ccea0a30)
#### [1.13.5](https://git.odit.services/lfk/frontend/compare/1.13.4...1.13.5)
> 20 May 2025
- add missing cursor-pointer [`6500839`](https://git.odit.services/lfk/frontend/commit/650083965a35cf3b05b6b67389ff8035dc5fa3fa)
- refactor(DonationsOverview): drop checkboxes - they dont do anything [`06d22c9`](https://git.odit.services/lfk/frontend/commit/06d22c929f94587d9bdbcb4abfc0a770cf94a771)
- chore(release): 1.13.5 [`e2a1c9a`](https://git.odit.services/lfk/frontend/commit/e2a1c9a508c6061e55438afefcd641e3d9423aaa)
#### [1.13.4](https://git.odit.services/lfk/frontend/compare/1.13.3...1.13.4)
> 20 May 2025
- feat(donationcreate): improved focus handling [`a827279`](https://git.odit.services/lfk/frontend/commit/a82727916345c7e713d4225c4771ef3f23d1392c)
- chore(release): 1.13.4 [`bbf659e`](https://git.odit.services/lfk/frontend/commit/bbf659e52d249732fadb659fdbd24a89d2e8ec42)
- chore(deps): remove unused [`3842d8b`](https://git.odit.services/lfk/frontend/commit/3842d8b1048ce12f0f70bf3d0530590470f0d200)
- fix(donationcreate): clearing [`9298a0d`](https://git.odit.services/lfk/frontend/commit/9298a0dc922ee5ed5b7c9017c865ad4b68fca3c8)
- feat(donationcreate): autofocus runner input on page load [`b9e2e65`](https://git.odit.services/lfk/frontend/commit/b9e2e653310c686bc06b9f27c38b49e9c6a3eaef)
- fix(DonationCreate): remove duplicate spaces from getRunnerLabel [`30a26ef`](https://git.odit.services/lfk/frontend/commit/30a26ef3ed55d072cd9bf2aea1b200fadc2a05f1)
- fix(donationcreate): improved resetAll [`7d9314f`](https://git.odit.services/lfk/frontend/commit/7d9314f05c58c1b50901f3797c0b461c4c79e5d2)
- fix(DeleteDonationModal): cannot overflow [`ca066aa`](https://git.odit.services/lfk/frontend/commit/ca066aa7a7a8d7c46e0f59370b06636faf5736ca)
- feat(donationcreate): full width [`b0063cd`](https://git.odit.services/lfk/frontend/commit/b0063cdead5f71c334c36e5587a58e957825dbcd)
- feat(donationcreate): add runner id to select [`27e7bbb`](https://git.odit.services/lfk/frontend/commit/27e7bbb9d142fbea659e89fb2335cc6c567d14ce)
#### [1.13.3](https://git.odit.services/lfk/frontend/compare/1.13.2...1.13.3)
> 19 May 2025
- chore(release): 1.13.3 [`2139b19`](https://git.odit.services/lfk/frontend/commit/2139b197ba672275e2a0b5ffbcf7fa43f80874e6)
- Refactor code structure for improved readability and maintainability [`e3c6d5a`](https://git.odit.services/lfk/frontend/commit/e3c6d5a5c0eaac2c91432b0be37d6fa11e57f644)
- refactor(donation): Refactor donor selection and add new donor creation functionality [`8c3f009`](https://git.odit.services/lfk/frontend/commit/8c3f0092d2735b1c85976f4e6955780b1035f68a)
- fix(donation): Ensure all selections are cleared on reset [`4e1a944`](https://git.odit.services/lfk/frontend/commit/4e1a944a2d7d0d0666fb8d2181a9941d0f11957f)
#### [1.13.2](https://git.odit.services/lfk/frontend/compare/1.13.1...1.13.2)
> 16 May 2025
- chore(release): 1.13.2 [`6fad04c`](https://git.odit.services/lfk/frontend/commit/6fad04c86249613dacfe2bc75362cb32d109573d)
- feat(dashboard): Add permission checks for scan and donation creation links [`838dcbf`](https://git.odit.services/lfk/frontend/commit/838dcbfd7e0c09e8cf61a04952475934ad1e3b86)
#### [1.13.1](https://git.odit.services/lfk/frontend/compare/1.13.0...1.13.1)
> 16 May 2025
- chore(release): 1.13.1 [`f5df252`](https://git.odit.services/lfk/frontend/commit/f5df252857f20f8426b8ca566b9bb3ec50331880)
- feat(tools): Remove unnecessary validation display in donation creation [`285fc91`](https://git.odit.services/lfk/frontend/commit/285fc91c66d03aaa861da549cb739c3698beb892)
#### [1.13.0](https://git.odit.services/lfk/frontend/compare/1.12.8...1.13.0)
> 16 May 2025
- feat(tools): Basic mobile scanner [`500886e`](https://git.odit.services/lfk/frontend/commit/500886e4106f4b53fbc40fb0fa15653f574c8328)
- chore(release): 1.13.0 [`d95b6cf`](https://git.odit.services/lfk/frontend/commit/d95b6cf5894dd0b487353f36c9f3a436066fd4ef)
- feat(tools): Added tool for fast sponsoring creation [`51ba1c8`](https://git.odit.services/lfk/frontend/commit/51ba1c852cad6243e935409da1eacecc5dcfa5fa)
- feat(dev): Enable devserver with https-support to circumvent ios https requirements for camera access [`25c38ea`](https://git.odit.services/lfk/frontend/commit/25c38ea3812a529a90294ff8834bdb65c487f8c4)
- feat(tools): Remove requirement for ten-diget codes [`80ca7aa`](https://git.odit.services/lfk/frontend/commit/80ca7aa08bdd44591e2d3efaa8e59dd4db5c864e)
- feat(dashboard): Added scanclient tool to dashboard [`06cfd60`](https://git.odit.services/lfk/frontend/commit/06cfd603cae79e0237bbece43203083f198d03a1)
#### [1.12.8](https://git.odit.services/lfk/frontend/compare/1.12.7...1.12.8)
> 1 May 2025
- chore(release): 1.12.8 [`51d9b35`](https://git.odit.services/lfk/frontend/commit/51d9b35dc41fea0d0245fd136556f9fada3559da)
- feat(dasboard): Added section headers to main nav [`3a8533a`](https://git.odit.services/lfk/frontend/commit/3a8533a7baef02f7bc9780ce37be1a350bd92270)
- fic(locales): Updated dashboard translations [`5ac6fe3`](https://git.odit.services/lfk/frontend/commit/5ac6fe30b5b9e34043c734d51d5da137fdf7ac38)
- feat(runners): Created_via filters can now be set via query params [`14501d3`](https://git.odit.services/lfk/frontend/commit/14501d3828dd0d48ba0baeeddf936ba275f7b9b7)
- refactor(tools): Move tools to tools route [`16dc789`](https://git.odit.services/lfk/frontend/commit/16dc789db5d9ea41774c77622a579cc0d9bd95f2)
- refactor(tools): Move tools into shared directory instead of the non-descript "general" [`e4f9b1a`](https://git.odit.services/lfk/frontend/commit/e4f9b1a60551d7955def4d068d534cf17b1ea640)
#### [1.12.7](https://git.odit.services/lfk/frontend/compare/1.12.6...1.12.7)
> 1 May 2025
- chore(release): 1.12.7 [`c78bdfa`](https://git.odit.services/lfk/frontend/commit/c78bdfa5e24ada4909455064dd6b05cf34fc6df3)
- fix(deps): fresh lockfile [`b2ed2af`](https://git.odit.services/lfk/frontend/commit/b2ed2afd8a45a1a01ac6118b27941e3b4b3b611f)
- refactor(store): update refresh interval from 2min to 60min [`00d1988`](https://git.odit.services/lfk/frontend/commit/00d198895e15174b70a8d229974b4baa7d0ed8fc)
#### [1.12.6](https://git.odit.services/lfk/frontend/compare/1.12.5...1.12.6)
> 1 May 2025
- feat(pdfs): Experimental generation of large runner card files [`93422b9`](https://git.odit.services/lfk/frontend/commit/93422b97799c5e45c89acadd34f33b1a11b04617)
- chore(release): 1.12.6 [`b5c079d`](https://git.odit.services/lfk/frontend/commit/b5c079da9a0545d146e9f3029a543e04c907add3)
#### [1.12.5](https://git.odit.services/lfk/frontend/compare/1.12.4...1.12.5)
> 1 May 2025
- chore(release): 1.12.5 [`6dcfd9a`](https://git.odit.services/lfk/frontend/commit/6dcfd9a4fedd1e44894c9803482576bc650fb4db)
- fix(locales): Fixed translation [`2139524`](https://git.odit.services/lfk/frontend/commit/21395241de4de8f3a6b8404758d09c01d8a6f95f)
- feat(runners): Show total donations in runner detail [`f27c716`](https://git.odit.services/lfk/frontend/commit/f27c716296e228ecccbf500a21130f1bc47ea52d)
- chore(deps): Bump @odit/lfk-client-js to 1.2.7 [`6d19199`](https://git.odit.services/lfk/frontend/commit/6d1919974aacd74a265cf9ce0c9ed501028f0aa3)
- fix: Update release script to include --only-version flag [`8d2cb13`](https://git.odit.services/lfk/frontend/commit/8d2cb13195856f47022d414f3243e9a21457832b)
#### [1.12.4](https://git.odit.services/lfk/frontend/compare/1.12.3...1.12.4)
> 28 April 2025
- chore(release): 1.12.4 [`e61e8b0`](https://git.odit.services/lfk/frontend/commit/e61e8b063af75539b7db93c5ca42965417019f29)
- fix: Disable ios auto zooming on inputs [`073c78d`](https://git.odit.services/lfk/frontend/commit/073c78d98afd1c2f08b190aeda942a634e9bb888)
#### [1.12.3](https://git.odit.services/lfk/frontend/compare/1.12.2...1.12.3)
> 28 April 2025
- feat: Fast card replacement view [`7f802d5`](https://git.odit.services/lfk/frontend/commit/7f802d57f81d913634f28e2def29c183b0fdd098)
- chore(release): 1.12.3 [`85e4faf`](https://git.odit.services/lfk/frontend/commit/85e4faf898b4844bb2fcaf87e332c1471ef14b57)
#### [1.12.2](https://git.odit.services/lfk/frontend/compare/1.12.1...1.12.2)
> 28 April 2025
- feat(cardassignment): Now with hand scanner support [`9e8c236`](https://git.odit.services/lfk/frontend/commit/9e8c236281f6686318c27dcb1bd02dfbc2b30ee8)
- chore(release): 1.12.2 [`868dc3f`](https://git.odit.services/lfk/frontend/commit/868dc3f7e23f42f6bf1f752272ad21673be9c1c4)
#### [1.12.1](https://git.odit.services/lfk/frontend/compare/1.12.0...1.12.1)
> 28 April 2025
- chore(release): 1.12.1 [`827fb31`](https://git.odit.services/lfk/frontend/commit/827fb317bc946268e4a1d60b15f3805b67b240f6)
- fix(donations): Don't show enter payment for anon donations [`32f72df`](https://git.odit.services/lfk/frontend/commit/32f72df10583a08efb26e0983c0c5c829ab03e19)
- chore(deps): Fresh lock [`1ec8e21`](https://git.odit.services/lfk/frontend/commit/1ec8e2186bdcd69c2acbc785feef4927973bc986)
- fix(donations): Support anon donations in deletion modal [`27187b4`](https://git.odit.services/lfk/frontend/commit/27187b428da1e757f85392d77d49670f51a19829)
- fix(donations): Don't show details for anon donations [`ecd418c`](https://git.odit.services/lfk/frontend/commit/ecd418c5db0910d64cdf5336d72a10ebff38e065)
- refactor(i18n): Update translations [`77a4328`](https://git.odit.services/lfk/frontend/commit/77a432817ef644ff1be8a5ebcd284b7d67f742bd)
- fix(donations): Remove paid from anon donations [`31a4ff9`](https://git.odit.services/lfk/frontend/commit/31a4ff9d909742df3ed3d6cecae57870e287afcc)
- fix(donations): Move amount to extra line [`edd5da8`](https://git.odit.services/lfk/frontend/commit/edd5da89a7e741d48078125f68b6dd6d3d88a7a1)
- refactor(i18n): Shortened translation [`657fb04`](https://git.odit.services/lfk/frontend/commit/657fb04f1b5b439ef5fac834740ba00548b758de)
- feat(donation): Use new endpoint for creating anon donations [`e28f543`](https://git.odit.services/lfk/frontend/commit/e28f543d89efff80a84131df59bfb26a5ea92d14)
- fix(donations): Translate modal title [`dc1e6b7`](https://git.odit.services/lfk/frontend/commit/dc1e6b7a67c4761a007ffe3b71fd851fb569fb7c)
- chore(deps): Bump @odit/lfk-client-js [`cb315d9`](https://git.odit.services/lfk/frontend/commit/cb315d94fd2331bd49aa6d54b9ca0bfbf11f00d9)
- feat(shared): Hide link while keeping width [`724e844`](https://git.odit.services/lfk/frontend/commit/724e84441e8b71b7d89a8c3804467edebfd58365)
#### [1.12.0](https://git.odit.services/lfk/frontend/compare/1.11.5...1.12.0)
> 28 April 2025
- feat: anonymous donations [`9c03e35`](https://git.odit.services/lfk/frontend/commit/9c03e359a4e8f43452475b02bcabcb354987ab75)
- refactor: use modern tailwindcss features [`1505080`](https://git.odit.services/lfk/frontend/commit/1505080afdd8b272b76584e2777df732001ce004)
- chore(release): 1.12.0 [`5dcb4cb`](https://git.odit.services/lfk/frontend/commit/5dcb4cb508b204c5634804811a9f37db78e764ce)
#### [1.11.5](https://git.odit.services/lfk/frontend/compare/1.11.4...1.11.5)
> 25 April 2025
- chore(release): 1.11.5 [`8cb6093`](https://git.odit.services/lfk/frontend/commit/8cb6093f0b3474c0952a8a51a47683262fc31f8f)
- fix(cards): Update table for edit events [`27396e1`](https://git.odit.services/lfk/frontend/commit/27396e17f2cd8be72c9c8100afe6ec75ac66dceb)
#### [1.11.4](https://git.odit.services/lfk/frontend/compare/1.11.3...1.11.4)
> 25 April 2025
- feat(CardAssign): styled buttons [`f9993c6`](https://git.odit.services/lfk/frontend/commit/f9993c60f565d7270b3a269e934b42b945c24b99)
- chore(release): 1.11.4 [`fefd5c8`](https://git.odit.services/lfk/frontend/commit/fefd5c8237b91ac7fd7ecf467a6feb1100f84811)
- feat(CardAssignment): support EAN13 [`8e314f8`](https://git.odit.services/lfk/frontend/commit/8e314f8676e5c189c09a33c9a382a8b984af5e30)
#### [1.11.3](https://git.odit.services/lfk/frontend/compare/1.11.2...1.11.3)
> 25 April 2025
- chore(deps): Updated lock [`bce6d48`](https://git.odit.services/lfk/frontend/commit/bce6d484a9b1d7c40d4575a64ad528da2cc8fb3c)
- chore(release): 1.11.3 [`98a3b07`](https://git.odit.services/lfk/frontend/commit/98a3b072370f029f82f79a75b8f809bc1b41d82f)
- fix(cards): Z-Index for bulk card modal [`efad6fd`](https://git.odit.services/lfk/frontend/commit/efad6fdf2eae12c904575727b61fc61ed8a27d14)
#### [1.11.2](https://git.odit.services/lfk/frontend/compare/1.11.1...1.11.2)
> 23 April 2025
- chore(release): 1.11.2 [`2dea19d`](https://git.odit.services/lfk/frontend/commit/2dea19df8990b34a56a345fef05487684ecec8c4)
- fix(cardassignment): handle card not found [`6c98646`](https://git.odit.services/lfk/frontend/commit/6c986467d39344a32e598b834a115932ab8c9a1f)
#### [1.11.1](https://git.odit.services/lfk/frontend/compare/1.11.0...1.11.1)
> 22 April 2025
- chore(release): 1.11.1 [`3cedbeb`](https://git.odit.services/lfk/frontend/commit/3cedbebe40b7699e39edc133e4bcbc16f4e7eb56)
- ci: fix build [`dc986e4`](https://git.odit.services/lfk/frontend/commit/dc986e4fe5d462909e34b8bbf9a3c10accb2a00b)
#### [1.11.0](https://git.odit.services/lfk/frontend/compare/1.10.6...1.11.0)
> 22 April 2025
- chore: move to tailwind v4 [`81a8ce0`](https://git.odit.services/lfk/frontend/commit/81a8ce002cd5a470f5ac4f064360c09bcf815ac8)
- chore(deps): bump [`244be47`](https://git.odit.services/lfk/frontend/commit/244be471f07a6ab77768fb599cc2381a319e4e22)
- chore: move privacy & imprint to website links [`3288ffb`](https://git.odit.services/lfk/frontend/commit/3288ffb3cc52bcfbb2b94c373932e9d614fba80f)
- fix: modal position & z index [`a12c4f8`](https://git.odit.services/lfk/frontend/commit/a12c4f87d2db2271a6b123c3a009bd8d2f7663a4)
- chore: tailwind class cleanup [`80ab4e0`](https://git.odit.services/lfk/frontend/commit/80ab4e037e44edf1f076d4a365ef36c614e7cb0a)
- fix(cardassignment): debounce w/ immediate start [`1d6ed99`](https://git.odit.services/lfk/frontend/commit/1d6ed990736c6156b67f0715ee1f86aeb2be0390)
- fix(RunnerDetail): move to standard html select for groups [`e1bf435`](https://git.odit.services/lfk/frontend/commit/e1bf435080a3845a4a29f9b8bce7aba3e9220cf9)
- chore(release): 1.11.0 [`cae6be6`](https://git.odit.services/lfk/frontend/commit/cae6be6f867aece8997799e90f783a7b5f9b7602)
- feat(ImportRunnerModal): sort groups by name [`3124574`](https://git.odit.services/lfk/frontend/commit/312457494cb0da805df5ec2811604ea45a09963d)
- feat(cards): badge style for runner links [`1605b0f`](https://git.odit.services/lfk/frontend/commit/1605b0f7b2c12d2b247f40aefc03d2d053fea60d)
#### [1.10.6](https://git.odit.services/lfk/frontend/compare/1.10.5...1.10.6)
> 22 April 2025
- feat: improved mobile card assignment [`d841727`](https://git.odit.services/lfk/frontend/commit/d841727439e42c940d56001f08da4ae2399176a7)
- chore(release): 1.10.6 [`3fab344`](https://git.odit.services/lfk/frontend/commit/3fab3447799a5a28db741643189a2c4217b03714)
#### [1.10.5](https://git.odit.services/lfk/frontend/compare/1.10.4...1.10.5)
> 22 April 2025
- chore(release): 1.10.5 [`87df34b`](https://git.odit.services/lfk/frontend/commit/87df34bb56c04c1efd22ecbe0b857198358f93d5)
- feat(runners): add distance sorting [`9c3b742`](https://git.odit.services/lfk/frontend/commit/9c3b742a98e17837fbcabe308fb2774dc13b03b8)
#### [1.10.4](https://git.odit.services/lfk/frontend/compare/1.10.3...1.10.4)
> 22 April 2025
- chore(release): 1.10.4 [`68bf371`](https://git.odit.services/lfk/frontend/commit/68bf3717f9cf15a547c51e245452d4e013f7acad)
- fix(runners): Fix runner overview sort [`f88c6e0`](https://git.odit.services/lfk/frontend/commit/f88c6e0dbab620887e180de0d3eab03b8215a477)
#### [1.10.3](https://git.odit.services/lfk/frontend/compare/1.10.2...1.10.3)
> 17 April 2025
- feat(pdf): Send selfservicelink for generation [`01e77a9`](https://git.odit.services/lfk/frontend/commit/01e77a97f3f28700e0249d35afd9641b56d9c55d)
- chore(release): 1.10.3 [`12050cd`](https://git.odit.services/lfk/frontend/commit/12050cdda95b5f8eb5b414b81655d06faae3ef1e)
- chore(deps): Bump @odit/lfk-client-js [`10824b5`](https://git.odit.services/lfk/frontend/commit/10824b5d9b207e14a37fa23e90d54337d76e60a9)
#### [1.10.2](https://git.odit.services/lfk/frontend/compare/1.10.1...1.10.2)
> 11 April 2025
- refactor(runners): filter table for created_via [`785b9e0`](https://git.odit.services/lfk/frontend/commit/785b9e0b60a9961f99d0c519d6bb12dc735ac605)
- chore(release): 1.10.2 [`d9870e0`](https://git.odit.services/lfk/frontend/commit/d9870e03bc3175ee9b299174a19f257d6046a718)
#### [1.10.1](https://git.odit.services/lfk/frontend/compare/1.10.0...1.10.1)
> 9 April 2025
- feat: runner list filtered by created_via [`991716a`](https://git.odit.services/lfk/frontend/commit/991716a7f55d0414111ad264ad1e93de9e82971a)
- chore(release): 1.10.1 [`fce2bc6`](https://git.odit.services/lfk/frontend/commit/fce2bc645e040322f4d1b98a1ed1ab5df7227b6d)
#### [1.10.0](https://git.odit.services/lfk/frontend/compare/1.9.11...1.10.0)
> 9 April 2025
- feat: working CardAssignment [`ac4ef8f`](https://git.odit.services/lfk/frontend/commit/ac4ef8fb6ded5c5d5678a651420e356b3ef45399)
- chore(release): 1.10.0 [`aa720f2`](https://git.odit.services/lfk/frontend/commit/aa720f2460079e35eed9d87a2ac580a3305efbd5)
#### [1.9.11](https://git.odit.services/lfk/frontend/compare/1.9.10...1.9.11)
> 8 April 2025
- feat(dash): add runnersViaKiosk [`5291f8a`](https://git.odit.services/lfk/frontend/commit/5291f8a4d1721cd0c745191ebc85f221c34a23c8)
- chore(release): 1.9.11 [`eae0afd`](https://git.odit.services/lfk/frontend/commit/eae0afda23a54020e25821c0188d8cbec3139593)
#### [1.9.10](https://git.odit.services/lfk/frontend/compare/1.9.9...1.9.10)
> 8 April 2025
- feat: add experimental ui for mobile card assignment [`d7c9c27`](https://git.odit.services/lfk/frontend/commit/d7c9c27ec7a1fea1cbaf26914843d044bbae32fe)
- chore(release): 1.9.10 [`e2d7de1`](https://git.odit.services/lfk/frontend/commit/e2d7de1e9e1fd134f54876fa80f19f94fbea3672)
#### [1.9.9](https://git.odit.services/lfk/frontend/compare/1.9.8...1.9.9)
> 4 April 2025
- chore(release): 1.9.9 [`153b1b3`](https://git.odit.services/lfk/frontend/commit/153b1b3c2badee4826be614c3dbaafc10e1fbfea)
- fix(CopyScanStationTokenModal): code sizes [`ec63c7c`](https://git.odit.services/lfk/frontend/commit/ec63c7c1c51ccaf25bdd1eacffda66c820003a4c)
#### [1.9.8](https://git.odit.services/lfk/frontend/compare/1.9.7...1.9.8)
> 2 April 2025
- feat(GenerateSponsoringContracts): show download progress [`e261d5e`](https://git.odit.services/lfk/frontend/commit/e261d5e345f3175672bf86646ed838dd23400e50)
- chore(release): 1.9.8 [`05c2535`](https://git.odit.services/lfk/frontend/commit/05c253569877a45f3c4759262255ca70aa9ee4a3)
#### [1.9.7](https://git.odit.services/lfk/frontend/compare/1.9.6...1.9.7)
> 2 April 2025
- fix: ImportRunnerModal scrolling & team select [`766eeab`](https://git.odit.services/lfk/frontend/commit/766eeab49fb3ca5715c19dbf9bc53cb71124d3df)
- chore(release): 1.9.7 [`c00497d`](https://git.odit.services/lfk/frontend/commit/c00497d7760a935965cc83213f72f35999a3c168)
#### [1.9.6](https://git.odit.services/lfk/frontend/compare/1.9.5...1.9.6)
> 29 March 2025
- chore(release): 1.9.6 [`3c9b404`](https://git.odit.services/lfk/frontend/commit/3c9b404234c7d7d2f0c48256be2130a0ed8ae047)
- pnpm allow builds [`9c56b38`](https://git.odit.services/lfk/frontend/commit/9c56b3883eeab9e1a5e1c4921bfb6528c230e0d4)
#### [1.9.5](https://git.odit.services/lfk/frontend/compare/1.9.4...1.9.5)
> 29 March 2025
- feat: modal improvements [`d7e84a7`](https://git.odit.services/lfk/frontend/commit/d7e84a79a892294d532cc93aa3391c14a7a5ce99)
- chore(release): 1.9.5 [`3d506db`](https://git.odit.services/lfk/frontend/commit/3d506db97502399e8b381b4cf38af2f07a584aec)
#### [1.9.4](https://git.odit.services/lfk/frontend/compare/1.9.3...1.9.4)
> 29 March 2025
- feat: improve modals [`90b0fec`](https://git.odit.services/lfk/frontend/commit/90b0fec2366b608d163decdcd8798e879cf8218d)
- chore(release): 1.9.4 [`102471e`](https://git.odit.services/lfk/frontend/commit/102471eaaae390d3ef815afde9ac4081be7d5dbc)
#### [1.9.3](https://git.odit.services/lfk/frontend/compare/1.9.2...1.9.3)
> 29 March 2025
- feat: modal improvements [`fbe38ee`](https://git.odit.services/lfk/frontend/commit/fbe38eede95813e163a390b693790d78ce75c215)
- feat: modal improvements [`22551c3`](https://git.odit.services/lfk/frontend/commit/22551c379f704b0d9c28c499f7d3f5a37f1533ca)
- ci: only tagged runs for now [`e9dffce`](https://git.odit.services/lfk/frontend/commit/e9dffcea835cbcd6b5eb4ed1cc3feb62a9e831db)
- chore(release): 1.9.3 [`4883e17`](https://git.odit.services/lfk/frontend/commit/4883e179e7090cf90783dcdecd5df8a422880188)
- feat: modal improvements [`13c6e96`](https://git.odit.services/lfk/frontend/commit/13c6e96292613d9619f779f2557201cf0b938753)
- feat(OrgDetail): improve selfservice link copy [`f547c0c`](https://git.odit.services/lfk/frontend/commit/f547c0cc817d7db0c70df4059dad753e9b16c1c9)
- chore(deps): pnpm@10.7 [`b9563d7`](https://git.odit.services/lfk/frontend/commit/b9563d75dd15519d9ec5d425d628d232e7609913)
- fix: sidebar [`a102af5`](https://git.odit.services/lfk/frontend/commit/a102af5a78c83cd54b4981bff2f6c8d54cf8c74c)
#### [1.9.2](https://git.odit.services/lfk/frontend/compare/1.9.1...1.9.2)
> 28 March 2025
- chore: update lfk client [`f4542ad`](https://git.odit.services/lfk/frontend/commit/f4542adf3b7c757d907c979b989450b64553d750)
- feat(dashboard): show runners via selfservice count [`0ee43f8`](https://git.odit.services/lfk/frontend/commit/0ee43f80a65bb5b83d51d6c098bd203bc09e2f1f)
- chore(release): 1.9.2 [`3a56942`](https://git.odit.services/lfk/frontend/commit/3a569422ad7d68d0009fa73229dd73ee00be87a9)
- refactor: change release message [`9f0623d`](https://git.odit.services/lfk/frontend/commit/9f0623d194a7784d4ede3cb6a6cd10d0aea4a180)
#### [1.9.1](https://git.odit.services/lfk/frontend/compare/1.9.0...1.9.1)
> 28 March 2025
- refactor: project cleanup [`04897c7`](https://git.odit.services/lfk/frontend/commit/04897c7d2e89cb7e834815907409698ad6758637)
- 🚀RELEASE v1.9.1 [`5bab95a`](https://git.odit.services/lfk/frontend/commit/5bab95a9423d9da8c17165732b988ca868f950a5)
- feat(RunnerDetail): show created_via [`a4fbaba`](https://git.odit.services/lfk/frontend/commit/a4fbabaf9a0a9a26b6c6782056f11b8a646b8f16)
- feat(ConfirmTeamDeletionModal): success toast [`831f369`](https://git.odit.services/lfk/frontend/commit/831f36946d5db777ca77855161f653f861cbd56e)
#### [1.9.0](https://git.odit.services/lfk/frontend/compare/1.8.2...1.9.0)
> 28 March 2025
- feat: improved ConfirmOrgDeletionModal [`fecf3b5`](https://git.odit.services/lfk/frontend/commit/fecf3b59a320afafee52c95b361edec644c5cbff)
- feat: modal improvements [`f5a46aa`](https://git.odit.services/lfk/frontend/commit/f5a46aa203ca2adf2e4e6fe4863629ca80f1becb)
- feat: improve ConfirmTeamDeletionModal [`a2cd54f`](https://git.odit.services/lfk/frontend/commit/a2cd54fba4a987f7f7dbab22cc958f9aea2817ff)
- feat: improve modals [`443371e`](https://git.odit.services/lfk/frontend/commit/443371e2fdc42506e6e87379bd65facbd8f22d7d)
- feat: improve toasts [`481f6b6`](https://git.odit.services/lfk/frontend/commit/481f6b686e77ffa36ee08b62f653d626dd9124c9)
- feat: improve modals [`e7a69eb`](https://git.odit.services/lfk/frontend/commit/e7a69ebdca668a5d78b52d092aa1bca6259aa19b)
- 🚀RELEASE v1.9.0 [`b7e6fda`](https://git.odit.services/lfk/frontend/commit/b7e6fdaeacf17a7cc77109460b5e2c6d3775ef7b)
- feat: improve translations [`d7ab924`](https://git.odit.services/lfk/frontend/commit/d7ab9247cd2eab4f7269b23de5fada76a99ac8bc)
- feat: improved translations [`18a4623`](https://git.odit.services/lfk/frontend/commit/18a4623e71dfd942f2268203ce713030acfb2d9d)
- feat: improve translations [`9048f3d`](https://git.odit.services/lfk/frontend/commit/9048f3df774df233705a41b08012193447eab803)
- feat: improved sidebar z-index [`968a7cc`](https://git.odit.services/lfk/frontend/commit/968a7ccc0e7917bf1a42ac8f85f358880951cc2a)
- feat: improved track deletion ui feedback [`194c3c4`](https://git.odit.services/lfk/frontend/commit/194c3c4886e3f3206d76d8634be9d3dd2fa02d8d)
#### [1.8.2](https://git.odit.services/lfk/frontend/compare/1.8.1...1.8.2)
> 26 March 2025
- feat: improvement of card,certificate,sponsoringcontract action buttons [`7633b7b`](https://git.odit.services/lfk/frontend/commit/7633b7b05671342bc30e0bbecbcd9450e06b5e4d)
- 🚀RELEASE v1.8.2 [`6249502`](https://git.odit.services/lfk/frontend/commit/6249502a88ec5bfba6dfbc3ad5ede82d71d0d9e2)
- feat(dashboard): active item for teams + runners [`8a78034`](https://git.odit.services/lfk/frontend/commit/8a780340792445fff1f78db994fb78acb5da8304)
#### [1.8.1](https://git.odit.services/lfk/frontend/compare/1.8.0...1.8.1)
> 26 March 2025
- 🚀RELEASE v1.8.1 [`b8e6b24`](https://git.odit.services/lfk/frontend/commit/b8e6b24bf32379c3f4a1679d422e6fdcc45f7c99)
- fix(pdf_generation): Only load direct runners for direct calls [`97b7ca9`](https://git.odit.services/lfk/frontend/commit/97b7ca931f607ee64509ad10c2269632fc691091)
#### [1.8.0](https://git.odit.services/lfk/frontend/compare/1.7.0...1.8.0)
> 26 March 2025
- wip [`824ecfa`](https://git.odit.services/lfk/frontend/commit/824ecfab2e976cd7c6cd2851be8a9be5c6b686e1)
- wip [`0a6cf61`](https://git.odit.services/lfk/frontend/commit/0a6cf619b09be837d5503f4695250c7edaeeaff5)
- feat: improve fonts + button positions [`c37fb98`](https://git.odit.services/lfk/frontend/commit/c37fb98bed377744981e927ea8d22db9e20c55ca)
- 🚀RELEASE v1.8.0 [`48dd9ac`](https://git.odit.services/lfk/frontend/commit/48dd9acde595b882630855d5e6af3cfa18fc9ecf)
- wip [`1bc5314`](https://git.odit.services/lfk/frontend/commit/1bc53146b9f024f3cab613b227d29304d687c92b)
- wip [`e82350d`](https://git.odit.services/lfk/frontend/commit/e82350df4af082d2bbb322658c6c022d83b819ae)
- wip [`37cdbba`](https://git.odit.services/lfk/frontend/commit/37cdbba0a3563875e19bee560f2cd5c8fc2d7a6e)
- feat: improve input readability [`79e6a42`](https://git.odit.services/lfk/frontend/commit/79e6a4212d06029766d0a853686ed97879ebd349)
- wip [`5f5d827`](https://git.odit.services/lfk/frontend/commit/5f5d8277b98363ef15a92621fca0a209345aca95)
- chore(deps): bump [`bb2319a`](https://git.odit.services/lfk/frontend/commit/bb2319a78d253a2d6239a0d3daedc90fd29abdd0)
- feat: cleanup TeamDetail + OrgDetail [`f734d1e`](https://git.odit.services/lfk/frontend/commit/f734d1e3f643a500a6432a389c3103045cc51262)
- refactor(ci): Switch to actions for dev [`847fa28`](https://git.odit.services/lfk/frontend/commit/847fa288f1b5bbc422cc2944bbe66e80c5a00407)
- refactor(ci): Add Gitea workflow for building release images and remove Woodpecker configuration [`3ec18a6`](https://git.odit.services/lfk/frontend/commit/3ec18a696435ada26bf2de2220b190dc630a9759)
- feat: athiti font [`391186d`](https://git.odit.services/lfk/frontend/commit/391186d01f3b96638a3569dc2843bf181dc3f02c)
- fix(DonorDetail): donor deletion [`5147a20`](https://git.odit.services/lfk/frontend/commit/5147a20b3c4a46968482b1e3517047351c94f77e)
- feat(dashboard): full width for sidebar items [`975f145`](https://git.odit.services/lfk/frontend/commit/975f145444e5a478524ea2cbbfb9059b93617185)
- wip [`3d3ce29`](https://git.odit.services/lfk/frontend/commit/3d3ce2918bc20cf1080a2b5153ddd8aaf51374b4)
- feat(RunnerOrganizationService.runnerOrganizationControllerGetRunners): load all runners in org [`7c10d95`](https://git.odit.services/lfk/frontend/commit/7c10d95c1c68f4842fd323698e004a5ebf2c96cf)
- wip [`050a146`](https://git.odit.services/lfk/frontend/commit/050a146ae070d67d8308db4b9612fd6eacbb9923)
- fix(ci): Correct tag pattern syntax in release workflow [`e567bb3`](https://git.odit.services/lfk/frontend/commit/e567bb35c3b3f6eb73a2f0bc72f601e70f881ac8)
#### [1.7.0](https://git.odit.services/lfk/frontend/compare/1.6.0...1.7.0)
> 17 December 2024
- refactor(pdfgeneration): Switch cards over to new service [`e230984`](https://git.odit.services/lfk/frontend/commit/e23098410c7d0b326cdbbb3a4b63fed10611e252)
- refactor(pdfgeneration): Switch to new document-server api [`878d971`](https://git.odit.services/lfk/frontend/commit/878d9714cbc0a60cfd96bd1faf8af6af46e6fb5e)
- refactor(pdfgeneration): Switched contract generation over to new document-server [`f99b7f4`](https://git.odit.services/lfk/frontend/commit/f99b7f4bb8f166bb966022ddd10689c082d248f0)
- refactor(cards): Switched over to new document-server api [`65ce02e`](https://git.odit.services/lfk/frontend/commit/65ce02e777e6e9b3cfed248de680e5f292b3a639)
- 🚀RELEASE v1.7.0 [`ae056cd`](https://git.odit.services/lfk/frontend/commit/ae056cd88cb27f003845fa4534553cde841c7f99)
- fix(pdfgeneration): Added parent_group [`7f989b2`](https://git.odit.services/lfk/frontend/commit/7f989b206b16e2687d01a38da8e3ea9be0a52ba5)
#### [1.6.0](https://git.odit.services/lfk/frontend/compare/1.5.3...1.6.0)
> 11 December 2024
- refactor(orgs): Swtich to new selfservice baseurl [`e2d6fbb`](https://git.odit.services/lfk/frontend/commit/e2d6fbb513dc9fe7ce05855edb4b0b4b5daeb07a)
- chore: bump [`04494d2`](https://git.odit.services/lfk/frontend/commit/04494d2a2a542f25f785f3bb23e49e5eb0691c0a)
#### [1.5.3](https://git.odit.services/lfk/frontend/compare/1.5.2...1.5.3)
> 26 November 2024
- feat(dx): Yarn support [`fc15c68`](https://git.odit.services/lfk/frontend/commit/fc15c68cba0d1986563eaf63da3a68784a685a9e)
- feat(about): cleanup ui [`84aa846`](https://git.odit.services/lfk/frontend/commit/84aa846b87186b52a2f8632724d4f2cb70af062b)
- feat(dashboard): reorder menu items [`e967d8d`](https://git.odit.services/lfk/frontend/commit/e967d8d20c6972b64b0096594a09043553e9c7e5)
- fix: unexpected/ missing props [`d803f3d`](https://git.odit.services/lfk/frontend/commit/d803f3d4905d6f792b77d17025467ac13c29068b)
- chore(deps): bump some [`68b4309`](https://git.odit.services/lfk/frontend/commit/68b4309164eac40b6fda969b60a7e238985d49f8)
- 🚀RELEASE v1.5.3 [`477c650`](https://git.odit.services/lfk/frontend/commit/477c650f3f6dd2eadf5f1cc404e8fc9b02a7841b)
- fix(ci): Switch over to new woodpecker version [`7ba890d`](https://git.odit.services/lfk/frontend/commit/7ba890dfd7ba908ebef0338f6faa5e7d804cb5ef)
- refactor(ci): Only build licences, don't export [`32b5f54`](https://git.odit.services/lfk/frontend/commit/32b5f5420bf9ff656b713d61b3a0113b9d6cb69f)
- feat: cleanup random page toasts [`ad4db88`](https://git.odit.services/lfk/frontend/commit/ad4db882f0f4d00a80ae5e0072e09c071c07ffa2)
- fix(ci): Update git pushb settings [`ee87f82`](https://git.odit.services/lfk/frontend/commit/ee87f82799ce559fd43d671ab412f2643eafeac6)
- fix(ci): Update relase machanism [`7c08f52`](https://git.odit.services/lfk/frontend/commit/7c08f522aa4b2986544a4c0e5d3261c4c7296121)
- fix(ci): Install pnpm [`e211554`](https://git.odit.services/lfk/frontend/commit/e211554579b1f27d13194eff4aad76f6f030141e)
- fix(orgs): ImportRunnerModal props [`5468766`](https://git.odit.services/lfk/frontend/commit/5468766d875a6278f01ed1fd9573688374befdd5)
#### [1.5.2](https://git.odit.services/lfk/frontend/compare/1.5.1...1.5.2)
> 21 November 2024
- feat: improved dashboard titles ui + a11y [`21453ef`](https://git.odit.services/lfk/frontend/commit/21453ef272665c0b7c7b04009b7b74e110fbd988)
- feat: improved dashboard titles ui + a11y [`c883920`](https://git.odit.services/lfk/frontend/commit/c883920caaaaef30a8e54dd0e7eecd68943f3041)
- feat(dashboard): improved a11y of active sidebar menu item [`a50447f`](https://git.odit.services/lfk/frontend/commit/a50447f457ecc045995efb7b952b07ea09c91373)
- feat: improved mobile buttons + search ui [`38fb111`](https://git.odit.services/lfk/frontend/commit/38fb111f7a2b5a1a01b17b00e89ee081e4b91bd9)
- feat(i18n/de): rename "Track" to "Laufstrecke" [`1018243`](https://git.odit.services/lfk/frontend/commit/10182433f825968ee55298399b231173698a795c)
- 🚀RELEASE v1.5.2 [`3532968`](https://git.odit.services/lfk/frontend/commit/3532968b3399b985b1ed28ba6b89a13f35f9289b)
- feat(dashboard): improved mobile ui hamburger button [`b338f33`](https://git.odit.services/lfk/frontend/commit/b338f33a63ad8e98ab44deff2f80dbd5fe2a0fc2)
- feat(dashboard): match greeting style with rest of titles [`b1a2044`](https://git.odit.services/lfk/frontend/commit/b1a20446314d1b25e9f653bd2767b072fd629f97)
- feat(dashboard): add lfk icon and app name to mobile nav bar [`6bb49db`](https://git.odit.services/lfk/frontend/commit/6bb49db4eee95486f5a947d708b80a7a94d36933)
- feat(users/UsersOverview): improve ui by adding borders to badges [`cb82200`](https://git.odit.services/lfk/frontend/commit/cb82200481c629a0dd8b235821115ae4276948ca)
#### [1.5.1](https://git.odit.services/lfk/frontend/compare/1.5.0...1.5.1)
> 21 November 2024
- chore(deps): pnpm@9 [`35bec9f`](https://git.odit.services/lfk/frontend/commit/35bec9fe584b93cd52e8bab4e469713468a67f70)
- chore(deps): bump some [`8fae1fb`](https://git.odit.services/lfk/frontend/commit/8fae1fb6b3e033f789d2568cbd2640c0d163dc53)
- fix(scanstations): CopyScanStationTokenModal open after create [`372fa11`](https://git.odit.services/lfk/frontend/commit/372fa110ec402dae166a302f2209c79353983148)
- 🚀RELEASE v1.5.1 [`9abf74d`](https://git.odit.services/lfk/frontend/commit/9abf74d6d217e7745c1055bdbfbe97de7b14572f)
- fix(config): add explicit window.config [`91d2f46`](https://git.odit.services/lfk/frontend/commit/91d2f46b934bcba1429bd1d96e772c25c42a3e28)
- fix(dockerfile): AS casing [`50e81a6`](https://git.odit.services/lfk/frontend/commit/50e81a6cb5773381e153cbec3bed7db820ced84a)
- refactor(scanstations/CopyScanStationTokenModal): drop dispatch [`a5e72a1`](https://git.odit.services/lfk/frontend/commit/a5e72a18e368b5a7ee7b4e1894de613ecb767f28)
- chore(deps): node:23.2.0 [`93d67bd`](https://git.odit.services/lfk/frontend/commit/93d67bdba90a67b45d8895d9facaf66e908d53d6)
- fix(tracks/AddTrackModal): i18n [`c60bae4`](https://git.odit.services/lfk/frontend/commit/c60bae45334c2aa90d8931da07691c196469da46)
- fix: tailwind config [`43ac878`](https://git.odit.services/lfk/frontend/commit/43ac878d44b556c6d7811610f6fe0c9a5eff305f)
#### [1.5.0](https://git.odit.services/lfk/frontend/compare/1.4.13...1.5.0)
> 20 November 2024
- feat(ci)!: Switch to woodpecker [`fb8206f`](https://git.odit.services/lfk/frontend/commit/fb8206ff130f4f65dcf619a2a786e7d5895b77a1)
- 🚀RELEASE v1.5.0 [`ceabd06`](https://git.odit.services/lfk/frontend/commit/ceabd06a4319c3c9ffab680f909730d5bd789540)
- fix(components): Add missing toast imports [`9bfc0c5`](https://git.odit.services/lfk/frontend/commit/9bfc0c5338933e832d5df50457c7978c026d8df6)
#### [1.4.13](https://git.odit.services/lfk/frontend/compare/1.4.12...1.4.13)
> 31 July 2023
- 🚀RELEASE v1.4.13 [`dceb0ef`](https://git.odit.services/lfk/frontend/commit/dceb0ef46197dc56e29c5f52a5bd8f9fe9b70b27)
- Show donations as euro in export [`88bc198`](https://git.odit.services/lfk/frontend/commit/88bc1982cab4481e2e9245f81eff27e095b66a0f)
- new license file version [CI SKIP] [`6193eff`](https://git.odit.services/lfk/frontend/commit/6193eff38e1a9d5726bc7d572ab36b921de843d0)
#### [1.4.12](https://git.odit.services/lfk/frontend/compare/1.4.11...1.4.12)
> 18 May 2023
- 🚀RELEASE v1.4.12 [`65f1d22`](https://git.odit.services/lfk/frontend/commit/65f1d222050b0dec81fc847c1921b6135a55ce50)
- fix(donation/payment): Funny javascript number to float conversion where integers were needed [`d867c08`](https://git.odit.services/lfk/frontend/commit/d867c08aba234d3a7fe9e2311d37dc5e96fc2afc)
- new license file version [CI SKIP] [`08642d7`](https://git.odit.services/lfk/frontend/commit/08642d7618faeae31f0acfe776642c9fa156e5ff)
#### [1.4.11](https://git.odit.services/lfk/frontend/compare/1.4.10...1.4.11)
> 10 May 2023
- chore(deps): Lockfile [`f77460b`](https://git.odit.services/lfk/frontend/commit/f77460bb0c8ce6d0f3d83a077017d5fc7bf55af7)
- 🚀RELEASE v1.4.11 [`373484c`](https://git.odit.services/lfk/frontend/commit/373484c2424bea7ae0d70d342e0ae2076aab1b6a)
- feat(orgs): Show total distance [`574e0dc`](https://git.odit.services/lfk/frontend/commit/574e0dcb051305bde2fc76d8456a35baec0cf309)
- chore(deps): More bumps [`7b19a0a`](https://git.odit.services/lfk/frontend/commit/7b19a0aa08bb6c89c51d27c0d05777e8fcfdad17)
#### [1.4.10](https://git.odit.services/lfk/frontend/compare/1.4.9...1.4.10)
> 10 May 2023
- chore(deps): Bumped svelte-table [`29a2854`](https://git.odit.services/lfk/frontend/commit/29a2854671b3af5b85ea96d050a9076f47b6575d)
- 🚀RELEASE v1.4.10 [`c3e9c27`](https://git.odit.services/lfk/frontend/commit/c3e9c27cd3d4b916f1661d4958cabab038918587)
- chore(deps): Pin and bump [`8e6786e`](https://git.odit.services/lfk/frontend/commit/8e6786e72227b3f07cc805f0957d5b7fd123ec13)
- chore(deps): Bumped scanclientjs [`6ad4056`](https://git.odit.services/lfk/frontend/commit/6ad40564e3e342046f6ee19fab9e455ec3bbff9b)
#### [1.4.9](https://git.odit.services/lfk/frontend/compare/1.4.8...1.4.9)
> 9 May 2023
- 🚀RELEASE v1.4.9 [`776973b`](https://git.odit.services/lfk/frontend/commit/776973bfe9b34c26a1c80d5e458cc2644dd9036b)
- Changed the in table replacement method [`d9a47f8`](https://git.odit.services/lfk/frontend/commit/d9a47f882c1c6bcf98ef85d50d70c010d54b326e)
- Fixed empty return [`6025e43`](https://git.odit.services/lfk/frontend/commit/6025e43baa8516657a60a1de9a82c2189221c6ac)
#### [1.4.8](https://git.odit.services/lfk/frontend/compare/1.4.7...1.4.8)
> 9 May 2023
- Switched donor loading to non-paginated [`59fe2df`](https://git.odit.services/lfk/frontend/commit/59fe2dfabb224863876c4db31a965c34a51a9369)
- 🚀RELEASE v1.4.8 [`4235758`](https://git.odit.services/lfk/frontend/commit/4235758a6d1499715287d6ab193cc87c68d5742e)
#### [1.4.7](https://git.odit.services/lfk/frontend/compare/1.4.6...1.4.7)
> 4 May 2023
- Paginated modal data loading [`a8a7711`](https://git.odit.services/lfk/frontend/commit/a8a771114df6eb57d5b1d5497a5be49e619d4102)
- Moved loading to onmount [`4e0a2c8`](https://git.odit.services/lfk/frontend/commit/4e0a2c83015bde5e360c5fb2c0babbeaa03dc2b5)
- 🚀RELEASE v1.4.7 [`6364536`](https://git.odit.services/lfk/frontend/commit/6364536dcd840c71f7cb6afb31bbc4f160ac4f73)
#### [1.4.6](https://git.odit.services/lfk/frontend/compare/1.4.5...1.4.6)
> 4 May 2023
- 🚀RELEASE v1.4.6 [`b6fed92`](https://git.odit.services/lfk/frontend/commit/b6fed92a176af1c975484d9146ee5634e0031401)
- fix(donor/details): don't load donations [`a2ff5b8`](https://git.odit.services/lfk/frontend/commit/a2ff5b8a142ce4e6b8876f64935f9787ec44a51e)
- fix(donor/detail): Set email to null to avoid vaidation errors [`97b57ae`](https://git.odit.services/lfk/frontend/commit/97b57aeb0cc9058542a36dea9c8b2852169c250f)
- fix(donor/detail): Set phone to null to avoid vaidation errors [`e25ed1f`](https://git.odit.services/lfk/frontend/commit/e25ed1fff9b200605d5d2b78238b774ec7289aaa)
#### [1.4.5](https://git.odit.services/lfk/frontend/compare/1.4.4...1.4.5)
> 4 May 2023
- Revert "revert: buggy pagination" [`dacb2f8`](https://git.odit.services/lfk/frontend/commit/dacb2f8ace373f6594fc64af133971af053f00c0)
- fix: Removed dynamic pagesize adjustments [`803d64c`](https://git.odit.services/lfk/frontend/commit/803d64c78caa570d31d6055e70e2d2af6834f04b)
- 🚀RELEASE v1.4.5 [`0284f18`](https://git.odit.services/lfk/frontend/commit/0284f18beb8b24d4d4d071eca13bc5868666232c)
#### [1.4.4](https://git.odit.services/lfk/frontend/compare/1.4.3...1.4.4)
> 4 May 2023
- 🚀RELEASE v1.4.4 [`b7a5396`](https://git.odit.services/lfk/frontend/commit/b7a53960e5f37ae089d77bc11668d917145e2abb)
- fix(AddDonationModal): missing toast dismiss on success distance donation [`66f1e6b`](https://git.odit.services/lfk/frontend/commit/66f1e6b4fe1350ee79673a0aff97e36f44179c92)
#### [1.4.3](https://git.odit.services/lfk/frontend/compare/1.4.2...1.4.3)
> 4 May 2023
- revert: buggy pagination [`b264864`](https://git.odit.services/lfk/frontend/commit/b2648645e8fc05f8742ecfc592557f954261671b)
- 🚀RELEASE v1.4.3 [`33166bf`](https://git.odit.services/lfk/frontend/commit/33166bfafcffb9d86dfc7dfcd2cb8ba5c85da7e7)
#### [1.4.2](https://git.odit.services/lfk/frontend/compare/1.4.1...1.4.2)
> 4 May 2023
- 🚀RELEASE v1.4.2 [`53e3ddb`](https://git.odit.services/lfk/frontend/commit/53e3ddb751c1150a4640ae6302e4df5b88cedc51)
- fix(GenerateRunnerCertificates): missing toast import [`d49f545`](https://git.odit.services/lfk/frontend/commit/d49f545d94acabc0c96860f212466b7a4cbe7dab)
- fix(DonorDetail): missing toast import [`edc2dca`](https://git.odit.services/lfk/frontend/commit/edc2dcab92c3cace05335a283a849c3c978ec8ec)
#### [1.4.1](https://git.odit.services/lfk/frontend/compare/1.4.0...1.4.1)
> 1 May 2023
- 🚀RELEASE v1.4.1 [`3b98c99`](https://git.odit.services/lfk/frontend/commit/3b98c99b72f24b8552e2b2334f13622bdf6ef90d)
- Fixed translation [`1da775a`](https://git.odit.services/lfk/frontend/commit/1da775a09b8be90a49e06aed16df917d221ee989)
#### [1.4.0](https://git.odit.services/lfk/frontend/compare/1.3.4...1.4.0)
> 1 May 2023
- formatting, full migration to svelte-french-toast [`46d076a`](https://git.odit.services/lfk/frontend/commit/46d076af9d65ebb11504a7e6879753780b69db2c)
- drop gridjs (TracksOverview Actions will need to be re-implemented) [`8b92230`](https://git.odit.services/lfk/frontend/commit/8b922309b990c42fcfd57b939abacf4d8c99e638)
- 🚀RELEASE v1.4.0 [`f0475bd`](https://git.odit.services/lfk/frontend/commit/f0475bd9a08d99f58b4d3dce584cd6a3a8630e56)
- Added track update toasts [`103ad57`](https://git.odit.services/lfk/frontend/commit/103ad57ddc8a35ff971bef44053a9e32a7b68233)
- drop legacy ThFilter components [`bc4ac0f`](https://git.odit.services/lfk/frontend/commit/bc4ac0f3160571cd412361de82ef4555ee068677)
- text cleanups, StatCard improvements [`a2f9dbb`](https://git.odit.services/lfk/frontend/commit/a2f9dbbe014b5ae9705e8e7b6944f7f7c576d22e)
- Updated the track editing switch logic [`a953349`](https://git.odit.services/lfk/frontend/commit/a953349c1478b912e08f88c1fb70c74af0bc9bbb)
- chore(deps): bump all [`6154ca7`](https://git.odit.services/lfk/frontend/commit/6154ca7ddfb8b6ad0e1644b8c6756d51f2fbb858)
- svelte-french-toast + translations [`8fb1e0c`](https://git.odit.services/lfk/frontend/commit/8fb1e0ca0f51c90270fb5e1a05be5e8273238a2c)
- Implemented delete for new track table [`081a141`](https://git.odit.services/lfk/frontend/commit/081a141218ab7de2620f7b06083697368d44bf6c)
- striped tabled [`5e82638`](https://git.odit.services/lfk/frontend/commit/5e82638f3594298d0542cd03d5d6aa80aa383b9d)
- add svelte-french-toast [`56c3365`](https://git.odit.services/lfk/frontend/commit/56c33656562079bb773491c8aecedea3f6acdb74)
- Editing update logic [`2856c5c`](https://git.odit.services/lfk/frontend/commit/2856c5c1b786f732b0db80324ea74513e8be186d)
- monospace fonts for IDs in overviews [`811f5d5`](https://git.odit.services/lfk/frontend/commit/811f5d575496be43e5e48197813112d35e79a81f)
- Full track table editing [`e9cf2bc`](https://git.odit.services/lfk/frontend/commit/e9cf2bc8498fc02332059880d7a6994348165b76)
- drop propfilepic [`6952b87`](https://git.odit.services/lfk/frontend/commit/6952b8727f06c520cb60a00acfde1dff52d4f345)
- fix: scan laptime formatting [`01b415d`](https://git.odit.services/lfk/frontend/commit/01b415d4cb147879e959e86d053dc02cae8cfdc9)
- Dynamicly adjust page size for large datasets [`064197d`](https://git.odit.services/lfk/frontend/commit/064197d2226da772907099ecf96c3ab984c9af59)
- ScansOverview full month formatting [`69ec7fc`](https://git.odit.services/lfk/frontend/commit/69ec7fc1fecc67751643ce35f22925f3132b8792)
- translations [`8be40e2`](https://git.odit.services/lfk/frontend/commit/8be40e2d80336f72989deb3e5e20a7cd8f7fb6f1)
- drop middlename for admin users [`daeea24`](https://git.odit.services/lfk/frontend/commit/daeea24e0e99b8a95665167d62d0ee830bdea3de)
- add missing striped tables [`8eaad82`](https://git.odit.services/lfk/frontend/commit/8eaad8219a109fa8b4bd1f719d7079bff8b7c041)
- translation cleanups [`663cb29`](https://git.odit.services/lfk/frontend/commit/663cb29ccde4fa15317f764147187c5b82e748d5)
- Added edit button for trackscans [`175d867`](https://git.odit.services/lfk/frontend/commit/175d86745fb9bfce03fe5f5c638b52467b688938)
- a11y cleanup [`6bc92f4`](https://git.odit.services/lfk/frontend/commit/6bc92f4a080f0c506793866d99c97ccb87ba15b8)
- a11y fix OrgOverview [`0fca035`](https://git.odit.services/lfk/frontend/commit/0fca0352c59cdccb99716355591f88ff573ac949)
- README update [`d32eb82`](https://git.odit.services/lfk/frontend/commit/d32eb8266b0e9daec4b9ba52832d5e5118abec45)
- a11y improvements [`4c81e3c`](https://git.odit.services/lfk/frontend/commit/4c81e3c43218be4b23d137b386520c71d19f5844)
- cleanup legacy TeamsOverview text [`6c1a707`](https://git.odit.services/lfk/frontend/commit/6c1a70716665d57f1326c4475030ae15a7c459e0)
- fix: a11y in About page [`e904ab0`](https://git.odit.services/lfk/frontend/commit/e904ab0b8494ff57579c8954a8eb713fc223a88f)
- improved login [`dbaf857`](https://git.odit.services/lfk/frontend/commit/dbaf85799ac9e56d8760450cfe357df016f10da7)
- chore(deps): node:20.0.0 [`81d4da6`](https://git.odit.services/lfk/frontend/commit/81d4da655099100c631d450caafbf7039fa20592)
- cleanup MainDashContent [`763a01a`](https://git.odit.services/lfk/frontend/commit/763a01af09b36004ceccfa6b182b7dc5ea070128)
- Updated store directory for dockerfil [`bbf8170`](https://git.odit.services/lfk/frontend/commit/bbf8170cb98410bbcd8dc51bb122beee615312ee)
- Bump dockerfile node [`15d8afe`](https://git.odit.services/lfk/frontend/commit/15d8afefbb6b697a6cbdb2d803a7d8edcea4e650)
- Pinned config files used [`f3bcc01`](https://git.odit.services/lfk/frontend/commit/f3bcc01685f3ea3ef6786a8e7d9a5b1a4f829d53)
- Switched build image node version [`9523860`](https://git.odit.services/lfk/frontend/commit/95238606d52ca58985b91ea03f7e9f490fdf2310)
- Merge pull request 'next' (#180) from next into dev [`8c628f2`](https://git.odit.services/lfk/frontend/commit/8c628f23dcfb1f6f120d19bb3ecdb422ca5093cd)
- improved StatCard readability [`7d82536`](https://git.odit.services/lfk/frontend/commit/7d8253618b18719549824ed19e024b8828c9df06)
- new license file version [CI SKIP] [`38a6650`](https://git.odit.services/lfk/frontend/commit/38a665024eb1df3eba66c61d8cb3199000b629e5)
#### [1.3.4](https://git.odit.services/lfk/frontend/compare/1.3.3...1.3.4)
> 19 April 2023
- 🚀RELEASE v1.3.4 [`e7b2c64`](https://git.odit.services/lfk/frontend/commit/e7b2c647981111650b3e2e471f4b5195fa6b65b6)
- Smaller sponsoring page size [`7cb6b63`](https://git.odit.services/lfk/frontend/commit/7cb6b63eb9596da4ee84369b220c3e680c607032)
#### [1.3.3](https://git.odit.services/lfk/frontend/compare/1.3.2...1.3.3)
> 19 April 2023
- 🚀RELEASE v1.3.3 [`d6d88f5`](https://git.odit.services/lfk/frontend/commit/d6d88f5f60716ca496a17f09b835b23223ec495d)
- bumped lfk-client-js [`2c208c4`](https://git.odit.services/lfk/frontend/commit/2c208c438185892270a0ebd37deb6a7c9ac08fc0)
#### [1.3.2](https://git.odit.services/lfk/frontend/compare/1.3.1...1.3.2)
> 19 April 2023
- 🚀RELEASE v1.3.2 [`39bc6c4`](https://git.odit.services/lfk/frontend/commit/39bc6c49450964510f996369d014f92c569188ae)
- fix(donors): Shortened texts [`b94e3b7`](https://git.odit.services/lfk/frontend/commit/b94e3b745f2febbe91e16a7a26f96b47d347ab92)
- feat(donations): Resolve donations via donor [`6f337ae`](https://git.odit.services/lfk/frontend/commit/6f337aeee16267d1e67e3d3855b63b6f2e57979f)
- fix(donors): Removed debug infos [`5d48060`](https://git.odit.services/lfk/frontend/commit/5d48060834717b2244172a0914e2690f8fe634d9)
#### [1.3.1](https://git.odit.services/lfk/frontend/compare/1.3.0...1.3.1)
> 19 April 2023
- feat(donations): Donation table filtering [`91ab199`](https://git.odit.services/lfk/frontend/commit/91ab199769c9f4f8051c74ad43a701db321f3995)
- feat(donors): Added name and address filtering [`27b4dde`](https://git.odit.services/lfk/frontend/commit/27b4dde7551995c9d7e8ca33a9bd97d429a35801)
- 🚀RELEASE v1.3.1 [`c842c20`](https://git.odit.services/lfk/frontend/commit/c842c203e2fbf0a201297d475db9047c0691bd52)
- More filtering [`5bcfc8d`](https://git.odit.services/lfk/frontend/commit/5bcfc8db752fce96e9f523d14cefff1a4f675661)
#### [1.3.0](https://git.odit.services/lfk/frontend/compare/1.2.0...1.3.0)
> 19 April 2023
- feat(donations): Implemented donation deletion via confirm modal [`505fb8c`](https://git.odit.services/lfk/frontend/commit/505fb8cb08b81a7dcb08561bdda0f6464f140d3e)
- 🚀RELEASE v1.3.0 [`e75be49`](https://git.odit.services/lfk/frontend/commit/e75be49be42c3d5581e2204bfa064bfa3778c1b6)
- feat(donationsoverview): Switched donations overview to datatable [`133470b`](https://git.odit.services/lfk/frontend/commit/133470b6f2a63ec087f27c98ef260648a8672e5f)
- feat(donations): Implemented add donation payment via datatable refresh [`e5c9265`](https://git.odit.services/lfk/frontend/commit/e5c92655886ad9a6fcd7565fadd7955c477c3595)
- feat(donations): Donations reactive create and load into datatable [`02003ec`](https://git.odit.services/lfk/frontend/commit/02003ec80efc16aabd126710a6eeac18df43f841)
- feat(DonationsOverview): i18n loading text [`8f8858f`](https://git.odit.services/lfk/frontend/commit/8f8858f10071ddf9988d0ec0e3c4a891db24a102)
- new license file version [CI SKIP] [`4289034`](https://git.odit.services/lfk/frontend/commit/4289034436869750205a946247e7ab5f9892fe98)
#### [1.2.0](https://git.odit.services/lfk/frontend/compare/1.1.0...1.2.0)
> 19 April 2023
- feat(donoroverview): Added datatable formatters [`d98fb0d`](https://git.odit.services/lfk/frontend/commit/d98fb0d5b288c987a45ccbf2bb026ccaab539a71)
- 🚀RELEASE v1.2.0 [`fdc7d80`](https://git.odit.services/lfk/frontend/commit/fdc7d80bbf9bd698128e9ec4f91fa813499777a9)
- feat(donors): Load donors paginated [`5014bf5`](https://git.odit.services/lfk/frontend/commit/5014bf5bc5873cfe4ae04d71b4aff12b257dd2e3)
- feat(donorsoverview): Dynamicly add newly generated donors [`352551e`](https://git.odit.services/lfk/frontend/commit/352551e168b5dced5e7353e82655908d82d28af0)
- feat(donorsoverview): Implemented delete confirmation with datatable [`7aec050`](https://git.odit.services/lfk/frontend/commit/7aec050419f6f1bf853c3e1bc655b01725ed3b65)
#### [1.1.0](https://git.odit.services/lfk/frontend/compare/1.0.0...1.1.0)
> 19 April 2023
- 🚀RELEASE v1.1.0 [`0708cab`](https://git.odit.services/lfk/frontend/commit/0708cabc75e63a876e54a0b343318f8d934ae319)
- feat(dashboar): Added total donors to overview [`e0b6148`](https://git.odit.services/lfk/frontend/commit/e0b61486b089aa1e611ef3569b1521fc331ec0e4)
- feat(dashboard): Updated stats icons [`4fcb26c`](https://git.odit.services/lfk/frontend/commit/4fcb26cf9371e27e5d7e474b3558ef354e9114c0)
- feat(dashboard): Added average sponsoring [`269def2`](https://git.odit.services/lfk/frontend/commit/269def20d114ededaba3153bbd50ec2ddd70e1c6)
- feat(dashboard): Added total donations [`7b2b598`](https://git.odit.services/lfk/frontend/commit/7b2b59858839b98370af6fb1e6028ba0a1639186)
- feat(dashboard): Added average distance [`b8de9e0`](https://git.odit.services/lfk/frontend/commit/b8de9e0e427b3a8b56e6354ad7168ae12c7cce85)
- Lockfile [`3f86f74`](https://git.odit.services/lfk/frontend/commit/3f86f7412ffc4bc27328ad1f7d3c3118546e7e29)
- Bumped client [`6454d96`](https://git.odit.services/lfk/frontend/commit/6454d960de3f9f5ea86679f157b3b7e7cffde74d)
- new license file version [CI SKIP] [`2a64094`](https://git.odit.services/lfk/frontend/commit/2a640940062765a470387103a72ed14a2411d97b)
### [1.0.0](https://git.odit.services/lfk/frontend/compare/0.19.0...1.0.0)
> 19 April 2023
- 🚀RELEASE v1.0.0 [`8da7578`](https://git.odit.services/lfk/frontend/commit/8da7578a0a46a3e97d8c870e29399f6e8821c9fa)
- Merge pull request 'feature/175-request_pagination' (#176) from feature/175-request_pagination into dev [`e9ce964`](https://git.odit.services/lfk/frontend/commit/e9ce9644ff03f981cec6e9ad56aa5fdf0ff71ef4)
- Donation paginated loading [`ccf8656`](https://git.odit.services/lfk/frontend/commit/ccf865687b34016931a702c0a9b98a0a18e2b03a)
- Paginated scan loading [`cac34db`](https://git.odit.services/lfk/frontend/commit/cac34db1fd3bf5dc7c7be64b3a76ca4c8c77938d)
- Implemented Async loading of cards via pagination (500 cards per request) [`c33dfcf`](https://git.odit.services/lfk/frontend/commit/c33dfcfddddfed0902f3fa9b1d8a1d3e1560262f)
- Paginated runner loading (1000 per page) [`faf3893`](https://git.odit.services/lfk/frontend/commit/faf3893180bb735bea6f1ea58c896686b89949fe)
- Allways set loaded to true [`52439aa`](https://git.odit.services/lfk/frontend/commit/52439aa5bc8cfb1d78d5dfce55b1a0df640ad8f5)
- Bumped lfk client [`019e14a`](https://git.odit.services/lfk/frontend/commit/019e14ab1f99906f13d36c7148d0f4b7894072f2)
- new license file version [CI SKIP] [`a6ce04c`](https://git.odit.services/lfk/frontend/commit/a6ce04c90386f16abf235cc7b2f95aeea5011c7d)
#### [0.19.0](https://git.odit.services/lfk/frontend/compare/0.18.4...0.19.0)
> 17 April 2023
- 🚀RELEASE v0.19.0 [`94a64ca`](https://git.odit.services/lfk/frontend/commit/94a64ca69078c7fe2935eeb5f955fab95a79cb85)
- Merge pull request 'feature/173-scanstation_configcodes' (#174) from feature/173-scanstation_configcodes into dev [`165c154`](https://git.odit.services/lfk/frontend/commit/165c1542338c58f2abf42fef2e7b84b40d1e2d9c)
- I18n [`e60c09e`](https://git.odit.services/lfk/frontend/commit/e60c09e19c9cc20338906e84f4db4e009d926360)
- Implemented config code generation [`4b63427`](https://git.odit.services/lfk/frontend/commit/4b6342727ee0ea38597750d8c99edc301f1ccc2d)
- Styling [`4834d14`](https://git.odit.services/lfk/frontend/commit/4834d1484c3fb6ecd4a1b56aa9fbb8125c641a62)
- Adjusted size on smaller devices [`318547d`](https://git.odit.services/lfk/frontend/commit/318547db46045e41de64d5688368e85cd6fb8035)
- Lockfile [`947d01c`](https://git.odit.services/lfk/frontend/commit/947d01cf7fc7fe2ee88c56e017b0d663f1f3b4f9)
- Barcode placeholder [`cb5fa52`](https://git.odit.services/lfk/frontend/commit/cb5fa52cd9a97490b50fb0c02c26615b49650c08)
- Added bwip-js for barcode generation [`3563394`](https://git.odit.services/lfk/frontend/commit/3563394fb33d661890327e2ae08c400830b37844)
#### [0.18.4](https://git.odit.services/lfk/frontend/compare/0.18.3...0.18.4)
> 15 April 2023
- 🚀RELEASE v0.18.4 [`269d7a7`](https://git.odit.services/lfk/frontend/commit/269d7a7defdde059ef2bb5103262cf734e9babe9)
- Hide address2 in orgs by default [`e95f233`](https://git.odit.services/lfk/frontend/commit/e95f2333b0b958ed00c0e097b43aac2e70ad0d38)
#### [0.18.3](https://git.odit.services/lfk/frontend/compare/0.18.2...0.18.3)
> 15 April 2023
- 🚀RELEASE v0.18.3 [`950217e`](https://git.odit.services/lfk/frontend/commit/950217e0a350f9999b879475edf41f2f11c48179)
- Dont show adress 2 in runner overview [`5e65fb3`](https://git.odit.services/lfk/frontend/commit/5e65fb33013c3dad38e7ad6740b017ae206f278f)
#### [0.18.2](https://git.odit.services/lfk/frontend/compare/0.18.1...0.18.2)
> 15 April 2023
- 🚀RELEASE v0.18.2 [`2a294cd`](https://git.odit.services/lfk/frontend/commit/2a294cde040044bbebfb9c8b34b6c91b27772741)
- Added timestamps to scanoverview [`cffbd17`](https://git.odit.services/lfk/frontend/commit/cffbd17dc77054048cc9b14891f960f9a3fd18cb)
- Push in releaseit [`e95420d`](https://git.odit.services/lfk/frontend/commit/e95420d79c3227c0ca0cf0c0b599970c2b7d690e)
#### [0.18.1](https://git.odit.services/lfk/frontend/compare/0.18.0...0.18.1)
> 15 April 2023
- 🚀RELEASE v0.18.1 [`00de8c3`](https://git.odit.services/lfk/frontend/commit/00de8c3d75e90cd4614f42111f5f45bedde64130)
- Missing scanstation translations [`30e3396`](https://git.odit.services/lfk/frontend/commit/30e33968978bf33cedb31bcbf63fac273e1664f5)
- fix: button onclick a11y [`9fe53b0`](https://git.odit.services/lfk/frontend/commit/9fe53b0b9c71e8a6b4aa3f317327ffe729df0834)
- fix(ConfirmStatsClientDeletion): ScanStationService -> StatsClientService [`5291e04`](https://git.odit.services/lfk/frontend/commit/5291e049a1d2e880fbe277095da91b70d4812c3f)
- fix(ConfirmScanStationDeletion): donorControllerRemove -> scanStationControllerRemove [`08fbb50`](https://git.odit.services/lfk/frontend/commit/08fbb504c958415ce75e1e426296f870f0f1358d)
#### [0.18.0](https://git.odit.services/lfk/frontend/compare/0.17.3...0.18.0)
> 12 April 2023
- Moved filter function to typed version [`#171`](https://git.odit.services/lfk/frontend/issues/171)
- ScansOverview: migrate to datatable [`#168`](https://git.odit.services/lfk/frontend/issues/168)
- 🚀RELEASE v0.18.0 [`eb80406`](https://git.odit.services/lfk/frontend/commit/eb80406fdb8abf3f76bca742095e8f1f03480a56)
- wip: ScansOverview -> new datatable [`c87561f`](https://git.odit.services/lfk/frontend/commit/c87561f63b90ab951daf91d9b8b54ba96a94cc7f)
- Basic card table replace [`5662c3b`](https://git.odit.services/lfk/frontend/commit/5662c3b6da67c00c94254bf39f8820e531fc93ef)
- RunnersOverview: table responsiveness [`bf1e715`](https://git.odit.services/lfk/frontend/commit/bf1e715261c0076fd8543dd1187c516209a73b16)
- Basic card delete modal [`8ffe8ef`](https://git.odit.services/lfk/frontend/commit/8ffe8eff0623767809cdc49ea15cf2d30b609285)
- wip: delete scans [`f105cc0`](https://git.odit.services/lfk/frontend/commit/f105cc0a41972261610d03015ecd5ee492dab8e2)
- Added delete runner modal [`fd5db7d`](https://git.odit.services/lfk/frontend/commit/fd5db7d68ab4b32f7bb14bd0275a447b749e1fd8)
- Extracted table bottom [`8f50555`](https://git.odit.services/lfk/frontend/commit/8f50555a06a35d0b11ac1d9201851d00d915c4b8)
- fix: formatting [`9def0b2`](https://git.odit.services/lfk/frontend/commit/9def0b27c9958c5c75466f56c17b6dc4d44d2b50)
- fix(CardsOverview): table scroll + checkbox posititioning [`9c13b2f`](https://git.odit.services/lfk/frontend/commit/9c13b2f9e938ad36cb74c66a8f91a5b872c8c2d1)
- Extracted table header [`c241961`](https://git.odit.services/lfk/frontend/commit/c241961d0ab0d6b8c3bf0453c1a21d84027ed62f)
- drop legacy dependencies [`c98eb49`](https://git.odit.services/lfk/frontend/commit/c98eb49ae31d1d28de79c7a9c945ade50828f07f)
- scan delete working [`c7dcf7c`](https://git.odit.services/lfk/frontend/commit/c7dcf7c66d7141117e08462ad05f6f441565d012)
- Add Card appends current cards [`195d182`](https://git.odit.services/lfk/frontend/commit/195d182cc977b4ac9f342f09a9ea69d461892a95)
- Moved data loading to onmount [`a35af6f`](https://git.odit.services/lfk/frontend/commit/a35af6f02055115d60d040baaf22dabbeda38498)
- Add runners reactivity [`06411dc`](https://git.odit.services/lfk/frontend/commit/06411dc14747fc9803da009a29688789c31dfc42)
- quick cleanup [`ebdd1c2`](https://git.odit.services/lfk/frontend/commit/ebdd1c2c0c2191c60dfacd0c4be989748628ca63)
- Extracted deletion into function of overview [`a0727a0`](https://git.odit.services/lfk/frontend/commit/a0727a02913708de06be8bf1b9f8cbae5489f080)
- Moved update card logic to overview [`53b945c`](https://git.odit.services/lfk/frontend/commit/53b945c72fa5b30f03eff8bb83f8f25d85d79b07)
- wip:ScanValid badge [`bd4952e`](https://git.odit.services/lfk/frontend/commit/bd4952ee575b4cd8caea3608b14d8c51b37a55eb)
- yeeted extra table styling [`52a02c8`](https://git.odit.services/lfk/frontend/commit/52a02c82d2bdabdf3e632910aa415c1ffb97fd7f)
- Added custom runner filter [`7c6d39b`](https://git.odit.services/lfk/frontend/commit/7c6d39b5fa60d1b834c9fb74dd26dfc2225161d6)
- Scan reactive add [`f5d14f2`](https://git.odit.services/lfk/frontend/commit/f5d14f2e1810462cba99b2482320746f0750d4a1)
- Moved code around [`239f79f`](https://git.odit.services/lfk/frontend/commit/239f79fecba732c807f4296cf524be15c1ccff1e)
- Scan deletion [`7d8c68a`](https://git.odit.services/lfk/frontend/commit/7d8c68a4550407e8c871545e7a0c5f622a1e3b9c)
- Added status filter function [`f6c1fea`](https://git.odit.services/lfk/frontend/commit/f6c1fea17ce64d890ac810d9dff058dd589c2989)
- Added delete toast [`e2ddb5a`](https://git.odit.services/lfk/frontend/commit/e2ddb5a14cc8b450c00129709bf5b933a47866dd)
- Fixed killing of the dom [`2f62c7a`](https://git.odit.services/lfk/frontend/commit/2f62c7ae89b7dda3bc0efd4d56e677898a3f197f)
- Cards details modal [`f6985da`](https://git.odit.services/lfk/frontend/commit/f6985daec71e4d922acd8a9b2673fd41317b075f)
- Reload table data on delete [`3e8dac3`](https://git.odit.services/lfk/frontend/commit/3e8dac3203f56723a3dad4a35887d60fc03d4ae3)
- Added middle-name [`d811058`](https://git.odit.services/lfk/frontend/commit/d8110580e9a33cfb0c6e9cbdce630262d5b5d4c1)
- Fixed id sorting [`7cec2a0`](https://git.odit.services/lfk/frontend/commit/7cec2a00c513678cae8643ef3905d566a538bde0)
- Import add to datatable [`2a91562`](https://git.odit.services/lfk/frontend/commit/2a915620c9adcb481be53e3c919bd5f14a5108ee)
- Scans deletion [`6d9d8a4`](https://git.odit.services/lfk/frontend/commit/6d9d8a4724135df98ed1bec74b0cc20a8bdbda53)
- fix(DeleteRunnerModal): ESC key [`1df505e`](https://git.odit.services/lfk/frontend/commit/1df505ea00968f65e7aff85baa80f8935fd2eb7b)
- Versionbuilder [`1613ae7`](https://git.odit.services/lfk/frontend/commit/1613ae7de6e84aa5f9df6774a8e820fcf515b14a)
- Fixed edit dispatch [`c681570`](https://git.odit.services/lfk/frontend/commit/c68157013431eeb1150e37fea87da02d5f7b032e)
- ScansOverview: use CardRunner link [`4b171fd`](https://git.odit.services/lfk/frontend/commit/4b171fd04f4fc895f9919294858e546686504c65)
- Added custom filter to scan overview [`2c198cf`](https://git.odit.services/lfk/frontend/commit/2c198cfde89363142c1c958e6961b7823196e04e)
- Disable sort for actions [`fc2c290`](https://git.odit.services/lfk/frontend/commit/fc2c2907c43e5b1fa192b571cf93b2f2e492158f)
- Delete modal logic [`da33005`](https://git.odit.services/lfk/frontend/commit/da3300562a25cf39945a94f873ec27f0d5fb9dc0)
- Module [`9ae5e62`](https://git.odit.services/lfk/frontend/commit/9ae5e62e5d84097082bc29fe20b329f6e614f6ed)
- new license file version [CI SKIP] [`ed1caa7`](https://git.odit.services/lfk/frontend/commit/ed1caa7be7673ef08f9c0074101f3cfb52963ab7)
- Merge pull request 'experiment/tanstack' (#172) from experiment/tanstack into dev [`d88f3a5`](https://git.odit.services/lfk/frontend/commit/d88f3a5a2737921536c5eb109734abafd7609e51)
- Removed pnpm store [`eb13f03`](https://git.odit.services/lfk/frontend/commit/eb13f038a1990ed2966552ab1d36df4a76e5a99a)
- dependency update, drop focusTrap, first tanstack experiment in RunnersOverview [`9bec95e`](https://git.odit.services/lfk/frontend/commit/9bec95ede80908e45ad0f18a66dbb45784f7e33e)
- Added translations [`dcabed4`](https://git.odit.services/lfk/frontend/commit/dcabed4e93134a1f04305c47536f1cc93b0cfb31)
- new license file version [CI SKIP] [`d0fe6a2`](https://git.odit.services/lfk/frontend/commit/d0fe6a2e8513638c4d8e2b3dafd48b91490eab3b)
- Bumped pnpm to 8 [`ee91748`](https://git.odit.services/lfk/frontend/commit/ee91748b3c7c2fb4f196d76121713ac42465b9dd)
- drop old datatables [`cb5f2b7`](https://git.odit.services/lfk/frontend/commit/cb5f2b73d0a5f71875f48d381382919f9bee364e)
- wip: pagination [`a59dbbe`](https://git.odit.services/lfk/frontend/commit/a59dbbe50ede47e0dada906d10887cc6b1ae3264)
- Added sort [`842248e`](https://git.odit.services/lfk/frontend/commit/842248e4c43bb94a0e73981d2d848e0723687932)
- cleanup headers [`5ecf838`](https://git.odit.services/lfk/frontend/commit/5ecf838dd231269c3c4f0d1e3376ff157457b04a)
- Moved filter function to shared [`2304b12`](https://git.odit.services/lfk/frontend/commit/2304b12c1cca6a64573223906ab0561ba9050ec5)
- Group filters [`0283df2`](https://git.odit.services/lfk/frontend/commit/0283df22c84740dd978e09120985215b65d7a12a)
- feat(RunnersOverview): row selection [`6993511`](https://git.odit.services/lfk/frontend/commit/6993511c67cc81303a1eece7fc2a4218dc818afc)
- Basic details and delete buttons [`9363773`](https://git.odit.services/lfk/frontend/commit/9363773fa1910975d48746572fe166363986c6ab)
- Added delete cards button [`70307a9`](https://git.odit.services/lfk/frontend/commit/70307a9e8272f22801c6765c947f8576cc1d1102)
- ScansOverview: add ThFilterTrack [`008835c`](https://git.odit.services/lfk/frontend/commit/008835c24f833aebbecc53921b0901a76f25bf06)
- Yeeted old datatable references [`8925261`](https://git.odit.services/lfk/frontend/commit/89252619b1f6d478c287178bfffc573cf574a8fa)
- Added filter [`a9cdac4`](https://git.odit.services/lfk/frontend/commit/a9cdac4f74a80afe3d0ce677ed5256201b75b29b)
- Moved sort onclick to header only [`3de7b63`](https://git.odit.services/lfk/frontend/commit/3de7b632e058e9ca9ab951de9e86cc2a40eceb68)
- Switched drone to pnpm cache [`d79608e`](https://git.odit.services/lfk/frontend/commit/d79608edbb7ca68b0a26f6c021a141373308081f)
- pagination size [`45a7a90`](https://git.odit.services/lfk/frontend/commit/45a7a90cb8ee933d064c5891b3532cb1ece38d38)
- RunnersOverview: add filter keyboard support [`9415584`](https://git.odit.services/lfk/frontend/commit/94155845f020679e8765d198b5ab4735e1d1142c)
- RunnersOverview: pass selectedRunners to actions [`d4ab76e`](https://git.odit.services/lfk/frontend/commit/d4ab76ea1be1789aaa53ca57d33b2356c6804ffd)
- fix pagination next prev [`cac851f`](https://git.odit.services/lfk/frontend/commit/cac851f2b19dafa01b08d1da612e34cbe3eb4133)
- Moved docker to pnpm with cache [`4cbd265`](https://git.odit.services/lfk/frontend/commit/4cbd26580e51b854fdfd369056f24a30b04100a1)
- fix: TracksOverview [`c799088`](https://git.odit.services/lfk/frontend/commit/c7990882cfa4ac6c3a2ffa696dcbf3b62790c1fe)
- Table header i18n [`070a20a`](https://git.odit.services/lfk/frontend/commit/070a20a2e599e9f00b1bd0e944f9d68d08dc1cba)
- default to 50 rows pagination [`dc866dd`](https://git.odit.services/lfk/frontend/commit/dc866dd5403ae681193c5e325b1f6d4068f80b61)
- improved tablefilters/groupFilter [`3abf608`](https://git.odit.services/lfk/frontend/commit/3abf608b15fcfc2cf65b705d28d394917e7ac333)
- Updated group name conversion [`9111ad1`](https://git.odit.services/lfk/frontend/commit/9111ad147c7537036de834f80db6e557d8d6f0d9)
- show certificate, runnercard, sponsoring contract section [`49c2cd5`](https://git.odit.services/lfk/frontend/commit/49c2cd5c4b32e1b75fe0dae48c1b470608936704)
- add basic table styling [`c5e8409`](https://git.odit.services/lfk/frontend/commit/c5e84090795636c7a80d6cabe46168e436d40f50)
- Cards deleted migrations [`ef077b4`](https://git.odit.services/lfk/frontend/commit/ef077b4e6ad722e6d1c9efd5060165f649fd94c9)
- ignore pnpm store from now on [`fe62ad5`](https://git.odit.services/lfk/frontend/commit/fe62ad5539bc94b242c30c2952bf4c115cc9abfd)
- ammendme [`d31fe23`](https://git.odit.services/lfk/frontend/commit/d31fe2363b0f167b937f7d67908e75c709b324d8)
- updated default table row count [`245db06`](https://git.odit.services/lfk/frontend/commit/245db0617306bab23993caf00dc5ba0405e95625)
- fix: checkbox styling [`64db553`](https://git.odit.services/lfk/frontend/commit/64db5531858b2275d27c53a347da6c0416a278e8)
- Added distance conversion [`333214a`](https://git.odit.services/lfk/frontend/commit/333214aa8f945975ee2dcbd1cfa285e5e75c9d02)
- Added filterfunctions [`a9a965d`](https://git.odit.services/lfk/frontend/commit/a9a965d6981b07b02e3f752e65e1e5c4e9df7147)
- card/ThFilterRunner: text align left [`7083b3d`](https://git.odit.services/lfk/frontend/commit/7083b3d8d2c81fa4ab14f21d379befbb2dab5b6c)
- import [`0240e1d`](https://git.odit.services/lfk/frontend/commit/0240e1dca2e162b8c79f294f5c651b4a38455235)
- fix: table sort button + search style [`57dce34`](https://git.odit.services/lfk/frontend/commit/57dce34fc5b04c297c5341db24ec47c2f1d39595)
- No longer switching pnpm store path [`e5241d6`](https://git.odit.services/lfk/frontend/commit/e5241d619beda90d271485b6c09142b1e7b9b5f1)
- Reactivated generate cards modal [`8f33640`](https://git.odit.services/lfk/frontend/commit/8f33640bec8f79987b1319fb0765535fe493d496)
- Always load bulk created cards [`f89023e`](https://git.odit.services/lfk/frontend/commit/f89023e24ab30c90a8cacf2e9590d1dfa1a743d5)
- ScansOverview: fallback for manual scans [`1ec9556`](https://git.odit.services/lfk/frontend/commit/1ec9556aa64b996d0bdd9cd3ee50a767a154f5f6)
- fix min-w th [`b35375c`](https://git.odit.services/lfk/frontend/commit/b35375c929ad8d525b2761c321ecef0399917828)
- Close modal on delete and import toastify [`03b7ada`](https://git.odit.services/lfk/frontend/commit/03b7ada5ef3cbf64f178d3d0d88c448d76a42885)
- Fixed styling [`13254b2`](https://git.odit.services/lfk/frontend/commit/13254b24dd75ca3eb47fd53d5368f047545e8420)
- Implemented table buttons [`11a56f8`](https://git.odit.services/lfk/frontend/commit/11a56f87e89866bd9b2f7029ef683fd139d60aa4)
- RunnersOverview: disable debug log [`2c992a0`](https://git.odit.services/lfk/frontend/commit/2c992a0e63fa72055d28785fe7f5c21fd26f1fea)
- RunnersOverview: TopActionSection: add margin top [`88f96ac`](https://git.odit.services/lfk/frontend/commit/88f96acc3c31c748d3da29b4564dd681a89208b9)
- Unused filter function value [`38d3e19`](https://git.odit.services/lfk/frontend/commit/38d3e1912cd6d5362db236c34e0c602b52060b73)
- Make the linter happy [`fbc14fd`](https://git.odit.services/lfk/frontend/commit/fbc14fd7b47009bf64a8824e7b60373eea031e3f)
- Dsable column filter for distance [`67eff0e`](https://git.odit.services/lfk/frontend/commit/67eff0eda9b7d49d1abe784247afdbfbd7a76b00)
- getPaginationRowModel [`238082b`](https://git.odit.services/lfk/frontend/commit/238082b657998f513111e832556926decbe84e21)
- -> onkeyup [`aecbabe`](https://git.odit.services/lfk/frontend/commit/aecbabe52221c0ab8d94a42dc8ac42f4f397fde8)
- removed unused import [`9811ede`](https://git.odit.services/lfk/frontend/commit/9811ede3b23a488a49962a43dae5abad60f93341)
- drop redundant button role [`e7eddb4`](https://git.odit.services/lfk/frontend/commit/e7eddb4f08632cc803b8752e3561319fab538843)
- Filter id as equals [`2699b06`](https://git.odit.services/lfk/frontend/commit/2699b06d7c42608b1b3ce2037908a78143038441)
- debug table [`fd0d45f`](https://git.odit.services/lfk/frontend/commit/fd0d45f721396787ad35fca2a7fd0f26d98b23e9)
- add TODO for ScansOverview status filter [`9505c2b`](https://git.odit.services/lfk/frontend/commit/9505c2b03005f4e667082cb4527fea863dbbf7fc)
#### [0.17.3](https://git.odit.services/lfk/frontend/compare/0.17.2...0.17.3) #### [0.17.3](https://git.odit.services/lfk/frontend/compare/0.17.2...0.17.3)
> 15 March 2023
- dependency fixes [`3ea7a01`](https://git.odit.services/lfk/frontend/commit/3ea7a015a9beba3c2e4d3eb966f24ff6d4ac786e) - dependency fixes [`3ea7a01`](https://git.odit.services/lfk/frontend/commit/3ea7a015a9beba3c2e4d3eb966f24ff6d4ac786e)
- 🚀RELEASE v0.17.3 [`85705b6`](https://git.odit.services/lfk/frontend/commit/85705b6e684d0c41d3dc770cef7bffb199101576)
- set pnpm to @7 [`4432941`](https://git.odit.services/lfk/frontend/commit/44329413ed2ca23f74e86db041b2c25b2b1c2a2b) - set pnpm to @7 [`4432941`](https://git.odit.services/lfk/frontend/commit/44329413ed2ca23f74e86db041b2c25b2b1c2a2b)
#### [0.17.2](https://git.odit.services/lfk/frontend/compare/0.17.1...0.17.2) #### [0.17.2](https://git.odit.services/lfk/frontend/compare/0.17.1...0.17.2)

View File

@ -1,16 +1,15 @@
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build FROM registry.odit.services/hub/library/node:19.7.0-alpine3.16 as build
ARG NPM_REGISTRY_URL=https://registry.npmjs.org ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml vite.config.js index.html ./ COPY package.json pnpm-lock.yaml *.config.js *.config.cjs index.html ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.8 RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8 && pnpm i
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i
COPY src ./src COPY src ./src
COPY public ./public COPY public ./public
RUN pnpm build RUN pnpm build
# final image # final image
FROM registry.odit.services/library/nginx-brotli:3.15 AS final FROM registry.odit.services/library/nginx-brotli:3.15 as final
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./nginx.conf /etc/nginx/nginx.conf

View File

@ -1,27 +1,22 @@
# @odit/lfk-frontend # @odit/lfk-frontend
## ✒️ Overview ## ✒️ Overview
This is an API client for [https://git.odit.services/lfk/backend](@lfk/backend) This is an API client for [https://git.odit.services/lfk/backend](@lfk/backend)
- WebApp built with [Svelte](https://svelte.dev), [WindiCSS](https://windicss.org/) (to compile [TailwindCSS](https://tailwindcss.com/)) and [Vite](https://vitejs.dev).
This application is intended for use by admin users/ members only. This application is intended for use by admin users/ members only.
## 🚀 Getting Started ## 🚀 Getting Started
``` ```
pnpm i yarn
``` ```
## Development ## Development
``` ```
pnpm dev yarn dev
/ /
pnpm dev --open yarn dev --open
``` ```
## Build ## Build
```
pnpm build
``` ```
yarn build
```

View File

@ -1,8 +1,8 @@
version: "3" version: "3"
services: services:
httpd: httpd:
build: . build: .
volumes: volumes:
- ./public/env.sample.js:/usr/share/nginx/html/env.js - ./public/env.sample.js:/usr/share/nginx/html/env.js
ports: ports:
- 4050:80 - 4050:80

View File

@ -1,22 +1,22 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head>
<meta charset="utf-8" /> <head>
<link rel="icon" href="/favicon.png" /> <meta charset="utf-8" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="icon" href="/favicon.png" />
<link rel="apple-touch-icon" href="/lfk-logo.png" /> <link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#FFFFFF" /> <link rel="apple-touch-icon" href="/lfk-logo.png">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> <meta name="theme-color" content="#FFFFFF">
<meta name="description" content="Lauf Für Kaya! - Admin" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lauf für Kaya! - Admin</title> <meta name="description" content="Lauf Für Kaya! - Admin" />
</head> <title>Lauf für Kaya! - Admin</title>
</head>
<body>
<span style="display: none; visibility: hidden" id="buildinfo" <body>
>RELEASE_INFO-1.14.2-RELEASE_INFO</span <span style="display: none;visibility: hidden;" id="buildinfo">RELEASE_INFO-0.17.3-RELEASE_INFO</span>
> <noscript>You need to enable JavaScript to run this app.</noscript>
<noscript>You need to enable JavaScript to run this app.</noscript> <script src="/env.js"></script>
<script src="/env.js"></script> <script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.js"></script> </body>
</body>
</html> </html>

View File

@ -1,18 +1,16 @@
import fs from "fs"; const fs = require('fs');
// get all language files // get all language files
const files = fs.readdirSync("./src/locales/"); const files = fs.readdirSync('./src/locales/');
files.forEach((f) => { files.forEach((f) => {
// read file as object // read file as object
const unordered = JSON.parse(fs.readFileSync(`src/locales/${f}`)); const unordered = JSON.parse(fs.readFileSync(`src/locales/${f}`));
// order object by keys alpabetically A-Z // order object by keys alpabetically A-Z
const ordered = Object.keys(unordered) const ordered = Object.keys(unordered).sort().reduce((obj, key) => {
.sort() obj[key] = unordered[key];
.reduce((obj, key) => { return obj;
obj[key] = unordered[key]; }, {});
return obj; // format output as json for commit diff compatibility
}, {}); const out = JSON.stringify(ordered, 0, 4);
// format output as json for commit diff compatibility // write output file
const out = JSON.stringify(ordered, 0, 4); fs.writeFileSync(`src/locales/${f}`, out);
// write output file
fs.writeFileSync(`src/locales/${f}`, out);
}); });

View File

@ -1,67 +1,63 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.14.2", "version": "0.17.3",
"type": "module", "scripts": {
"scripts": { "i18n-order": "node order.js",
"i18n-order": "node order.js", "dev": "vite",
"dev": "vite", "build": "vite build",
"format": "prettier --write --plugin-search-dir=. .", "release": "release-it",
"build": "vite build", "licenses:export": "license-exporter --json -o public"
"release": "release-it --only-version", },
"licenses:export": "license-exporter --json -o public" "license": "CC-BY-NC-SA-4.0",
}, "devDependencies": {
"license": "CC-BY-NC-SA-4.0", "@sveltejs/vite-plugin-svelte": "1.0.0-next.6",
"devDependencies": { "@odit/license-exporter": "0.0.12",
"@odit/license-exporter": "0.2.0", "@types/html-minifier": "4.0.2",
"@sveltejs/vite-plugin-svelte": "2.1.1", "auto-changelog": "2.4.0",
"@types/papaparse": "^5.3.15", "autoprefixer": "10.4.14",
"@types/underscore": "^1.13.0", "html-minifier": "4.0.0",
"auto-changelog": "2.5.0", "postcss": "8.4.21",
"prettier": "3.5.3", "release-it": "14.6.1",
"prettier-plugin-svelte": "3.3.3", "svelte-preprocess": "4.7.0",
"release-it": "17.10.0", "svelte-select": "3.17.0",
"svelte-select": "3.17.0", "tailwindcss": "3.2.7",
"vite": "6.3.2", "vite": "2.1.5"
"vite-plugin-mkcert": "^1.17.8" },
}, "release-it": {
"release-it": { "git": {
"git": { "commit": true,
"commit": true, "requireCleanWorkingDir": false,
"requireCleanWorkingDir": false, "commitMessage": "🚀RELEASE v${version}",
"commitMessage": "chore(release): ${version}", "push": false,
"push": true, "tag": true,
"tag": true, "tagName": null,
"tagName": "${version}", "tagAnnotation": "v${version}"
"tagAnnotation": "${version}" },
}, "npm": {
"npm": { "publish": false
"publish": false },
}, "hooks": {
"hooks": { "after:bump": "npx auto-changelog --commit-limit false -p -u --hide-credit && git add CHANGELOG.md && node versionbuilder.js && git add index.html && node order.js && git add src/locales"
"after:bump": "npx auto-changelog --commit-limit false -p -u --hide-credit && git add CHANGELOG.md && node versionbuilder.js && git add index.html && node order.js && git add src/locales" }
} },
}, "dependencies": {
"dependencies": { "tinro": "0.6.12",
"@bwip-js/browser": "^4.6.0", "toastify-js": "1.12.0",
"@fontsource/athiti": "^5.2.5", "validator": "13.9.0",
"@odit/lfk-client-js": "1.2.7", "xlsx": "0.16.9",
"@paralleldrive/cuid2": "2.2.2", "@odit/lfk-client-js": "0.14.0",
"@tailwindcss/vite": "^4.1.4", "@vincjo/datatables": "^1.4.0",
"@tanstack/svelte-table": "8.9.1", "check-password-strength": "2.0.7",
"check-password-strength": "2.0.10", "csvtojson": "2.0.10",
"html5-qrcode": "^2.3.8", "gridjs": "3.4.0",
"localforage": "1.10.0", "localforage": "1.10.0",
"papaparse": "^5.5.2", "marked": "2.0.3",
"svelte": "3.58.0", "svelte": "3.37.0",
"svelte-french-toast": "1.2.0", "svelte-focus-trap": "1.2.0",
"svelte-i18n": "4.0.1", "svelte-i18n": "3.3.9",
"tailwindcss": "^4.1.4", "@paralleldrive/cuid2": "^2.2.0"
"tinro": "0.6.12", },
"underscore": "^1.13.7", "volta": {
"validator": "13.15.0", "node": "19.7.0"
"xlsx": "0.18.5" }
},
"volta": {
"node": "20.0.0"
}
} }

5916
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +0,0 @@
onlyBuiltDependencies:
- es5-ext
- esbuild

6
postcss.config.cjs Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

Binary file not shown.

View File

@ -1,12 +1,9 @@
const config = { const config = {
baseurl: "http://localhost:4010", baseurl: 'http://localhost:4010',
baseurl_selfservice: "http://localhost:5174", baseurl_documentserver: 'http://localhost:4010/documents',
baseurl_documentserver: "http://localhost:4010/documents", documentserver_key: 'NqZSYTy5AFQ7MppbLW5moqpTk7u7YrNUHKYhKYuThnnya2WpCOIU694hIZT1FzYe',
documentserver_key: // optional
"NqZSYTy5AFQ7MppbLW5moqpTk7u7YrNUHKYhKYuThnnya2WpCOIU694hIZT1FzYe", default_username: 'demo',
// optional default_password: 'demo',
default_username: "demo", prefersHashRouting: true
default_password: "demo",
prefersHashRouting: true,
}; };
window.config = config;

Binary file not shown.

File diff suppressed because one or more lines are too long

4
public/logo.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 98.1 118">
<path fill="#ff3e00" d="M91.8 15.6C80.9-.1 59.2-4.7 43.6 5.2L16.1 22.8A31.25 31.25 0 001.9 43.9c-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.7-9-.4-18.2-5.7-25.5"/>
<path fill="#fff" d="M40.9 103.9a21.8 21.8 0 01-23.4-8.7c-3.2-4.4-4.4-9.9-3.5-15.3l.6-2.6.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1a6.62 6.62 0 008.8 2L65.5 72c1.4-.9 2.3-2.2 2.6-3.8.3-1.6-.1-3.3-1-4.6a6.56 6.56 0 00-8.8-1.9l-10.5 6.7a18.6 18.6 0 01-5.6 2.4 21.8 21.8 0 01-23.4-8.7 20.2 20.2 0 01-3.4-15.3c.9-5.2 4.1-9.9 8.6-12.7l27.5-17.5c1.7-1.1 3.6-1.9 5.6-2.5a21.8 21.8 0 0123.4 8.7c3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1a6.56 6.56 0 00-8.8-1.9L32.4 46.1c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6a6.56 6.56 0 008.8 1.9l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5a21.8 21.8 0 0123.4 8.7c3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7l-27.5 17.5c-1.7 1.1-3.6 1.9-5.6 2.5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,39 +1,34 @@
{ {
"name": "Lauf für Kaya! - Admin", "name": "Lauf für Kaya! - Admin",
"short_name": "LfK!Admin", "short_name": "LfK!Admin",
"start_url": "/?utm_source=pwa", "start_url": "/?utm_source=pwa",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"display": "standalone", "display": "standalone",
"background_color": "#fff", "background_color": "#fff",
"theme_color": "#fff", "theme_color": "#fff",
"description": "Lauf für Kaya! - Admin", "description": "Lauf für Kaya! - Admin",
"shortcuts": [ "shortcuts": [
{ {
"name": "Users", "name": "Users",
"url": "/users/?utm_source=pwa" "url": "/users/?utm_source=pwa"
} }
], ],
"icons": [ "icons": [
{ {
"src": "/favicon.png", "src": "/favicon.png",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/favicon.png", "src": "/favicon.png",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/lfk-logo.png", "src": "/lfk-logo.png",
"sizes": "1540x144", "sizes": "1540x144",
"type": "image/png" "type": "image/png"
}, },
{ { "src": "/maskable_icon_x1.png", "sizes": "750x750", "type": "image/png", "purpose": "any maskable" }
"src": "/maskable_icon_x1.png", ]
"sizes": "750x750",
"type": "image/png",
"purpose": "any maskable"
}
]
} }

1
public/privacy_en.md Normal file
View File

@ -0,0 +1 @@
Nostrud tempor dolor aute ea excepteur aute mollit elit eiusmod exercitation. Magna laborum pariatur adipisicing pariatur cupidatat exercitation duis aliquip pariatur sint exercitation deserunt labore. Consectetur id laboris dolore nostrud do velit ipsum. Eu laboris velit do commodo ad ea sint ex cillum. Cillum ipsum qui eiusmod laborum mollit sunt dolore incididunt. Cillum sunt culpa veniam voluptate et qui ut magna anim occaecat ut mollit dolor. Duis irure proident eu incididunt dolore sunt nisi aute dolore amet eu fugiat laboris quis.

View File

@ -1,4 +1,6 @@
<script> <script>
import "toastify-js/src/toastify.css";
import "gridjs/dist/theme/mermaid.css";
import { Route, router } from "tinro"; import { Route, router } from "tinro";
router.subscribe((routeInfo) => { router.subscribe((routeInfo) => {
window.scrollTo(0, 0); window.scrollTo(0, 0);
@ -22,10 +24,11 @@
name: "lfk_admin", name: "lfk_admin",
version: 1.0, version: 1.0,
storeName: "lfk_admin", storeName: "lfk_admin",
description: "LfK! admin dashboard", description: "LfK! admin dashbaord",
}); });
window.onunhandledrejection = (event) => { window.onunhandledrejection = (event) => {
if (event.reason.toString() == "Error: Unauthorized") { if (event.reason.toString() == "Error: Unauthorized") {
console.log("Found 1");
localForage.clear(); localForage.clear();
location.replace("/"); location.replace("/");
} }
@ -41,7 +44,6 @@
import Settings from "./components/settings/Settings.svelte"; import Settings from "./components/settings/Settings.svelte";
import Transition from "./components/base/Transition.svelte"; import Transition from "./components/base/Transition.svelte";
import Orgs from "./components/orgs/Orgs.svelte"; import Orgs from "./components/orgs/Orgs.svelte";
import CardAssignment from "./components/tools/CardAssignment.svelte";
import Runners from "./components/runners/Runners.svelte"; import Runners from "./components/runners/Runners.svelte";
import Footer from "./components/general/Footer.svelte"; import Footer from "./components/general/Footer.svelte";
import TracksOverview from "./components/tracks/TracksOverview.svelte"; import TracksOverview from "./components/tracks/TracksOverview.svelte";
@ -54,6 +56,8 @@
import UserPermissions from "./components/users/UserPermissions.svelte"; import UserPermissions from "./components/users/UserPermissions.svelte";
import GroupPermissions from "./components/groups/GroupPermissions.svelte"; import GroupPermissions from "./components/groups/GroupPermissions.svelte";
import RunnerDetail from "./components/runners/RunnerDetail.svelte"; import RunnerDetail from "./components/runners/RunnerDetail.svelte";
import Imprint from "./components/general/Imprint.svelte";
import Privacy from "./components/general/Privacy.svelte";
import ResetPassword from "./components/auth/ResetPassword.svelte"; import ResetPassword from "./components/auth/ResetPassword.svelte";
import Contacts from "./components/contacts/Contacts.svelte"; import Contacts from "./components/contacts/Contacts.svelte";
import ContactDetail from "./components/contacts/ContactDetail.svelte"; import ContactDetail from "./components/contacts/ContactDetail.svelte";
@ -68,27 +72,32 @@
import Scans from "./components/scans/Scans.svelte"; import Scans from "./components/scans/Scans.svelte";
import ScanDetail from "./components/scans/ScanDetail.svelte"; import ScanDetail from "./components/scans/ScanDetail.svelte";
import Cards from "./components/cards/Cards.svelte"; import Cards from "./components/cards/Cards.svelte";
import StatsClients from "./components/statsclients/StatsClients.svelte"; import StatsClients from "./components/statsclients/StatsClients.svelte";
import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte"; import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte";
import CardReplacement from "./components/tools/CardReplacement.svelte";
import ScanClient from "./components/tools/ScanClient.svelte";
import DonationCreate from "./components/tools/DonationCreate.svelte";
store.init(); store.init();
</script> </script>
<Route> <Route>
{#if $router.path === "/forgot_password"} {#if $router.path === '/forgot_password'}
<Route path="/forgot_password"> <Route path="/forgot_password">
<ForgotPassword /> <ForgotPassword />
</Route> </Route>
{:else if $router.path.includes("/reset")} {:else if $router.path.includes('/reset')}
<Route path="/reset/:resetkey" let:params> <Route path="/reset/:resetkey" let:params>
<ResetPassword {params} /> <ResetPassword {params} />
</Route> </Route>
{:else if $router.path === "/about"} {:else if $router.path === '/about'}
<Route path="/about"> <Route path="/about">
<About /> <About />
</Route> </Route>
{:else if $router.path === '/imprint'}
<Route path="/imprint">
<Imprint />
</Route>
{:else if $router.path === '/privacy'}
<Route path="/privacy">
<Privacy />
</Route>
{:else if $store.isLoggedIn} {:else if $store.isLoggedIn}
<Dashboard> <Dashboard>
<Transition> <Transition>
@ -128,27 +137,13 @@
<Route path="/:trackid" let:params /> <Route path="/:trackid" let:params />
</Route> </Route>
<Route path="/runners/*"> <Route path="/runners/*">
<Route path="/" let:meta> <Route path="/">
<Runners created_via={meta.query.created_via} /> <Runners />
</Route> </Route>
<Route path="/:runnerid" let:params> <Route path="/:runnerid" let:params>
<RunnerDetail {params} /> <RunnerDetail {params} />
</Route> </Route>
</Route> </Route>
<Route path="/tools/*">
<Route path="/cardassignment/">
<CardAssignment />
</Route>
<Route path="/cardreplacement/">
<CardReplacement />
</Route>
<Route path="/scanclient/">
<ScanClient />
</Route>
<Route path="/donationcreate/">
<DonationCreate />
</Route>
</Route>
<Route path="/teams/*"> <Route path="/teams/*">
<Route path="/"> <Route path="/">
<Teams /> <Teams />

View File

@ -1,22 +1,28 @@
<script> <script>
import { AuthService } from "@odit/lfk-client-js"; import { ApiError, AuthService } from "@odit/lfk-client-js";
import toast from "svelte-french-toast";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import Toastify from "toastify-js";
import "toastify-js/src/toastify.css";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
let reset_mail_sent = false; let reset_mail_sent = false;
let usersEmail = ""; let usersEmail = "";
function reset() { function reset() {
if (isEmail(usersEmail)) { if (isEmail(usersEmail)) {
toast.loading($_("mail-validation-in-progress"));
AuthService.authControllerGetResetToken("de", { email: usersEmail }) AuthService.authControllerGetResetToken("de", { email: usersEmail })
.then((resp) => { .then((resp) => {
toast.dismiss(); Toastify({
text: $_("mail-validation-in-progress"),
duration: 3500,
}).showToast();
reset_mail_sent = true; reset_mail_sent = true;
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
toast($_("invalid-mail-reset")); Toastify({
text: $_("invalid-mail-reset"),
duration: 3500,
}).showToast();
} }
} }
</script> </script>
@ -26,18 +32,17 @@
<div class="max-w-md w-full py-12 px-6"> <div class="max-w-md w-full py-12 px-6">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-lg text-center font-bold text-gray-900"> <p class="mt-6 text-lg text-center font-bold text-gray-900">
{$_("application_name")} {$_('application_name')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900"> <p class="mt-2 mb-2 text-sm text-center text-gray-900">
{$_("password-reset-mail-sent", { values: { usersEmail: usersEmail } })} {$_('password-reset-mail-sent', { values: { usersEmail: usersEmail } })}
</p> </p>
<div class="mt-6"> <div class="mt-6">
<div class="mt-6"> <div class="mt-6">
<a <a
href="/" href="/"
class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm" class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm">
> {$_('goback')}
{$_("goback")}
</a> </a>
</div> </div>
</div> </div>
@ -48,26 +53,25 @@
<div class="max-w-md w-full py-12 px-6"> <div class="max-w-md w-full py-12 px-6">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-lg text-center font-bold text-gray-900"> <p class="mt-6 text-lg text-center font-bold text-gray-900">
{$_("application_name")} {$_('application_name')}
</p> </p>
<p class="mt-6 text-sm text-center text-gray-900"> <p class="mt-6 text-sm text-center text-gray-900">
{$_("forgot_password")} {$_('forgot_password')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900"> <p class="mt-2 mb-2 text-sm text-center text-gray-900">
{$_("dont-panic-were-resetting-it")} {$_('dont-panic-were-resetting-it')}
</p> </p>
<div> <div>
<div class="rounded-md shadow-sm"> <div class="rounded-md shadow-sm">
<div> <div>
<input <input
aria-label={$_("e-mail-adress")} aria-label={$_('e-mail-adress')}
name="email" name="email"
type="email" type="email"
required="" required=""
class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm" class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border text-gray-900 rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm"
placeholder={$_("e-mail-adress")} placeholder={$_('e-mail-adress')}
bind:value={usersEmail} bind:value={usersEmail} />
/>
</div> </div>
</div> </div>
@ -75,22 +79,19 @@
<button <button
on:click={reset} on:click={reset}
type="submit" type="submit"
class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm" class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm">
>
<span class="absolute left-0 inset-y pl-3"> <span class="absolute left-0 inset-y pl-3">
<svg <svg
class="h-5 w-5 text-gray-500" class="h-5 w-5 text-gray-500"
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20">
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</span> </span>
{$_("reset-my-password")} {$_('reset-my-password')}
</button> </button>
</div> </div>
<div class="mt-6"> <div class="mt-6">
@ -99,30 +100,24 @@
<div class="w-full border-t border-gray-300" /> <div class="w-full border-t border-gray-300" />
</div> </div>
<div class="relative flex justify-center text-sm"> <div class="relative flex justify-center text-sm">
<span class="px-2 bg-gray-100 text-gray-500" <span
>{$_("dont-have-your-email-connected")}</span class="px-2 bg-gray-100 text-gray-500">{$_('dont-have-your-email-connected')}</span>
>
</div> </div>
</div> </div>
<span <span
class="mt-2 text-sm px-2 bg-gray-100 text-gray-500 justify-center relative flex" class="mt-2 text-sm px-2 bg-gray-100 text-gray-500 justify-center relative flex">{$_('cannot-reset-your-password-directly')}</span>
>{$_("cannot-reset-your-password-directly")}</span
>
<div class="mt-6"> <div class="mt-6">
<a <a
href="mailto:lfk@odit.services" href="mailto:lfk@odit.services"
class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm" class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm">
> {$_('send-a-mail-to-lfk-odit-services')}
{$_("send-a-mail-to-lfk-odit-services")}
</a> </a>
</div> </div>
<div class="mt-6"> <div class="mt-6">
<a <a
href="/" href="/"
class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm" class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md text-gray-900 font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm">{$_('goback')}</a>
>{$_("goback")}</a
>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@
import { OpenAPI, AuthService } from "@odit/lfk-client-js"; import { OpenAPI, AuthService } from "@odit/lfk-client-js";
import Footer from "../general/Footer.svelte"; import Footer from "../general/Footer.svelte";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
// ------ // ------
let username = config.default_username || ""; let username = config.default_username || "";
let password = config.default_password || ""; let password = config.default_password || "";
@ -20,6 +20,11 @@
OpenAPI.TOKEN = value.access_token; OpenAPI.TOKEN = value.access_token;
const jwtinfo = JSON.parse(atob(OpenAPI.TOKEN.split(".")[1])); const jwtinfo = JSON.parse(atob(OpenAPI.TOKEN.split(".")[1]));
store.login(value, jwtinfo); store.login(value, jwtinfo);
Toastify({
text: $_("welcome_wavinghand"),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
} }
} }
}); });
@ -28,7 +33,10 @@
// prevent login button spamming // prevent login button spamming
if (last_loginclick_processed && is_blocked_by_autologin === false) { if (last_loginclick_processed && is_blocked_by_autologin === false) {
last_loginclick_processed = false; last_loginclick_processed = false;
toast.loading($_("login_is_checked")); Toastify({
text: $_("login_is_checked"),
duration: 500,
}).showToast();
let postdata = {}; let postdata = {};
if (isEmail(username)) { if (isEmail(username)) {
postdata = { postdata = {
@ -48,18 +56,31 @@
const jwtinfo = JSON.parse(atob(OpenAPI.TOKEN.split(".")[1])); const jwtinfo = JSON.parse(atob(OpenAPI.TOKEN.split(".")[1]));
store.login(result.access_token, jwtinfo); store.login(result.access_token, jwtinfo);
location.replace("/"); location.replace("/");
toast.dismiss(); Toastify({
text: $_("welcome_wavinghand"),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}) })
.catch((err) => { .catch((err) => {
toast.dismiss(); Toastify({
toast.error($_("error_on_login")); text: $_("error_on_login"),
duration: 500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
}) })
.finally(() => { .finally(() => {
last_loginclick_processed = true; last_loginclick_processed = true;
}); });
// last login was not processed yet // last login was not processed yet
} else { } else {
toast($_("please-wait-a-moment-your-login-is-still-being-processed")); Toastify({
text: $_('please-wait-a-moment-your-login-is-still-being-processed'),
duration: 1500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} }
}; };
function handleKeydown(e) { function handleKeydown(e) {
@ -70,37 +91,34 @@
</script> </script>
<div <div
class="min-h-screen flex items-center justify-center bg-gray-100 text-gray-900" class="min-h-screen flex items-center justify-center bg-gray-100 text-gray-900">
>
<div class="max-w-md w-full py-12 px-6" role="main"> <div class="max-w-md w-full py-12 px-6" role="main">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-xl text-center font-bold">{$_("application_name")}</p> <p class="mt-6 text-lg text-center font-bold">{$_('application_name')}</p>
<p class="mt-2 mb-6 text-sm text-center">{$_("log_in_to_your_account")}</p> <p class="mt-6 text-sm text-center">{$_('log_in_to_your_account')}</p>
<div> <div>
<div class="rounded-md shadow-sm"> <div class="rounded-md shadow-sm">
<div> <div>
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<input <input
autofocus autofocus
aria-label={$_("email_address_or_username")} aria-label={$_('email_address_or_username')}
type="text" type="text"
required="" required=""
class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm" class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-t-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm"
on:keydown={handleKeydown} on:keydown={handleKeydown}
placeholder={$_("email_address_or_username")} placeholder={$_('email_address_or_username')}
bind:value={username} bind:value={username} />
/>
</div> </div>
<div class="-mt-px relative"> <div class="-mt-px relative">
<input <input
aria-label={$_("password")} aria-label={$_('password')}
type="password" type="password"
required="" required=""
bind:value={password} bind:value={password}
class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm" class="border-gray-300 placeholder-gray-500 appearance-none rounded-none relative block w-full px-3 py-2 border rounded-b-md focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm"
on:keydown={handleKeydown} on:keydown={handleKeydown}
placeholder={$_("password")} placeholder={$_('password')} />
/>
</div> </div>
</div> </div>
@ -108,33 +126,29 @@
<button <button
on:click={login} on:click={login}
type="submit" type="submit"
class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm" class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm">
>
<span class="absolute left-0 inset-y pl-3"> <span class="absolute left-0 inset-y pl-3">
<svg <svg
class="h-5 w-5 text-gray-500" class="h-5 w-5 text-gray-500"
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20">
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</span> </span>
{$_("log_in")} {$_('log_in')}
</button> </button>
</div> </div>
</div> </div>
<!-- <div class="mt-2"> <div class="mt-2">
<a <a
href="/forgot_password" href="/forgot_password"
class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm" class="block w-full text-center py-2 px-3 border border-gray-300 rounded-md font-medium hover:border-gray-400 focus:outline-none focus:border-gray-400 sm:text-sm">
> {$_('forgot_password')}
{$_("forgot_password")}
</a> </a>
</div> --> </div>
</div> </div>
</div> </div>
<Footer /> <Footer />

View File

@ -1,52 +1,52 @@
<script context="module"> <script context="module">
import { passwordStrength } from "check-password-strength"; import { passwordStrength } from "check-password-strength";
export function password_strong_enough(password_change) { export function password_strong_enough(password_change) {
let strength = passwordStrength(password_change); let strength = passwordStrength(password_change);
return ( return (
strength?.contains.includes("lowercase") && strength?.contains.includes("lowercase") &&
strength?.contains.includes("uppercase") && strength?.contains.includes("uppercase") &&
strength?.contains.includes("number") && strength?.contains.includes("number") &&
strength?.length > 9 strength?.length > 9
); );
} }
export function password_strong_enough_and_equal( export function password_strong_enough_and_equal(
password_change, password_change,
password_confirm password_confirm
) { ) {
return ( return (
password_strong_enough(password_change) && password_strong_enough(password_change) &&
password_change === password_confirm password_change === password_confirm
); );
} }
</script> </script>
<script> <script>
import { getLocaleFromNavigator, _ } from "svelte-i18n"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import { passwordStrength as Strength } from "check-password-strength"; import { passwordStrength as Strength } from "check-password-strength";
export let password_change; export let password_change;
export let password_confirm; export let password_confirm;
$: strength = Strength(password_change); $: strength = Strength(password_change);
$: passwords_match = $: passwords_match =
!password_confirm || password_confirm === password_change; !password_confirm || password_confirm === password_change;
</script> </script>
<div class="ml-4"> <div class="ml-4">
<ul class="list-disc font-medium tracking-wide text-red-500 text-xs"> <ul class="list-disc font-medium tracking-wide text-red-500 text-xs">
{#if !strength.contains.includes("lowercase")} {#if !strength.contains.includes('lowercase')}
<li>{$_("must-contain-a-lowercase-letter")}</li> <li>{$_('must-contain-a-lowercase-letter')}</li>
{/if} {/if}
{#if !strength.contains.includes("uppercase")} {#if !strength.contains.includes('uppercase')}
<li>{$_("must-contain-a-uppercase-letter")}</li> <li>{$_('must-contain-a-uppercase-letter')}</li>
{/if} {/if}
{#if !strength.contains.includes("number")} {#if !strength.contains.includes('number')}
<li>{$_("must-contain-a-number")}</li> <li>{$_('must-contain-a-number')}</li>
{/if} {/if}
{#if !(strength.length > 9)} {#if !(strength.length > 9)}
<li>{$_("must-be-at-least-10-characters-long")}</li> <li>{$_('must-be-at-least-10-characters-long')}</li>
{/if} {/if}
{#if !(passwords_match == true)} {#if !(passwords_match == true)}
<li>{$_("passwords-dont-match")}</li> <li>{$_('passwords-dont-match')}</li>
{/if} {/if}
</ul> </ul>
</div> </div>

View File

@ -1,7 +1,8 @@
<script> <script>
import { AuthService } from "@odit/lfk-client-js"; import { AuthService } from "@odit/lfk-client-js";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
import "toastify-js/src/toastify.css";
import PasswordStrength, { import PasswordStrength, {
password_strong_enough, password_strong_enough,
} from "../auth/PasswordStrength.svelte"; } from "../auth/PasswordStrength.svelte";
@ -10,97 +11,101 @@
export let params; export let params;
function set_new_password() { function set_new_password() {
if (password.trim() !== "") { if (password.trim() !== "") {
toast.loading($_("password-reset-in-progress")); Toastify({
text: $_("password-reset-in-progress"),
duration: 3500,
}).showToast();
AuthService.authControllerResetPassword(atob(params.resetkey), { AuthService.authControllerResetPassword(atob(params.resetkey), {
password, password,
}) })
.then((resp) => { .then((resp) => {
toast.dismiss(); Toastify({
toast($_("password-reset-successful")); text: $_("password-reset-successful"),
duration: 3500,
}).showToast();
state = "reset_success"; state = "reset_success";
}) })
.catch((err) => { .catch((err) => {
state = "reset_error"; state = "reset_error";
}); });
} else { } else {
toast.dismiss(); Toastify({
toast.error($_("please-provide-a-password")); text: $_("please-provide-a-password"),
duration: 3500,
}).showToast();
} }
} }
</script> </script>
{#if state === "reset_success"} {#if state === 'reset_success'}
<div class="min-h-screen flex items-center justify-center bg-gray-100"> <div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="max-w-md w-full py-12 px-6"> <div class="max-w-md w-full py-12 px-6">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-lg text-center font-bold text-gray-900"> <p class="mt-6 text-lg text-center font-bold text-gray-900">
{$_("application_name")} {$_('application_name')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900 font-bold"> <p class="mt-2 mb-2 text-sm text-center text-gray-900 font-bold">
{$_("successful-password-reset")} {$_('successful-password-reset')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900"> <p class="mt-2 mb-2 text-sm text-center text-gray-900">
{$_("you-can-now-use-your-new-password-to-log-in-to-your-account")} {$_('you-can-now-use-your-new-password-to-log-in-to-your-account')}
</p> </p>
<div class="mt-6"> <div class="mt-6">
<div class="mt-6"> <div class="mt-6">
<a <a
href="/login/" href="/login/"
class="text-center relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm" class="text-center relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm">
> {$_('go-to-login')}
{$_("go-to-login")}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{:else if state === "reset_error"} {:else if state === 'reset_error'}
<div class="min-h-screen flex items-center justify-center bg-gray-100"> <div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="max-w-md w-full py-12 px-6"> <div class="max-w-md w-full py-12 px-6">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-lg text-center font-bold text-gray-900"> <p class="mt-6 text-lg text-center font-bold text-gray-900">
{$_("application_name")} {$_('application_name')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900 font-bold"> <p class="mt-2 mb-2 text-sm text-center text-gray-900 font-bold">
{$_("password-reset-failed")} {$_('password-reset-failed')}
</p> </p>
<p class="mt-2 mb-2 text-sm text-center text-gray-900"> <p class="mt-2 mb-2 text-sm text-center text-gray-900">
{$_("please-request-a-new-reset-mail")} {$_('please-request-a-new-reset-mail')}
</p> </p>
<div class="mt-6"> <div class="mt-6">
<div class="mt-6"> <div class="mt-6">
<a <a
href="/forgot_password/" href="/forgot_password/"
class="text-center relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm" class="text-center relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm">
> {$_('request-a-new-reset-mail')}
{$_("request-a-new-reset-mail")}
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{:else if state === "reset_in_progress"} {:else if state === 'reset_in_progress'}
<div class="min-h-screen flex items-center justify-center bg-gray-100"> <div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="max-w-md w-full py-12 px-6"> <div class="max-w-md w-full py-12 px-6">
<img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" /> <img style="height:10rem;" class="mx-auto" src="/lfk-logo.png" alt="" />
<p class="mt-6 text-lg text-center font-bold text-gray-900"> <p class="mt-6 text-lg text-center font-bold text-gray-900">
{$_("application_name")} {$_('application_name')}
</p> </p>
<p class="mt-2 mb-4 text-md text-center text-gray-900"> <p class="mt-2 mb-4 text-md text-center text-gray-900">
{$_("reset-password")} {$_('reset-password')}
</p> </p>
<div> <div>
<div class="rounded-md shadow-sm"> <div class="rounded-md shadow-sm">
<div> <div>
<input <input
aria-label={$_("new-password")} aria-label={$_('new-password')}
name="password" name="password"
type="password" type="password"
required="" required=""
class="border-gray-300 placeholder-gray-500 appearance-none rounded-md relative block w-full px-3 py-2 border text-gray-900 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm" class="border-gray-300 placeholder-gray-500 appearance-none rounded-md relative block w-full px-3 py-2 border text-gray-900 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 focus:z-10 sm:text-sm"
placeholder={$_("new-password")} placeholder={$_('new-password')}
bind:value={password} bind:value={password} />
/>
</div> </div>
<PasswordStrength bind:password_change={password} /> <PasswordStrength bind:password_change={password} />
</div> </div>
@ -111,22 +116,19 @@
disabled={!password_strong_enough(password)} disabled={!password_strong_enough(password)}
class:opacity-50={!password_strong_enough(password)} class:opacity-50={!password_strong_enough(password)}
type="submit" type="submit"
class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm" class="relative block w-full py-2 px-3 border border-transparent rounded-md text-white font-semibold bg-gray-800 hover:bg-gray-700 focus:bg-gray-900 focus:outline-none focus:shadow-outline sm:text-sm">
>
<span class="absolute left-0 inset-y pl-3"> <span class="absolute left-0 inset-y pl-3">
<svg <svg
class="h-5 w-5 text-gray-500" class="h-5 w-5 text-gray-500"
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20">
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
clip-rule="evenodd" clip-rule="evenodd" />
/>
</svg> </svg>
</span> </span>
{$_("reset-my-password")} {$_('reset-my-password')}
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,274 @@
<!--
This example requires Tailwind CSS v2.0+
This example requires some changes to your config:
```
// tailwind.config.js
module.exports = {
// ...
plugins: [
// ...
require('@tailwindcss/forms'),
]
}
```
-->
<div>
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="px-4 sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">Profile</h3>
<p class="mt-1 text-sm text-gray-600">
This information will be displayed publicly so be careful what you share.
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="#" method="POST">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<div class="grid grid-cols-3 gap-6">
<div class="col-span-3 sm:col-span-2">
<label for="company_website" class="block text-sm font-medium text-gray-700">
Website
</label>
<div class="mt-1 flex rounded-md shadow-sm">
<span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm">
http://
</span>
<input type="text" name="company_website" id="company_website" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-r-md sm:text-sm border-gray-300" placeholder="www.example.com">
</div>
</div>
</div>
<div>
<label for="about" class="block text-sm font-medium text-gray-700">
About
</label>
<div class="mt-1">
<textarea id="about" name="about" rows="3" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 mt-1 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com"></textarea>
</div>
<p class="mt-2 text-sm text-gray-500">
Brief description for your profile. URLs are hyperlinked.
</p>
</div>
<div>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block text-sm font-medium text-gray-700">
Photo
</label>
<div class="mt-2 flex items-center">
<span class="inline-block h-12 w-12 rounded-full overflow-hidden bg-gray-100">
<svg class="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</span>
<button type="button" class="ml-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Change
</button>
</div>
</div>
<div>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block text-sm font-medium text-gray-700">
Cover photo
</label>
<div class="mt-2 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
<span>Upload a file</span>
<input id="file-upload" name="file-upload" type="file" class="sr-only">
</label>
<p class="pl-1">or drag and drop</p>
</div>
<p class="text-xs text-gray-500">
PNG, JPG, GIF up to 10MB
</p>
</div>
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200"></div>
</div>
</div>
<div class="mt-10 sm:mt-0">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="px-4 sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">Personal Information</h3>
<p class="mt-1 text-sm text-gray-600">
Use a permanent address where you can receive mail.
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="#" method="POST">
<div class="shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3">
<label for="first_name" class="block text-sm font-medium text-gray-700">First name</label>
<input type="text" name="first_name" id="first_name" autocomplete="given-name" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-3">
<label for="last_name" class="block text-sm font-medium text-gray-700">Last name</label>
<input type="text" name="last_name" id="last_name" autocomplete="family-name" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-4">
<label for="email_address" class="block text-sm font-medium text-gray-700">Email address</label>
<input type="text" name="email_address" id="email_address" autocomplete="email" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-3">
<label for="country" class="block text-sm font-medium text-gray-700">Country / Region</label>
<select id="country" name="country" autocomplete="country" class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option>United States</option>
<option>Canada</option>
<option>Mexico</option>
</select>
</div>
<div class="col-span-6">
<label for="street_address" class="block text-sm font-medium text-gray-700">Street address</label>
<input type="text" name="street_address" id="street_address" autocomplete="street-address" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-6 lg:col-span-2">
<label for="city" class="block text-sm font-medium text-gray-700">City</label>
<input type="text" name="city" id="city" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-3 lg:col-span-2">
<label for="state" class="block text-sm font-medium text-gray-700">State / Province</label>
<input type="text" name="state" id="state" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
<div class="col-span-6 sm:col-span-3 lg:col-span-2">
<label for="postal_code" class="block text-sm font-medium text-gray-700">ZIP / Postal</label>
<input type="text" name="postal_code" id="postal_code" autocomplete="postal-code" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md">
</div>
</div>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="hidden sm:block" aria-hidden="true">
<div class="py-5">
<div class="border-t border-gray-200"></div>
</div>
</div>
<div class="mt-10 sm:mt-0">
<div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1">
<div class="px-4 sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900">Notifications</h3>
<p class="mt-1 text-sm text-gray-600">
Decide which communications you'd like to receive and how.
</p>
</div>
</div>
<div class="mt-5 md:mt-0 md:col-span-2">
<form action="#" method="POST">
<div class="shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<fieldset>
<legend class="text-base font-medium text-gray-900">By Email</legend>
<div class="mt-4 space-y-4">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="comments" name="comments" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="comments" class="font-medium text-gray-700">Comments</label>
<p class="text-gray-500">Get notified when someones posts a comment on a posting.</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="candidates" name="candidates" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="candidates" class="font-medium text-gray-700">Candidates</label>
<p class="text-gray-500">Get notified when a candidate applies for a job.</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="offers" name="offers" type="checkbox" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded">
</div>
<div class="ml-3 text-sm">
<label for="offers" class="font-medium text-gray-700">Offers</label>
<p class="text-gray-500">Get notified when a candidate accepts or rejects an offer.</p>
</div>
</div>
</div>
</fieldset>
<fieldset>
<div>
<legend class="text-base font-medium text-gray-900">Push Notifications</legend>
<p class="text-sm text-gray-500">These are delivered via SMS to your mobile phone.</p>
</div>
<div class="mt-4 space-y-4">
<div class="flex items-center">
<input id="push_everything" name="push_notifications" type="radio" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
<label for="push_everything" class="ml-3 block text-sm font-medium text-gray-700">
Everything
</label>
</div>
<div class="flex items-center">
<input id="push_email" name="push_notifications" type="radio" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
<label for="push_email" class="ml-3 block text-sm font-medium text-gray-700">
Same as email
</label>
</div>
<div class="flex items-center">
<input id="push_nothing" name="push_notifications" type="radio" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300">
<label for="push_nothing" class="ml-3 block text-sm font-medium text-gray-700">
No push notifications
</label>
</div>
</div>
</fieldset>
</div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button type="submit" class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</form>
</div>
</div>
</div>

View File

@ -4,22 +4,19 @@
<body class="antialiased font-sans"> <body class="antialiased font-sans">
<div class="flex min-h-screen"> <div class="flex min-h-screen">
<div class="w-full bg-white flex items-center justify-center"> <div class="w-full bg-white flex items-center justify-center ">
<div class="max-w-sm m-8"> <div class="max-w-sm m-8">
<div class="text-black text-5xl md:text-15xl font-black"> <div class="text-black text-5xl md:text-15xl font-black">
{$_("internal-error")} {$_('internal-error')}
</div> </div>
<div class="w-16 h-1 bg-purple-light my-3 md:my-6" /> <div class="w-16 h-1 bg-purple-light my-3 md:my-6" />
<p <p
class="text-grey-darker text-2xl md:text-3xl font-light mb-8 leading-normal" class="text-grey-darker text-2xl md:text-3xl font-light mb-8 leading-normal">
> {$_('generic-ui-logic-error')}
{$_("generic-ui-logic-error")}
</p> </p>
<a <a
href="/" href="/"
class="bg-transparent text-grey-darkest font-bold uppercase tracking-wide py-3 px-6 border-2 border-grey-light hover:border-grey rounded-lg" class="bg-transparent text-grey-darkest font-bold uppercase tracking-wide py-3 px-6 border-2 border-grey-light hover:border-grey rounded-lg">{$_('goback')}</a>
>{$_("goback")}</a
>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,7 +5,7 @@
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"> <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8"> <span class="inline-block align-middle mr-8">
<b>{$_("general_promise_error")}</b> <b class="capitalize">{$_('general_promise_error')}</b>
{error} {error}
</span> </span>
</div> </div>

View File

@ -1,25 +1,24 @@
export function getlang(langkeys) { export function getlang(langkeys) {
return { return {
search: { search: {
placeholder: langkeys.search, placeholder: langkeys.search
}, },
sort: { sort: {
sortAsc: langkeys.sort_column_ascending, sortAsc: langkeys.sort_column_ascending,
sortDesc: langkeys.sort_column_descending, sortDesc: langkeys.sort_column_descending
}, },
pagination: { pagination: {
previous: langkeys.previous, previous: langkeys.previous,
next: langkeys.next, next: langkeys.next,
navigate: (page, pages) => navigate: (page, pages) => `${langkeys.page} ${page} ${langkeys.of} ${pages}`,
`${langkeys.page} ${page} ${langkeys.of} ${pages}`, page: (page) => `${langkeys.page} ${page}`,
page: (page) => `${langkeys.page} ${page}`, showing: langkeys.showing,
showing: langkeys.showing, of: langkeys.of,
of: langkeys.of, to: langkeys.to,
to: langkeys.to, results: langkeys.records
results: langkeys.records, },
}, loading: langkeys.loading,
loading: langkeys.loading, noRecordsFound: langkeys.no_matching_records_found,
noRecordsFound: langkeys.no_matching_records_found, error: langkeys.an_error_happened_while_fetching_the_data
error: langkeys.an_error_happened_while_fetching_the_data, };
};
} }

View File

@ -3,4 +3,4 @@
Or as others may call it: Real big bullshit time. Or as others may call it: Real big bullshit time.
Issue: https://git.odit.services/lfk/frontend/issues/136 Issue: https://git.odit.services/lfk/frontend/issues/136
--> -->
<div class="opacity-50" /> <div class="opacity-50"></div>

View File

@ -1,10 +1,10 @@
/** Dispatch event on click outside of node */ /** Dispatch event on click outside of node */
export function clickOutside(node) { export function clickOutside(node) {
const handleClick = (event) => { const handleClick = (event) => {
if (event.target.getAttribute("data-id") === "modal_backdrop") { if (event.target.getAttribute('data-id') === 'modal_backdrop') {
node.dispatchEvent(new CustomEvent("click_outside", node)); node.dispatchEvent(new CustomEvent('click_outside', node));
} }
}; };
document.removeEventListener("click", handleClick, true); document.removeEventListener('click', handleClick, true);
document.addEventListener("click", handleClick, true); document.addEventListener('click', handleClick, true);
} }

File diff suppressed because one or more lines are too long

View File

@ -1,218 +1,240 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerCardService } from "@odit/lfk-client-js"; import { RunnerCardService } from "@odit/lfk-client-js";
import { createEventDispatcher } from "svelte"; import Toastify from "toastify-js";
import toast from "svelte-french-toast"; export let bulk_modal_open;
import DocumentServer from "../pdf_generation/DocumentServer"; export let current_cards;
export let bulk_modal_open; function focus(el) {
const dispatch = createEventDispatcher(); el.focus();
const documentServer = new DocumentServer(config.baseurl_documentserver,config.documentserver_key); }
$: card_count = 0;
$: is_card_count_valid = card_count > 0;
$: card_count = 0; $: processed_last_submit = true;
$: is_card_count_valid = card_count > 0; $: createbtnenabled = is_card_count_valid;
$: processed_last_submit = true; (() => {
$: createbtnenabled = is_card_count_valid; document.onkeydown = (e) => {
(() => { e = e || window.event;
document.onkeydown = (e) => { if (e.key === "Escape") {
e = e || window.event; bulk_modal_open = false;
if (e.key === "Escape") { }
bulk_modal_open = false; if (e.keyCode === 13) {
} if (createbtnenabled === true) {
if (e.keyCode === 13) { createbtnenabled = false;
if (createbtnenabled === true) { submit_with_print();
createbtnenabled = false; }
submit_with_print(); }
} };
} })();
}; function submit_without_print() {
})(); if (processed_last_submit === true) {
function submit_without_print() { processed_last_submit = false;
if (processed_last_submit === true) { const toast = Toastify({
processed_last_submit = false; text: $_("creating-blanco-cards"),
toast.loading($_("creating-blanco-cards")); duration: -1,
RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, true) }).showToast();
.then((result) => { RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, false)
bulk_modal_open = false; .then((result) => {
// bulk_modal_open = false;
toast.dismiss(); //
toast.success($_("created-blanco-cards")); Toastify({
dispatch("created", { cards: result }); text: $_("created-blanco-cards"),
}) duration: 500,
.catch((err) => { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
// }).showToast();
}) })
.finally(() => { .catch((err) => {
processed_last_submit = true; //
}); })
} .finally(() => {
} processed_last_submit = true;
//
function submit_with_print() { toast.hideToast();
if (processed_last_submit === true) { });
processed_last_submit = false; }
toast.dismiss(); }
toast.loading($_("creating-blanco-cards"));
RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, true) function submit_with_print() {
.then((result) => { if (processed_last_submit === true) {
bulk_modal_open = false; processed_last_submit = false;
// const toast = Toastify({
toast.dismiss(); text: $_("creating-blanco-cards"),
toast.success($_("created-blanco-cards")); duration: -1,
toast.loading($_("generating-pdf")); }).showToast();
dispatch("created", { cards: result }); RunnerCardService.runnerCardControllerPostBlancoBulk(card_count, true)
documentServer.generateCards(result, "de") .then((result) => {
.then((blob) => { bulk_modal_open = false;
const url = window.URL.createObjectURL(blob); //
let a = document.createElement("a"); Toastify({
a.href = url; text: $_("created-blanco-cards"),
a.download = "Bulkcards.pdf"; duration: 500,
document.body.appendChild(a); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
a.click(); }).showToast();
a.remove(); current_cards = current_cards.concat(result);
toast.dismiss(); const toast = Toastify({
toast.success($_("pdf-successfully-generated")); text: $_("generating-pdf"),
}) duration: -1,
.catch((err) => { }).showToast();
console.error(err); fetch(
}); `${config.baseurl_documentserver}/cards?&download=true&key=${config.documentserver_key}`,
}) {
.catch((err) => { method: "POST",
// headers: {
}) "Content-Type": "application/json",
.finally(() => { },
processed_last_submit = true; body: JSON.stringify(result),
}); }
} )
} .then((response) => {
</script> if (response.status != "200") {
toast.hideToast();
{#if bulk_modal_open} Toastify({
<div text: $_("pdf-generation-failed"),
class="fixed z-10 inset-0 overflow-y-hidden" duration: 3500,
use:clickOutside backgroundColor:
on:click_outside={() => { "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
bulk_modal_open = false; }).showToast();
}} } else {
> return response.blob();
<div }
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" })
> .then((blob) => {
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> const url = window.URL.createObjectURL(blob);
<div let a = document.createElement("a");
class="absolute inset-0 bg-gray-500 opacity-75" a.href = url;
data-id="modal_backdrop" a.download = "Bulkcards.pdf";
/> document.body.appendChild(a);
</div> a.click();
<span a.remove();
class="hidden sm:inline-block sm:align-middle sm:h-screen" toast.hideToast();
aria-hidden="true">&#8203;</span Toastify({
> text: $_("pdf-successfully-generated"),
<div duration: 3500,
class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full relative z-10" backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
role="dialog" }).showToast();
aria-modal="true" })
aria-labelledby="modal-headline" .catch((err) => {
> console.error(err);
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> });
<div class=""> })
<div .catch((err) => {
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" //
> })
<svg .finally(() => {
class="size-6 text-blue-600" processed_last_submit = true;
fill="currentColor" //
xmlns="http://www.w3.org/2000/svg" toast.hideToast();
viewBox="0 0 24 24" });
width="24" }
height="24" }
><path fill="none" d="M0 0h24v24H0z" /> </script>
<path
fill="currentColor" {#if bulk_modal_open}
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" <div
/></svg class="fixed z-10 inset-0 overflow-y-auto"
> use:focusTrap
</div> use:clickOutside
<div class="mt-3 sm:mt-0"> on:click_outside={() => {
<h3 class="text-lg leading-6 font-medium text-gray-900"> bulk_modal_open = false;
{$_("create-bulk-blanco-cards")} }}>
</h3> <div
<div class="mb-6"> class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<p class="text-sm text-gray-500"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
{$_( <div
"just-enter-how-many-you-want-and-the-system-will-create-them" class="absolute inset-0 bg-gray-500 opacity-75"
)} data-id="modal_backdrop" />
</p> </div>
</div> <span
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> class="hidden sm:inline-block sm:align-middle sm:h-screen"
<div class="col-span-6"> aria-hidden="true">&#8203;</span>
<label <div
for="amount" class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full"
class="block text-sm font-medium text-gray-700" role="dialog"
>{$_("amount")}</label aria-modal="true"
> aria-labelledby="modal-headline">
<div class="mt-1 flex rounded-md shadow-sm"> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<input <div class="sm:flex sm:items-start">
autocomplete="off" <div
class:border-red-500={!is_card_count_valid} class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w- rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
class:focus:border-red-500={!is_card_count_valid} <svg
class:focus:ring-red-500={!is_card_count_valid} class="h-6 w-6 text-blue-600"
bind:value={card_count} fill="currentColor"
type="number" xmlns="http://www.w3.org/2000/svg"
step="1" viewBox="0 0 24 24"
name="amount" width="24"
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 p-2" height="24"><path fill="none" d="M0 0h24v24H0z" />
placeholder="400" <path
/> fill="currentColor"
<span d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" /></svg>
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm" </div>
>{$_("cards")}</span <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
> <h3 class="text-lg leading-6 font-medium text-gray-900">
</div> {$_('create-bulk-blanco-cards')}
{#if !is_card_count_valid} </h3>
<span <div class="mt-2 mb-6">
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <p class="text-sm text-gray-500">
> {$_('just-enter-how-many-you-want-and-the-system-will-create-them')}
{$_("you-must-create-at-least-one-card-or-cancel")} </p>
</span> </div>
{/if} <div class="grid grid-cols-6 gap-6">
</div> <div class="col-span-6">
</div> <label
</div> for="amount"
</div> class="block text-sm font-medium text-gray-700">{$_('amount')}</label>
</div> <div class="mt-1 flex rounded-md shadow-sm">
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <input
<button autocomplete="off"
disabled={!createbtnenabled} class:border-red-500={!is_card_count_valid}
class:opacity-50={!createbtnenabled} class:focus:border-red-500={!is_card_count_valid}
on:click={submit_with_print} class:focus:ring-red-500={!is_card_count_valid}
type="button" bind:value={card_count}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" type="number"
> step="1"
{$_("create-and-generate-pdf")} name="amount"
</button> class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 p-2"
<button placeholder="400" />
disabled={!createbtnenabled} <span
class:opacity-50={!createbtnenabled} class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm">{$_('cards')}</span>
on:click={submit_without_print} </div>
type="button" {#if !is_card_count_valid}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-400 text-base font-medium text-white hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" <span
> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{$_("create-without-pdf")} {$_('you-must-create-at-least-one-card-or-cancel')}
</button> </span>
<button {/if}
on:click={() => { </div>
bulk_modal_open = false; </div>
}} </div>
type="button" </div>
class="mr-auto w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" </div>
> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
{$_("cancel")} <button
</button> disabled={!createbtnenabled}
</div> class:opacity-50={!createbtnenabled}
</div> on:click={submit_with_print}
</div> type="button"
</div> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{/if} {$_('create-and-generate-pdf')}
</button>
<button
disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled}
on:click={submit_without_print}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-400 text-base font-medium text-white hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create-without-pdf')}
</button>
<button
on:click={() => {
bulk_modal_open = false;
}}
type="button"
class="mr-auto mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
{$_('cancel')}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,191 +1,204 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerCardService, RunnerService } from "@odit/lfk-client-js"; import {
import Select from "svelte-select"; RunnerCardService,
import { createEventDispatcher } from "svelte"; RunnerService,
import toast from "svelte-french-toast"; ScanService,
export let modal_open; } from "@odit/lfk-client-js";
import Select from "svelte-select";
const dispatch = createEventDispatcher(); import Toastify from "toastify-js";
const getRunnerLabel = (option) => { export let modal_open;
if (option.middlename) { export let current_cards;
return option.firstname + " " + option.middlename + " " + option.lastname;
} const getRunnerLabel = (option) => {
return option.firstname + " " + option.lastname; if (option.middlename) {
}; return option.firstname + " " + option.middlename + " " + option.lastname;
}
const filterRunners = (label, filterText, option) => { return option.firstname + " " + option.lastname;
if (filterText.startsWith("#")) { };
return option.value.id == parseInt(filterText.replace("#", ""));
} const filterRunners = (label, filterText, option) => {
return ( if (filterText.startsWith("#")) {
label.toLowerCase().includes(filterText.toLowerCase()) || return option.value.id == parseInt(filterText.replace("#",""))
option.value.toString().startsWith(filterText.toLowerCase()) }
); return (
}; label.toLowerCase().includes(filterText.toLowerCase()) ||
function focus(el) { option.value.toString().startsWith(filterText.toLowerCase())
el.focus(); );
} };
$: runner = 0; function focus(el) {
$: enabled = true; el.focus();
$: processed_last_submit = true; }
$: runner = 0;
let loading = true; $: enabled = true;
let runners = []; $: processed_last_submit = true;
RunnerService.runnerControllerGetAll().then((val) => {
runners = val.map((r) => { let loading = true;
return { label: getRunnerLabel(r), value: r }; let runners = [];
}); RunnerService.runnerControllerGetAll().then((val) => {
loading = false; runners = val.map((r) => {
}); return { label: getRunnerLabel(r), value: r };
$: createbtnenabled = true; });
(() => { loading = false;
document.onkeydown = (e) => { });
e = e || window.event; $: createbtnenabled = true;
if (e.key === "Escape") { (() => {
modal_open = false; document.onkeydown = (e) => {
} e = e || window.event;
if (e.keyCode === 13) { if (e.key === "Escape") {
if (createbtnenabled === true) { modal_open = false;
createbtnenabled = false; }
submit(); if (e.keyCode === 13) {
} if (createbtnenabled === true) {
} createbtnenabled = false;
}; submit();
})(); }
function submit() { }
if (processed_last_submit === true) { };
processed_last_submit = false; })();
toast.loading($_("adding-card")); function submit() {
let postdata = { if (processed_last_submit === true) {
runner, processed_last_submit = false;
enabled, const toast = Toastify({
}; text: $_("adding-card"),
RunnerCardService.runnerCardControllerPost(postdata) duration: -1,
.then((result) => { }).showToast();
runner = 0; let postdata = {
modal_open = false; runner,
// enabled,
toast.dismiss(); };
toast.success($_("card-added")); RunnerCardService.runnerCardControllerPost(postdata)
dispatch("created", { cards: [result] }); .then((result) => {
}) runner = 0;
.catch((err) => { modal_open = false;
// //
}) Toastify({
.finally(() => { text: $_("card-added"),
processed_last_submit = true; duration: 500,
}); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
} }).showToast();
} current_cards.push(result);
</script> current_cards = current_cards;
})
{#if modal_open} .catch((err) => {
<div //
class="fixed z-10 inset-0 overflow-y-hidden" })
use:clickOutside .finally(() => {
on:click_outside={() => { processed_last_submit = true;
modal_open = false; //
}} toast.hideToast();
> });
<div }
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" }
> </script>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div {#if modal_open}
class="absolute inset-0 bg-gray-500 opacity-75" <div
data-id="modal_backdrop" class="fixed z-10 inset-0 overflow-y-auto"
/> use:focusTrap
</div> use:clickOutside
<span on:click_outside={() => {
class="hidden sm:inline-block sm:align-middle sm:h-screen" modal_open = false;
aria-hidden="true">&#8203;</span }}
> >
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"
role="dialog" >
aria-modal="true" <div class="fixed inset-0 transition-opacity" aria-hidden="true">
aria-labelledby="modal-headline" <div
> class="absolute inset-0 bg-gray-500 opacity-75"
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> data-id="modal_backdrop"
<div class=""> />
<div </div>
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" <span
> class="hidden sm:inline-block sm:align-middle sm:h-screen"
<svg aria-hidden="true">&#8203;</span
class="size-6 text-blue-600" >
fill="currentColor" <div
xmlns="http://www.w3.org/2000/svg" class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
viewBox="0 0 24 24" role="dialog"
width="24" aria-modal="true"
height="24" aria-labelledby="modal-headline"
><path fill="none" d="M0 0h24v24H0z" /> >
<path <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
fill="currentColor" <div class="sm:flex sm:items-start">
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" <div
/></svg class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
> >
</div> <svg
<div class="mt-3 sm:mt-0"> class="h-6 w-6 text-blue-600"
<h3 class="text-lg leading-6 font-medium text-gray-900"> fill="currentColor"
{$_("create-a-new-card")} xmlns="http://www.w3.org/2000/svg"
</h3> viewBox="0 0 24 24"
<div class="mb-6"> width="24"
<p class="text-sm text-gray-500"> height="24"
{$_("you-can-provide-a-runner-but-you-dont-have-to")} ><path fill="none" d="M0 0h24v24H0z" />
{$_( <path
"if-you-want-to-create-multiple-blanco-cards-try-the-add-bulk-button" fill="currentColor"
)} d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z"
</p> /></svg
</div> >
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> </div>
<div class="col-span-6"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<label <h3 class="text-lg leading-6 font-medium text-gray-900">
for="donor" {$_("create-a-new-card")}
class="block text-sm font-medium text-gray-700" </h3>
>{$_("runner")}</label <div class="mt-2 mb-6">
> <p class="text-sm text-gray-500">
<Select {$_("you-can-provide-a-runner-but-you-dont-have-to")}
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {$_(
itemFilter={(label, filterText, option) => "if-you-want-to-create-multiple-blanco-cards-try-the-add-bulk-button"
filterRunners(label, filterText, option)} )}
items={runners} </p>
bind:loading </div>
showChevron={!loading} <div class="grid grid-cols-6 gap-6">
placeholder={$_("search-for-runner-by-name-or-id")} <div class="col-span-6">
noOptionsMessage={$_("no-runners-found")} <label
on:select={(selectedValue) => for="donor"
(runner = selectedValue.detail.value.id)} class="block text-sm font-medium text-gray-700"
on:clear={() => (runner = null)} >{$_("runner")}</label
/> >
</div> <Select
</div> containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
</div> itemFilter={(label, filterText, option) =>
</div> filterRunners(label, filterText, option)}
</div> items={runners}
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> bind:loading
<button showChevron={!loading}
disabled={!createbtnenabled} placeholder={$_("search-for-runner-by-name-or-id")}
class:opacity-50={!createbtnenabled} noOptionsMessage={$_("no-runners-found")}
on:click={submit} on:select={(selectedValue) =>
type="button" (runner = selectedValue.detail.value.id)}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" on:clear={() => (runner = null)}
> />
{$_("create")} </div>
</button> </div>
<button </div>
on:click={() => { </div>
modal_open = false; </div>
}} <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
type="button" <button
class="cancel_modal_button" disabled={!createbtnenabled}
> class:opacity-50={!createbtnenabled}
{$_("cancel")} on:click={submit}
</button> type="button"
</div> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm"
</div> >
</div> {$_("create")}
</div> </button>
{/if} <button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,200 +1,192 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerCardService, RunnerService } from "@odit/lfk-client-js"; import { RunnerCardService, RunnerService } from "@odit/lfk-client-js";
import Select from "svelte-select"; import Select from "svelte-select";
import { createEventDispatcher } from "svelte"; import Toastify from "toastify-js";
import toast from "svelte-french-toast"; import { createEventDispatcher } from "svelte";
export let edit_modal_open; export let edit_modal_open;
export let runner = {}; export let current_cards;
export let editable = {}; export let runner = {};
export let original_data = {}; export let editable = {};
const getRunnerLabel = (option) => export let original_data = {};
option.firstname + " " + (option.middlename || "") + " " + option.lastname; const getRunnerLabel = (option) =>
const filterRunners = (label, filterText, option) => { option.firstname + " " + (option.middlename || "") + " " + option.lastname;
if (filterText.startsWith("#")) { const filterRunners = (label, filterText, option) =>
return option.value.id == parseInt(filterText.replace("#", "")); label.toLowerCase().includes(filterText.toLowerCase()) ||
} option.value.toString().startsWith(filterText.toLowerCase());
return ( function focus(el) {
label.toLowerCase().includes(filterText.toLowerCase()) || el.focus();
option.value.toString().startsWith(filterText.toLowerCase()) }
); $: runners = [];
}; $: enabled = true;
$: processed_last_submit = true;
function focus(el) { const dispatch = createEventDispatcher();
el.focus(); function dataUpdated() {
} dispatch('dataUpdated',);
$: runners = []; }
$: enabled = true; RunnerService.runnerControllerGetAll().then((val) => {
$: processed_last_submit = true; runners = val.map((r) => {
const dispatch = createEventDispatcher(); return { label: getRunnerLabel(r), value: r };
RunnerService.runnerControllerGetAll().then((val) => { });
runners = val.map((r) => { });
return { label: getRunnerLabel(r), value: r }; $: createbtnenabled = !(
}); JSON.stringify(editable) === JSON.stringify(original_data)
}); );
$: createbtnenabled = !( (() => {
JSON.stringify(editable) === JSON.stringify(original_data) document.onkeydown = (e) => {
); e = e || window.event;
(() => { if (e.key === "Escape") {
document.onkeydown = (e) => { edit_modal_open = false;
e = e || window.event; }
if (e.key === "Escape") { if (e.keyCode === 13) {
edit_modal_open = false; if (createbtnenabled === true) {
} createbtnenabled = false;
if (e.keyCode === 13) { submit();
if (createbtnenabled === true) { }
createbtnenabled = false; }
submit(); };
} })();
} function submit() {
}; if (processed_last_submit === true) {
})(); processed_last_submit = false;
function submit() { const toast = Toastify({
if (processed_last_submit === true) { text: $_("updating-card"),
processed_last_submit = false; duration: -1,
toast.loading($_("updating-card")); }).showToast();
RunnerCardService.runnerCardControllerPut(original_data.id, editable) RunnerCardService.runnerCardControllerPut(original_data.id, editable)
.then((result) => { .then((result) => {
runner = {}; let id = original_data.id;
editable = {}; runner = {};
original_data = {}; editable = {};
edit_modal_open = false; original_data = {};
// edit_modal_open = false;
toast.dismiss(); //
toast.success($_("card-updated")); Toastify({
dispatch("dataUpdated", { card: result }); text: $_("card-updated"),
}) duration: 500,
.catch((err) => { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
// }).showToast();
}) current_cards[current_cards.findIndex((c) => c.id === id)] = result;
.finally(() => { current_cards = current_cards;
processed_last_submit = true; dataUpdated();
}); })
} .catch((err) => {
} //
</script> })
.finally(() => {
{#if edit_modal_open} processed_last_submit = true;
<div //
class="fixed z-10 inset-0 overflow-y-hidden" toast.hideToast();
use:clickOutside });
on:click_outside={() => { }
edit_modal_open = false; }
}} </script>
>
<div {#if edit_modal_open}
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" <div
> class="fixed z-10 inset-0 overflow-y-auto"
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> use:focusTrap
<div use:clickOutside
class="absolute inset-0 bg-gray-500 opacity-75" on:click_outside={() => {
data-id="modal_backdrop" edit_modal_open = false;
/> }}>
</div> <div
<span class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
class="hidden sm:inline-block sm:align-middle sm:h-screen" <div class="fixed inset-0 transition-opacity" aria-hidden="true">
aria-hidden="true">&#8203;</span <div
> class="absolute inset-0 bg-gray-500 opacity-75"
<div data-id="modal_backdrop" />
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" </div>
role="dialog" <span
aria-modal="true" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-labelledby="modal-headline" aria-hidden="true">&#8203;</span>
> <div
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
<div class=""> role="dialog"
<div aria-modal="true"
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<svg <div class="sm:flex sm:items-start">
class="size-6 text-blue-600" <div
fill="currentColor" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 24 24" class="h-6 w-6 text-blue-600"
width="24" fill="currentColor"
height="24" xmlns="http://www.w3.org/2000/svg"
><path fill="none" d="M0 0h24v24H0z" /> viewBox="0 0 24 24"
<path width="24"
fill="currentColor" height="24"><path fill="none" d="M0 0h24v24H0z" />
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" <path
/></svg fill="currentColor"
> d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" /></svg>
</div> </div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("edit-a-card")} {$_('edit-a-card')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_("you-can-provide-a-runner-but-you-dont-have-to")} {$_('you-can-provide-a-runner-but-you-dont-have-to')}
</p> </p>
</div> </div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="runner" for="runner"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('runner')}</label>
>{$_("runner")}</label <Select
> containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
<Select itemFilter={(label, filterText, option) => filterRunners(label, filterText, option)}
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" items={runners}
itemFilter={(label, filterText, option) => showChevron={true}
filterRunners(label, filterText, option)} placeholder={$_('search-for-runner-by-name-or-id')}
items={runners} noOptionsMessage={$_('no-runners-found')}
showChevron={true} bind:selectedValue={runner}
placeholder={$_("search-for-runner-by-name-or-id")} on:select={(selectedValue) => (editable.runner = selectedValue.detail.value.id)}
noOptionsMessage={$_("no-runners-found")} on:clear={() => (editable.runner = null)} />
bind:selectedValue={runner} </div>
on:select={(selectedValue) => <div class="col-span-6">
(editable.runner = selectedValue.detail.value.id)} <p class="text-gray-500">
on:clear={() => (editable.runner = null)} <input
/> id="enabled"
</div> on:change={() => {
<div class="col-span-6"> editable.enabled = !editable.enabled;
<p class="text-gray-500"> }}
<input name="enabled"
id="enabled" type="checkbox"
on:change={() => { checked={editable.enabled}
editable.enabled = !editable.enabled; class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
}} {$_('this-card-is')}
name="enabled" {#if editable.enabled}
type="checkbox" {$_('enabled')}
checked={editable.enabled} {:else}{$_('disabled')}{/if}
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" </p>
/> </div>
{$_("this-card-is")} </div>
{#if editable.enabled} </div>
{$_("enabled")} </div>
{:else}{$_("disabled")}{/if} </div>
</p> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
</div> <button
</div> disabled={!createbtnenabled}
</div> class:opacity-50={!createbtnenabled}
</div> on:click={submit}
</div> type="button"
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
<button {$_('save-changes')}
disabled={!createbtnenabled} </button>
class:opacity-50={!createbtnenabled} <button
on:click={submit} on:click={() => {
type="button" edit_modal_open = false;
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" }}
> type="button"
{$_("save-changes")} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
</button> {$_('cancel')}
<button </button>
on:click={() => { </div>
edit_modal_open = false; </div>
}} </div>
type="button" </div>
class="cancel_modal_button" {/if}
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,16 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let runner;
</script>
{#if !runner}
{$_("non-blanko")}
{:else}
<a class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-blue-100 text-blue-800" href={`/runners/${runner.id}`}>
{#if runner.middlename}
{runner.firstname} {runner.middlename} {runner.lastname}
{:else}
{runner.firstname} {runner.lastname}
{/if}
</a>
{/if}

View File

@ -1,16 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let enabled = false;
</script>
{#if enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("enabled")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
>{$_("disabled")}</span
>
{/if}

View File

@ -1,53 +1,40 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import AddCardBulkModal from "./AddCardBulkModal.svelte"; import AddCardBulkModal from "./AddCardBulkModal.svelte";
import AddCardModal from "./AddCardModal.svelte"; import AddCardModal from "./AddCardModal.svelte";
import CardsOverview from "./CardsOverview.svelte"; import CardsOverview from "./CardsOverview.svelte";
$: current_cards = []; $: current_cards = [];
export let modal_open = false; export let modal_open = false;
export let bulk_modal_open = false; export let bulk_modal_open = false;
let addCards; </script>
</script>
<section class="container p-5">
<section class="container p-5"> <span class="mb-1 text-3xl font-extrabold leading-tight">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> {$_('cards')}
{$_("cards")} {#if store.state.jwtinfo.userdetails.permissions.includes('CARD:CREATE')}
</h4> <button
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")} on:click={() => {
<button modal_open = true;
on:click={() => { }}
modal_open = true; type="button"
}} class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
type="button" {$_('add-card')}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" </button>
> <button
{$_("add-card")} on:click={() => {
</button> bulk_modal_open = true;
<button }}
on:click={() => { type="button"
bulk_modal_open = true; class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
}} {$_('create-bulk-cards')}
type="button" </button>
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" {/if}
> </span>
{$_("create-bulk-cards")} <CardsOverview bind:current_cards />
</button> </section>
{/if}
<CardsOverview bind:current_cards bind:addCards /> {#if store.state.jwtinfo.userdetails.permissions.includes('CARD:CREATE')}
</section> <AddCardModal bind:current_cards bind:modal_open />
<AddCardBulkModal bind:current_cards bind:bulk_modal_open />
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")} {/if}
<AddCardModal
bind:modal_open
on:created={(event) => {
addCards(event.detail.cards);
}}
/>
<AddCardBulkModal
bind:bulk_modal_open
on:created={(event) => {
addCards(event.detail.cards);
}}
/>
{/if}

View File

@ -1,12 +1,12 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import cards_empty from "./cards.svg"; import cards_empty from "./cards.svg";
</script> </script>
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="m-auto mt-2" style="height:15rem" src={cards_empty} alt="" /> <img class="m-auto" style="height:15rem" src={cards_empty} alt="" />
<span class="font-bold">{$_("there-are-no-cards-yet")}</span><br /> <span class="font-bold">{$_('there-are-no-cards-yet')}</span><br />
<span>{$_("add-your-first-card")}</span> <span>{$_('add-your-first-card')}</span>
</p> </p>
</div> </div>

View File

@ -1,314 +1,233 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { RunnerCardService } from "@odit/lfk-client-js"; import { RunnerCardService } from "@odit/lfk-client-js";
import store from "../../store"; import store from "../../store";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
import CardsEmptyState from "./CardsEmptyState.svelte"; import { DataHandler, Datatable, Th, ThFilter } from "@vincjo/datatables";
import CardDetailModal from "./CardDetailModal.svelte"; import CardsEmptyState from "./CardsEmptyState.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import CardDetailModal from "./CardDetailModal.svelte";
import InputElement from "../shared/InputElement.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import { import ThFilterStatus from "./ThFilterStatus.svelte";
createSvelteTable, import ThFilterRunner from "./ThFilterRunner.svelte";
flexRender, export let edit_modal_open = false;
getCoreRowModel, export let runner = {};
getFilteredRowModel, export let editable = {};
getPaginationRowModel, export let original_data = {};
getSortedRowModel, export let current_cards = [];
renderComponent, const handler = new DataHandler(current_cards, { rowsPerPage: 50 });
} from "@tanstack/svelte-table"; const rows = handler.getRows();
import { writable } from "svelte/store"; $: active_deletes = [];
import TableBottom from "../shared/TableBottom.svelte"; $: cards_show = generate_cards.length > 0;
import TableActions from "../shared/TableActions.svelte"; $: generate_cards = [];
import TableHeader from "../shared/TableHeader.svelte"; const cards_promise = RunnerCardService.runnerCardControllerGetAll().then(
import CardStatus from "./CardStatus.svelte"; (val) => {
import CardRunner from "./CardRunner.svelte"; current_cards = val;
import { onMount } from "svelte"; handler.setRows(val);
import { runnerFilter, statusFilter } from "../shared/tablefilters"; }
import DeleteCardModal from "./DeleteCardModal.svelte"; );
const getRunnerLabel = (option) =>
export let edit_modal_open = false; option?.firstname +
export let runner = {}; " " +
export let editable = {}; (option?.middlename || "") +
export let original_data = {}; " " +
export let current_cards = []; (option?.lastname || "{$_('non-blanko')}");
export const addCards = (cards) => { function open_edit_modal(card) {
current_cards = current_cards.concat(...cards); if (card.runner?.id) {
options.update((options) => ({ runner = Object.assign(
...options, { runner },
data: current_cards, { label: getRunnerLabel(card.runner), value: card.runner }
})); );
}; card.runner = card.runner.id;
} else {
$: dataLoaded = false; card.runner = null;
$: selected = runner = null;
$table?.getSelectedRowModel().rows.map((row) => row.index) || []; }
$: selectedCards = editable = Object.assign(editable, card);
$table?.getSelectedRowModel().rows.map((row) => row.original) || []; original_data = Object.assign(original_data, card);
$: active_delete = undefined; edit_modal_open = true;
$: cards_show = generate_cards.length > 0; }
$: generate_cards = []; </script>
const columns = [ {#if store.state.jwtinfo.userdetails.permissions.includes("CARD:UPDATE")}
{ <CardDetailModal
accessorKey: "code", bind:current_cards
header: () => $_("code"), bind:edit_modal_open
filterFn: `includesString`, bind:runner
}, bind:editable
{ bind:original_data
accessorKey: "runner", on:dataUpdated={handler.setRows(current_cards)}
header: () => $_("runner"), />
cell: (info) => { {/if}
return renderComponent(CardRunner, { runner: info.getValue() });
}, {#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")}
filterFn: `runner`, {#await cards_promise}
}, <div
{ class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
accessorKey: "enabled", role="alert"
cell: (info) => { >
return renderComponent(CardStatus, { enabled: info.getValue() }); <p class="font-bold">{$_("loading-cards")}</p>
}, <p class="text-sm">{$_("this-might-take-a-moment")}</p>
header: () => $_("status"), </div>
filterFn: `status`, {:then}
}, {#if current_cards.length === 0}
{ <CardsEmptyState />
accessorKey: "actions", {:else}
header: () => $_("action"), <div class="h-12">
cell: (info) => { <GenerateRunnerCards bind:cards_show bind:generate_cards />
return renderComponent(TableActions, { </div>
detailsAction: () => { <Datatable {handler}>
open_edit_modal( <table>
current_cards[ <thead>
current_cards.findIndex((r) => r.id == info.row.original.id) <tr>
] <th style="border-bottom: 1px solid #ddd;">
); <input
}, type="checkbox"
deleteAction: () => { class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
active_delete = checked={generate_cards.length == current_cards.length}
current_cards[ on:click={() => {
current_cards.findIndex((r) => r.id == info.row.original.id) if (generate_cards.length != current_cards.length) {
]; generate_cards = current_cards;
}, } else {
deleteEnabled: generate_cards = [];
store.state.jwtinfo.userdetails.permissions.includes("CARD:DELETE"), }
}); }}
}, />
enableColumnFilter: false, </th>
enableSorting: false, <Th {handler} orderBy="code">{$_("code")}</Th>
}, <Th {handler} orderBy="runner">{$_("runner")}</Th>
]; <Th {handler} orderBy="status">{$_("status")}</Th>
<th style="border-bottom: 1px solid #ddd;">{$_("action")}</th>
const options = writable({ </tr>
data: [], <tr>
columns: columns, <th style="border-bottom: 1px solid #ddd;" />
initialState: { <ThFilter {handler} filterBy="code" />
pagination: { <ThFilterRunner {handler} />
pageSize: 50, <ThFilterStatus {handler} />
}, <th style="border-bottom: 1px solid #ddd;" />
}, </tr>
filterFns: { </thead>
runner: runnerFilter, <tbody>
status: statusFilter, {#each $rows as row}
}, <tr>
enableRowSelection: true, <td>
getCoreRowModel: getCoreRowModel(), <input
getFilteredRowModel: getFilteredRowModel(), type="checkbox"
getPaginationRowModel: getPaginationRowModel(), class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
getSortedRowModel: getSortedRowModel(), checked={generate_cards.filter((i) => i.id == row.id)
}); .length > 0}
on:click={() => {
const table = createSvelteTable(options); if (
generate_cards.findIndex((i) => i.id == row.id) == -1
function open_edit_modal(card) { ) {
const getRunnerLabel = (option) => generate_cards.push(row);
option.firstname + generate_cards = generate_cards;
" " + } else {
(option.middlename || "") + generate_cards = generate_cards.filter(
" " + (r) => r.id != row.id
option.lastname; );
if (card.runner?.id) { }
runner = Object.assign( console.log(generate_cards);
{ runner }, }}
{ label: getRunnerLabel(card.runner), value: card.runner } />
); </td>
card.runner = card.runner.id; <td>{row.code}</td>
} else { <td>
card.runner = null; {#if row.runner}
runner = null; <a
} href="../runners/{row.runner.id}"
editable = Object.assign(editable, card); class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
original_data = Object.assign(original_data, card); >{row.runner.firstname}
edit_modal_open = true; {row.runner.middlename || ""}
} {row.runner.lastname}</a
>
async function deleteCard(delete_card_id) { {:else}{$_("non-blanko")}{/if}
await RunnerCardService.runnerCardControllerRemove(delete_card_id, true); </td>
current_cards = current_cards.filter((r) => r.id !== delete_card_id); <td>
options.update((options) => ({ {#if row.enabled}
...options, <span
data: current_cards, class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
})); >{$_("enabled")}</span
toast.success($_("card-deleted")); >
} {:else}
<span
onMount(async () => { class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
let page = 0; >{$_("disabled")}</span
let pagesize = 500; >
while (page >= 0) { {/if}
const cards = await RunnerCardService.runnerCardControllerGetAll( </td>
page, <td>
pagesize {#if active_deletes[row.id] === true}
); <button
if (cards.length == 0) { on:click={() => {
page = -2; active_deletes[row.id] = false;
} }}
tabindex="0"
current_cards = current_cards.concat(...cards); class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
options.update((options) => ({ >{$_("cancel-delete")}</button
...options, >
data: current_cards, <button
})); on:click={() => {
RunnerCardService.runnerCardControllerRemove(
dataLoaded = true; row.id,
page++; true
} )
}); .then((resp) => {
</script> current_cards = current_cards.filter(
(obj) => obj.id !== row.id
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:UPDATE")} );
<CardDetailModal })
bind:edit_modal_open .catch((err) => {});
bind:runner }}
bind:editable tabindex="0"
bind:original_data class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
on:dataUpdated={(editevent) => { >{$_("confirm-delete")}</button
console.log(editevent.detail.card) >
current_cards = current_cards.filter((c) => c.id !== editevent.detail.card.id).concat([editevent.detail.card]).sort((a, b) => a.code - b.code); {:else}
options.update((options) => ({ <button
...options, on:click={() => {
data: current_cards, open_edit_modal(row);
})); }}
}} class="text-indigo-600 hover:text-indigo-900"
/> >{$_("details")}</button
{/if} >
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:DELETE")}
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} <button
<DeleteCardModal on:click={() => {
delete_card={active_delete} active_deletes[row.id] = true;
modal_open={active_delete != undefined} }}
on:delete={(event) => { tabindex="0"
deleteCard(event.detail.id); class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
}} >{$_("delete")}</button
/> >
{#if !dataLoaded} {/if}
<div {/if}
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" </td>
role="alert" </tr>
> {/each}
<p class="font-bold">{$_("loading-cards")}</p> </tbody>
<p class="text-sm">{$_("this-might-take-a-moment")}</p> </table>
</div> </Datatable>
{:else if current_cards.length === 0} {/if}
<CardsEmptyState /> {:catch error}
{:else} <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<div class="h-12 mt-1"> <span class="inline-block align-middle mr-8">
{#if selected.length > 0} <b class="capitalize">{$_("general_promise_error")}</b>
<button {error}
type="button" </span>
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm inline-flex" </div>
id="options-menu" {/await}
on:click={async () => { {/if}
const prom = [];
for (const card of selectedCards) { <style>
prom.push( table tbody {
await RunnerCardService.runnerCardControllerRemove( display: block;
card.id, overflow-y: scroll;
true }
)
); table thead,
} table tbody tr {
await Promise.all(prom); display: table;
for (const card of selectedCards) { width: 100%;
current_cards = current_cards.filter((r) => r.id !== card.id); table-layout: fixed;
} }
options.update((options) => ({ </style>
...options,
data: current_cards,
}));
$table.resetRowSelection();
}}
>
{$_("delete-cards")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
{/if}
<GenerateRunnerCards
cards_show={selected.length > 0}
bind:generate_cards={selectedCards}
/>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="border-b border-gray-400">
{#each $table.getHeaderGroups() as headerGroup}
<tr class="select-none">
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px">
<InputElement
type="checkbox"
checked={$table.getIsAllRowsSelected()}
indeterminate={$table.getIsSomeRowsSelected()}
on:change={() => $table.toggleAllRowsSelected()}
/>
</th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $table.getRowModel().rows as row}
<tr class="odd:bg-white even:bg-gray-100">
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px">
<InputElement
type="checkbox"
checked={row.getIsSelected()}
on:change={() => row.toggleSelected()}
/>
</td>
{#each row.getVisibleCells() as cell}
<td>
<svelte:component
this={flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<TableBottom {table} {selected} />
{/if}
{/if}
<style>
table tbody tr td:nth-child(2) {
font-family: monospace;
}
</style>

View File

@ -1,123 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_card = {
id: 0,
code: "",
runner: {
firstname: "",
lastname: "",
},
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_card.id });
modal_open = false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="size-6 text-blue-600"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z"
/></svg
>
</div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("please-confirm-the-deletion-of-card")}
</h3>
<div class="w-full">
{$_("card")} #{delete_card.code}<br />
<span class="inline-block">
{$_("runner")}:
{#if delete_card.runner}
<span class="inline-block"
>{delete_card.runner.firstname}
{delete_card.runner.lastname}</span
>
{:else}
{$_("non-blanko")}
{/if}</span
>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10">
<button
on:click={submit}
type="button"
class="confirm_deletion_button"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="cancel_modal_button"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -0,0 +1,57 @@
<script>
export let handler;
let filterValue = "";
</script>
<th>
<input
on:input={() => {
setTimeout(() => {
const v = filterValue.toLowerCase();
handler.filter(v, (c) => {
// if (v === "") {
// return c;
// }
if (!c.runner && v === "blanko") {
return "blanko";
}
if (v.startsWith("#")) {
return `#${c.runner?.id}`;
}
if (c.runner) {
let runnerName = `${c.runner.firstname} ${c.runner.lastname}`;
if (c.runner.middlename) {
runnerName = `${c.runner.firstname} ${c.runner.middlename} ${c.runner.lastname}`;
}
runnerName = runnerName.toLowerCase();
return runnerName;
}
return "";
});
}, 150);
}}
bind:value={filterValue}
type="text"
name="runnerfilter"
id="runnerfilter"
/>
</th>
<style>
th {
border-bottom: 1px solid #e0e0e0;
}
input {
margin: -1px 0 0 0;
padding: 0;
width: 100%;
height: 24px;
border: none;
text-align: left;
background: inherit;
outline: 0;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,45 @@
<script>
import { _ } from "svelte-i18n";
export let handler;
let selected = "all";
</script>
<th>
<select
on:input={() => {
setTimeout(() => {
if (`${selected}`.trim()) {
if (selected === "all") {
handler.filter("", "enabled");
} else {
handler.filter(selected, "enabled");
}
}
}, 50);
}}
bind:value={selected}
name="statusfilter"
id="statusfilter"
>
<option value="all">{$_("all")}</option>
<option value="true">{$_("enabled")}</option>
<option value="false">{$_("disabled")}</option>
</select>
</th>
<style>
th {
border-bottom: 1px solid #e0e0e0;
}
select {
margin: -1px 0 0 0;
padding: 0;
width: 100%;
height: 24px;
border: none;
text-align: left;
background: inherit;
outline: 0;
font-size: 14px;
}
</style>

View File

@ -1,7 +1,7 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { import {
GroupContactService, GroupContactService,
RunnerTeamService, RunnerTeamService,
@ -9,7 +9,7 @@
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import isMobilePhone from "validator/es/lib/isMobilePhone"; import isMobilePhone from "validator/es/lib/isMobilePhone";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
export let modal_open; export let modal_open;
export let current_contacts; export let current_contacts;
$: selected_team = []; $: selected_team = [];
@ -43,7 +43,7 @@
$: address_zipcode_value = ""; $: address_zipcode_value = "";
$: address_city_value = ""; $: address_city_value = "";
$: processed_last_submit = true; $: processed_last_submit = true;
$: address_checked = false; $: address_checked = true;
$: isPhoneValidOrEmpty = $: isPhoneValidOrEmpty =
(phone_input_value.includes("+") && (phone_input_value.includes("+") &&
isMobilePhone( isMobilePhone(
@ -85,7 +85,10 @@
function submit() { function submit() {
if (processed_last_submit === true) { if (processed_last_submit === true) {
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("contact-is-being-added")); const toast = Toastify({
text: $_('contact-is-being-added'),
duration: -1,
}).showToast();
let address = {}; let address = {};
if (address_checked === true) { if (address_checked === true) {
address = { address = {
@ -119,8 +122,11 @@
email_input_value = ""; email_input_value = "";
modal_open = false; modal_open = false;
// //
toast.dismiss(); Toastify({
toast.success($_("contact-added")); text: $_('contact-added'),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_contacts.push(result); current_contacts.push(result);
current_contacts = current_contacts; current_contacts = current_contacts;
}) })
@ -129,6 +135,8 @@
}) })
.finally(() => { .finally(() => {
processed_last_submit = true; processed_last_submit = true;
//
toast.hideToast();
}); });
} }
} }
@ -136,71 +144,59 @@
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
modal_open = false; modal_open = false;
}} }}>
>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div <div
class="absolute inset-0 bg-gray-500 opacity-75" class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" data-id="modal_backdrop" />
/>
</div> </div>
<span <span
class="hidden sm:inline-block sm:align-middle sm:h-screen" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span aria-hidden="true">&#8203;</span>
>
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <div class="sm:flex sm:items-start">
<div class="">
<div <div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
>
<svg <svg
class="size-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"><path fill="none" d="M0 0h24v24H0z" />
><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" /></svg>
/></svg
>
</div> </div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("create-a-new-contact")} {$_('create-a-new-contact')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_('please-provide-the-required-information-to-add-a-new-contact')}
"please-provide-the-required-information-to-add-a-new-contact"
)}
</p> </p>
</div> </div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="firstname" for="firstname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('first-name')}</label>
>{$_("first-name")}</label
>
<input <input
use:focus use:focus
autocomplete="off" autocomplete="off"
placeholder={$_("first-name")} placeholder={$_('first-name')}
class:border-red-500={!isFirstnameValid} class:border-red-500={!isFirstnameValid}
class:focus:border-red-500={!isFirstnameValid} class:focus:border-red-500={!isFirstnameValid}
class:focus:ring-red-500={!isFirstnameValid} class:focus:ring-red-500={!isFirstnameValid}
@ -208,41 +204,34 @@
bind:this={firstname_input} bind:this={firstname_input}
type="text" type="text"
name="firstname" name="firstname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('first-name-is-required')}
{$_("first-name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="trackname" for="trackname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('middle-name')}</label>
>{$_("middle-name")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("middle-name")} placeholder={$_('middle-name')}
bind:value={middlename_input_value} bind:value={middlename_input_value}
bind:this={middlename_input} bind:this={middlename_input}
type="text" type="text"
name="trackname" name="trackname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="lastname" for="lastname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('last-name')}</label>
>{$_("last-name")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("last-name")} placeholder="{$_('last-name')}"
class:border-red-500={!isLastnameValid} class:border-red-500={!isLastnameValid}
class:focus:border-red-500={!isLastnameValid} class:focus:border-red-500={!isLastnameValid}
class:focus:ring-red-500={!isLastnameValid} class:focus:ring-red-500={!isLastnameValid}
@ -250,28 +239,23 @@
bind:this={lastname_input} bind:this={lastname_input}
type="text" type="text"
name="lastname" name="lastname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('last-name-is-required')}
{$_("last-name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="team" for="team"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('teams')}</label>
>{$_("teams")}</label
>
<select <select
name="team" name="team"
multiple multiple
bind:value={selected_team} bind:value={selected_team}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2">
>
{#each teams as team} {#each teams as team}
<option value={team.id}> <option value={team.id}>
{team.parentGroup.name} {team.parentGroup.name}
@ -287,12 +271,10 @@
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="phone" for="phone"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('phone')}</label>
>{$_("phone")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("phone")} placeholder={$_('phone')}
class:border-red-500={!isPhoneValidOrEmpty} class:border-red-500={!isPhoneValidOrEmpty}
class:focus:border-red-500={!isPhoneValidOrEmpty} class:focus:border-red-500={!isPhoneValidOrEmpty}
class:focus:ring-red-500={!isPhoneValidOrEmpty} class:focus:ring-red-500={!isPhoneValidOrEmpty}
@ -300,27 +282,21 @@
bind:this={phone_input} bind:this={phone_input}
type="tel" type="tel"
name="phone" name="phone"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isPhoneValidOrEmpty} {#if !isPhoneValidOrEmpty}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {@html $_('the-provided-phone-number-is-invalid-less-than-br-greater-than-please-enter-a-valid-international-number')}
{@html $_(
"the-provided-phone-number-is-invalid-less-than-br-greater-than-please-enter-a-valid-international-number"
)}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="email" for="email"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('e-mail-adress')}</label>
>{$_("e-mail-adress")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("e-mail-adress")} placeholder={$_('e-mail-adress')}
class:border-red-500={!isEmailValidOrEmpty} class:border-red-500={!isEmailValidOrEmpty}
class:focus:border-red-500={!isEmailValidOrEmpty} class:focus:border-red-500={!isEmailValidOrEmpty}
class:focus:ring-red-500={!isEmailValidOrEmpty} class:focus:ring-red-500={!isEmailValidOrEmpty}
@ -328,13 +304,11 @@
bind:this={email_input} bind:this={email_input}
type="email" type="email"
name="email" name="email"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isEmailValidOrEmpty} {#if !isEmailValidOrEmpty}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-email-is-required')}
{$_("valid-email-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
@ -345,25 +319,22 @@
id="comments" id="comments"
name="comments" name="comments"
type="checkbox" type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-semibold text-gray-700" <label
>{$_("address")}</label for="comments"
> class="font-medium text-gray-700">{$_('address')}</label>
</div> </div>
</div> </div>
{#if address_checked === true} {#if address_checked === true}
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="address1" for="address1"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('address')}</label>
>{$_("address")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("address")} placeholder="{$_('address')}"
class:border-red-500={!isAddress1Valid} class:border-red-500={!isAddress1Valid}
class:focus:border-red-500={!isAddress1Valid} class:focus:border-red-500={!isAddress1Valid}
class:focus:ring-red-500={!isAddress1Valid} class:focus:ring-red-500={!isAddress1Valid}
@ -371,41 +342,34 @@
bind:this={address_input1} bind:this={address_input1}
type="text" type="text"
name="address1" name="address1"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isAddress1Valid} {#if !isAddress1Valid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('address-is-required')}
{$_("address-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="address2" for="address2"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
>{$_("apartment-suite-etc")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("apartment-suite-etc")} placeholder={$_('apartment-suite-etc')}
bind:value={address_input2_value} bind:value={address_input2_value}
bind:this={address_input2} bind:this={address_input2}
type="text" type="text"
name="address2" name="address2"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="zipcode" for="zipcode"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('zip-postal-code')}</label>
>{$_("zip-postal-code")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("zip-postal-code")} placeholder={$_('zip-postal-code')}
class:border-red-500={!iszipcodevalid} class:border-red-500={!iszipcodevalid}
class:focus:border-red-500={!iszipcodevalid} class:focus:border-red-500={!iszipcodevalid}
class:focus:ring-red-500={!iszipcodevalid} class:focus:ring-red-500={!iszipcodevalid}
@ -413,25 +377,21 @@
bind:this={address_zipcode} bind:this={address_zipcode}
type="text" type="text"
name="zipcode" name="zipcode"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !iszipcodevalid} {#if !iszipcodevalid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-zipcode-postal-code-is-required')}
{$_("valid-zipcode-postal-code-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="city" for="city"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('city')}</label>
>{$_("city")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("city")} placeholder="{$_('city')}"
class:border-red-500={!iscityvalid} class:border-red-500={!iscityvalid}
class:focus:border-red-500={!iscityvalid} class:focus:border-red-500={!iscityvalid}
class:focus:ring-red-500={!iscityvalid} class:focus:ring-red-500={!iscityvalid}
@ -439,13 +399,11 @@
bind:this={address_city} bind:this={address_city}
type="text" type="text"
name="city" name="city"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !iscityvalid} {#if !iscityvalid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-city-is-required')}
{$_("valid-city-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
@ -454,24 +412,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
disabled={!createbtnenabled} disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled} class:opacity-50={!createbtnenabled}
on:click={submit} on:click={submit}
type="button" type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('create')}
{$_("create")}
</button> </button>
<button <button
on:click={() => { on:click={() => {
modal_open = false; modal_open = false;
}} }}
type="button" type="button"
class="cancel_modal_button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('cancel')}
{$_("cancel")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,399 +1,394 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import { import {
GroupContactService, GroupContactService,
RunnerTeamService, RunnerTeamService,
RunnerOrganizationService, RunnerOrganizationService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import Toastify from "toastify-js";
import isEmail from "validator/es/lib/isEmail"; import PromiseError from "../base/PromiseError.svelte";
import toast from "svelte-french-toast"; import isEmail from "validator/es/lib/isEmail";
let data_loaded = false; let data_loaded = false;
let orgs = []; let orgs = [];
let teams = []; let teams = [];
export let params; export let params;
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: changes_performed = !( $: changes_performed = !(
JSON.stringify(original_data) === JSON.stringify(editable) JSON.stringify(original_data) === JSON.stringify(editable)
); );
$: isEmailValid = $: isEmailValid =
(editable.email || "") === "" || (editable.email || "") === "" ||
(editable.email && isEmail(editable.email || "")); (editable.email && isEmail(editable.email || ""));
$: isFirstnameValid = editable.firstname !== ""; $: isFirstnameValid = editable.firstname !== "";
$: isLastnameValid = editable.lastname !== ""; $: isLastnameValid = editable.lastname !== "";
$: save_enabled = $: save_enabled =
changes_performed && changes_performed &&
isFirstnameValid && isFirstnameValid &&
isLastnameValid && isLastnameValid &&
isEmailValid && isEmailValid &&
isPhoneValidOrEmpty && isPhoneValidOrEmpty &&
((isAddress1Valid && iszipcodevalid && iscityvalid) || ((isAddress1Valid && iszipcodevalid && iscityvalid) ||
editable.address_checked === false); editable.address_checked === false);
const promise = GroupContactService.groupContactControllerGetOne( const promise = GroupContactService.groupContactControllerGetOne(
params.contact params.contact
).then((data) => { ).then((data) => {
data_loaded = true; data_loaded = true;
original_data = Object.assign(original_data, data); original_data = Object.assign(original_data, data);
editable = Object.assign(editable, original_data); editable = Object.assign(editable, original_data);
editable.groups = editable.groups.map((g) => g.id); editable.groups = editable.groups.map((g) => g.id);
original_data.groups = original_data.groups.map((g) => g.id); original_data.groups = original_data.groups.map((g) => g.id);
editable.address_checked = editable.address.address1 !== null; editable.address_checked = editable.address.address1 !== null;
original_data.address_checked = editable.address.address1 !== null; original_data.address_checked = editable.address.address1 !== null;
if (editable.address_checked === false) { if(editable.address_checked===false){
editable.address = { editable.address = {
address1: "", address1: "",
address2: "", address2: "",
city: "", city: "",
postalcode: "", postalcode: "",
country: "", country: ""
}; }
} }
}); });
RunnerOrganizationService.runnerOrganizationControllerGetAll().then((val) => { RunnerOrganizationService.runnerOrganizationControllerGetAll().then((val) => {
orgs = val; orgs = val;
}); });
RunnerTeamService.runnerTeamControllerGetAll().then((val) => { RunnerTeamService.runnerTeamControllerGetAll().then((val) => {
teams = val; teams = val;
}); });
$: isPhoneValidOrEmpty = $: isPhoneValidOrEmpty =
editable.phone?.includes("+") || editable.phone?.includes("+") ||
editable.phone === "" || editable.phone === "" ||
editable.phone === null; editable.phone === null;
$: isAddress1Valid = editable.address?.address1?.trim().length !== 0; $: isAddress1Valid = editable.address?.address1?.trim().length !== 0;
$: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0; $: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0;
$: iscityvalid = editable.address?.city?.trim().length !== 0; $: iscityvalid = editable.address?.city?.trim().length !== 0;
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast.loading($_("contact-is-being-updated")); Toastify({
editable.address.country = "DE"; text: $_("contact-is-being-updated"),
if (editable.address_checked === false) { duration: 2500,
editable.address = null; }).showToast();
} editable.address.country = "DE";
if (editable.email) editable.email = editable.email; if (editable.address_checked === false) {
if (editable.phone) editable.phone = editable.phone; editable.address = null;
if (editable.middlename) editable.middlename = editable.middlename; }
GroupContactService.groupContactControllerPut(original_data.id, editable) if (editable.email) editable.email = editable.email;
.then((resp) => { if (editable.phone) editable.phone = editable.phone;
Object.assign(original_data, editable); if (editable.middlename) editable.middlename = editable.middlename;
original_data = original_data; GroupContactService.groupContactControllerPut(original_data.id, editable)
toast.dismiss(); .then((resp) => {
toast.success($_("updated-contact")); Object.assign(original_data, editable);
}) original_data=original_data;
.catch((err) => {}); Toastify({
} else { text: $_("updated-contact"),
} duration: 2500,
} backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
function deleteContact() { }).showToast();
GroupContactService.groupContactControllerRemove(original_data.id, true) })
.then((resp) => { .catch((err) => {});
location.replace("./"); } else {
}) }
.catch((err) => {}); }
} function deleteContact() {
GroupContactService.groupContactControllerRemove(original_data.id, true)
.then((resp) => {
location.replace("./");
})
.catch((err) => {});
}
</script> </script>
{#await promise} {#await promise}
{$_("loading-contact-details")} {$_('loading-contact-details')}
{:then} {:then}
<section class="container p-5 select-none"> <section class="container p-5 select-none">
<div class="flex flex-row mb-4"> <div class="flex flex-row mb-4">
<div class="w-full"> <div class="w-full">
<nav class="w-full flex"> <nav class="w-full flex">
<ol class="list-none flex flex-row items-center justify-start"> <ol class="list-none flex flex-row items-center justify-start">
<li class="flex items-center"> <li class="flex items-center">
<a class="mr-2" href="./" <svg
><svg fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" viewBox="0 0 24 24"
height="24" width="24"
viewBox="0 0 24 24" height="24"><path fill="none" d="M0 0h24v24H0z" />
fill="none" <path
stroke="currentColor" d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" /></svg>
stroke-width="2" </li>
stroke-linecap="round" <li class="flex items-center ml-2">
stroke-linejoin="round" <a class="mr-2" href="./">{$_('contacts')}</a><svg
class="inline-block" stroke="currentColor"
><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg fill="none"
> stroke-width="2"
{$_("contacts")}</a viewBox="0 0 24 24"
> stroke-linecap="round"
</li> stroke-linejoin="round"
</ol> class="h-3 w-3 mr-2 stroke-current"
</nav> height="1em"
</div> width="1em"
</div> xmlns="http://www.w3.org/2000/svg"><line
<div class="mb-4 text-3xl font-extrabold leading-tight"> x1="5"
{original_data.firstname} y1="12"
{original_data.middlename || ""} x2="19"
{original_data.lastname} y2="12" />
<div data-id="contact_actions_${editable.id}"> <polyline points="12 5 19 12 12 19" /></svg>
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:DELETE")} </li>
{#if delete_triggered} <li class="flex items-center">
<button <span class="mr-2">{original_data.firstname}
on:click={deleteContact} {original_data.middlename || ''}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" {original_data.lastname}</span>
>{$_("confirm-deletion")}</button </li>
> </ol>
<button </nav>
on:click={() => { </div>
delete_triggered = !delete_triggered; </div>
}} <div class="mb-8 text-3xl font-extrabold leading-tight">
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm" {original_data.firstname}
>{$_("cancel")}</button {original_data.middlename || ''}
> {original_data.lastname}
{/if} <span data-id="contact_actions_${editable.id}">
{#if !delete_triggered} {#if store.state.jwtinfo.userdetails.permissions.includes('CONTACT:DELETE')}
<button {#if delete_triggered}
on:click={() => { <button
delete_triggered = true; on:click={deleteContact}
}} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('confirm-deletion')}</button>
type="button" <button
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" on:click={() => {
>{$_("delete-contact")}</button delete_triggered = !delete_triggered;
> }}
{/if} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm">{$_('cancel')}</button>
{/if} {/if}
{#if !delete_triggered} {#if !delete_triggered}
<button <button
disabled={!save_enabled} on:click={() => {
class:opacity-50={!save_enabled} delete_triggered = true;
type="button" }}
on:click={submit} type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-contact')}</button>
>{$_("save-changes")}</button {/if}
> {/if}
{/if} {#if !delete_triggered}
</div> <button
</div> disabled={!save_enabled}
<!-- --> class:opacity-50={!save_enabled}
<div class="text-sm w-full mt-2"> type="button"
<label for="firstname" class="font-semibold text-gray-700" on:click={submit}
>{$_("first-name")}</label class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
> {/if}
<input </span>
autocomplete="off" </div>
placeholder={$_("first-name")} <!-- -->
type="text" <div class="text-sm w-full">
class:border-red-500={!isFirstnameValid} <label
class:focus:border-red-500={!isFirstnameValid} for="firstname"
class:focus:ring-red-500={!isFirstnameValid} class="font-medium text-gray-700">{$_('first-name')}</label>
bind:value={editable.firstname} <input
name="firstname" autocomplete="off"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" placeholder={$_('first-name')}
/> type="text"
{#if !isFirstnameValid} class:border-red-500={!isFirstnameValid}
<span class:focus:border-red-500={!isFirstnameValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class:focus:ring-red-500={!isFirstnameValid}
> bind:value={editable.firstname}
{$_("first-name-is-required")} name="firstname"
</span> class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
{/if} {#if !isFirstnameValid}
</div> <span
<div class="text-sm w-full mt-2"> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
<label for="middlename" class="font-semibold text-gray-700" {$_('first-name-is-required')}
>{$_("middle-name")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="text-sm w-full">
placeholder={$_("middle-name")} <label
type="text" for="middlename"
bind:value={editable.middlename} class="font-medium text-gray-700">{$_('middle-name')}</label>
name="middlename" <input
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" autocomplete="off"
/> placeholder={$_('middle-name')}
</div> type="text"
<div class="text-sm w-full mt-2"> bind:value={editable.middlename}
<label for="lastname" class="font-semibold text-gray-700" name="middlename"
>{$_("last-name")}</label class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
> </div>
<input <div class="text-sm w-full">
autocomplete="off" <label
placeholder={$_("last-name")} for="lastname"
type="text" class="font-medium text-gray-700">{$_('last-name')}</label>
bind:value={editable.lastname} <input
class:border-red-500={!isLastnameValid} autocomplete="off"
class:focus:border-red-500={!isLastnameValid} placeholder={$_('last-name')}
class:focus:ring-red-500={!isLastnameValid} type="text"
name="lastname" bind:value={editable.lastname}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class:border-red-500={!isLastnameValid}
/> class:focus:border-red-500={!isLastnameValid}
{#if !isLastnameValid} class:focus:ring-red-500={!isLastnameValid}
<span name="lastname"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
> {#if !isLastnameValid}
{$_("last-name-is-required")} <span
</span> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{/if} {$_('last-name-is-required')}
</div> </span>
<div class="text-sm w-full mt-2"> {/if}
<label for="email" class="font-semibold text-gray-700" </div>
>{$_("e-mail-adress")}</label <div class="text-sm w-full">
> <label
<input for="email"
autocomplete="off" class="font-medium text-gray-700">{$_('e-mail-adress')}</label>
placeholder={$_("e-mail-adress")} <input
type="email" autocomplete="off"
bind:value={editable.email} placeholder={$_('e-mail-adress')}
class:border-red-500={!isEmailValid} type="email"
class:focus:border-red-500={!isEmailValid} bind:value={editable.email}
class:focus:ring-red-500={!isEmailValid} class:border-red-500={!isEmailValid}
name="email" class:focus:border-red-500={!isEmailValid}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class:focus:ring-red-500={!isEmailValid}
/> name="email"
{#if !isEmailValid} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
<span {#if !isEmailValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <span
> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{$_("valid-email-is-required")} {$_('valid-email-is-required')}
</span> </span>
{/if} {/if}
</div> </div>
<div class="text-sm w-full mt-2"> <div class="text-sm w-full">
<label for="phone" class="font-semibold text-gray-700">{$_("phone")}</label> <label for="phone" class="font-medium text-gray-700">{$_('phone')}</label>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("phone")} placeholder={$_('phone')}
type="tel" type="tel"
class:border-red-500={!isPhoneValidOrEmpty} class:border-red-500={!isPhoneValidOrEmpty}
class:focus:border-red-500={!isPhoneValidOrEmpty} class:focus:border-red-500={!isPhoneValidOrEmpty}
class:focus:ring-red-500={!isPhoneValidOrEmpty} class:focus:ring-red-500={!isPhoneValidOrEmpty}
bind:value={editable.phone} bind:value={editable.phone}
name="phone" name="phone"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/> {#if !isPhoneValidOrEmpty}
{#if !isPhoneValidOrEmpty} <span
<span class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" {$_('valid-international-phone-number-is-required')}
> </span>
{$_("valid-international-phone-number-is-required")} {/if}
</span> </div>
{/if} <div class="text-sm w-full">
</div> <span class="font-medium text-gray-700">{$_('groups')}</span>
<div class="text-sm w-full mt-2"> <select
<span class="font-semibold text-gray-700">{$_("groups")}</span> bind:value={editable.groups}
<select name="team"
bind:value={editable.groups} multiple
name="team" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2">
multiple {#each teams as team}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <option value={team.id}>
> {team.parentGroup.name}
{#each teams as team} &gt;
<option value={team.id}> {team.name}
{team.parentGroup.name} </option>
&gt; {/each}
{team.name} {#each orgs as org}
</option> <option value={org.id}>{org.name}</option>
{/each} {/each}
{#each orgs as org} </select>
<option value={org.id}>{org.name}</option> </div>
{/each} <!-- -->
</select> <div class="flex items-start mt-2">
</div> <div class="flex items-center h-5">
<!-- --> <input
<div class="flex items-start mt-2"> bind:checked={editable.address_checked}
<div class="flex items-center h-5"> id="comments"
<input name="comments"
bind:checked={editable.address_checked} type="checkbox"
id="comments" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
name="comments" </div>
type="checkbox" <div class="ml-3 text-sm">
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" <label
/> for="comments"
</div> class="font-medium text-gray-700">{$_('address')}</label>
<div class="ml-3 text-sm"> </div>
<label for="comments" class="font-semibold text-gray-700" </div>
>{$_("address")}</label {#if editable.address_checked === true}
> <div class="col-span-6">
</div> <label
</div> for="address1"
{#if editable.address_checked === true} class="block text-sm font-medium text-gray-700">{$_('address')}</label>
<div class="col-span-6"> <input
<label for="address1" class="block text-sm font-medium text-gray-700" autocomplete="off"
>{$_("address")}</label placeholder="Address"
> class:border-red-500={!isAddress1Valid}
<input class:focus:border-red-500={!isAddress1Valid}
autocomplete="off" class:focus:ring-red-500={!isAddress1Valid}
placeholder="Address" bind:value={editable.address.address1}
class:border-red-500={!isAddress1Valid} type="text"
class:focus:border-red-500={!isAddress1Valid} name="address1"
class:focus:ring-red-500={!isAddress1Valid} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
bind:value={editable.address.address1} {#if !isAddress1Valid}
type="text" <span
name="address1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {$_('address-is-required')}
/> </span>
{#if !isAddress1Valid} {/if}
<span </div>
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <div class="col-span-6">
> <label
{$_("address-is-required")} for="address2"
</span> class="block text-sm font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
{/if} <input
</div> autocomplete="off"
<div class="col-span-6"> placeholder={$_('apartment-suite-etc')}
<label for="address2" class="block text-sm font-medium text-gray-700" bind:value={editable.address.address2}
>{$_("apartment-suite-etc")}</label type="text"
> name="address2"
<input class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
autocomplete="off" </div>
placeholder={$_("apartment-suite-etc")} <div class="col-span-6">
bind:value={editable.address.address2} <label
type="text" for="zipcode"
name="address2" class="block text-sm font-medium text-gray-700">{$_('zip-postal-code')}</label>
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <input
/> autocomplete="off"
</div> placeholder={$_('zip-postal-code')}
<div class="col-span-6"> class:border-red-500={!iszipcodevalid}
<label for="zipcode" class="block text-sm font-medium text-gray-700" class:focus:border-red-500={!iszipcodevalid}
>{$_("zip-postal-code")}</label class:focus:ring-red-500={!iszipcodevalid}
> bind:value={editable.address.postalcode}
<input type="text"
autocomplete="off" name="zipcode"
placeholder={$_("zip-postal-code")} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
class:border-red-500={!iszipcodevalid} {#if !iszipcodevalid}
class:focus:border-red-500={!iszipcodevalid} <span
class:focus:ring-red-500={!iszipcodevalid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
bind:value={editable.address.postalcode} {$_('valid-zipcode-postal-code-is-required')}
type="text" </span>
name="zipcode" {/if}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" </div>
/> <div class="col-span-6">
{#if !iszipcodevalid} <label
<span for="city"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="block text-sm font-medium text-gray-700">{$_('city')}</label>
> <input
{$_("valid-zipcode-postal-code-is-required")} autocomplete="off"
</span> placeholder={$_('city')}
{/if} class:border-red-500={!iscityvalid}
</div> class:focus:border-red-500={!iscityvalid}
<div class="col-span-6"> class:focus:ring-red-500={!iscityvalid}
<label for="city" class="block text-sm font-medium text-gray-700" bind:value={editable.address.city}
>{$_("city")}</label type="text"
> name="city"
<input class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
autocomplete="off" {#if !iscityvalid}
placeholder={$_("city")} <span
class:border-red-500={!iscityvalid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class:focus:border-red-500={!iscityvalid} {$_('valid-city-is-required')}
class:focus:ring-red-500={!iscityvalid} </span>
bind:value={editable.address.city} {/if}
type="text" </div>
name="city" {/if}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" </section>
/>
{#if !iscityvalid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
>
{$_("valid-city-is-required")}
</span>
{/if}
</div>
{/if}
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@ -8,23 +8,22 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_("contacts")} {$_('contacts')}
</h4> {#if store.state.jwtinfo.userdetails.permissions.includes('CONTACT:CREATE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:CREATE")} <button
<button on:click={() => {
on:click={() => { modal_open = true;
modal_open = true; }}
}} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm" {$_('create-a-new-contact')}
> </button>
{$_("create-a-new-contact")} {/if}
</button> </span>
{/if}
<ContactsOverview bind:current_contacts /> <ContactsOverview bind:current_contacts />
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes('CONTACT:CREATE')}
<AddContactModal bind:current_contacts bind:modal_open /> <AddContactModal bind:current_contacts bind:modal_open />
{/if} {/if}

View File

@ -9,8 +9,8 @@
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="w-full h-44" src={team_empty} alt="" /> <img class="w-full h-44" src={team_empty} alt="" />
<span class="font-bold">{$_("there-are-no-contacts-added-yet")}</span><br /> <span class="font-bold">{$_('there-are-no-contacts-added-yet')}</span><br />
<span>{$_("add-your-first-contact")}</span> <span>{$_('add-your-first-contact')}</span>
</p> </p>
</div> </div>

View File

@ -1,198 +1,177 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { GroupContactService } from "@odit/lfk-client-js"; import Toastify from "toastify-js";
const promise = GroupContactService.groupContactControllerGetAll().then( import { GroupContactService } from "@odit/lfk-client-js";
(result) => { const promise = GroupContactService.groupContactControllerGetAll().then(
current_contacts = result; (result) => {
} current_contacts = result;
); }
import store from "../../store"; );
import ContactsEmptyState from "./ContactsEmptyState.svelte"; import store from "../../store";
import toast from "svelte-french-toast"; import ContactsEmptyState from "./ContactsEmptyState.svelte";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
export let current_contacts = []; export let current_contacts = [];
</script> </script>
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes('TEAM:GET')}
{#await promise} {#await promise}
<div <div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert" role="alert">
> <p class="font-bold">{$_('contacts-are-being-loaded')}</p>
<p class="font-bold">{$_("contacts-are-being-loaded")}</p> <p class="text-sm">{$_('this-might-take-a-moment')}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p> </div>
</div> {:then}
{:then} {#if current_contacts.length === 0}
{#if current_contacts.length === 0} <ContactsEmptyState />
<ContactsEmptyState /> {:else}
{:else} <input
<input type="search"
type="search" bind:value={searchvalue}
bind:value={searchvalue} placeholder={$_('datatable.search')}
placeholder={$_("datatable.search")} aria-label={$_('datatable.search')}
aria-label={$_("datatable.search")} class="gridjs-input gridjs-search-input mb-4" />
class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" <div
/> class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<div <table class="divide-y divide-gray-200 w-full">
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll" <thead class="bg-gray-50">
> <tr>
<table class="divide-y divide-gray-200 w-full"> <th
<thead class="bg-gray-50"> scope="col"
<tr class="odd:bg-white even:bg-gray-100"> class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th {$_('name')}
scope="col" </th>
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" <th
> scope="col"
{$_("name")} class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> {$_('groups')}
<th </th>
scope="col" <th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" scope="col"
> class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_("groups")} {$_('address')}
</th> </th>
<th <th scope="col" class="relative px-6 py-3">
scope="col" <span class="sr-only">{$_('action')}</span>
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" </th>
> </tr>
{$_("address")} </thead>
</th> <tbody class="divide-y divide-gray-200">
<th scope="col" class="relative px-6 py-3"> {#each current_contacts as t}
<span class="sr-only">{$_("action")}</span> {#if Object.values(t)
</th> .toString()
</tr> .toLowerCase()
</thead> .includes(searchvalue)}
<tbody class="divide-y divide-gray-200"> <tr data-rowid="team_{t.id}">
{#each current_contacts as t} <td class="px-6 py-4 whitespace-nowrap">
{#if Object.values(t) <div class="flex items-center">
.toString() <div class="ml-4">
.toLowerCase() <div class="text-sm font-medium text-gray-900">
.includes(searchvalue)} {t.firstname}
<tr {t.middlename || ''}
class="odd:bg-white even:bg-gray-100" {t.lastname}
data-rowid="team_{t.id}" </div>
> </div>
<td class="px-6 py-4 whitespace-nowrap"> </div>
<div class="flex items-center"> </td>
<div class="ml-4"> <td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900"> <div class="flex items-center">
{t.firstname} <div class="ml-4">
{t.middlename || ""} <div class="text-sm font-medium text-gray-900">
{t.lastname} {#if t.groups.length > 0}
</div> {#each t.groups as g}
</div> {#if g.responseType === 'RUNNERORGANIZATION'}
</div> <a
</td> href="../orgs/{g.id}"
<td class="px-6 py-4 whitespace-nowrap"> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.name}</a>
<div class="flex items-center"> {:else}
<div <a
class="text-sm font-medium text-gray-900 gap-0.5 flex flex-wrap" href="../teams/{g.id}"
> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{g.parentGroup.name}
{#if t.groups.length > 0} &gt;
{#each t.groups as g} {g.name}</a>
{#if g.responseType === "RUNNERORGANIZATION"} {/if}
<a {/each}
href="../orgs/{g.id}" {:else}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current" {$_('contact-is-not-a-member-in-any-group')}
>{g.name}</a {/if}
> </div>
{:else} </div>
<a </div>
href="../teams/{g.id}" </td>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current" <td class="px-6 py-4 whitespace-nowrap">
>{g.parentGroup.name} <div class="flex items-center">
&gt; <div class="ml-4">
{g.name}</a <div class="text-sm font-medium text-gray-900">
> {#if t.address.address1 !== null}
{/if} {t.address.address1}<br />
{/each} {t.address.address2 || ''}<br />
{:else} {t.address.postalcode}
{$_("contact-is-not-a-member-in-any-group")} {t.address.city}
{/if} {t.address.country}
</div> {/if}
</div> </div>
</td> </div>
<td class="px-6 py-4 whitespace-nowrap"> </div>
<div class="flex items-center"> </td>
<div class="ml-4"> {#if active_deletes[t.id] === true}
<div class="text-sm font-medium text-gray-900"> <td
{#if t.address.address1 !== null} class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
{t.address.address1}<br /> <button
{t.address.address2 || ""}<br /> on:click={() => {
{t.address.postalcode} active_deletes[t.id] = false;
{t.address.city} }}
{t.address.country} tabindex="0"
{/if} class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
</div> <button
</div> on:click={() => {
</div> GroupContactService.groupContactControllerRemove(t.id, false).then(
</td> (resp) => {
{#if active_deletes[t.id] === true} current_contacts = current_contacts.filter(
<td (obj) => obj.id !== t.id
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" );
> Toastify({
<button text: $_('contact-deleted'),
on:click={() => { duration: 500,
active_deletes[t.id] = false; backgroundColor:
}} 'linear-gradient(to right, #00b09b, #96c93d)',
tabindex="0" }).showToast();
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer" }
>{$_("cancel-delete")}</button );
> }}
<button tabindex="0"
on:click={() => { class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
toast.loading($_("deleting-contact")); </td>
GroupContactService.groupContactControllerRemove( {:else}
t.id, <td
false class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
).then((resp) => { <a
current_contacts = current_contacts.filter( href="./{t.id}"
(obj) => obj.id !== t.id class="text-indigo-600 hover:text-indigo-900">{$_('details')}</a>
); {#if store.state.jwtinfo.userdetails.permissions.includes('TEAM:DELETE')}
toast.dismiss(); <button
toast.success($_("contact-deleted")); on:click={() => {
}); active_deletes[t.id] = true;
}} }}
tabindex="0" tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
>{$_("confirm-delete")}</button {/if}
> </td>
</td> {/if}
{:else} </tr>
<td {/if}
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" {/each}
> </tbody>
<a </table>
href="./{t.id}" </div>
class="text-indigo-600 hover:text-indigo-900" {/if}
>{$_("details")}</a {:catch error}
> <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")} <span class="inline-block align-middle mr-8">
<button <b class="capitalize">{$_('general_promise_error')}</b>
on:click={() => { {error}
active_deletes[t.id] = true; </span>
}} </div>
tabindex="0" {/await}
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b>{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
{/if} {/if}

View File

@ -1,534 +1,361 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import localForage from "localforage"; import localForage from "localforage";
import store from "../../store"; import store from "../../store";
import { router } from "tinro"; import { router } from "tinro";
import NoComponentLoaded from "../base/NoComponentLoaded.svelte"; import NoComponentLoaded from "../base/NoComponentLoaded.svelte";
import { AuthService } from "@odit/lfk-client-js"; import { AuthService } from "@odit/lfk-client-js";
import { Toaster } from "svelte-french-toast"; $: navOpen = false;
$: navOpen = false; function logout() {
function logout() { localForage.clear();
localForage.clear(); location.replace("/");
location.replace("/"); }
} </script>
</script>
<style>
<section class="min-h-screen bg-gray-50"> .collapsed_navigation {
<div transform: translateX(-100%);
class:collapsed_navigation={!navOpen} }
style="z-index:11;" @media (min-width: 768px) {
class="select-none fixed top-0 left-0 h-full pb-10 overflow-x-hidden overflow-y-auto transition origin-left transform border-r w-60 bg-gray-50" .collapsed_navigation {
> transform: translateX(0px);
<a href="/" class="flex items-center px-4 py-5"> }
<img src="/lfk-logo.png" alt="Logo" class="h-10" /> }
<h3 class="text-lg font-bold">LfK!Admin</h3> </style>
</a>
<nav class="text-sm font-medium text-gray-600" aria-label="Main Navigation"> <section class="min-h-screen bg-gray-50">
<a <div
class:activenav={$router.path === "/"} class:collapsed_navigation={!navOpen}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" class="select-none fixed top-0 left-0 z-20 h-full pb-10 overflow-x-hidden overflow-y-auto transition origin-left transform border-r w-60 bg-gray-50">
href="/" <a href="/" class="flex items-center px-4 py-5">
> <img src="/lfk-logo.png" alt="Logo" class="h-10" />
<svg <h3 class="text-lg">Lauf für Kaya! Admin</h3>
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" </a>
xmlns="http://www.w3.org/2000/svg" <nav class="text-sm font-medium text-gray-600" aria-label="Main Navigation">
viewBox="0 0 20 20" <a
fill="currentColor" class:bg-gray-100={$router.path === '/'}
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<path href="/">
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" <svg
/> class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
</svg> xmlns="http://www.w3.org/2000/svg"
<span>{$_("dashboard-title")}</span> viewBox="0 0 20 20"
</a> fill="currentColor">
<h2 class="px-4 py-2 text-xs font-semibold text-gray-600 uppercase"> <path
{$_("quick-tools")} d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</h2> </svg>
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET") && store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} <span>{$_('dashboard-title')}</span>
<a </a>
class:activenav={$router.path.includes("/tools/cardassignment/")} {#if store.state.jwtinfo.userdetails.permissions.includes('ORGANIZATION:GET')}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <a
href="/tools/cardassignment/" class:bg-gray-100={$router.path.includes('/orgs/')}
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<svg href="/orgs/">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 24 24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"
<path width="24"
fill-rule="evenodd" height="24"><path fill="none" d="M0 0h24v24H0z" />
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" <path
clip-rule="evenodd" d="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z" /></svg>
/> <span>{$_('orgs')}</span>
</svg> </a>
{/if}
<span>{$_("card_assignment_menu")}</span> {#if store.state.jwtinfo.userdetails.permissions.includes('USER:GET')}
</a> <a
<a class:bg-gray-100={$router.path === '/users/'}
class:activenav={$router.path.includes("/tools/cardreplacement/")} class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" href="/users/">
href="/tools/cardreplacement/" <svg
> xmlns="http://www.w3.org/2000/svg"
<svg width="24"
xmlns="http://www.w3.org/2000/svg" height="24"
viewBox="0 0 24 24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
> <path
<path d="M12 14v8H4a8 8 0 018-8zm0-1a6 6 0 110-12 6 6 0 010 12zm2.6 5.81a3.51 3.51 0 010-1.62l-1-.57 1-1.74 1 .58a3.5 3.5 0 011.4-.82V13.5h2v1.15a3.5 3.5 0 011.4.8l1-.57 1 1.74-1 .57a3.51 3.51 0 010 1.62l1 .57-1 1.74-1-.58a3.5 3.5 0 01-1.4.82v1.14h-2v-1.15a3.5 3.5 0 01-1.4-.8l-1 .57-1-1.74 1-.57zM18 17a1 1 0 100 2 1 1 0 000-2z" /></svg>
fill-rule="evenodd" <span>{$_('users')}</span>
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" </a>
clip-rule="evenodd" {/if}
/> {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:GET')}
</svg> <a
class:bg-gray-100={$router.path === '/groups/'}
<span>{$_("card-replacement-menu")}</span> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
</a> href="/groups/">
{/if} <svg
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:CREATE")} class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
<a fill="currentColor"
class:activenav={$router.path.includes("/tools/scanclient/")} width="24"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" height="24"
href="/tools/scanclient/" xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 640 512"><path
<svg fill="currentColor"
xmlns="http://www.w3.org/2000/svg" d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></svg>
viewBox="0 0 24 24" <span>{$_('user-groups')}</span>
fill="currentColor" </a>
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" {/if}
> {#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:GET')}
<path <a
fill-rule="evenodd" class:bg-gray-100={$router.path === '/runners/'}
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
clip-rule="evenodd" href="/runners/">
/> <svg
</svg> xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
<span>{$_("scanclient")}</span> class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
</a> fill="currentColor"
{/if} width="24"
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")} height="24"><path fill="none" d="M0 0h24v24H0z" />
<a <path
class:activenav={$router.path.includes("/tools/donationcreate/")} d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" /></svg>
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <span>{$_('runners')}</span>
href="/tools/donationcreate/" </a>
> {/if}
<svg {#if store.state.jwtinfo.userdetails.permissions.includes('TEAM:GET')}
xmlns="http://www.w3.org/2000/svg" <a
viewBox="0 0 24 24" class:bg-gray-100={$router.path === '/teams/'}
fill="currentColor" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" href="/teams/">
> <svg
<path class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
fill-rule="evenodd" fill="currentColor"
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" width="24"
clip-rule="evenodd" height="24"
/> xmlns="http://www.w3.org/2000/svg"
</svg> viewBox="0 0 640 512"><path
fill="currentColor"
<span>{$_("donation-quick-add")}</span> d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></svg>
</a> <span>{$_('teams')}</span>
{/if} </a>
<h2 class="px-4 py-2 text-xs font-semibold text-gray-600 uppercase"> {/if}
{$_("management")} {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:GET')}
</h2> <a
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET")} class:bg-gray-100={$router.path.includes('/donors/')}
<a class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
class:activenav={$router.path.includes("/runners/")} href="/donors/">
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <svg
href="/runners/" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
> fill="currentColor"
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
viewBox="0 0 24 24" width="24"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" height="24"><path fill="none" d="M0 0h24v24H0z" />
fill="currentColor" <path
width="24" d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" /></svg>
height="24" <span>{$_('donors')}</span>
><path fill="none" d="M0 0h24v24H0z" /> </a>
<path {/if}
d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:GET')}
/></svg <a
> class:bg-gray-100={$router.path.includes('/donations/')}
<span>{$_("runners")}</span> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
</a> href="/donations/">
{/if} <svg
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
<a fill="currentColor"
class:activenav={$router.path.includes("/teams/")} xmlns="http://www.w3.org/2000/svg"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" viewBox="0 0 24 24"
href="/teams/" width="24"
> height="24"><path fill="none" d="M0 0h24v24H0z" />
<svg <path
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" /></svg>
fill="currentColor" <span>{$_('donations')}</span>
width="24" </a>
height="24" {/if}
xmlns="http://www.w3.org/2000/svg" {#if store.state.jwtinfo.userdetails.permissions.includes('TRACK:GET')}
viewBox="0 0 640 512" <a
><path class:bg-gray-100={$router.path === '/tracks/'}
fill="currentColor" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" href="/tracks/">
/></svg <svg
> class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
<span>{$_("teams")}</span> fill="currentColor"
</a> width="24"
{/if} height="24"
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")} xmlns="http://www.w3.org/2000/svg"
<a viewBox="0 0 640 512"><path
class:activenav={$router.path.includes("/orgs/")} fill="currentColor"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" d="M635.7 167.2L556.1 31.7c-8.8-15-28.3-20.1-43.5-11.5l-69 39.1L503.3 161c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L416 75l-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L333.2 122 278 153.3 337.8 255c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-59.7-101.7-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-27.9-47.5-55.2 31.3 59.7 101.7c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L84.9 262.9l-69 39.1C.7 310.7-4.6 329.8 4.2 344.8l79.6 135.6c8.8 15 28.3 20.1 43.5 11.5L624.1 210c15.2-8.6 20.4-27.8 11.6-42.8z" /></svg>
href="/orgs/" <span>{$_('tracks')}</span>
> </a>
<svg {/if}
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" {#if store.state.jwtinfo.userdetails.permissions.includes('CARD:GET')}
fill="currentColor" <a
xmlns="http://www.w3.org/2000/svg" class:bg-gray-100={$router.path === '/cards/'}
viewBox="0 0 24 24" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
width="24" href="/cards/">
height="24" <svg
><path fill="none" d="M0 0h24v24H0z" /> class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
<path fill="currentColor"
d="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z" width="24"
/></svg height="24"
> xmlns="http://www.w3.org/2000/svg"
<span>{$_("orgs")}</span> viewBox="0 0 24 24">
</a> <path fill="none" d="M0 0h24v24H0z" />
{/if} <path
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")} fill="currentColor"
<a d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" /></svg>
class:activenav={$router.path.includes("/donors/")} <span>{$_('cards')}</span>
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" </a>
href="/donors/" {/if}
> {#if store.state.jwtinfo.userdetails.permissions.includes('SCAN:GET')}
<svg <a
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" class:bg-gray-100={$router.path === '/scans/'}
fill="currentColor" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
xmlns="http://www.w3.org/2000/svg" href="/scans/">
viewBox="0 0 24 24" <svg
width="24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
height="24" fill="currentColor"
><path fill="none" d="M0 0h24v24H0z" /> width="24"
<path height="24"
d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" xmlns="http://www.w3.org/2000/svg"
/></svg viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
> <path
<span>{$_("donors")}</span> fill="currentColor"
</a> d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" /></svg>
{/if} <span>Scans</span>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")} </a>
<a {/if}
class:activenav={$router.path.includes("/donations/")} {#if store.state.jwtinfo.userdetails.permissions.includes('CONTACT:GET')}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <a
href="/donations/" class:bg-gray-100={$router.path === '/contacts/'}
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<svg href="/contacts/">
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" <svg
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
width="24" viewBox="0 0 24 24"
height="24" width="24"
><path fill="none" d="M0 0h24v24H0z" /> height="24"><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" /></svg>
/></svg <span>{$_('contacts')}</span>
> </a>
<span>{$_("donations")}</span> {/if}
</a> {#if store.state.jwtinfo.userdetails.permissions.includes('STATION:GET')}
{/if} <a
{#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")} class:bg-gray-100={$router.path === '/scanstations/'}
<a class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
class:activenav={$router.path === "/tracks/"} href="/scanstations/">
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <svg
href="/tracks/" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
> fill="currentColor"
<svg width="24"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" height="24"
fill="currentColor" viewBox="0 0 24 24"
width="24" xmlns="http://www.w3.org/2000/svg"><path
height="24" fill="none"
xmlns="http://www.w3.org/2000/svg" d="M0 0h24v24H0z" />
viewBox="0 0 640 512" <path
><path fill="currentColor"
fill="currentColor" d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" /></svg>
d="M635.7 167.2L556.1 31.7c-8.8-15-28.3-20.1-43.5-11.5l-69 39.1L503.3 161c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L416 75l-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L333.2 122 278 153.3 337.8 255c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-59.7-101.7-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-27.9-47.5-55.2 31.3 59.7 101.7c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L84.9 262.9l-69 39.1C.7 310.7-4.6 329.8 4.2 344.8l79.6 135.6c8.8 15 28.3 20.1 43.5 11.5L624.1 210c15.2-8.6 20.4-27.8 11.6-42.8z" <span>{$_('scanstations')}</span>
/></svg </a>
> {/if}
<span>{$_("tracks")}</span> {#if store.state.jwtinfo.userdetails.permissions.includes('STATSCLIENT:GET')}
</a> <a
{/if} class:bg-gray-100={$router.path === '/statsclients/'}
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<a href="/statsclients/">
class:activenav={$router.path === "/cards/"} <svg
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
href="/cards/" fill="currentColor"
> width="24"
<svg height="24"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" viewBox="0 0 24 24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path
width="24" fill="none"
height="24" d="M0 0h24v24H0z" />
xmlns="http://www.w3.org/2000/svg" <path
viewBox="0 0 24 24" fill="currentColor"
> d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" /></svg>
<path fill="none" d="M0 0h24v24H0z" /> <span>{$_('statsclients')}</span>
<path </a>
fill="currentColor" {/if}
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" <a
/></svg class:bg-gray-100={$router.path === '/settings/'}
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<span>{$_("cards")}</span> href="/settings/">
</a> <svg
{/if} class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")} xmlns="http://www.w3.org/2000/svg"
<a viewBox="0 0 20 20"
class:activenav={$router.path.includes("/scans/")} fill="currentColor">
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <path
href="/scans/" fill-rule="evenodd"
> d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
<svg clip-rule="evenodd" />
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" </svg>
fill="currentColor" <span>{$_('settings')}</span>
width="24" </a>
height="24" <a
xmlns="http://www.w3.org/2000/svg" class:bg-gray-100={$router.path === '/about/'}
viewBox="0 0 24 24" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
><path fill="none" d="M0 0h24v24H0z" /> href="/about/">
<path <svg
fill="currentColor" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" xmlns="http://www.w3.org/2000/svg"
/></svg fill="none"
> stroke="currentColor"
<span>Scans</span> stroke-width="2"
</a> stroke-linecap="round"
{/if} stroke-linejoin="round"
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")} viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
<a <path d="M12 16v-4M12 8h.01" /></svg>
class:activenav={$router.path.includes("/contacts/")} <span>{$_('about')}</span>
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" </a>
href="/contacts/" <span
> tabindex="0"
<svg class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
fill="currentColor" on:click={() => {
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" AuthService.authControllerLogout();
xmlns="http://www.w3.org/2000/svg" logout();
viewBox="0 0 24 24" }}>
width="24" <svg
height="24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
><path fill="none" d="M0 0h24v24H0z" /> fill="currentColor"
<path width="24"
d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" height="24"
/></svg xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
<span>{$_("contacts")}</span> <path
</a> d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22zm7-6v-3h-8v-2h8V8l5 4-5 4z" /></svg>
{/if} <span>{$_('logout')}</span>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")} </span>
<a </nav>
class:activenav={$router.path.includes("/scanstations/")} </div>
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <div class="ml-0 transition md:ml-60">
href="/scanstations/" <header
> class="flex items-center justify-between w-full px-4 bg-white border-b h-14 md:hidden">
<svg <button
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" on:click={() => {
fill="currentColor" navOpen = true;
width="24" }}
height="24" class="block btn btn-light md:hidden">
viewBox="0 0 24 24" <span class="sr-only">Menu</span><svg
xmlns="http://www.w3.org/2000/svg" class="w-4 h-4"
><path fill="none" d="M0 0h24v24H0z" /> xmlns="http://www.w3.org/2000/svg"
<path viewBox="0 0 20 20"
fill="currentColor" fill="currentcolor"><path
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" fill-rule="evenodd"
/></svg d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4A1 1 0 013 5zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
> clip-rule="evenodd" /></svg></button>
<span>{$_("scanstations")}</span> </header>
</a> <slot>
{/if} <NoComponentLoaded />
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")} </slot>
<a </div>
class:activenav={$router.path.includes("/statsclients/")} {#if navOpen === true}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" <div
href="/statsclients/" on:click={() => {
> navOpen = false;
<svg console.log({ navOpen });
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" }}
fill="currentColor" class:hidden={!navOpen}
width="24" class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden" />
height="24" {/if}
viewBox="0 0 24 24" </section>
xmlns="http://www.w3.org/2000/svg"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z"
/></svg
>
<span>{$_("statsclients")}</span>
</a>
{/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")}
<a
class:activenav={$router.path.includes("/users/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
href="/users/"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor"
viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 14v8H4a8 8 0 018-8zm0-1a6 6 0 110-12 6 6 0 010 12zm2.6 5.81a3.51 3.51 0 010-1.62l-1-.57 1-1.74 1 .58a3.5 3.5 0 011.4-.82V13.5h2v1.15a3.5 3.5 0 011.4.8l1-.57 1 1.74-1 .57a3.51 3.51 0 010 1.62l1 .57-1 1.74-1-.58a3.5 3.5 0 01-1.4.82v1.14h-2v-1.15a3.5 3.5 0 01-1.4-.8l-1 .57-1-1.74 1-.57zM18 17a1 1 0 100 2 1 1 0 000-2z"
/></svg
>
<span>{$_("users")}</span>
</a>
{/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")}
<a
class:activenav={$router.path.includes("/groups/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
href="/groups/"
>
<svg
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor"
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
><path
fill="currentColor"
d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z"
/></svg
>
<span>{$_("user-groups")}</span>
</a>
{/if}
<h2 class="px-4 py-2 text-xs font-semibold text-gray-600 uppercase">
{$_("system")}
</h2>
<a
class:activenav={$router.path === "/settings/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
href="/settings/"
>
<svg
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
clip-rule="evenodd"
/>
</svg>
<span>{$_("settings")}</span>
</a>
<a
class:activenav={$router.path === "/about/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
href="/about/"
>
<svg
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
><circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" /></svg
>
<span>{$_("about")}</span>
</a>
<button
tabindex="0"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold"
on:click={() => {
AuthService.authControllerLogout();
logout();
}}
>
<svg
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor"
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2a9.985 9.985 0 0 1 8 4h-2.71a8 8 0 1 0 .001 12h2.71A9.985 9.985 0 0 1 12 22zm7-6v-3h-8v-2h8V8l5 4-5 4z"
/></svg
>
<span>{$_("logout")}</span>
</button>
</nav>
</div>
<div class="ml-0 transition md:ml-60">
<header
class="flex items-center w-full px-4 bg-white border-b h-14 md:hidden"
>
<button
on:click={() => {
navOpen = true;
}}
class="block btn btn-light md:hidden"
>
<span class="sr-only">Menu</span><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
<span class="inline-block">
<img src="/lfk-logo.png" alt="Logo" class="h-8 inline-block" />
<span class="text-lg font-bold">LfK!Admin</span>
</span>
</header>
<Toaster position="top-right" />
<slot>
<NoComponentLoaded />
</slot>
</div>
{#if navOpen === true}
<button
on:click={() => {
navOpen = false;
}}
class:hidden={!navOpen}
class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden"
/>
{/if}
</section>
<style>
.collapsed_navigation {
transform: translateX(-100%);
}
@media (min-width: 768px) {
.collapsed_navigation {
transform: translateX(0px);
}
}
</style>

View File

@ -1,263 +1,157 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { StatsService } from "@odit/lfk-client-js"; import { StatsService } from "@odit/lfk-client-js";
import store from "../../store"; import StatCards from "./StatCard.svelte";
import StatCard from "./StatCard.svelte"; import store from "../../store";
const stats_promise = StatsService.statsControllerGet(); import StatCard from "./StatCard.svelte";
let navOpen = false;
const stats_promise = StatsService.statsControllerGet();
</script> </script>
<div class="p-2 md:p-5 overflow-x-hidden"> <div
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> class="p-5 overflow-x-hidden"
{$_("dashboard-greeting")} on:click={() => {
<span class="text-blue-500" navOpen = false;
>{store.state.jwtinfo.userdetails.firstname} }}
{store.state.jwtinfo.userdetails.lastname}</span >
> <h1 class="text-3xl leading-tight">
</h4> <span class="font-extrabold">{$_("dashboard-title")}</span>
{#await stats_promise} <span>
<div -
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" {$_("dashboard-greeting")},
role="alert" <span class="text-blue-500"
> >{store.state.jwtinfo.userdetails.firstname}
<p class="font-bold">{$_("stats-are-being-loaded")}</p> {store.state.jwtinfo.userdetails.lastname}</span
<p class="text-sm">{$_("this-might-take-a-moment")}</p> ></span
</div> >
{:then stats} </h1>
<div <h1>{$_("general-stats")}</h1>
class="grid gap-1 grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6 sm:gap-4" {#await stats_promise}
> <div
<StatCard class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
title={$_("runners")} role="alert"
value={stats.total_runners} >
href="/runners/" <p class="font-bold">{$_("stats-are-being-loaded")}</p>
> <p class="text-sm">{$_("this-might-take-a-moment")}</p>
<svg </div>
height="24" {:then stats}
width="24" <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
fill="currentColor" <StatCard
xmlns="http://www.w3.org/2000/svg" title={$_("runners")}
viewBox="0 0 24 24" value={stats.total_runners}
><path d="M0 0h24v24H0z" fill="none" /> href="/runners/"
<path >
d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-3.6 13.9l1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2 7 1.4z" <svg
/></svg height="24"
> width="24"
</StatCard> fill="currentColor"
<StatCard xmlns="http://www.w3.org/2000/svg"
title={$_("total-scans")} viewBox="0 0 24 24"
value={stats.total_scans} ><path d="M0 0h24v24H0z" fill="none" />
href="/scans/" <path
> d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-3.6 13.9l1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2 7 1.4z"
<svg /></svg
fill="currentColor" >
width="24" </StatCard>
height="24" <StatCard
xmlns="http://www.w3.org/2000/svg" title={$_("total-scans")}
viewBox="0 0 24 24" value={stats.total_scans}
><path fill="none" d="M0 0h24v24H0z" /> href="/scans/"
<path >
fill="currentColor" <svg
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" stroke="currentColor"
/></svg fill="currentColor"
> stroke-width="2"
</StatCard> viewBox="0 0 24 24"
<StatCard stroke-linecap="round"
title={$_("total-donors")} stroke-linejoin="round"
value={stats.total_donors} size="24"
href="/donors/" class="stroke-current text-grey-500"
> height="24"
<svg width="24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" ><polyline points="22 12 18 12 15 21 9 3 6 12 2 12" /></svg
viewBox="0 0 24 24" >
width="24" </StatCard>
height="24" <StatCard
><path fill="none" d="M0 0h24v24H0z" /> title={$_("total-donations")}
<path value={`${(stats.total_donation / 100).toFixed(2)} €`}
d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" href="/donations/"
/></svg >
> <svg
</StatCard> xmlns="http://www.w3.org/2000/svg"
<StatCard height="24"
title={$_("total-donation-count")} fill="currentColor"
value={stats.total_donations} width="24"
href="/donations/" ><path d="M0 0h24v24H0z" fill="none" />
> <path
<svg d="M15 18.5A6.48 6.48 0 019.24 15H15v-2H8.58c-.05-.33-.08-.66-.08-1s.03-.67.08-1H15V9H9.24A6.491 6.491 0 0115 5.5c1.61 0 3.09.59 4.23 1.57L21 5.3A8.955 8.955 0 0015 3c-3.92 0-7.24 2.51-8.48 6H3v2h3.06a8.262 8.262 0 000 2H3v2h3.52c1.24 3.49 4.56 6 8.48 6 2.31 0 4.41-.87 6-2.3l-1.78-1.77c-1.13.98-2.6 1.57-4.22 1.57z"
fill="currentColor" /></svg
xmlns="http://www.w3.org/2000/svg" >
viewBox="0 0 24 24" </StatCard>
width="24" <StatCard
height="24" title={$_("total-distance")}
><path fill="none" d="M0 0h24v24H0z" /> value={`${stats.total_distance / 1000} km`}
<path href="#"
d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" >
/></svg <svg
> fill="currentColor"
</StatCard> xmlns="http://www.w3.org/2000/svg"
<StatCard height="24"
title={$_("average-donation")} width="24"
value={`${parseFloat(stats.average_donation / 100).toLocaleString( ><path d="M0 0h24v24H0z" fill="none" />
undefined, <path
{ d="M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H3V8h2v4h2V8h2v4h2V8h2v4h2V8h2v4h2V8h2v8z"
minimumFractionDigits: 2, /></svg
maximumFractionDigits: 2, >
} </StatCard>
)}`} <StatCard
href="/donations/" title={$_("count_teams")}
> value={stats.total_teams}
<svg href="/teams/"
xmlns="http://www.w3.org/2000/svg" >
height="24" <svg
fill="currentColor" stroke="currentColor"
width="24" fill="none"
><path d="M0 0h24v24H0z" fill="none" /> stroke-width="2"
<path viewBox="0 0 24 24"
d="M15 18.5A6.48 6.48 0 019.24 15H15v-2H8.58c-.05-.33-.08-.66-.08-1s.03-.67.08-1H15V9H9.24A6.491 6.491 0 0115 5.5c1.61 0 3.09.59 4.23 1.57L21 5.3A8.955 8.955 0 0015 3c-3.92 0-7.24 2.51-8.48 6H3v2h3.06a8.262 8.262 0 000 2H3v2h3.52c1.24 3.49 4.56 6 8.48 6 2.31 0 4.41-.87 6-2.3l-1.78-1.77c-1.13.98-2.6 1.57-4.22 1.57z" stroke-linecap="round"
/></svg stroke-linejoin="round"
> size="24"
</StatCard> class="stroke-current text-grey-500"
<StatCard height="24"
title={$_("total-donations")} width="24"
value={`${parseFloat(stats.total_donation / 100).toLocaleString( xmlns="http://www.w3.org/2000/svg"
undefined, ><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
{ <circle cx="9" cy="7" r="4" />
minimumFractionDigits: 2, <path d="M23 21v-2a4 4 0 0 0-3-3.87" />
maximumFractionDigits: 2, <path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg
} >
)}`} </StatCard>
href="/donations/" <StatCard
> title={$_("count_organizations")}
<svg value={stats.total_orgs}
xmlns="http://www.w3.org/2000/svg" href="/orgs/"
height="24" >
fill="currentColor" <svg
width="24" height="24"
><path d="M0 0h24v24H0z" fill="none" /> fill="currentColor"
<path width="24"
d="M15 18.5A6.48 6.48 0 019.24 15H15v-2H8.58c-.05-.33-.08-.66-.08-1s.03-.67.08-1H15V9H9.24A6.491 6.491 0 0115 5.5c1.61 0 3.09.59 4.23 1.57L21 5.3A8.955 8.955 0 0015 3c-3.92 0-7.24 2.51-8.48 6H3v2h3.06a8.262 8.262 0 000 2H3v2h3.52c1.24 3.49 4.56 6 8.48 6 2.31 0 4.41-.87 6-2.3l-1.78-1.77c-1.13.98-2.6 1.57-4.22 1.57z" xmlns="http://www.w3.org/2000/svg"
/></svg viewBox="0 0 24 24"
> ><path fill="none" d="M0 0h24v24H0z" />
</StatCard> <path
<StatCard d="M17 11V3H7v4H3v14h8v-4h2v4h8V11h-4zM7 19H5v-2h2v2zm0-4H5v-2h2v2zm0-4H5V9h2v2zm4 4H9v-2h2v2zm0-4H9V9h2v2zm0-4H9V5h2v2zm4 8h-2v-2h2v2zm0-4h-2V9h2v2zm0-4h-2V5h2v2zm4 12h-2v-2h2v2zm0-4h-2v-2h2v2z"
title={$_("total-distance")} /></svg
value={`${stats.total_distance / 1000}km`} >
href="/scans/" </StatCard>
> </div>
<svg {:catch error}
fill="currentColor" <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
xmlns="http://www.w3.org/2000/svg" <span class="inline-block align-middle mr-8">
height="24" <b class="capitalize">{$_("general_promise_error")}</b>
width="24" {error}
><path d="M0 0h24v24H0z" fill="none" /> </span>
<path </div>
d="M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H3V8h2v4h2V8h2v4h2V8h2v4h2V8h2v4h2V8h2v8z" {/await}
/></svg
>
</StatCard>
<StatCard
title={$_("average-distance")}
value={`${parseFloat(stats.average_distance / 1000).toLocaleString(
undefined,
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}
)}km`}
href="/scans/"
>
<svg
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
height="24"
width="24"
><path d="M0 0h24v24H0z" fill="none" />
<path
d="M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H3V8h2v4h2V8h2v4h2V8h2v4h2V8h2v4h2V8h2v8z"
/></svg
>
</StatCard>
<StatCard
title={$_("count_teams")}
value={stats.total_teams}
href="/teams/"
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
size="24"
class="stroke-current text-grey-500"
height="24"
width="24"
xmlns="http://www.w3.org/2000/svg"
><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg
>
</StatCard>
<StatCard
title={$_("count_organizations")}
value={stats.total_orgs}
href="/orgs/"
>
<svg
height="24"
fill="currentColor"
width="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M17 11V3H7v4H3v14h8v-4h2v4h8V11h-4zM7 19H5v-2h2v2zm0-4H5v-2h2v2zm0-4H5V9h2v2zm4 4H9v-2h2v2zm0-4H9V9h2v2zm0-4H9V5h2v2zm4 8h-2v-2h2v2zm0-4h-2V9h2v2zm0-4h-2V5h2v2zm4 12h-2v-2h2v2zm0-4h-2v-2h2v2z"
/></svg
>
</StatCard>
<StatCard
title={$_("runner_via_selfservice")}
value={stats.runnersViaSelfservice}
href="/runners/?created_via=selfservice"
>
<svg
height="24"
width="24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path d="M0 0h24v24H0z" fill="none" />
<path
d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-3.6 13.9l1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2 7 1.4z"
/></svg
>
</StatCard>
<StatCard
title={$_('runners_via_kiosk')}
value={stats.runnersViaKiosk}
href="/runners/?created_via=kiosk"
>
<svg
height="24"
width="24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
><path d="M0 0h24v24H0z" fill="none" />
<path
d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-3.6 13.9l1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2 7 1.4z"
/></svg
>
</StatCard>
</div>
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b>{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
</div> </div>

View File

@ -1,21 +1,22 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
export let href = "#"; export let href = "#"
export let title = ""; export let title = "";
export let value = ""; export let value = "";
</script> </script>
<a {href}> <a href={href}>
<div class="p-3 py-4 sm:p-4 rounded-lg bg-white border border-grey-100"> <div
class="p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-md sm:text-xs uppercase font-normal text-grey-500"> <div class="text-xs uppercase font-light text-grey-500">
{title} {title}
</div> </div>
<div class="text-2xl sm:text-xl font-bold font-mono">{value}</div> <div class="text-xl font-bold">{value}</div>
</div> </div>
<slot /> <slot></slot>
</div> </div>
</div> </div>
</a> </a>

View File

@ -1,380 +1,324 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { import {
DonationService, DonationService,
DonorService, DonorService,
RunnerService, RunnerService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import Select from "svelte-select"; import Select from "svelte-select";
import { createEventDispatcher, onMount } from "svelte"; import Toastify from "toastify-js";
import toast from "svelte-french-toast"; import { is_promise } from "svelte/internal";
export let modal_open; export let modal_open;
const dispatch = createEventDispatcher(); export let current_donations;
const getDonorLabel = (option) => const getDonorLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname; option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const filterDonors = (label, filterText, option) => const filterDonors = (label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id.toString().startsWith(filterText.toLowerCase()); option.value.id.toString().startsWith(filterText.toLowerCase());
$: donor = 0; function focus(el) {
$: runner = 0; el.focus();
$: donors = []; }
$: runners = []; $: donor = 0;
$: type = "distance"; $: runner = 0;
$: is_paid = false; $: donors = [];
$: amount_input = 0; $: runners = [];
$: processed_last_submit = true; $: is_fixed = false;
$: is_amount_valid = amount_input > 0; $: is_paid = false;
$: createbtnenabled = is_amount_valid; DonorService.donorControllerGetAll().then((val) => {
(() => { donors = val.map((r) => {
document.onkeydown = (e) => { return { label: getDonorLabel(r), value: r };
e = e || window.event; });
if (e.key === "Escape") { });
modal_open = false; RunnerService.runnerControllerGetAll().then((val) => {
} runners = val.map((r) => {
if (e.keyCode === 13) { return { label: getDonorLabel(r), value: r };
if (createbtnenabled === true) { });
createbtnenabled = false; });
submit(); $: amount_input = 0;
} $: processed_last_submit = true;
} $: is_amount_valid = amount_input > 0;
}; $: createbtnenabled = is_amount_valid;
})(); (() => {
function submit() { document.onkeydown = (e) => {
if (processed_last_submit === true) { e = e || window.event;
let amount_cent = Math.floor(amount_input * 100); if (e.key === "Escape") {
processed_last_submit = false; modal_open = false;
toast.loading($_("adding-donation")); }
if (type === "fixed") { if (e.keyCode === 13) {
let postdata = { if (createbtnenabled === true) {
donor, createbtnenabled = false;
amount: amount_cent, submit();
paidAmount: 0, }
}; }
if (is_paid) { };
postdata.paidAmount = amount_cent; })();
} function submit() {
DonationService.donationControllerPostFixed(postdata) if (processed_last_submit === true) {
.then((result) => { let amount_cent = Math.floor(amount_input * 100);
donor = donors[0].id || 0; processed_last_submit = false;
runner = runners[0].id || 0; const toast = Toastify({
amount_input = 0; text: $_('adding-donation'),
modal_open = false; duration: -1,
// }).showToast();
toast.dismiss(); if (is_fixed) {
toast.success($_("donation_added")); let postdata = {
dispatch("created", { donations: [result] }); donor,
}) amount: amount_cent,
.catch((err) => { paidAmount: 0
// };
}) if(is_paid){
.finally(() => { postdata.paidAmount = amount_cent;
processed_last_submit = true; }
}); DonationService.donationControllerPostFixed(postdata)
} else if (type === "anonymous") { .then((result) => {
let postdata = { donor = donors[0].id || 0;
amount: amount_cent, runner = runners[0].id || 0;
}; amount_input = 0;
DonationService.donationControllerPostAnonymous(postdata) modal_open = false;
.then((result) => { //
amount_input = 0; Toastify({
modal_open = false; text: $_('donation_added'),
// duration: 500,
toast.dismiss(); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
toast.success($_("donation_added")); }).showToast();
dispatch("created", { donations: [result] }); current_donations.push(result);
}) current_donations = current_donations;
.catch((err) => { })
// .catch((err) => {
}) //
.finally(() => { })
processed_last_submit = true; .finally(() => {
}); processed_last_submit = true;
} else if (type === "distance") { //
let postdata = { toast.hideToast();
donor, });
runner, } else {
amountPerDistance: amount_cent, let postdata = {
}; donor,
DonationService.donationControllerPostDistance(postdata) runner,
.then((result) => { amountPerDistance: amount_cent,
donor = donors[0].id || 0; };
runner = runners[0].id || 0; DonationService.donationControllerPostDistance(postdata)
amount_input = 0; .then((result) => {
modal_open = false; donor = donors[0].id || 0;
// runner = runners[0].id || 0;
toast.dismiss(); amount_input = 0;
toast.success($_("donation_added")); modal_open = false;
dispatch("created", { donations: [result] }); //
}) Toastify({
.catch((err) => { text: $_('donation_added'),
// duration: 500,
}) backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
.finally(() => { }).showToast();
processed_last_submit = true; current_donations.push(result);
}); current_donations = current_donations;
} })
} .catch((err) => {
} //
})
onMount(async () => { .finally(() => {
donors = (await DonorService.donorControllerGetAll()).map((r) => { processed_last_submit = true;
return { label: getDonorLabel(r), value: r }; //
}); toast.hideToast();
runners = (await RunnerService.runnerControllerGetAll()).map((r) => { });
return { label: getDonorLabel(r), value: r }; }
}); }
}); }
</script> </script>
{#if modal_open} <style>
<div .toggle:before {
class="fixed z-10 inset-0 overflow-y-hidden" content: "";
use:clickOutside position: absolute;
on:click_outside={() => { width: 1.25rem;
modal_open = false; height: 1.25rem;
}} border-radius: 50%;
> top: 0;
<div left: 0;
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" transform: scale(1.1);
> box-shadow: 0 0.125rem 0.5rem rgba(0, 0, 0, 0.2);
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> background-color: white;
<div transition: 0.2s ease-in-out;
class="absolute inset-0 bg-neutral-500 opacity-75" }
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="size-6 text-blue-600"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z"
/></svg
>
</div>
<div class="mt-3">
<h3 class="text-xl leading-6 font-medium text-neutral-900">
{$_("add-donation")}
</h3>
<nav
class="relative z-0 flex border border-neutral-200 rounded-xl overflow-hidden mb-2"
>
<button
on:click={() => {
type = "distance";
}}
type="button"
id="bar-with-underline-item-1"
class:donation_active_tab={type === "distance"}
class:donation_inactive_tab={type !== "distance"}
aria-selected={type === "distance"}
role="tab"
>
{$_("spende_pro_km")}
</button>
<button
on:click={() => {
type = "fixed";
}}
type="button"
id="bar-with-underline-item-2"
class:donation_active_tab={type === "fixed"}
class:donation_inactive_tab={type !== "fixed"}
aria-selected={type === "fixed"}
role="tab"
>
{$_("festbetrag")}
</button>
<button
on:click={() => {
type = "anonymous";
}}
type="button"
id="bar-with-underline-item-3"
class:donation_active_tab={type === "anonymous"}
class:donation_inactive_tab={type !== "anonymous"}
aria-selected={type === "anonymous"}
role="tab"
>
{$_("anonyme_spende")}
</button>
</nav>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> .toggle:checked {
{#if type === "anonymous"} /* @apply: bg-indigo-400; */
<div class="col-span-6"> background-color: #7f9cf5;
<label }
for="donation_amount_eur"
class="block text-sm font-medium text-neutral-900" .toggle:checked:before {
> left: 1.25rem;
{$_("donation-amount")}</label }
> </style>
<div class="mt-1 flex rounded-md shadow-sm">
<input {#if modal_open}
autocomplete="off" <div
class:border-red-500={!is_amount_valid} class="fixed z-10 inset-0 overflow-y-auto"
class:focus:border-red-500={!is_amount_valid} use:focusTrap
class:focus:ring-red-500={!is_amount_valid} use:clickOutside
bind:value={amount_input} on:click_outside={() => {
type="number" modal_open = false;
step="0.01" }}>
name="donation_amount_eur" <div
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 p-2" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
placeholder="2.00" <div class="fixed inset-0 transition-opacity" aria-hidden="true">
/> <div
<span class="absolute inset-0 bg-gray-500 opacity-75"
class="inline-flex items-center px-3 rounded-r-md border border-neutral-300 bg-neutral-50 text-neutral-500 text-sm" data-id="modal_backdrop" />
>€</span </div>
> <span
</div> class="hidden sm:inline-block sm:align-middle sm:h-screen"
{#if !is_amount_valid} aria-hidden="true">&#8203;</span>
<span <div
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
> role="dialog"
{$_("donation-amount-must-be-greater-that-0-00eur")} aria-modal="true"
</span> aria-labelledby="modal-headline">
{/if} <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
</div> <div class="sm:flex sm:items-start">
{:else} <div
<div class="col-span-6"> class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<label <svg
for="donor" class="h-6 w-6 text-blue-600"
class="block text-sm font-medium text-neutral-900" fill="currentColor"
>{$_("donor")}</label xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"
<Select width="24"
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 rounded-md p-2" height="24"><path fill="none" d="M0 0h24v24H0z" />
itemFilter={(label, filterText, option) => <path
filterDonors(label, filterText, option)} d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" /></svg>
items={donors} </div>
showChevron={true} <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
placeholder={$_("search-for-donor-name-or-id")} <h3 class="text-lg leading-6 font-medium text-gray-900">
noOptionsMessage={$_("no-donors-found")} {#if is_fixed}
on:select={(selectedValue) => {$_('create-a-new-fixed-donation')}
(donor = selectedValue.detail.value.id)} {:else}{$_('create-a-new-distance-donation')}{/if}
on:clear={() => (donors = null)} </h3>
/> <label class="content-center align-middle object-center">
</div> <span
{#if type === "distance"} class="ml-2 text-base"
<div class="col-span-6"> class:text-gray-300={is_fixed}>{$_('distance-donation')}</span>
<label <input
for="donor" class="toggle relative w-10 h-5 transition-all duration-200 ease-in-out bg-gray-400 rounded-full shadow-inner outline-none appearance-none align-middle"
class="block text-sm font-medium text-neutral-900" type="checkbox"
>{$_("runner")}</label bind:checked={is_fixed} />
> <span
<Select class="ml-2 text-base "
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 rounded-md p-2" class:text-gray-300={!is_fixed}>{$_('fixed-donation')}</span>
itemFilter={(label, filterText, option) => </label>
filterDonors(label, filterText, option)} <div class="mt-2 mb-6">
items={runners} <p class="text-sm text-gray-500">
showChevron={true} {$_('please-provide-the-nessecary-information-to-create-a-new-donation')}
placeholder={$_("search-for-runner-by-name-or-id")} </p>
noOptionsMessage={$_("no-runners-found")} </div>
on:select={(selectedValue) => <div class="grid grid-cols-6 gap-6">
(runner = selectedValue.detail.value.id)} <div class="col-span-6">
on:clear={() => (runner = null)} <label
/> for="donor"
</div> class="block text-sm font-medium text-gray-700">{$_('donor')}</label>
{/if} <Select
<div class="col-span-6"> containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
<label itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
for="donation_amount_eur" items={donors}
class="block text-sm font-medium text-neutral-900" showChevron={true}
> placeholder={$_('search-for-donor-name-or-id')}
{#if type === "fixed"} noOptionsMessage={$_('no-donors-found')}
{$_("donation-amount")} on:select={(selectedValue) => (donor = selectedValue.detail.value.id)}
{:else}{$_("amount-per-kilometer")}{/if}</label on:clear={() => (donors = null)} />
> </div>
<div class="mt-1 flex rounded-md shadow-sm"> {#if !is_fixed}
<input <div class="col-span-6">
autocomplete="off" <label
class:border-red-500={!is_amount_valid} for="donor"
class:focus:border-red-500={!is_amount_valid} class="block text-sm font-medium text-gray-700">{$_('runner')}</label>
class:focus:ring-red-500={!is_amount_valid} <Select
bind:value={amount_input} containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
type="number" itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
step="0.01" items={runners}
name="donation_amount_eur" showChevron={true}
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-neutral-300 border bg-neutral-50 text-neutral-800 p-2" placeholder={$_('search-for-runner-by-name-or-id')}
placeholder="2.00" noOptionsMessage={$_('no-runners-found')}
/> on:select={(selectedValue) => (runner = selectedValue.detail.value.id)}
<span on:clear={() => (runner = null)} />
class="inline-flex items-center px-3 rounded-r-md border border-neutral-300 bg-neutral-50 text-neutral-500 text-sm" </div>
>€</span {/if}
> <div class="col-span-6">
</div> <label
{#if !is_amount_valid} for="donation_amount_eur"
<span class="block text-sm font-medium text-gray-700">
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" {#if !is_fixed}
> {$_('amount-per-kilometer')}
{$_("donation-amount-must-be-greater-that-0-00eur")} {:else}{$_('donation-amount')}{/if}</label>
</span> <div class="mt-1 flex rounded-md shadow-sm">
{/if} <input
</div> autocomplete="off"
{/if} class:border-red-500={!is_amount_valid}
{#if type === "fixed"} class:focus:border-red-500={!is_amount_valid}
<div class="flex"> class:focus:ring-red-500={!is_amount_valid}
<input bind:value={amount_input}
bind:checked={is_paid} type="number"
type="checkbox" step="0.01"
class="shrink-0 mt-0.5 border-neutral-200 rounded-sm text-blue-600 focus:ring-blue-500 checked:border-blue-500 disabled:opacity-50 disabled:pointer-events-none" name="donation_amount_eur"
id="hs-default-checkbox" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 p-2"
/> placeholder="2.00" />
<label <span
for="hs-default-checkbox" class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm">€</span>
class="text-base text-neutral-900 ms-2 font-medium" </div>
>{$_("already-paid")}</label {#if !is_amount_valid}
> <span
</div> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{/if} {$_('donation-amount-must-be-greater-that-0-00eur')}
</div> </span>
</div> {/if}
</div> </div>
</div> {#if is_fixed}
<div <div class="col-span-6">
class="bg-neutral-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10" <label
> for="paid"
<button class="block text-sm font-medium text-gray-700">{$_('already-paid')}</label>
disabled={!createbtnenabled} <p class="text-gray-500">
class:opacity-50={!createbtnenabled} <input
on:click={submit} id="paid"
type="button" bind:checked={is_paid}
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" name="paid"
> type="checkbox"
{$_("create")} class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" >
</button> <span class="align-text-bottom">
<button
on:click={() => { {#if is_paid}
modal_open = false; {$_('paid')}
}} {:else}
type="button" {$_('open')}
class="w-full justify-center rounded-md border border-neutral-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-neutral-900 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 hidden lg:block" {/if}
> </span>
{$_("cancel")} </p>
</button> </div>
</div> {/if}
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled}
on:click={submit}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create')}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
{$_('cancel')}
</button>
</div>
</div>
</div>
</div>
{/if} {/if}

View File

@ -1,17 +1,19 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { DonationService } from "@odit/lfk-client-js"; import { DonationService } from "@odit/lfk-client-js";
import { createEventDispatcher } from "svelte"; import Toastify from "toastify-js";
import toast from "svelte-french-toast";
export let payment_modal_open = false; export let payment_modal_open = false;
export let current_donations = [];
export let editable = {};
export let original_data = {}; export let original_data = {};
export let paid_amount_input = 0; export let paid_amount_input = 0;
const dispatch = createEventDispatcher(); $:processed_last_submit=true;
$: processed_last_submit = true; function focus(el) {
$: createbtnenabled = el.focus();
is_paid_amount_valid && }
!(paid_amount_input * 100 == original_data.paidAmount); $: createbtnenabled = is_paid_amount_valid && !(paid_amount_input*100 == original_data.paidAmount)
$: is_paid_amount_valid = paid_amount_input > 0; $: is_paid_amount_valid = paid_amount_input > 0;
(() => { (() => {
document.onkeydown = (e) => { document.onkeydown = (e) => {
@ -30,45 +32,62 @@
function submit() { function submit() {
if (processed_last_submit === true) { if (processed_last_submit === true) {
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("updating-donation")); const toast = Toastify({
const editable = Object.assign({}, original_data); text: $_('updating-donation'),
duration: -1,
}).showToast();
editable.donor = editable.donor.id; editable.donor = editable.donor.id;
editable.paidAmount = Math.round(paid_amount_input * 100); editable.paidAmount = paid_amount_input*100;
if (editable.responseType == "DISTANCEDONATION" || editable.runner) { if(editable.responseType == "DISTANCEDONATION" || editable.runner){
editable.runner = editable.runner.id; editable.runner = editable.runner.id;
DonationService.donationControllerPutDistance( DonationService.donationControllerPutDistance(original_data.id, editable)
original_data.id, .then((result) => {
editable let id = original_data.id;
) editable = {};
.then((result) => { original_data = {};
payment_modal_open = false; payment_modal_open = false;
// //
toast.dismiss(); Toastify({
text: $_('donation-updated'),
toast.success($_("donation-updated")); duration: 500,
dispatch("created", { donation: result }); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}) }).showToast();
.catch((err) => { current_donations[current_donations.findIndex((c) => c.id === id)] = result;
// current_donations = current_donations;
}) })
.finally(() => { .catch((err) => {
processed_last_submit = true; //
}); })
} else { .finally(() => {
DonationService.donationControllerPutFixed(original_data.id, editable) processed_last_submit = true;
.then((result) => { //
payment_modal_open = false; toast.hideToast();
// });
toast.dismiss(); }
toast.success($_("donation-updated")); else{
dispatch("created", { donation: result }); DonationService.donationControllerPutFixed(original_data.id, editable)
}) .then((result) => {
.catch((err) => { let id = original_data.id;
// editable = {};
}) original_data = {};
.finally(() => { payment_modal_open = false;
processed_last_submit = true; //
}); Toastify({
text: $_('donation-updated'),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_donations[current_donations.findIndex((c) => c.id === id)] = result;
current_donations = current_donations;
})
.catch((err) => {
//
})
.finally(() => {
processed_last_submit = true;
//
toast.hideToast();
});
} }
} }
} }
@ -76,72 +95,58 @@
{#if payment_modal_open} {#if payment_modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
payment_modal_open = false; payment_modal_open = false;
}} }}>
>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div <div
class="absolute inset-0 bg-gray-500 opacity-75" class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" data-id="modal_backdrop" />
/>
</div> </div>
<span <span
class="hidden sm:inline-block sm:align-middle sm:h-screen" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span aria-hidden="true">&#8203;</span>
>
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="inline-block align-bottom bg-white rounded-lg text-left shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <div class="sm:flex sm:items-start">
<div class="">
<div <div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
>
<svg <svg
class="size-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"><path fill="none" d="M0 0h24v24H0z" />
><path fill="none" d="M0 0h24v24H0z" />
<path <path
fill="currentColor" fill="currentColor"
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" /></svg>
/></svg
>
</div> </div>
<div class="mt-3 text-left"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("enter-payment")} {$_('enter-payment')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_('you-can-enter-the-donations-paid-amount-manually-or-use-the-max-button-to-use-the-donations-exact-amount')}
"you-can-enter-the-donations-paid-amount-manually-or-use-the-max-button-to-use-the-donations-exact-amount"
)}
</p> </p>
</div> </div>
<div class="grid grid-cols gap-2 lg:gap-6"> <div class="grid grid-cols gap-6">
<div class="w-full"> <div class="w-full">
<label <label
for="token" for="token"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('paid-amount')}</label>
>{$_("paid-amount")}</label <div class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full">
> <input
<div
class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full"
>
<input
autocomplete="off" autocomplete="off"
class:border-red-500={!is_paid_amount_valid} class:border-red-500={!is_paid_amount_valid}
class:focus:border-red-500={!is_paid_amount_valid} class:focus:border-red-500={!is_paid_amount_valid}
@ -151,55 +156,47 @@
step="0.01" step="0.01"
name="donation_amount_eur" name="donation_amount_eur"
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm p-2" class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm p-2"
placeholder="2.00" placeholder="2.00" />
/>
<button <button
on:click={() => { on:click={
paid_amount_input = paid_amount_input = ( ()=>{
original_data.amount / 100 paid_amount_input=paid_amount_input = (original_data.amount/100).toFixed(2);
).toFixed(2); }
}} }
class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm" class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm">MAX</button>
>MAX</button
>
<span <span
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm" class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm">€</span>
>€</span
>
</div> </div>
{#if !is_paid_amount_valid} {#if !is_paid_amount_valid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('payment-amount-must-be-greater-than-0-00eur')}
{$_("payment-amount-must-be-greater-than-0-00eur")}
</span> </span>
{/if} {/if}
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <button
<button disabled={!createbtnenabled}
disabled={!createbtnenabled} class:opacity-50={!createbtnenabled}
class:opacity-50={!createbtnenabled} on:click={submit}
on:click={submit} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" {$_('save-changes')}
> </button>
{$_("save-changes")} <button
</button> on:click={() => {
<button payment_modal_open = false;
on:click={() => { }}
payment_modal_open = false; type="button"
}} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
type="button" {$_('cancel')}
class="cancel_modal_button" </button>
> </div>
{$_("cancel")}
</button>
</div> </div>
</div> </div>
</div> </div>
</div>
{/if} {/if}

View File

@ -1,122 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_donation = {
id: 0,
runner: {
firstname: "",
lastname: "",
},
donor: {
firstname: "",
lastname: "",
},
amount: 0,
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_donation.id });
modal_open = false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="size-6 text-blue-600"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z"
/></svg
>
</div>
<div class="mt-3 sm:text-left max-h-[75vh]">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("please-confirm-the-deletion-of-donation")}
</h3>
<div class="w-full">
<span class="inline-block"
>{#if delete_donation.donor}<b>{$_("donor")}</b>: {delete_donation.donor.firstname}
{delete_donation.donor.lastname}{:else}{$_("anonymer_sponsor")}{/if}
<br>
<b>{$_("amount")}</b>: {`${(delete_donation.amount / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })}€`}</span
>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10">
<button
on:click={submit}
type="button"
class="confirm_deletion_button"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="cancel_modal_button"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,352 +1,341 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import { import {
DonationService, DonationService,
DonorService, DonorService,
RunnerService, RunnerService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
import PromiseError from "../base/PromiseError.svelte";
import Select from "svelte-select";
let data_loaded = false;
export let params;
$: delete_triggered = false;
$: original_data = {};
$: editable = {};
$: donor = {};
$: runner = {};
$: current_donors = [];
$: current_runners = [];
$: amount_input = 0;
$: is_amount_valid = amount_input > 0;
$: paid_amount_input = 0;
$: is_paid_amount_valid = paid_amount_input > 0;
$: is_everything_set =
editable.donor != null &&
((original_data.responseType == "DISTANCEDONATION" &&
editable?.runner != null) ||
original_data.responseType !== "DISTANCEDONATION");
$: changes_performed =
!(JSON.stringify(original_data) === JSON.stringify(editable)) ||
(original_data.responseType == "DISTANCEDONATION" &&
!(Math.floor(amount_input * 100) === original_data.amountPerDistance)) ||
(original_data.responseType !== "DISTANCEDONATION" &&
!(Math.floor(amount_input * 100) === original_data.amount)) ||
!(Math.floor(paid_amount_input * 100) === original_data.paidAmount);
$: save_enabled = changes_performed && is_amount_valid && is_everything_set;
import PromiseError from "../base/PromiseError.svelte"; const promise = DonationService.donationControllerGetOne(
import Select from "svelte-select"; params.donationid
let data_loaded = false; ).then((data) => {
export let params; data_loaded = true;
$: delete_triggered = false; original_data = Object.assign({}, data);
$: original_data = {}; editable = Object.assign({}, original_data);
$: editable = {}; paid_amount_input = data.paidAmount / 100;
$: donor = {}; if (data.responseType == "DISTANCEDONATION") {
$: runner = {}; amount_input = data.amountPerDistance / 100;
$: current_donors = []; RunnerService.runnerControllerGetAll().then((val) => {
$: current_runners = []; current_runners = val.map((r) => {
$: amount_input = 0; return { label: getDonorLabel(r), value: r };
$: is_amount_valid = amount_input > 0; });
$: paid_amount_input = 0; runner = current_runners.find((g) => g.value.id == editable.runner.id);
$: is_paid_amount_valid = paid_amount_input > 0; });
$: is_everything_set = } else {
editable.donor != null && amount_input = data.amount / 100;
((original_data.responseType == "DISTANCEDONATION" && }
editable?.runner != null) || DonorService.donorControllerGetAll().then((val) => {
original_data.responseType !== "DISTANCEDONATION"); current_donors = val.map((r) => {
$: changes_performed = return { label: getDonorLabel(r), value: r };
!(JSON.stringify(original_data) === JSON.stringify(editable)) || });
(original_data.responseType == "DISTANCEDONATION" && donor = current_donors.find((g) => g.value.id == editable.donor.id);
!(Math.floor(amount_input * 100) === original_data.amountPerDistance)) || });
(original_data.responseType !== "DISTANCEDONATION" && });
!(Math.floor(amount_input * 100) === original_data.amount)) || const getDonorLabel = (option) =>
!(Math.floor(paid_amount_input * 100) === original_data.paidAmount); option.firstname + " " + (option.middlename || "") + " " + option.lastname;
$: save_enabled = changes_performed && is_amount_valid && is_everything_set; const filterDonors = (label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id.toString().startsWith(filterText.toLowerCase());
const promise = DonationService.donationControllerGetOne( function submit() {
params.donationid if (data_loaded === true && save_enabled) {
).then((data) => { Toastify({
data_loaded = true; text: $_('updating-donation'),
original_data = Object.assign({}, data); duration: 2500,
editable = Object.assign({}, original_data); }).showToast();
paid_amount_input = data.paidAmount / 100; let postdata = {};
if (data.responseType == "DISTANCEDONATION") { editable.paidAmount = paid_amount_input*100;
amount_input = data.amountPerDistance / 100; if (original_data.responseType === "DISTANCEDONATION") {
RunnerService.runnerControllerGetAll().then((val) => { editable.amountPerDistance = Math.floor(amount_input * 100);
current_runners = val.map((r) => { postdata = Object.assign(postdata, editable);
return { label: getDonorLabel(r), value: r }; postdata.runner = postdata.runner.id;
}); postdata.donor = postdata.donor.id;
runner = current_runners.find((g) => g.value.id == editable.runner.id); DonationService.donationControllerPutDistance(
}); original_data.id,
} else { postdata
amount_input = data.amount / 100; )
} .then((resp) => {
DonorService.donorControllerGetAll().then((val) => { Object.assign(original_data, editable);
current_donors = val.map((r) => { original_data = original_data;
return { label: getDonorLabel(r), value: r }; Toastify({
}); text: $_('donation-updated'),
donor = current_donors.find((g) => g.value.id == editable.donor.id); duration: 2500,
}); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}); }).showToast();
const getDonorLabel = (option) => })
option.firstname + " " + (option.middlename || "") + " " + option.lastname; .catch((err) => {});
const filterDonors = (label, filterText, option) => } else {
label.toLowerCase().includes(filterText.toLowerCase()) || editable.amount = Math.floor(amount_input * 100);
option.value.id.toString().startsWith(filterText.toLowerCase()); postdata = Object.assign(postdata, editable);
postdata.donor = postdata.donor.id;
function submit() { DonationService.donationControllerPutFixed(original_data.id, postdata)
if (data_loaded === true && save_enabled) { .then((resp) => {
toast($_("updating-donation")); Object.assign(original_data, editable);
let postdata = {}; original_data = original_data;
editable.paidAmount = paid_amount_input * 100; Toastify({
if (original_data.responseType === "DISTANCEDONATION") { text: $_('donation-updated'),
editable.amountPerDistance = Math.floor(amount_input * 100); duration: 2500,
postdata = Object.assign(postdata, editable); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
postdata.runner = postdata.runner.id; }).showToast();
postdata.donor = postdata.donor.id; })
DonationService.donationControllerPutDistance( .catch((err) => {});
original_data.id, }
postdata } else {
) }
.then((resp) => { }
Object.assign(original_data, editable); function deleteDonation() {
original_data = original_data; DonationService.donationControllerRemove(original_data.id, false)
toast.success($_("donation-updated")); .then((resp) => {
}) Toastify({
.catch((err) => {}); text: $_('donation-deleted'),
} else { duration: 500,
editable.amount = Math.floor(amount_input * 100); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
postdata = Object.assign(postdata, editable); }).showToast();
postdata.donor = postdata.donor.id; location.replace("./");
DonationService.donationControllerPutFixed(original_data.id, postdata) })
.then((resp) => { .catch((err) => {
Object.assign(original_data, editable); modal_open = true;
original_data = original_data; delete_donor = original_data;
toast.success($_("donation-updated")); });
}) }
.catch((err) => {});
}
} else {
}
}
function deleteDonation() {
DonationService.donationControllerRemove(original_data.id, false)
.then((resp) => {
toast.success($_("donation-deleted"));
location.replace("./");
})
.catch((err) => {
modal_open = true;
delete_donor = original_data;
});
}
</script> </script>
{#await promise} {#await promise}
{$_("loading-donation-details")} {$_('loading-donation-details')}
{:then} {:then}
<section class="container p-5 select-none"> <section class="container p-5 select-none">
<div class="flex flex-row mb-4"> <div class="flex flex-row mb-4">
<div class="mt-2 w-full"> <div class="w-full">
<nav class="w-full flex"> <nav class="w-full flex">
<ol class="list-none flex flex-row items-center justify-start"> <ol class="list-none flex flex-row items-center justify-start">
<li class="flex items-center"> <li class="flex items-center">
<a class="mr-2" href="./" <svg
><svg fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" viewBox="0 0 24 24"
height="24" width="24"
viewBox="0 0 24 24" height="24"><path fill="none" d="M0 0h24v24H0z" />
fill="none" <path
stroke="currentColor" d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" /></svg>
stroke-width="2" </li>
stroke-linecap="round" <li class="flex items-center ml-2">
stroke-linejoin="round" <a class="mr-2" href="./">{$_('donations')}</a><svg
class="inline-block" stroke="currentColor"
><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg fill="none"
> stroke-width="2"
{$_("donations")}</a viewBox="0 0 24 24"
> stroke-linecap="round"
</li> stroke-linejoin="round"
</ol> class="h-3 w-3 mr-2 stroke-current"
</nav> height="1em"
</div> width="1em"
</div> xmlns="http://www.w3.org/2000/svg"><line
<div class="mb-4 text-3xl font-extrabold leading-tight"> x1="5"
{original_data.donor.firstname} y1="12"
{original_data.donor.middlename || ""} x2="19"
{original_data.donor.lastname} y2="12" />
&gt; <polyline points="12 5 19 12 12 19" /></svg>
{#if original_data.responseType == "DISTANCEDONATION"} </li>
{original_data.runner.firstname} <li class="flex items-center">
{original_data.runner.middlename || ""} <span class="mr-2">{original_data.id}</span>
{original_data.runner.lastname} </li>
{:else} </ol>
{$_("fixed-donation")}: </nav>
{amount_input.toFixed(2).toLocaleString("de-DE", { valute: "EUR" })} </div>
{/if} </div>
[#{original_data.id}] <div class="mb-8 text-3xl font-extrabold leading-tight">
<div data-id="donation_actions_${original_data.id}"> {original_data.donor.firstname}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:DELETE")} {original_data.donor.middlename || ''}
{#if delete_triggered} {original_data.donor.lastname}
<button &gt;
on:click={deleteDonation} {#if original_data.responseType == 'DISTANCEDONATION'}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" {original_data.runner.firstname}
>{$_("confirm-deletion")}</button {original_data.runner.middlename || ''}
> {original_data.runner.lastname}
<button {:else}
on:click={() => { {$_('fixed-donation')}:
delete_triggered = !delete_triggered; {amount_input.toFixed(2).toLocaleString('de-DE', { valute: 'EUR' })}
}} {/if}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm" <span data-id="donation_actions_${original_data.id}">
>{$_("cancel")}</button {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:DELETE')}
> {#if delete_triggered}
{/if} <button
{#if !delete_triggered} on:click={deleteDonation}
<button class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:">{$_('confirm-deletion')}</button>
on:click={() => { <button
delete_triggered = true; on:click={() => {
}} delete_triggered = !delete_triggered;
type="button" }}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:">{$_('cancel')}</button>
>{$_("delete-donation")}</button {/if}
> {#if !delete_triggered}
{/if} <button
{/if} on:click={() => {
{#if !delete_triggered} delete_triggered = true;
<button }}
disabled={!save_enabled} type="button"
class:opacity-50={!save_enabled} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:">{$_('delete-donation')}</button>
type="button" {/if}
on:click={submit} {/if}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" {#if !delete_triggered}
>{$_("save-changes")}</button <button
> disabled={!save_enabled}
{/if} class:opacity-50={!save_enabled}
</div> type="button"
</div> on:click={submit}
<!-- --> class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:">{$_('save-changes')}</button>
<div> {/if}
<span class="font-semibold text-gray-700" </span>
>{$_("total-donation-amount")}:</span </div>
> <!-- -->
<span <div>
>{(editable.amount / 100) <span
.toFixed(2) class="font-medium text-gray-700">{$_('total-donation-amount')}:</span>
.toLocaleString("de-DE", { valute: "EUR" })}€</span <span>{(editable.amount / 100)
> .toFixed(2)
| .toLocaleString('de-DE', { valute: 'EUR' })}€</span>
<span class="font-semibold text-gray-700">{$_("paid-amount")}:</span> |
<span <span
>{(editable.paidAmount / 100) class="font-medium text-gray-700">{$_('paid-amount')}:</span>
.toFixed(2) <span>{(editable.paidAmount / 100)
.toLocaleString("de-DE", { valute: "EUR" })}€</span .toFixed(2)
> .toLocaleString('de-DE', { valute: 'EUR' })}€</span>
| |
<span class="font-semibold text-gray-700">{$_("status")}:</span> <span
{#if editable.status == "PAID"} class="font-medium text-gray-700">{$_('status')}:</span>
<span {#if editable.status =="PAID"}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800" <span
>{$_("paid")}</span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">{$_('paid')}</span>
> {:else}
{:else} <span
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">{$_('open')}</span>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800" {/if}
>{$_("open")}</span </div>
> <br>
{/if} <div class=" w-full">
</div> <label
<br /> for="donor"
<div class=" mt-2 w-full"> class="block font-medium text-gray-700">{$_('donor')}</label>
<label for="donor" class="block font-semibold text-gray-700" <Select
>{$_("donor")}</label containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
> itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
<Select items={current_donors}
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" showChevron={true}
itemFilter={(label, filterText, option) => placeholder={$_('search-for-donor-name-or-id')}
filterDonors(label, filterText, option)} noOptionsMessage={$_('no-donors-found')}
items={current_donors} bind:selectedValue={donor}
showChevron={true} on:select={(selectedValue) => {editable.donor = selectedValue.detail.value; editable.donor.donationAmount=original_data.donor.donationAmount; editable.donor.paidDonationAmount =original_data.donor.paidDonationAmount}}
placeholder={$_("search-for-donor-name-or-id")} on:clear={() => (editable.donor = null)} />
noOptionsMessage={$_("no-donors-found")} </div>
bind:selectedValue={donor} {#if original_data.responseType == 'DISTANCEDONATION'}
on:select={(selectedValue) => { <div class=" w-full">
editable.donor = selectedValue.detail.value; <label
editable.donor.donationAmount = original_data.donor.donationAmount; for="donor"
editable.donor.paidDonationAmount = class="block font-medium text-gray-700">{$_('runner')}</label>
original_data.donor.paidDonationAmount; <Select
}} containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
on:clear={() => (editable.donor = null)} itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
/> items={current_runners}
</div> showChevron={true}
{#if original_data.responseType == "DISTANCEDONATION"} placeholder={$_('search-for-runner-by-name-or-id')}
<div class=" mt-2 w-full"> noOptionsMessage={$_('no-runners-found')}
<label for="donor" class="block font-semibold text-gray-700" bind:selectedValue={runner}
>{$_("runner")}</label on:select={(selectedValue) => (editable.runner = selectedValue.detail.value)}
> on:clear={() => (editable.runner = null)} />
<Select </div>
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {/if}
itemFilter={(label, filterText, option) => <div class=" w-full">
filterDonors(label, filterText, option)} <label for="lastname" class="font-medium text-gray-700">
items={current_runners} {#if original_data.responseType == 'DISTANCEDONATION'}
showChevron={true} {$_('amount-per-kilometer')}
placeholder={$_("search-for-runner-by-name-or-id")} {:else}{$_('donation-amount')}{/if}
noOptionsMessage={$_("no-runners-found")} </label>
bind:selectedValue={runner} <div class="mt-1 flex rounded-md shadow-sm">
on:select={(selectedValue) => <input
(editable.runner = selectedValue.detail.value)} autocomplete="off"
on:clear={() => (editable.runner = null)} class:border-red-500={!is_amount_valid}
/> class:focus:border-red-500={!is_amount_valid}
</div> class:focus:ring-red-500={!is_amount_valid}
{/if} bind:value={amount_input}
<div class=" mt-2 w-full"> type="number"
<label for="lastname" class="font-semibold text-gray-700"> step="0.01"
{#if original_data.responseType == "DISTANCEDONATION"} name="donation_amount_eur"
{$_("amount-per-kilometer")} class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 p-2"
{:else}{$_("donation-amount")}{/if} placeholder="2.00" />
</label> <span
<div class="mt-1 flex rounded-md shadow-sm"> class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 ">€</span>
<input </div>
autocomplete="off" {#if !is_amount_valid}
class:border-red-500={!is_amount_valid} <span
class:focus:border-red-500={!is_amount_valid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class:focus:ring-red-500={!is_amount_valid} {$_('donation-amount-must-be-greater-that-0-00eur')}
bind:value={amount_input} </span>
type="number" {/if}
step="0.01" </div>
name="donation_amount_eur" <div class="w-full">
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 p-2" <label
placeholder="2.00" for="token"
/> class="block text-sm font-medium text-gray-700">{$_('paid-amount')}</label>
<span <div class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full">
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500" <input
>€</span autocomplete="off"
> class:border-red-500={!is_amount_valid}
</div> class:focus:border-red-500={!is_amount_valid}
{#if !is_amount_valid} class:focus:ring-red-500={!is_amount_valid}
<span bind:value={paid_amount_input}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" type="number"
> step="0.01"
{$_("donation-amount-must-be-greater-that-0-00eur")} name="donation_amount_eur"
</span> class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm p-2"
{/if} placeholder="2.00" />
</div> <button
<div class="mt-2 w-full"> on:click={
<label for="token" class="block font-semibold text-gray-700" ()=>{
>{$_("paid-amount")}</label paid_amount_input=paid_amount_input = (original_data.amount/100).toFixed(2);
> }
<div }
class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full" class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm">MAX</button>
> <span
<input class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm">€</span>
autocomplete="off" </div>
class:border-red-500={!is_amount_valid} {#if !is_paid_amount_valid}
class:focus:border-red-500={!is_amount_valid} <span
class:focus:ring-red-500={!is_amount_valid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
bind:value={paid_amount_input} {$_('payment-amount-must-be-greater-than-0-00eur')}
type="number" </span>
step="0.01" {/if}
name="donation_amount_eur" </div>
class="focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded-none rounded-l-md sm:text-sm p-2" </section>
placeholder="2.00"
/>
<button
on:click={() => {
paid_amount_input = paid_amount_input = (
original_data.amount / 100
).toFixed(2);
}}
class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm"
>MAX</button
>
<span
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm"
>€</span
>
</div>
{#if !is_paid_amount_valid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
>
{$_("payment-amount-must-be-greater-than-0-00eur")}
</span>
{/if}
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@ -1,21 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let donor;
</script>
{#if !donor || donor.firstname == 0}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{$_('anonymer_sponsor')}</span
>
{:else}
<div class="flex items-center">
<a
href="../donors/{donor.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{donor.firstname}
{#if donor.middlename}{donor.middlename}{/if}
{donor.lastname}</a
>
</div>
{/if}

View File

@ -1,18 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let runner;
</script>
{#if !runner || runner.firstname == 0}
{$_("fixed-donation")}
{:else}
<div class="text-sm font-medium text-gray-900">
<a
href="../runners/{runner.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{runner.firstname}
{#if runner.middlename}{runner.middlename}{/if}
{runner.lastname}</a
>
</div>
{/if}

View File

@ -1,16 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let status;
</script>
{#if status == "PAID"}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("paid")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
>{$_("open")}</span
>
{/if}

View File

@ -1,28 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import TableActions from "../shared/TableActions.svelte";
export let detailsLink;
export let detailsAction;
export let deleteEnabled;
export let deleteAction;
export let paymentAction;
</script>
{#if paymentAction}
<button
on:click={paymentAction}
class="text-[#025a21] cursor-pointer hover:text-green-900 mr-4"
>{$_("enter-payment")}</button
>
{:else}
<span class="inline-block opacity-0 cursor-default mr-4" style=""
>{$_("enter-payment")}</span
>
{/if}
<TableActions
bind:detailsAction
bind:detailsLink
bind:deleteAction
bind:deleteEnabled
/>

View File

@ -5,32 +5,25 @@
import DonationsOverview from "./DonationsOverview.svelte"; import DonationsOverview from "./DonationsOverview.svelte";
$: current_donations = []; $: current_donations = [];
export let modal_open = false; export let modal_open = false;
let addDonations;
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_("donations")} {$_('donations')}
</h4> {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:CREATE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")} <button
<button on:click={() => {
on:click={() => { modal_open = true;
modal_open = true; }}
}} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm" {$_('add-donation')}
> </button>
{$_("add-donation")} {/if}
</button> </span>
{/if} <DonationsOverview bind:current_donations />
<DonationsOverview bind:current_donations bind:addDonations />
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:CREATE')}
<AddDonationModal <AddDonationModal bind:current_donations bind:modal_open />
on:created={(event) => {
addDonations(event.detail.donations);
}}
bind:modal_open
/>
{/if} {/if}

View File

@ -5,8 +5,8 @@
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="m-auto mt-2" style="height:15rem" src={donations_empty} alt="" /> <img class="m-auto" style="height:15rem" src={donations_empty} alt="" />
<span class="font-bold">{$_("there-are-no-donations-yet")}</span><br /> <span class="font-bold">{$_('there-are-no-donations-yet')}</span><br />
<span>{$_("add-your-fist-donation")}</span> <span>{$_('add-your-fist-donation')}</span>
</p> </p>
</div> </div>

View File

@ -1,283 +1,235 @@
<script> <script>
import { _ } from "svelte-i18n"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import { DonationService } from "@odit/lfk-client-js"; import { DonationService, DonorService } from "@odit/lfk-client-js";
import store from "../../store"; import store from "../../store";
import Toastify from "toastify-js";
import DonationsEmptyState from "./DonationsEmptyState.svelte"; import DonationsEmptyState from "./DonationsEmptyState.svelte";
import AddDonationPaymentModal from "./AddDonationPaymentModal.svelte"; import AddDonationPaymentModal from "./AddDonationPaymentModal.svelte";
import { onMount } from "svelte";
import {
createSvelteTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
renderComponent,
} from "@tanstack/svelte-table";
import { writable } from "svelte/store";
import TableBottom from "../shared/TableBottom.svelte";
import InputElement from "../shared/InputElement.svelte";
import TableHeader from "../shared/TableHeader.svelte";
import DonationDonor from "./DonationDonor.svelte";
import DonationRunner from "./DonationRunner.svelte";
import DonationStatus from "./DonationStatus.svelte";
import DonationTableAction from "./DonationTableAction.svelte";
import DeleteDonationModal from "./DeleteDonationModal.svelte";
import {
donationDonorFilter,
donationRunnerFilter,
} from "../shared/tablefilters";
import toast from "svelte-french-toast";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
$: active_edits = [];
$: selected =
$table?.getSelectedRowModel().rows.map((row) => row.index) || [];
$: dataLoaded = false;
export let current_donations = []; export let current_donations = [];
export const addDonations = (donations) => { export let payment_modal_open = false;
current_donations = current_donations.concat(...donations); export let editable = {};
options.update((options) => ({ export let original_data = {};
...options, export let paid_amount_input = 0;
data: current_donations, const donations_promise = DonationService.donationControllerGetAll().then(
})); (val) => {
}; current_donations = val;
//Section table
const columns = [
{
accessorKey: "id",
header: () => "id",
filterFn: `equalsString`,
},
{
accessorKey: "donor",
header: () => $_("donor"),
cell: (info) => {
return renderComponent(DonationDonor, { donor: info.getValue() });
},
filterFn: `donor`,
},
{
accessorKey: "runner",
header: () => $_("runner"),
cell: (info) => {
return renderComponent(DonationRunner, { runner: info.getValue() });
},
filterFn: `runner`,
},
{
accessorKey: "amountPerDistance",
header: () => $_("amount-per-kilometer"),
cell: (info) => {
if (!info.getValue()) {
return $_("fixed-donation");
}
return `${(info.getValue() / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })} €`;
},
enableColumnFilter: false,
},
{
accessorKey: "amount",
header: () => $_("donation-amount"),
cell: (info) => {
return `${(info.getValue() / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })} €`;
},
enableColumnFilter: false,
},
{
accessorKey: "paidAmount",
header: () => $_("total-paid-amount"),
cell: (info) => {
return `${(info.getValue() / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })} €`;
},
enableColumnFilter: false,
},
{
accessorKey: "status",
header: () => $_("status"),
cell: (info) => {
return renderComponent(DonationStatus, { status: info.getValue() });
},
enableColumnFilter: false,
},
{
accessorKey: "actions",
header: () => $_("action"),
cell: (info) => {
let detailsLink
let paymentAction
if (info.row.original.donor != undefined){
detailsLink = `./${info.row.original.id}`
paymentAction = () => {
active_edits = current_donations.filter(
(r) => r.id == info.row.original.id
);
}
}
return renderComponent(DonationTableAction, {
detailsLink: detailsLink,
deleteAction: () => {
active_deletes = current_donations.filter(
(r) => r.id == info.row.original.id
);
},
paymentAction: paymentAction,
deleteEnabled:
store.state.jwtinfo.userdetails.permissions.includes(
"DONATION:DELETE"
),
});
},
enableColumnFilter: false,
enableSorting: false,
},
];
const options = writable({
data: [],
columns: columns,
initialState: {
pagination: {
pageSize: 50,
},
},
filterFns: {
donor: donationDonorFilter,
runner: donationRunnerFilter,
},
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const table = createSvelteTable(options);
async function deleteDonation(delete_donation_id) {
await DonationService.donationControllerRemove(delete_donation_id, true);
current_donations = current_donations.filter(
(r) => r.id !== delete_donation_id
);
options.update((options) => ({
...options,
data: current_donations,
}));
toast.success($_("donation-deleted"));
}
onMount(async () => {
let page = 0;
let pagesize = 300;
while (page >= 0) {
const donations = await DonationService.donationControllerGetAll(
page,
pagesize
);
if (donations.length == 0) {
page = -2;
}
current_donations = current_donations.concat(...donations);
options.update((options) => ({
...options,
data: current_donations,
}));
dataLoaded = true;
page++;
} }
}); );
function should_display_based_on_id(id) {
if (searchvalue.toString().slice(-1) === "*") {
return id.toString().startsWith(searchvalue.replace("*", ""));
}
return id.toString() === searchvalue;
}
function open_payment_modal(donation) {
editable = Object.assign({}, donation);
original_data = Object.assign({}, donation);
paid_amount_input = (donation.paidAmount/100).toFixed(2);
payment_modal_open = true;
}
</script> </script>
<AddDonationPaymentModal <AddDonationPaymentModal bind:current_donations bind:original_data bind:editable bind:paid_amount_input bind:payment_modal_open />
original_data={active_edits[0]} {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:GET')}
payment_modal_open={active_edits.length > 0} {#await donations_promise}
paid_amount_input={(active_edits[0]?.paidAmount || 0) / 100}
on:created={(event) => {
current_donations = current_donations.map((d)=>{
if(d.id === event.detail.donation.id){
d.paidAmount = event.detail.donation.paidAmount;
}
return d;
})
options.update((options) => ({
...options,
data: current_donations,
}));
}}
/>
<DeleteDonationModal
delete_donation={active_deletes[0]}
modal_open={active_deletes.length > 0}
on:delete={(event) => {
deleteDonation(event.detail.id);
}}
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")}
{#if !dataLoaded}
<div <div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert" role="alert">
> <p class="font-bold">donations are being loaded</p>
<p class="font-bold">{$_("donations-are-being-loaded")}</p> <p class="text-sm">{$_('this-might-take-a-moment')}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div> </div>
{:else if current_donations.length === 0} {:then}
<DonationsEmptyState /> {#if current_donations.length === 0}
{:else} <DonationsEmptyState />
<input {:else}
type="search" <input
bind:value={searchvalue} type="search"
placeholder={$_("datatable.search")} bind:value={searchvalue}
aria-label={$_("datatable.search")} placeholder={$_('datatable.search')}
class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" aria-label={$_('datatable.search')}
/> class="gridjs-input gridjs-search-input mb-4" />
<div <div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll" class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
> <table class="divide-y divide-gray-200 w-full">
<table class="w-full"> <thead class="bg-gray-50">
<thead class="border-b border-gray-400"> <tr>
{#each $table.getHeaderGroups() as headerGroup} <th
<tr class="select-none"> scope="col"
{#each headerGroup.headers as header} class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<TableHeader {header} /> {$_('donor')}
{/each} </th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('runner')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('amount-per-kilometer')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('donation-amount')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('paid-amount')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('status')}
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">{$_('action')}</span>
</th>
</tr> </tr>
{/each} </thead>
</thead> <tbody class="divide-y divide-gray-200">
<tbody> {#each current_donations as donation}
{#each $table.getRowModel().rows as row} {#if donation.donor.firstname
<tr class="odd:bg-white even:bg-gray-100"> .toLowerCase()
{#each row.getVisibleCells() as cell} .includes(
<td> searchvalue.toLowerCase()
<svelte:component ) || donation.donor.lastname
this={flexRender( .toLowerCase()
cell.column.columnDef.cell, .includes(
cell.getContext() searchvalue.toLowerCase()
)} ) || donation.runner?.firstname
/> .toLowerCase()
</td> .includes(
{/each} searchvalue.toLowerCase()
</tr> ) || donation.runner?.lastname
{/each} .toLowerCase()
</tbody> .includes(
</table> searchvalue.toLowerCase()
) || should_display_based_on_id(donation.id)}
<tr data-rowid="donation_{donation.id}">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<a
href="../donors/{donation.donor.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{donation.donor.firstname}
{donation.donor.middlename || ''}
{donation.donor.lastname}</a>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if donation.runner}
<div class="text-sm font-medium text-gray-900">
<a
href="../runners/{donation.runner.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{donation.runner.firstname}
{donation.runner.middlename || ''}
{donation.runner.lastname}</a>
</div>
{:else}
<div class="text-sm font-medium text-gray-900">
{$_('fixed-donation')}
</div>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if donation.amountPerDistance}
<div class="text-sm font-medium text-gray-900">
{(donation.amountPerDistance / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€
</div>
{:else}
<div class="text-sm font-medium text-gray-900">
{$_('fixed-donation')}
</div>
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">
{(donation.amount / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">
{(donation.paidAmount / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if donation.status =="PAID"}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">{$_('paid')}</span>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">{$_('open')}</span>
{/if}
</td>
{#if active_deletes[donation.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {
active_deletes[donation.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
<button
on:click={() => {
DonationService.donationControllerRemove(donation.id, false).then(
(resp) => {
current_donations = current_donations.filter(
(obj) => obj.id !== donation.id
);
Toastify({
text: $_('donation-deleted'),
duration: 500,
backgroundColor:
'linear-gradient(to right, #00b09b, #96c93d)',
}).showToast();
}
);
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {open_payment_modal(donation);}}
class="text-[#025a21] hover:text-green-900 mr-4">{$_('enter-payment')}</button>
<a
href="./{donation.id}"
class="text-indigo-600 hover:text-indigo-900">{$_('details')}</a>
{#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:DELETE')}
<button
on:click={() => {
active_deletes[donation.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
{/if}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div> </div>
<div class="h-2" /> {/await}
<TableBottom {table} {selected} />
{/if}
{/if} {/if}
<style>
table tbody tr td:nth-child(2) {
font-family: monospace;
}
</style>

View File

@ -1,14 +1,15 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { DonorService } from "@odit/lfk-client-js"; import {
DonorService
} from "@odit/lfk-client-js";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import isMobilePhone from "validator/es/lib/isMobilePhone"; import isMobilePhone from "validator/es/lib/isMobilePhone";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
import toast from "svelte-french-toast";
export let modal_open; export let modal_open;
export let current_donors;
let firstname_input; let firstname_input;
let lastname_input; let lastname_input;
let middlename_input; let middlename_input;
@ -18,7 +19,6 @@
let address_input2; let address_input2;
let address_zipcode; let address_zipcode;
let address_city; let address_city;
const dispatch = createEventDispatcher();
function focus(el) { function focus(el) {
el.focus(); el.focus();
} }
@ -74,7 +74,10 @@
function submit() { function submit() {
if (processed_last_submit === true) { if (processed_last_submit === true) {
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("donor-is-being-added")); const toast = Toastify({
text: $_('donor-is-being-added'),
duration: -1,
}).showToast();
let address = {}; let address = {};
if (address_checked === true) { if (address_checked === true) {
address = { address = {
@ -89,7 +92,7 @@
firstname: firstname_input_value, firstname: firstname_input_value,
lastname: lastname_input_value, lastname: lastname_input_value,
address, address,
receiptNeeded: address_checked, receiptNeeded: address_checked
}; };
if (middlename_input_value) { if (middlename_input_value) {
postdata.middlename = middlename_input_value; postdata.middlename = middlename_input_value;
@ -108,15 +111,21 @@
email_input_value = ""; email_input_value = "";
modal_open = false; modal_open = false;
// //
toast.dismiss(); Toastify({
toast.success($_("donor-added")); text: $_('donor-added'),
dispatch("created", { donors: [result] }); duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_donors.push(result);
current_donors = current_donors;
}) })
.catch((err) => { .catch((err) => {
// //
}) })
.finally(() => { .finally(() => {
processed_last_submit = true; processed_last_submit = true;
//
toast.hideToast();
}); });
} }
} }
@ -124,71 +133,59 @@
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
modal_open = false; modal_open = false;
}} }}>
>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div <div
class="absolute inset-0 bg-gray-500 opacity-75" class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" data-id="modal_backdrop" />
/>
</div> </div>
<span <span
class="hidden sm:inline-block sm:align-middle sm:h-screen" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span aria-hidden="true">&#8203;</span>
>
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <div class="sm:flex sm:items-start">
<div class="">
<div <div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
>
<svg <svg
class="size-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"><path fill="none" d="M0 0h24v24H0z" />
><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" /></svg>
/></svg
>
</div> </div>
<div class="mt-3"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("create-a-new-donor")} {$_('create-a-new-donor')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_('please-provide-the-nessecary-information-to-add-a-new-donor')}
"please-provide-the-nessecary-information-to-add-a-new-donor"
)}
</p> </p>
</div> </div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="firstname" for="firstname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('first-name')}</label>
>{$_("first-name")}</label
>
<input <input
use:focus use:focus
autocomplete="off" autocomplete="off"
placeholder={$_("first-name")} placeholder={$_('first-name')}
class:border-red-500={!isFirstnameValid} class:border-red-500={!isFirstnameValid}
class:focus:border-red-500={!isFirstnameValid} class:focus:border-red-500={!isFirstnameValid}
class:focus:ring-red-500={!isFirstnameValid} class:focus:ring-red-500={!isFirstnameValid}
@ -196,41 +193,34 @@
bind:this={firstname_input} bind:this={firstname_input}
type="text" type="text"
name="firstname" name="firstname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('first-name-is-required')}
{$_("first-name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="trackname" for="trackname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('middle-name')}</label>
>{$_("middle-name")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("middle-name")} placeholder={$_('middle-name')}
bind:value={middlename_input_value} bind:value={middlename_input_value}
bind:this={middlename_input} bind:this={middlename_input}
type="text" type="text"
name="trackname" name="trackname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="lastname" for="lastname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('last-name')}</label>
>{$_("last-name")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("last-name")} placeholder="{$_('last-name')}"
class:border-red-500={!isLastnameValid} class:border-red-500={!isLastnameValid}
class:focus:border-red-500={!isLastnameValid} class:focus:border-red-500={!isLastnameValid}
class:focus:ring-red-500={!isLastnameValid} class:focus:ring-red-500={!isLastnameValid}
@ -238,25 +228,21 @@
bind:this={lastname_input} bind:this={lastname_input}
type="text" type="text"
name="lastname" name="lastname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('last-name-is-required')}
{$_("last-name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="phone" for="phone"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('phone')}</label>
>{$_("phone")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("phone")} placeholder={$_('phone')}
class:border-red-500={!isPhoneValidOrEmpty} class:border-red-500={!isPhoneValidOrEmpty}
class:focus:border-red-500={!isPhoneValidOrEmpty} class:focus:border-red-500={!isPhoneValidOrEmpty}
class:focus:ring-red-500={!isPhoneValidOrEmpty} class:focus:ring-red-500={!isPhoneValidOrEmpty}
@ -264,27 +250,21 @@
bind:this={phone_input} bind:this={phone_input}
type="tel" type="tel"
name="phone" name="phone"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isPhoneValidOrEmpty} {#if !isPhoneValidOrEmpty}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {@html $_('the-provided-phone-number-is-invalid-less-than-br-greater-than-please-enter-a-valid-international-number')}
{@html $_(
"the-provided-phone-number-is-invalid-less-than-br-greater-than-please-enter-a-valid-international-number"
)}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="email" for="email"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('e-mail-adress')}</label>
>{$_("e-mail-adress")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("e-mail-adress")} placeholder={$_('e-mail-adress')}
class:border-red-500={!isEmailValidOrEmpty} class:border-red-500={!isEmailValidOrEmpty}
class:focus:border-red-500={!isEmailValidOrEmpty} class:focus:border-red-500={!isEmailValidOrEmpty}
class:focus:ring-red-500={!isEmailValidOrEmpty} class:focus:ring-red-500={!isEmailValidOrEmpty}
@ -292,13 +272,11 @@
bind:this={email_input} bind:this={email_input}
type="email" type="email"
name="email" name="email"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isEmailValidOrEmpty} {#if !isEmailValidOrEmpty}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-email-is-required')}
{$_("valid-email-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
@ -309,22 +287,19 @@
id="comments" id="comments"
name="comments" name="comments"
type="checkbox" type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-semibold text-gray-700" <label
>{$_("receipt-needed")}</label for="comments"
> class="font-medium text-gray-700">{$_('receipt-needed')}</label>
</div> </div>
</div> </div>
{#if address_checked === true} {#if address_checked === true}
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="address1" for="address1"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('address')}</label>
>{$_("address")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder="Address" placeholder="Address"
@ -335,41 +310,34 @@
bind:this={address_input1} bind:this={address_input1}
type="text" type="text"
name="address1" name="address1"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isAddress1Valid} {#if !isAddress1Valid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('address-is-required')}
{$_("address-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="address2" for="address2"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
>{$_("apartment-suite-etc")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("apartment-suite-etc")} placeholder={$_('apartment-suite-etc')}
bind:value={address_input2_value} bind:value={address_input2_value}
bind:this={address_input2} bind:this={address_input2}
type="text" type="text"
name="address2" name="address2"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="zipcode" for="zipcode"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('zip-postal-code')}</label>
>{$_("zip-postal-code")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("zip-postal-code")} placeholder={$_('zip-postal-code')}
class:border-red-500={!iszipcodevalid} class:border-red-500={!iszipcodevalid}
class:focus:border-red-500={!iszipcodevalid} class:focus:border-red-500={!iszipcodevalid}
class:focus:ring-red-500={!iszipcodevalid} class:focus:ring-red-500={!iszipcodevalid}
@ -377,22 +345,18 @@
bind:this={address_zipcode} bind:this={address_zipcode}
type="text" type="text"
name="zipcode" name="zipcode"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !iszipcodevalid} {#if !iszipcodevalid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-zipcode-postal-code-is-required')}
{$_("valid-zipcode-postal-code-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="city" for="city"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">City</label>
>City</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder="City" placeholder="City"
@ -403,13 +367,11 @@
bind:this={address_city} bind:this={address_city}
type="text" type="text"
name="city" name="city"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !iscityvalid} {#if !iscityvalid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('valid-city-is-required')}
{$_("valid-city-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
@ -418,24 +380,22 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
disabled={!createbtnenabled} disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled} class:opacity-50={!createbtnenabled}
on:click={submit} on:click={submit}
type="button" type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('create')}
{$_("create")}
</button> </button>
<button <button
on:click={() => { on:click={() => {
modal_open = false; modal_open = false;
}} }}
type="button" type="button"
class="cancel_modal_button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('cancel')}
{$_("cancel")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,90 +1,92 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher } from "svelte"; import { focusTrap } from "svelte-focus-trap";
export let modal_open; import { DonorService } from "@odit/lfk-client-js";
export let delete_donor; import Toastify from "toastify-js";
const dispatch = createEventDispatcher(); import { createEventDispatcher } from "svelte";
function cancelDelete() { export let modal_open;
modal_open = false; export let delete_donor;
dispatch("cancelDelete", { id: delete_donor.id }); const dispatch = createEventDispatcher();
} function cancelDelete() {
function deleteDonor() { modal_open = false;
dispatch("delete", { id: delete_donor.id }); dispatch("cancelDelete", { id: delete_donor.id });
} }
function deleteDonor() {
DonorService.donorControllerRemove(
delete_donor.id,
true
)
.then((resp) => {
Toastify({
text: $_('donor-deleted'),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
location.replace("./");
})
.catch((err) => {});
}
</script> </script>
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside use:focusTrap
on:click_outside={cancelDelete} use:clickOutside
> on:click_outside={cancelDelete}>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div
<div class="absolute inset-0 bg-gray-500 opacity-75"
class="absolute inset-0 bg-gray-500 opacity-75" data-id="modal_backdrop" />
data-id="modal_backdrop" </div>
/> <span
</div> class="hidden sm:inline-block sm:align-middle sm:h-screen"
<span aria-hidden="true">&#8203;</span>
class="hidden sm:inline-block sm:align-middle sm:h-screen" <div
aria-hidden="true">&#8203;</span class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
> role="dialog"
<div aria-modal="true"
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" aria-labelledby="modal-headline">
role="dialog" <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
aria-modal="true" <div class="sm:flex sm:items-start">
aria-labelledby="modal-headline" <div
> class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <svg class="h-6 w-6 text-blue-600" fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.33 11.5h2.17A4.5 4.5 0 0116 16H9v1h8v-1a5.58 5.58 0 00-.89-3H19a5 5 0 014.52 2.85A13.15 13.15 0 0113 21c-2.76 0-5.1-.59-7-1.63v-9.3a6.97 6.97 0 013.33 1.43zM5 19a1 1 0 01-1 1H2a1 1 0 01-1-1v-9a1 1 0 011-1h2a1 1 0 011 1v9zM18 5a3 3 0 110 6 3 3 0 010-6zm-7-3a3 3 0 110 6 3 3 0 010-6z"/></svg>
<div class=""> </div>
<div <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" <h3 class="text-lg leading-6 font-medium text-gray-900">
> {$_('attention')}
<svg </h3>
class="size-6 text-blue-600" <div class="mt-2 mb-6">
fill="currentColor" <p class="text-sm text-gray-500">
xmlns="http://www.w3.org/2000/svg" {$_(
viewBox="0 0 24 24" 'do-you-want-to-delete-this-donor-with-all-related-donations'
><path fill="none" d="M0 0h24v24H0z" /><path )}
d="M9.33 11.5h2.17A4.5 4.5 0 0116 16H9v1h8v-1a5.58 5.58 0 00-.89-3H19a5 5 0 014.52 2.85A13.15 13.15 0 0113 21c-2.76 0-5.1-.59-7-1.63v-9.3a6.97 6.97 0 013.33 1.43zM5 19a1 1 0 01-1 1H2a1 1 0 01-1-1v-9a1 1 0 011-1h2a1 1 0 011 1v9zM18 5a3 3 0 110 6 3 3 0 010-6zm-7-3a3 3 0 110 6 3 3 0 010-6z" <br />
/></svg {$_('all-associated-donations-will-get-deleted-as-well')}
> </p>
</div> </div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto"> </div>
<h3 class="text-lg leading-6 font-medium text-gray-900"> </div>
{$_( </div>
"do-you-want-to-delete-this-donor-with-all-related-donations" <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
)} <button
</h3> on:click={deleteDonor}
<div class="mb-6"> type="button"
<p class="text-sm text-gray-500"> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_("all-associated-donations-will-get-deleted-as-well")} {$_('confirm-delete-donor-with-all-donations')}
</p> </button>
</div> <button
</div> on:click={cancelDelete}
</div> type="button"
</div> class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> {$_('cancel-keep-donor')}
<button </button>
on:click={deleteDonor} </div>
type="button" </div>
class="confirm_deletion_button" </div>
> </div>
{$_("confirm-delete-donor-with-all-donations")}
</button>
<button
on:click={cancelDelete}
type="button"
class="cancel_modal_button"
>
{$_("cancel-keep-donor")}
</button>
</div>
</div>
</div>
</div>
{/if} {/if}

View File

@ -1,14 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let address;
</script>
{#if !address || !address.address1}
{$_("no-address")}
{:else}
{address.address1}<br />
<!-- {address.address2 || ''}<br /> -->
{address.postalcode}
{address.city}
{address.country}
{/if}

View File

@ -1,413 +1,412 @@
<script> <script>
import { DonorService } from "@odit/lfk-client-js"; import { _ } from "svelte-i18n";
import { _ } from "svelte-i18n"; import store from "../../store";
import store from "../../store"; import { DonorService, DonationService } from "@odit/lfk-client-js";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
import isEmail from "validator/es/lib/isEmail"; import PromiseError from "../base/PromiseError.svelte";
import PromiseError from "../base/PromiseError.svelte"; import isEmail from "validator/es/lib/isEmail";
let data_loaded = false; import ConfirmDonorDeletion from "./ConfirmDonorDeletion.svelte";
export let params; let data_loaded = false;
$: delete_triggered = false; export let params;
$: original_data = {}; $: delete_triggered = false;
$: editable = {}; $: original_data = {};
$: changes_performed = !( $: editable = {};
JSON.stringify(original_data) === JSON.stringify(editable) $: current_donations = [];
); $: changes_performed = !(
$: isEmailValid = JSON.stringify(original_data) === JSON.stringify(editable)
(editable.email || "") === "" || );
(editable.email && isEmail(editable.email || "")); $: isEmailValid =
$: isFirstnameValid = editable.firstname !== ""; (editable.email || "") === "" ||
$: isLastnameValid = editable.lastname !== ""; (editable.email && isEmail(editable.email || ""));
$: save_enabled = $: isFirstnameValid = editable.firstname !== "";
changes_performed && $: isLastnameValid = editable.lastname !== "";
isFirstnameValid && $: save_enabled =
isLastnameValid && changes_performed &&
isEmailValid && isFirstnameValid &&
isPhoneValidOrEmpty && isLastnameValid &&
((isAddress1Valid && iszipcodevalid && iscityvalid) || isEmailValid &&
editable.address_checked === false); isPhoneValidOrEmpty &&
const promise = DonorService.donorControllerGetOne(params.donorid).then( ((isAddress1Valid && iszipcodevalid && iscityvalid) ||
(data) => { editable.address_checked === false);
data_loaded = true; const donation_promise = DonationService.donationControllerGetAll().then(
original_data = Object.assign(original_data, data); (val) => {
editable = Object.assign(editable, original_data); current_donations = val;
editable.address_checked = editable.address.address1 !== null; }
original_data.address_checked = editable.address.address1 !== null; );
if (editable.address_checked === false) { const promise = DonorService.donorControllerGetOne(params.donorid).then(
editable.address = { (data) => {
address1: "", data_loaded = true;
address2: "", original_data = Object.assign(original_data, data);
city: "", editable = Object.assign(editable, original_data);
postalcode: "", editable.address_checked = editable.address.address1 !== null;
country: "", original_data.address_checked = editable.address.address1 !== null;
}; if (editable.address_checked === false) {
} editable.address = {
} address1: "",
); address2: "",
$: isPhoneValidOrEmpty = city: "",
editable.phone?.includes("+") || postalcode: "",
editable.phone === "" || country: "",
editable.phone === null; };
$: isAddress1Valid = editable.address?.address1?.trim().length !== 0; }
$: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0; }
$: iscityvalid = editable.address?.city?.trim().length !== 0; );
function submit() { $: isPhoneValidOrEmpty =
if (data_loaded === true && save_enabled) { editable.phone?.includes("+") ||
toast($_("donor-is-being-updated")); editable.phone === "" ||
editable.address.country = "DE"; editable.phone === null;
if (editable.address_checked === false) { $: isAddress1Valid = editable.address?.address1?.trim().length !== 0;
editable.address = null; $: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0;
} $: iscityvalid = editable.address?.city?.trim().length !== 0;
if (editable.email) editable.email = editable.email; let modal_open = false;
else editable.email = null; let delete_donor = {};
if (editable.phone) editable.phone = editable.phone; function submit() {
else editable.phone = null; if (data_loaded === true && save_enabled) {
if (editable.middlename) editable.middlename = editable.middlename; Toastify({
editable.receiptNeeded = editable.address_checked; text: $_("donor-is-being-updated"),
DonorService.donorControllerPut(original_data.id, editable) duration: 2500,
.then((resp) => { }).showToast();
Object.assign(original_data, editable); editable.address.country = "DE";
original_data = original_data; if (editable.address_checked === false) {
toast.success($_("updated-donor")); editable.address = null;
}) }
.catch((err) => {}); if (editable.email) editable.email = editable.email;
} else { if (editable.phone) editable.phone = editable.phone;
} if (editable.middlename) editable.middlename = editable.middlename;
} editable.receiptNeeded = editable.address_checked;
function deleteDonor() { DonorService.donorControllerPut(original_data.id, editable)
DonorService.donorControllerRemove(original_data.id, true) .then((resp) => {
.then((resp) => { Object.assign(original_data, editable);
toast.success($_("donor-deleted")); original_data = original_data;
location.replace("./"); Toastify({
}) text: $_("updated-donor"),
.catch((err) => { duration: 2500,
console.log(err); backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}); }).showToast();
} })
.catch((err) => {});
} else {
}
}
function deleteDonor() {
DonorService.donorControllerRemove(original_data.id, false)
.then((resp) => {
Toastify({
text: $_("donor-deleted"),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
location.replace("./");
})
.catch((err) => {
modal_open = true;
delete_donor = original_data;
});
}
</script> </script>
{#await promise} <ConfirmDonorDeletion bind:modal_open bind:delete_donor />
{$_("loading-donor-details")} {#await promise && donation_promise}
{$_('loading-donor-details')}
{:then} {:then}
<section class="container p-5 select-none"> <section class="container p-5 select-none">
<div class="flex flex-row mb-4"> <div class="flex flex-row mb-4">
<div class="w-full"> <div class="w-full">
<nav class="w-full flex"> <nav class="w-full flex">
<ol class="list-none flex flex-row items-center justify-start"> <ol class="list-none flex flex-row items-center justify-start">
<li class="flex items-center"> <li class="flex items-center">
<a class="mr-2" href="./" <svg
><svg fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" viewBox="0 0 24 24"
height="24" width="24"
viewBox="0 0 24 24" height="24"><path fill="none" d="M0 0h24v24H0z" />
fill="none" <path
stroke="currentColor" d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" /></svg>
stroke-width="2" </li>
stroke-linecap="round" <li class="flex items-center ml-2">
stroke-linejoin="round" <a class="mr-2" href="./">{$_('donors')}</a><svg
class="inline-block" stroke="currentColor"
><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg fill="none"
> stroke-width="2"
{$_("donors")}</a viewBox="0 0 24 24"
> stroke-linecap="round"
</li> stroke-linejoin="round"
</ol> class="h-3 w-3 mr-2 stroke-current"
</nav> height="1em"
</div> width="1em"
</div> xmlns="http://www.w3.org/2000/svg"><line
<div class="mb-4 text-3xl font-extrabold leading-tight"> x1="5"
{original_data.firstname} y1="12"
{original_data.middlename || ""} x2="19"
{original_data.lastname} y2="12" />
<div data-id="donor_actions_${editable.id}"> <polyline points="12 5 19 12 12 19" /></svg>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:DELETE")} </li>
{#if delete_triggered} <li class="flex items-center">
<button <span class="mr-2">{original_data.firstname}
on:click={deleteDonor} {original_data.middlename || ''}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" {original_data.lastname}</span>
>{$_("confirm-deletion")}</button </li>
> </ol>
<button </nav>
on:click={() => { </div>
delete_triggered = !delete_triggered; </div>
}} <div class="mb-8 text-3xl font-extrabold leading-tight">
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm" {original_data.firstname}
>{$_("cancel")}</button {original_data.middlename || ''}
> {original_data.lastname}
{/if} <span data-id="donor_actions_${editable.id}">
{#if !delete_triggered} {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:DELETE')}
<button {#if delete_triggered}
on:click={() => { <button
delete_triggered = true; on:click={deleteDonor}
}} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:">{$_('confirm-deletion')}</button>
type="button" <button
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" on:click={() => {
>{$_("delete-donor")}</button delete_triggered = !delete_triggered;
> }}
{/if} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:">{$_('cancel')}</button>
{/if} {/if}
{#if !delete_triggered} {#if !delete_triggered}
<button <button
disabled={!save_enabled} on:click={() => {
class:opacity-50={!save_enabled} delete_triggered = true;
type="button" }}
on:click={submit} type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:">{$_('delete-donor')}</button>
>{$_("save-changes")}</button {/if}
> {/if}
{/if} {#if !delete_triggered}
</div> <button
</div> disabled={!save_enabled}
<!-- --> class:opacity-50={!save_enabled}
<div> type="button"
<span class="font-semibold text-gray-700" on:click={submit}
>{$_("total-donation-amount")}:</span class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:">{$_('save-changes')}</button>
> {/if}
<span </span>
>{(editable.donationAmount / 100) </div>
.toFixed(2) <!-- -->
.toLocaleString("de-DE", { valute: "EUR" })}€</span <div>
> <span
| class="font-medium text-gray-700">{$_('total-donation-amount')}:</span>
<span class="font-semibold text-gray-700">{$_("total-paid-amount")}:</span <span>{(editable.donationAmount / 100)
> .toFixed(2)
<span .toLocaleString('de-DE', { valute: 'EUR' })}€</span>
>{(editable.paidDonationAmount / 100) |
.toFixed(2) <span
.toLocaleString("de-DE", { valute: "EUR" })}€</span class="font-medium text-gray-700">{$_('total-paid-amount')}:</span>
> <span>{(editable.paidDonationAmount / 100)
<br /> .toFixed(2)
<span class="font-semibold text-gray-700">{$_("donations")}:</span> .toLocaleString('de-DE', { valute: 'EUR' })}€</span>
{#if original_data.donations.length > 0} <br />
{#each original_data.donations as d} <span class="font-medium text-gray-700">{$_('donations')}:</span>
{#if d.responseType === "DISTANCEDONATION"} {#if current_donations.filter((d) => d.donor.id == editable.id).length > 0}
<a {#each current_donations.filter((o) => o.donor.id == editable.id) as d}
href="../donations/{d.id}" {#if d.responseType === 'DISTANCEDONATION'}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1" <a
>{d.runner.firstname} href="../donations/{d.id}"
{d.runner.middlename || ""} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1">{d.runner.firstname}
{d.runner.lastname}</a {d.runner.middlename || ""}
> {d.runner.lastname}</a>
{:else} {:else}
<a <a
href="../donations/{d.id}" href="../donations/{d.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-700 text-white mr-1" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-700 text-white mr-1">{$_('fixed-donation')}:
>{$_("fixed-donation")}: {(d.amount / 100)
{(d.amount / 100) .toFixed(2)
.toFixed(2) .toLocaleString('de-DE', { valute: 'EUR' })}€</a>
.toLocaleString("de-DE", { valute: "EUR" })}€</a {/if}
> {/each}
{/if} {:else}{$_('donor-has-no-associated-donations')}{/if}
{/each} </div>
{:else}{$_("donor-has-no-associated-donations")}{/if} <div class=" w-full">
</div> <label
<div class="mt-2 w-full"> for="firstname"
<label for="firstname" class="font-semibold text-gray-700" class="font-medium text-gray-700">{$_('first-name')}</label>
>{$_("first-name")}</label <input
> autocomplete="off"
<input placeholder={$_('first-name')}
autocomplete="off" type="text"
placeholder={$_("first-name")} class:border-red-500={!isFirstnameValid}
type="text" class:focus:border-red-500={!isFirstnameValid}
class:border-red-500={!isFirstnameValid} class:focus:ring-red-500={!isFirstnameValid}
class:focus:border-red-500={!isFirstnameValid} bind:value={editable.firstname}
class:focus:ring-red-500={!isFirstnameValid} name="firstname"
bind:value={editable.firstname} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
name="firstname" {#if !isFirstnameValid}
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <span
/> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{#if !isFirstnameValid} {$_('first-name-is-required')}
<span </span>
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" {/if}
> </div>
{$_("first-name-is-required")} <div class=" w-full">
</span> <label
{/if} for="middlename"
</div> class="font-medium text-gray-700">{$_('middle-name')}</label>
<div class="mt-2 w-full"> <input
<label for="middlename" class="font-semibold text-gray-700" autocomplete="off"
>{$_("middle-name")}</label placeholder={$_('middle-name')}
> type="text"
<input bind:value={editable.middlename}
autocomplete="off" name="middlename"
placeholder={$_("middle-name")} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
type="text" </div>
bind:value={editable.middlename} <div class=" w-full">
name="middlename" <label
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" for="lastname"
/> class="font-medium text-gray-700">{$_('last-name')}</label>
</div> <input
<div class="mt-2 w-full"> autocomplete="off"
<label for="lastname" class="font-semibold text-gray-700" placeholder={$_('last-name')}
>{$_("last-name")}</label type="text"
> bind:value={editable.lastname}
<input class:border-red-500={!isLastnameValid}
autocomplete="off" class:focus:border-red-500={!isLastnameValid}
placeholder={$_("last-name")} class:focus:ring-red-500={!isLastnameValid}
type="text" name="lastname"
bind:value={editable.lastname} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
class:border-red-500={!isLastnameValid} {#if !isLastnameValid}
class:focus:border-red-500={!isLastnameValid} <span
class:focus:ring-red-500={!isLastnameValid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
name="lastname" {$_('last-name-is-required')}
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" </span>
/> {/if}
{#if !isLastnameValid} </div>
<span <div class=" w-full">
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <label
> for="email"
{$_("last-name-is-required")} class="font-medium text-gray-700">{$_('e-mail-adress')}</label>
</span> <input
{/if} autocomplete="off"
</div> placeholder={$_('e-mail-adress')}
<div class="mt-2 w-full"> type="email"
<label for="email" class="font-semibold text-gray-700" bind:value={editable.email}
>{$_("e-mail-adress")}</label class:border-red-500={!isEmailValid}
> class:focus:border-red-500={!isEmailValid}
<input class:focus:ring-red-500={!isEmailValid}
autocomplete="off" name="email"
placeholder={$_("e-mail-adress")} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
type="email" {#if !isEmailValid}
bind:value={editable.email} <span
class:border-red-500={!isEmailValid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class:focus:border-red-500={!isEmailValid} {$_('valid-email-is-required')}
class:focus:ring-red-500={!isEmailValid} </span>
name="email" {/if}
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" </div>
/> <div class=" w-full">
{#if !isEmailValid} <label for="phone" class="font-medium text-gray-700">{$_('phone')}</label>
<span <input
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" autocomplete="off"
> placeholder={$_('phone')}
{$_("valid-email-is-required")} type="tel"
</span> class:border-red-500={!isPhoneValidOrEmpty}
{/if} class:focus:border-red-500={!isPhoneValidOrEmpty}
</div> class:focus:ring-red-500={!isPhoneValidOrEmpty}
<div class="mt-2 w-full"> bind:value={editable.phone}
<label for="phone" class="font-semibold text-gray-700" name="phone"
>{$_("phone")}</label class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
> {#if !isPhoneValidOrEmpty}
<input <span
autocomplete="off" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
placeholder={$_("phone")} {$_('valid-international-phone-number-is-required')}
type="tel" </span>
class:border-red-500={!isPhoneValidOrEmpty} {/if}
class:focus:border-red-500={!isPhoneValidOrEmpty} </div>
class:focus:ring-red-500={!isPhoneValidOrEmpty} <div class="flex items-start mt-2">
bind:value={editable.phone} <div class="flex items-center h-5">
name="phone" <input
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" bind:checked={editable.address_checked}
/> id="comments"
{#if !isPhoneValidOrEmpty} name="comments"
<span type="checkbox"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
> </div>
{$_("valid-international-phone-number-is-required")} <div class="ml-3 ">
</span> <label
{/if} for="comments"
</div> class="font-medium text-gray-700">{$_('receipt-needed')}</label>
<div class="flex items-start mt-2"> </div>
<div class="flex items-center h-5"> </div>
<input {#if editable.address_checked === true}
bind:checked={editable.address_checked} <div class="col-span-6">
id="comments" <label
name="comments" for="address1"
type="checkbox" class="block font-medium text-gray-700">{$_('address')}</label>
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" <input
/> autocomplete="off"
</div> placeholder="Address"
<div class="ml-3"> class:border-red-500={!isAddress1Valid}
<label for="comments" class="font-semibold text-gray-700" class:focus:border-red-500={!isAddress1Valid}
>{$_("receipt-needed")}</label class:focus:ring-red-500={!isAddress1Valid}
> bind:value={editable.address.address1}
</div> type="text"
</div> name="address1"
{#if editable.address_checked === true} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
<div class="col-span-6"> {#if !isAddress1Valid}
<label for="address1" class="block font-medium text-gray-700" <span
>{$_("address")}</label class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('address-is-required')}
<input </span>
autocomplete="off" {/if}
placeholder="Address" </div>
class:border-red-500={!isAddress1Valid} <div class="col-span-6">
class:focus:border-red-500={!isAddress1Valid} <label
class:focus:ring-red-500={!isAddress1Valid} for="address2"
bind:value={editable.address.address1} class="block font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
type="text" <input
name="address1" autocomplete="off"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" placeholder={$_('apartment-suite-etc')}
/> bind:value={editable.address.address2}
{#if !isAddress1Valid} type="text"
<span name="address2"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
> </div>
{$_("address-is-required")} <div class="col-span-6">
</span> <label
{/if} for="zipcode"
</div> class="block font-medium text-gray-700">{$_('zip-postal-code')}</label>
<div class="col-span-6"> <input
<label for="address2" class="block font-medium text-gray-700" autocomplete="off"
>{$_("apartment-suite-etc")}</label placeholder={$_('zip-postal-code')}
> class:border-red-500={!iszipcodevalid}
<input class:focus:border-red-500={!iszipcodevalid}
autocomplete="off" class:focus:ring-red-500={!iszipcodevalid}
placeholder={$_("apartment-suite-etc")} bind:value={editable.address.postalcode}
bind:value={editable.address.address2} type="text"
type="text" name="zipcode"
name="address2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {#if !iszipcodevalid}
/> <span
</div> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
<div class="col-span-6"> {$_('valid-zipcode-postal-code-is-required')}
<label for="zipcode" class="block font-medium text-gray-700" </span>
>{$_("zip-postal-code")}</label {/if}
> </div>
<input <div class="col-span-6">
autocomplete="off" <label
placeholder={$_("zip-postal-code")} for="city"
class:border-red-500={!iszipcodevalid} class="block font-medium text-gray-700">{$_('city')}</label>
class:focus:border-red-500={!iszipcodevalid} <input
class:focus:ring-red-500={!iszipcodevalid} autocomplete="off"
bind:value={editable.address.postalcode} placeholder={$_('city')}
type="text" class:border-red-500={!iscityvalid}
name="zipcode" class:focus:border-red-500={!iscityvalid}
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class:focus:ring-red-500={!iscityvalid}
/> bind:value={editable.address.city}
{#if !iszipcodevalid} type="text"
<span name="city"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
> {#if !iscityvalid}
{$_("valid-zipcode-postal-code-is-required")} <span
</span> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{/if} {$_('valid-city-is-required')}
</div> </span>
<div class="col-span-6"> {/if}
<label for="city" class="block font-medium text-gray-700" </div>
>{$_("city")}</label {/if}
> </section>
<input
autocomplete="off"
placeholder={$_("city")}
class:border-red-500={!iscityvalid}
class:focus:border-red-500={!iscityvalid}
class:focus:ring-red-500={!iscityvalid}
bind:value={editable.address.city}
type="text"
name="city"
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm: border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
{#if !iscityvalid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
>
{$_("valid-city-is-required")}
</span>
{/if}
</div>
{/if}
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@ -1,29 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let donations;
</script>
{#if !donations || donations.length == 0}
{$_("donor-has-no-associated-donations")}
{:else}
{#each donations as donation}
{#if donation.responseType === "DISTANCEDONATION"}
<a
href="../donations/{donation.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1"
>{donation.runner.firstname}
{donation.runner.middlename || ""}
{donation.runner.lastname}</a
>
{:else}
<a
href="../donations/{donation.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-700 text-white mr-1"
>{$_("fixed-donation")}:
{(donation.amount / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })}€</a
>
{/if}
{/each}
{/if}

View File

@ -5,73 +5,50 @@
import DonorsOverview from "./DonorsOverview.svelte"; import DonorsOverview from "./DonorsOverview.svelte";
$: current_donors = []; $: current_donors = [];
export let modal_open = false; export let modal_open = false;
let addDonors;
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_("donors")} {$_('donors')}
</h4> {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:CREATE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:CREATE")} <button
<button on:click={() => {
on:click={() => { modal_open = true;
modal_open = true; }}
}} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" {$_('add-donor')}
> </button>
{$_("add-donor")} {/if}
</button> {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:GET')}
{/if} <button
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")} on:click={() => {
<button const data = (current_donors.filter(d=>d.receiptNeeded===true)).map(function (d) {
on:click={() => { d.address.address2=d.address.address2===""?"":" "+d.address.address2;
const data = current_donors const address=`${d.address.address1}${d.address.address2}, ${d.address.postalcode} ${d.address.city}, ${d.address.country}`;
.filter((d) => d.receiptNeeded === true) return [d.firstname,d.middlename,d.lastname,d.paidDonationAmount,address];
.map(function (d) { })
d.address.address2 = let csv = `${$_('csv_import__firstname')};${$_('csv_import__middlename')};${$_('csv_import__lastname')};${$_('total_donation_amount_in_eur')};${$_('address')}\n`;
d.address.address2 === "" ? "" : " " + d.address.address2; data.forEach(function(row) {
const address = `${d.address.address1}${d.address.address2}, ${d.address.postalcode} ${d.address.city}, ${d.address.country}`; csv += row.join(';');
return [ csv += "\n";
d.firstname, });
d.middlename, let hiddenElement = document.createElement('a');
d.lastname, hiddenElement.href = 'data:text/csv;charset=utf-8,' + encodeURI(csv);
(d.paidDonationAmount/100).toFixed(2), hiddenElement.target = '_blank';
address, hiddenElement.download = `${$_('filename_sponsoringquittungsliste')}.csv`;
]; hiddenElement.click();
}); hiddenElement.remove();
let csv = `${$_("csv_import__firstname")};${$_( }}
"csv_import__middlename" type="button"
)};${$_("csv_import__lastname")};${$_( class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
"total_donation_amount_in_eur" {$_('sponsoring-quittungs-liste_herunterladen')}
)};${$_("address")}\n`; </button>
data.forEach(function (row) { {/if}
csv += row.join(";"); </span>
csv += "\n"; <DonorsOverview bind:current_donors />
});
let hiddenElement = document.createElement("a");
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv);
hiddenElement.target = "_blank";
hiddenElement.download = `${$_(
"filename_sponsoringquittungsliste"
)}.csv`;
hiddenElement.click();
hiddenElement.remove();
}}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
>
{$_("sponsoring-quittungs-liste_herunterladen")}
</button>
{/if}
<DonorsOverview bind:current_donors bind:addDonors />
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:CREATE')}
<AddDonorModal <AddDonorModal bind:current_donors bind:modal_open />
on:created={(event) => {
addDonors(event.detail.donors);
}}
bind:modal_open
/>
{/if} {/if}

View File

@ -6,7 +6,7 @@
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="w-full" style="height:15rem" src={donors_empty} alt="" /> <img class="w-full" style="height:15rem" src={donors_empty} alt="" />
<span class="font-bold">{$_("there-are-no-donors-yet")}</span><br /> <span class="font-bold">{$_('there-are-no-donors-yet')}</span><br />
<span>{$_("add-your-first-donor")}</span> <span>{$_('add-your-first-donor')}</span>
</p> </p>
</div> </div>

View File

@ -1,262 +1,218 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { DonorService } from "@odit/lfk-client-js"; import { DonationService, DonorService } from "@odit/lfk-client-js";
import store from "../../store"; import store from "../../store";
import DonorsEmptyState from "./DonorsEmptyState.svelte"; import DonorsEmptyState from "./DonorsEmptyState.svelte";
import ConfirmDonorDeletion from "./ConfirmDonorDeletion.svelte"; import ConfirmDonorDeletion from "./ConfirmDonorDeletion.svelte";
import TableBottom from "../shared/TableBottom.svelte"; import Toastify from "toastify-js";
import {
createSvelteTable,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
renderComponent,
} from "@tanstack/svelte-table";
import { writable } from "svelte/store";
import { onMount } from "svelte";
import InputElement from "../shared/InputElement.svelte";
import TableHeader from "../shared/TableHeader.svelte";
import TableActions from "../shared/TableActions.svelte";
import DonorAddress from "./DonorAddress.svelte";
import DonorDonations from "./DonorDonations.svelte";
import { filterAddress, filterName } from "../shared/tablefilters";
import toast from "svelte-french-toast";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
$: selectedDonors = $: current_donations = [];
$table?.getSelectedRowModel().rows.map((row) => row.original) || []; let modal_open = false;
$: selected = let delete_donor = {};
$table?.getSelectedRowModel().rows.map((row) => row.index) || [];
$: dataLoaded = false;
export let current_donors = []; export let current_donors = [];
export const addDonors = (donors) => { const donors_promise = DonorService.donorControllerGetAll().then((val) => {
current_donors = current_donors.concat(...donors); current_donors = val;
options.update((options) => ({
...options,
data: current_donors,
}));
};
//Section table
const columns = [
{
accessorKey: "id",
header: () => "id",
filterFn: `equalsString`,
},
{
accessorKey: "name",
header: () => $_("name"),
cell: (info) => {
const d = info.row.original;
if (d.middlename) {
return `${d.firstname} ${d.middlename} ${d.lastname}`;
} else {
return `${d.firstname} ${d.lastname}`;
}
},
filterFn: `name`,
},
{
accessorKey: "address",
header: () => $_("contact-information"),
cell: (info) => {
return renderComponent(DonorAddress, { address: info.getValue() });
},
filterFn: `address`,
},
{
accessorKey: "donations",
header: () => $_("sponsorings"),
cell: (info) => {
return renderComponent(DonorDonations, { donations: info.getValue() });
},
enableColumnFilter: false,
},
{
accessorKey: "donationAmount",
header: () => $_("total-donation-amount"),
cell: (info) => {
return `${(info.getValue() / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })}€`;
},
enableColumnFilter: false,
},
{
accessorKey: "paidDonationAmount",
header: () => $_("total-paid-amount"),
cell: (info) => {
return `${(info.getValue() / 100)
.toFixed(2)
.toLocaleString("de-DE", { valute: "EUR" })}€`;
},
enableColumnFilter: false,
},
{
accessorKey: "actions",
header: () => $_("action"),
cell: (info) => {
return renderComponent(TableActions, {
detailsLink: `./${info.row.original.id}`,
deleteAction: () => {
active_deletes = current_donors.filter(
(r) => r.id == info.row.original.id
);
},
deleteEnabled:
store.state.jwtinfo.userdetails.permissions.includes(
"DONOR:DELETE"
),
});
},
enableColumnFilter: false,
enableSorting: false,
},
];
const options = writable({
data: [],
columns: columns,
initialState: {
pagination: {
pageSize: 50,
},
},
filterFns: {
name: filterName,
address: filterAddress,
},
enableRowSelection: true,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
}); });
const table = createSvelteTable(options); const donation_promise = DonationService.donationControllerGetAll().then(
(val) => {
current_donations = val;
}
);
function should_display_based_on_id(id) { function should_display_based_on_id(id) {
if (searchvalue.toString().slice(-1) === "*") { if (searchvalue.toString().slice(-1) === "*") {
return id.toString().startsWith(searchvalue.replace("*", "")); return id.toString().startsWith(searchvalue.replace("*", ""));
} }
return id.toString() === searchvalue; return id.toString() === searchvalue;
} }
onMount(async () => {
let page = 0;
let pagesize = 300;
while (page >= 0) {
const donors = await DonorService.donorControllerGetAll(page, pagesize);
if (donors.length == 0) {
page = -2;
}
current_donors = current_donors.concat(...donors);
options.update((options) => ({
...options,
data: current_donors,
}));
dataLoaded = true;
page++;
}
});
</script> </script>
<ConfirmDonorDeletion <ConfirmDonorDeletion
on:cancelDelete={(event) => { on:cancelDelete={(event) => {
active_deletes = active_deletes.filter((a) => a.id !== event.detail.id); modal_open = false;
active_deletes[event.detail.id] = false;
}} }}
on:delete={async (event) => { bind:modal_open
toast.loading($_("deleting-donor")); bind:delete_donor />
await DonorService.donorControllerRemove(event.detail.id, true); {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:GET')}
toast.dismiss(); {#await donors_promise && donation_promise}
toast.success($_("donor-deleted"));
current_donors = current_donors.filter((d) => d.id !== event.detail.id);
active_deletes = active_deletes.filter((a) => a.id !== event.detail.id);
options.update((options) => ({
...options,
data: current_donors,
}));
}}
modal_open={active_deletes.length > 0}
delete_donor={active_deletes[0]}
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")}
{#if !dataLoaded}
<div <div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert" role="alert">
> <p class="font-bold">{$_('donors-are-being-loaded')}</p>
<p class="font-bold">{$_("donors-are-being-loaded")}</p> <p class="text-sm">{$_('this-might-take-a-moment')}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div> </div>
{:else if current_donors.length === 0} {:then}
<DonorsEmptyState /> {#if current_donors.length === 0}
{:else} <DonorsEmptyState />
<input {:else}
type="search" <input
bind:value={searchvalue} type="search"
placeholder={$_("datatable.search")} bind:value={searchvalue}
aria-label={$_("datatable.search")} placeholder={$_('datatable.search')}
class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" aria-label={$_('datatable.search')}
/> class="gridjs-input gridjs-search-input mb-4" />
<div <div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll" class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
> <table class="divide-y divide-gray-200 w-full">
<table class="w-full"> <thead class="bg-gray-50">
<thead class="border-b border-gray-400"> <tr>
{#each $table.getHeaderGroups() as headerGroup} <th
<tr class="select-none"> scope="col"
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px"> class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<InputElement {$_('name')}
type="checkbox" </th>
checked={$table.getIsAllRowsSelected()} <th
indeterminate={$table.getIsSomeRowsSelected()} scope="col"
on:change={() => $table.toggleAllRowsSelected()} class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
/> {$_('contact-information')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('donations')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('total-donation-amount')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('total-paid-amount')}
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">{$_('action')}</span>
</th> </th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr> </tr>
{/each} </thead>
</thead> <tbody class="divide-y divide-gray-200">
<tbody> {#each current_donors as donor}
{#each $table.getRowModel().rows as row} {#if donor.firstname
<tr class="odd:bg-white even:bg-gray-100"> .toLowerCase()
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px"> .includes(
<InputElement searchvalue.toLowerCase()
type="checkbox" ) || donor.lastname
checked={row.getIsSelected()} .toLowerCase()
on:change={() => row.toggleSelected()} .includes(
/> searchvalue.toLowerCase()
</td> ) || should_display_based_on_id(donor.id)}
{#each row.getVisibleCells() as cell} <tr data-rowid="donor_{donor.id}">
<td> <td class="px-6 py-4 whitespace-nowrap">
<svelte:component <div class="flex items-center">
this={flexRender( <div class="ml-4">
cell.column.columnDef.cell, <div class="text-sm font-medium text-gray-900">
cell.getContext() {donor.firstname}
)} {donor.middlename || ''}
/> {donor.lastname}
</td> </div>
{/each} </div>
</tr> </div>
{/each} </td>
</tbody> <td class="px-6 py-4 whitespace-nowrap">
</table> {#if donor.email}
<div class="text-sm text-gray-500">{donor.email}</div>
{/if}
{#if donor.phone}
<div class="text-sm text-gray-500">{donor.phone}</div>
{/if}
{#if donor.address.address1 !== null}
{donor.address.address1}<br />
{donor.address.address2 || ''}<br />
{donor.address.postalcode}
{donor.address.city}
{donor.address.country}
{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{#if current_donations.filter((d) => d.donor.id == donor.id).length > 0}
{#each current_donations.filter((o) => o.donor.id == donor.id) as d}
{#if d.responseType === 'DISTANCEDONATION'}
<a
href="../donations/{d.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1">{d.runner.firstname}
{d.runner.middlename || ''}
{d.runner.lastname}</a>
{:else}
<a
href="../donations/{d.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-700 text-white mr-1">{$_('fixed-donation')}:
{(d.amount / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€</a>
{/if}
{/each}
{:else}{$_('donor-has-no-associated-donations')}{/if}
</td>
<td class="px-6 py-4 whitespace-nowrap">
{(donor.donationAmount / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€
</td>
<td class="px-6 py-4 whitespace-nowrap">
{(donor.paidDonationAmount / 100)
.toFixed(2)
.toLocaleString('de-DE', { valute: 'EUR' })}€
</td>
{#if active_deletes[donor.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {
active_deletes[donor.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
<button
on:click={() => {
DonorService.donorControllerRemove(donor.id, false)
.then((resp) => {
current_donors = current_donors.filter((obj) => obj.id !== donor.id);
Toastify({
text: 'Donor deleted',
duration: 500,
backgroundColor:
'linear-gradient(to right, #00b09b, #96c93d)',
}).showToast();
})
.catch((err) => {
modal_open = true;
delete_donor = donor;
});
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a
href="./{donor.id}"
class="text-indigo-600 hover:text-indigo-900">{$_('details')}</a>
{#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:DELETE')}
<button
on:click={() => {
active_deletes[donor.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
{/if}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div> </div>
<div class="h-2" /> {/await}
<TableBottom {table} {selected} />
{/if}
{/if} {/if}
<style>
table tbody tr td:nth-child(2) {
font-family: monospace;
}
</style>

View File

@ -1,196 +1,193 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
let modal_open = false; export let modal_open;
(function () { (function () {
document.onkeydown = function (e) { document.onkeydown = function (e) {
e = e || window.event; e = e || window.event;
if (e.key === "Escape") { if (e.key === "Escape") {
modal_open = false; modal_open = false;
} }
}; };
})(); })();
const license_promise = fetch("/licenses.json"); const license_promise = fetch("/licenses.json");
let licenses = []; let licenses = [];
$: currentlicense = ""; $: currentlicense = "";
$: licensetext = ""; $: licensetext = "";
license_promise license_promise
.then((response) => response.json()) .then((response) => response.json())
.then((json) => { .then((json) => {
licenses = json; licenses = json;
}); });
</script> </script>
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside use:focusTrap
on:click_outside={() => { use:clickOutside
modal_open = false; on:click_outside={() => {
}} modal_open = false;
> }}>
<div <div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 sm:block sm:p-0" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div
<div class="absolute inset-0 bg-gray-500 opacity-75"
class="absolute inset-0 bg-gray-500 opacity-75" data-id="modal_backdrop" />
data-id="modal_backdrop" </div>
/> <span
</div> class="hidden sm:inline-block sm:align-middle sm:h-screen"
<span aria-hidden="true">&#8203;</span>
class="hidden sm:inline-block sm:align-middle sm:h-screen" <div
aria-hidden="true">&#8203;</span class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
> role="dialog"
<div aria-modal="true"
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" aria-labelledby="modal-headline">
role="dialog" <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
aria-modal="true" <div class="sm:flex sm:items-start">
aria-labelledby="modal-headline" <div
> class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <svg
<div class=""> fill="currentColor"
<div class="h-6 w-6 text-blue-600"
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"
<svg width="24"
fill="currentColor" height="24"><path fill="none" d="M0 0h24v24H0z" />
class="size-6 text-blue-600" <path
xmlns="http://www.w3.org/2000/svg" d="M14 20v2H2v-2h12zM14.586.686l7.778 7.778L20.95 9.88l-1.06-.354L17.413 12l5.657 5.657-1.414 1.414L16 13.414l-2.404 2.404.283 1.132-1.415 1.414-7.778-7.778 1.415-1.414 1.13.282 6.294-6.293-.353-1.06L14.586.686z" /></svg>
viewBox="0 0 24 24" </div>
width="24" <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
height="24" <h3 class="text-lg leading-6 font-medium">
><path fill="none" d="M0 0h24v24H0z" /> {$_('read-license')}
<path </h3>
d="M14 20v2H2v-2h12zM14.586.686l7.778 7.778L20.95 9.88l-1.06-.354L17.413 12l5.657 5.657-1.414 1.414L16 13.414l-2.404 2.404.283 1.132-1.415 1.414-7.778-7.778 1.415-1.414 1.13.282 6.294-6.293-.353-1.06L14.586.686z" <div class="mt-2 mb-6">
/></svg <p class="text-sm text-gray-500">{currentlicense}</p>
> </div>
</div> <div class="mt-2 mb-6">
<div class="mt-3 sm:mt-0 sm:ml-4 sm:text-left"> <p class="text-sm text-gray-500">{licensetext}</p>
<h3 class="text-lg leading-6 font-medium"> </div>
{$_("read-license")} </div>
</h3> </div>
<div class="mb-6"> </div>
<p class="text-sm text-gray-500">{currentlicense}</p> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
</div> <button
<div class="mb-6"> on:click={() => {
<p class="text-sm text-gray-500">{licensetext}</p> modal_open = false;
</div> }}
</div> type="button"
</div> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
</div> {$_('close')}
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> </button>
<button </div>
on:click={() => { </div>
modal_open = false; </div>
}} </div>
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm"
>
{$_("close")}
</button>
</div>
</div>
</div>
</div>
{/if} {/if}
<!-- /// --> <!-- /// -->
<section class="container p-5"> <div class="pt-12 px-4 sm:px-6 lg:px-8 lg:pt-20 bg-gray-900 pb-12">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <div class="text-center mb-8">
{$_("about")} <h1
</h4> class="mt-9 font-display text-4xl leading-none font-semibold text-white sm:text-5xl lg:text-6xl">
<p class="mt-2 mb-2"> {$_('about')}
Lauf für Kaya! 🧾
<strong class="font-medium"> </h1>
{$_("by")} <p
<a href="https://odit.services" class="underline">ODIT.Services</a> class="mt-2 max-w-xl mx-auto text-xl lg:max-w-3xl lg:text-2xl text-gray-300">
</strong> Lauf für Kaya!
<br /> <strong class="text-white font-medium">
<span>{$_("lfk-is-os")}</span> {$_('by')}
</p> <a href="https://odit.services" class="underline">ODIT.Services</a>
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> </strong>
{$_("credits")} <br />
</h4> <span class="text-lg">{$_('lfk-is-os')}</span>
<p class="text-left">{$_("oss_credit_description")}</p> </p>
<div class="mt-5 overflow-x-auto"> </div>
{#await license_promise} </div>
<p>{$_("licenses-are-being-loaded")}</p>
{:then} <div class="pt-0 pb-16 overflow-hidden lg:pt-12 lg:py-24">
<table class="font-mono"> <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<thead class="border-b border-gray-400"> <h2 class="text-4xl font-display font-semibold md:text-5xl">
<tr class="odd:bg-white even:bg-gray-100"> {$_('credits')}
<th>{$_("dependency_name")}</th> </h2>
<th>{$_("license")}</th> <div class="max-w-3xl mx-auto text-xl leading-8 font-medium mt-8">
<th>{$_("repo_link")}</th> <p class="text-center">{$_('oss_credit_description')}</p>
<th>{$_("installed-version")}</th> </div>
<th>{$_("author")}</th> <div class="w-screen leading-8 pl-5 mt-5">
</tr> {#await license_promise}
</thead> <p class="text-center w-full">{$_('licenses-are-being-loaded')}</p>
<tbody> {:then}
{#each licenses as l} <table>
<tr class="odd:bg-white even:bg-gray-100 *:p-2"> <thead>
<td>{l.name}</td> <tr>
<td> <th>{$_('dependency_name')}</th>
<button <th>{$_('license')}</th>
class="underline cursor-pointer" <th>{$_('repo_link')}</th>
on:click={() => { <th>{$_('installed-version')}</th>
modal_open = true; <th>{$_('author')}</th>
currentlicense = l.name + "@" + l.version; </tr>
licensetext = </thead>
l.licensetext || $_("no-license-text-could-be-found"); <tbody>
}}>{l.license || "?"}</button {#each licenses as l}
> <tr>
</td> <td>{l.name}</td>
<td> <td>
{(l.repo?.url || l.repo) {l.license || '?'}<br /><span
.replace("git+", "") class="underline cursor-pointer"
.replace("git://", "")} on:click={() => {
</td> modal_open = true;
<td>{l.version || "?"}</td> currentlicense = l.name + '@' + l.version;
<td>{l.author?.name || l.author || "?"}</td> licensetext = l.licensetext || $_('no-license-text-could-be-found');
</tr> }}>{$_('read-license')}</span>
{/each} </td>
</tbody> <td>
</table> {(l.repo?.url || l.repo)
{:catch error} .replace('git+', '')
<div .replace('git://', '')}
class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500" </td>
> <td>{l.version || '?'}</td>
<span class="inline-block align-middle mr-8"> <td>{l.author?.name || l.author || '?'}</td>
<b>{$_("general_promise_error")}</b> </tr>
{error} {/each}
</span> </tbody>
</div> </table>
{/await} {:catch error}
</div> <div
<div class="w-full mt-8"> class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<p class="font-medium">{$_("icon-image-credits")}</p> <span class="inline-block align-middle mr-8">
<ul class="list-disc ml-6"> <b class="capitalize">{$_('general_promise_error')}</b>
<li> {error}
<a </span>
class="underline" </div>
target="_blank" {/await}
rel="noopener noreferrer" </div>
href="https://storyset.com">https://storyset.com</a <div class="w-full leading-8 mt-8">
> <p class="text-xl font-medium">{$_('icon-image-credits')}</p>
</li> <ul class="list-disc">
<li> <li>
<a <a
class="underline" class="underline"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href="https://undraw.co">https://undraw.co</a href="https://storyset.com">https://storyset.com</a>
> </li>
</li> <li>
<li> <a
<a class="underline"
class="underline" target="_blank"
target="_blank" rel="noopener noreferrer"
rel="noopener noreferrer" href="https://undraw.co">https://undraw.co</a>
href="https://remixicon.com">https://remixicon.com</a </li>
> <li>
</li> <a
</ul> class="underline"
</div> target="_blank"
</section> rel="noopener noreferrer"
href="https://remixicon.com">https://remixicon.com</a>
</li>
</ul>
</div>
</div>
</div>

View File

@ -1,55 +1,46 @@
<script> <script>
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
$: releaseinfo = ""; $: releaseinfo = "";
onMount(() => { onMount(() => {
releaseinfo = document releaseinfo = document
.getElementById("buildinfo") .getElementById("buildinfo")
.textContent.replace("RELEASE_INFO-", "") .textContent.replace("RELEASE_INFO-", "")
.replace("-RELEASE_INFO", ""); .replace("-RELEASE_INFO", "");
}); });
const year = new Date().getFullYear(); const year = new Date().getFullYear();
</script> </script>
<footer class="p-5 w-full"> <footer class="p-5 w-full">
<p class="text-sm text-gray-500 mt-4"> <p class="text-sm text-gray-500 mt-4">
Lauf für Kaya! Läufersystem - Copyright © Lauf für Kaya! Läufersystem - Copyright ©
{year} {year}
+ proudly powered by + proudly powered by
<a <a
class="underline" class="underline"
href="https://odit.services" href="https://odit.services"
rel="noopener,noreferrer" rel="noopener,noreferrer"
target="_blank">ODIT.Services</a target="_blank">ODIT.Services</a>
> </p>
</p> <p class="text-sm text-gray-500 mt-4">
<p class="text-sm text-gray-500 mt-4"> <a
<a class="underline"
class="underline" target="_blank"
target="_blank" rel="noopener, noreferrer"
rel="noopener, noreferrer" href="https://git.odit.services/lfk/frontend/">LfK!Frontend</a>@<a
href="https://git.odit.services/lfk/frontend/">LfK!Frontend</a class="underline"
>@<a target="_blank"
class="underline" rel="noopener, noreferrer"
target="_blank" href="https://git.odit.services/lfk/frontend/src/tag/{releaseinfo}">{releaseinfo}</a>
rel="noopener, noreferrer" -
href="https://git.odit.services/lfk/frontend/src/tag/{releaseinfo}" <a
>{releaseinfo}</a rel="noopener, noreferrer"
> class="underline"
- href="https://docs.lauf-fuer-kaya.de"
<a target="_blank">{$_('documentation')}</a>
rel="noopener, noreferrer" -
class="underline" <a class="underline" href="/privacy">{$_('privacy')}</a>
href="https://docs.lauf-fuer-kaya.de" -
target="_blank">{$_("documentation")}</a <a class="underline" href="/imprint">{$_('imprint')}</a>
> </p>
-
<a class="underline" href="https://lauf-fuer-kaya.de/datenschutz/"
>{$_("privacy")}</a
>
-
<a class="underline" href="https://lauf-fuer-kaya.de/impressum/"
>{$_("imprint")}</a
>
</p>
</footer> </footer>

View File

@ -0,0 +1,50 @@
<script>
import { _, getLocaleFromNavigator } from "svelte-i18n";
import marked from "marked";
import Footer from "./Footer.svelte";
import * as css from "../base/simple.css";
let html = "";
async function load() {
let md = await fetch("/imprint_" + getLocaleFromNavigator() + ".md");
let text = (await md.text()).toString();
if(text.includes("<meta")){
md.ok=false
}
if (!md.ok) {
md = await fetch("/imprint_en.md");
text = await md.text();
}
html = marked(text);
}
const promise = load();
</script>
<div class="pt-12 px-4 sm:px-6 lg:px-8 lg:pt-20 bg-gray-900 pb-12">
<div class="text-center mb-8">
<h1
class="mt-9 font-display text-4xl leading-none font-semibold text-white sm:text-5xl lg:text-6xl">
{$_('imprint')}
</h1>
</div>
</div>
<div class="pt-0 pb-16 overflow-hidden lg:pt-12 lg:py-24">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{#await promise}
<p class="text-center w-full">{$_('imprint-loading')}</p>
{:then}
<div class="simplecontent">
{@html html}
</div>
{:catch error}
<div
class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}
</div>
</div>
<Footer />

View File

@ -4,22 +4,19 @@
<body class="antialiased font-sans"> <body class="antialiased font-sans">
<div class="flex min-h-screen"> <div class="flex min-h-screen">
<div class="w-full bg-white flex items-center justify-center"> <div class="w-full bg-white flex items-center justify-center ">
<div class="max-w-sm m-8"> <div class="max-w-sm m-8">
<div class="text-black text-5xl md:text-15xl font-black"> <div class="text-black text-5xl md:text-15xl font-black">
{$_("404title")} {$_('404title')}
</div> </div>
<div class="w-16 h-1 bg-purple-light my-3 md:my-6" /> <div class="w-16 h-1 bg-purple-light my-3 md:my-6" />
<p <p
class="text-grey-darker text-2xl md:text-3xl font-light mb-8 leading-normal" class="text-grey-darker text-2xl md:text-3xl font-light mb-8 leading-normal">
> {$_('404message')}
{$_("404message")}
</p> </p>
<a <a
href="/" href="/"
class="bg-transparent text-grey-darkest font-bold uppercase tracking-wide py-3 px-6 border-2 border-grey-light hover:border-grey rounded-lg" class="bg-transparent text-grey-darkest font-bold uppercase tracking-wide py-3 px-6 border-2 border-grey-light hover:border-grey rounded-lg">{$_('goback')}</a>
>{$_("goback")}</a
>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,50 @@
<script>
import { _, getLocaleFromNavigator } from "svelte-i18n";
import marked from "marked";
import Footer from "./Footer.svelte";
import * as css from "../base/simple.css";
let html = "";
async function load() {
let md = await fetch("/privacy_" + getLocaleFromNavigator() + ".md");
let text = (await md.text()).toString();
if(text.includes("<meta")){
md.ok=false
}
if (!md.ok) {
md = await fetch("/privacy_en.md");
text = await md.text();
}
html = marked(text);
}
const promise = load();
</script>
<div class="pt-12 px-4 sm:px-6 lg:px-8 lg:pt-20 bg-gray-900 pb-12">
<div class="text-center mb-8">
<h1
class="mt-9 font-display text-4xl leading-none font-semibold text-white sm:text-5xl lg:text-6xl">
{$_('privacy')}
</h1>
</div>
</div>
<div class="pt-0 pb-16 overflow-hidden lg:pt-12 lg:py-24">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{#await promise}
<p class="text-center w-full">{$_('privacy-loading')}</p>
{:then}
<div class="simplecontent">
{@html html}
</div>
{:catch error}
<div
class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}
</div>
</div>
<Footer />

View File

@ -0,0 +1,82 @@
<script>
let open = false;
</script>
<div class="md:flex flex-col md:flex-row h-screen w-full">
<div
class="flex flex-col w-full md:w-64 text-gray-700 bg-white dark-mode:text-gray-200 dark-mode:bg-gray-800 flex-shrink-0">
<div
class="flex-shrink-0 px-8 py-4 flex flex-row items-center justify-between">
<a
href="/#/test"
class="text-lg font-semibold tracking-widest text-gray-900 uppercase rounded-lg dark-mode:text-white focus:outline-none focus:shadow-outline">Sidebar</a>
<button
class="rounded-lg md:hidden focus:outline-none focus:shadow-outline">
<svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
{#if open}
<path
fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
{/if}
{#if !open}
<path
fill-rule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z"
clip-rule="evenodd" />
{/if}
</svg>
</button>
</div>
<nav
:class:block={open}
:class:hidden={!open}
class="flex-grow md:block px-4 pb-4 md:pb-0 md:overflow-y-auto">
<a
class="block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-gray-200 rounded-lg dark-mode:bg-gray-700 dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Blog</a>
<a
class="block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Portfolio</a>
<a
class="block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">About</a>
<a
class="block px-4 py-2 mt-2 text-sm font-semibold text-gray-900 bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Contact</a>
<div class="relative">
<button
on:click={() => {
open = !open;
}}
class="flex flex-row items-center w-full px-4 py-2 mt-2 text-sm font-semibold text-left bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:focus:bg-gray-600 dark-mode:hover:bg-gray-600 md:block hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline">
<span>Dropdown</span>
<svg
fill="currentColor"
viewBox="0 0 20 20"
class="inline w-4 h-4 mt-1 ml-1 transition-transform duration-200 transform md:-mt-1"><path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" /></svg>
</button>
<div
class:block={open}
class:hidden={!open}
class="absolute right-0 w-full mt-2 origin-top-right rounded-md shadow-lg">
<div
class="px-2 py-2 bg-white rounded-md shadow dark-mode:bg-gray-800">
<a
class="block px-4 py-2 mt-2 text-sm font-semibold bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 md:mt-0 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Link #1</a>
<a
class="block px-4 py-2 mt-2 text-sm font-semibold bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 md:mt-0 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Link #2</a>
<a
class="block px-4 py-2 mt-2 text-sm font-semibold bg-transparent rounded-lg dark-mode:bg-transparent dark-mode:hover:bg-gray-600 dark-mode:focus:bg-gray-600 dark-mode:focus:text-white dark-mode:hover:text-white dark-mode:text-gray-200 md:mt-0 hover:text-gray-900 focus:text-gray-900 hover:bg-gray-200 focus:bg-gray-200 focus:outline-none focus:shadow-outline"
href="#">Link #3</a>
</div>
</div>
</div>
</nav>
</div>
</div>

View File

@ -1,8 +1,9 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import Toastify from "toastify-js";
import { UserGroupService } from "@odit/lfk-client-js"; import { UserGroupService } from "@odit/lfk-client-js";
import toast from "svelte-french-toast";
export let modal_open; export let modal_open;
export let current_groups; export let current_groups;
let description_input_value; let description_input_value;
@ -31,7 +32,10 @@
function submit() { function submit() {
if (processed_last_submit === true) { if (processed_last_submit === true) {
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("group-is-being-added")); const toast = Toastify({
text: $_('group-is-being-added'),
duration: -1,
}).showToast();
let postdata = { let postdata = {
name: name_input_value, name: name_input_value,
description: description_input_value, description: description_input_value,
@ -42,8 +46,11 @@
description_input_value = ""; description_input_value = "";
modal_open = false; modal_open = false;
// //
toast.dismiss(); Toastify({
toast.success($_("group-added")); text: $_('group-added'),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_groups.push(result); current_groups.push(result);
current_groups = current_groups; current_groups = current_groups;
}) })
@ -52,6 +59,8 @@
}) })
.finally(() => { .finally(() => {
processed_last_submit = true; processed_last_submit = true;
//
toast.hideToast();
}); });
} }
} }
@ -59,124 +68,105 @@
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
modal_open = false; modal_open = false;
}} }}>
>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div <div
class="absolute inset-0 bg-gray-500 opacity-75" class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" data-id="modal_backdrop" />
/>
</div> </div>
<span <span
class="hidden sm:inline-block sm:align-middle sm:h-screen" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span aria-hidden="true">&#8203;</span>
>
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <div class="sm:flex sm:items-start">
<div class="">
<div <div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512" viewBox="0 0 640 512"
class="size-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
width="24" width="24"
height="24" height="24"><path fill="none" d="M0 0h24v24H0z" />
><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></svg>
/></svg
>
</div> </div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("create-a-new-user-group")} {$_('create-a-new-user-group')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_('please-provide-the-required-information-for-creating-a-new-user-group')}
"please-provide-the-required-information-for-creating-a-new-user-group"
)}
</p> </p>
</div> </div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="firstname" for="firstname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('name')}</label>
>{$_("name")}</label
>
<input <input
use:focus use:focus
autocomplete="off" autocomplete="off"
placeholder={$_("name")} placeholder="{$_('name')}"
class:border-red-500={!isNameValid} class:border-red-500={!isNameValid}
class:focus:border-red-500={!isNameValid} class:focus:border-red-500={!isNameValid}
class:focus:ring-red-500={!isNameValid} class:focus:ring-red-500={!isNameValid}
bind:value={name_input_value} bind:value={name_input_value}
type="text" type="text"
name="firstname" name="firstname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isNameValid} {#if !isNameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('name-is-required')}
{$_("name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="trackname" for="trackname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('description-optional')}</label>
>{$_("description-optional")}</label
>
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("something-about-the-group")} placeholder="{$_('something-about-the-group')}"
bind:value={description_input_value} bind:value={description_input_value}
type="text" type="text"
name="trackname" name="trackname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
disabled={!createbtnenabled} disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled} class:opacity-50={!createbtnenabled}
on:click={submit} on:click={submit}
type="button" type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('create')}
{$_("create")}
</button> </button>
<button <button
on:click={() => { on:click={() => {
modal_open = false; modal_open = false;
}} }}
type="button" type="button"
class="cancel_modal_button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('cancel')}
{$_("cancel")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,227 +1,220 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import { UserGroupService } from "@odit/lfk-client-js"; import {
import toast from "svelte-french-toast"; UserGroupService
} from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import Toastify from "toastify-js";
let data_loaded = false; import PromiseError from "../base/PromiseError.svelte";
export let params; let data_loaded = false;
const promise = UserGroupService.userGroupControllerGetOne(params.groupid); export let params;
const colors = [ const promise = UserGroupService.userGroupControllerGetOne(params.groupid);
"#f3558e", const colors = [
"#17b978", "#f3558e",
"#3498db", "#17b978",
"#3f3b3b", "#3498db",
"#775ada", "#3f3b3b",
"#7ed6df_#000000", "#775ada",
"#000000", "#7ed6df_#000000",
"#21e6c1_#000000", "#000000",
"#c0392b", "#21e6c1_#000000",
"#d35400", "#c0392b",
"#7f8c8d", "#d35400",
"#6ab04c", "#7f8c8d",
"#4834d4", "#6ab04c",
"#ff1f5a", "#4834d4",
"#eac100", "#ff1f5a",
]; "#eac100",
let matched_colors = []; ];
$: delete_triggered = false; let matched_colors = [];
$: search_permission = ""; $: delete_triggered = false;
$: original_data = {}; $: search_permission = "";
$: editable = {}; $: original_data = {};
$: changes_performed = !( $: editable = {};
JSON.stringify(original_data) == JSON.stringify(editable) $: changes_performed = !(JSON.stringify(original_data) == JSON.stringify(editable));
); $: isGroupnameValid = editable.name !== "";
$: isGroupnameValid = editable.name !== ""; $: save_enabled =
$: save_enabled = changes_performed && isGroupnameValid; changes_performed && isGroupnameValid
promise.then((data) => { promise.then((data) => {
let current_target = ""; let current_target = "";
let colorindex = -1; let colorindex = -1;
data.permissions = data.permissions.sort(); data.permissions = data.permissions.sort();
data.permissions.forEach((p) => { data.permissions.forEach((p) => {
const target = p.split(":")[0]; const target = p.split(":")[0];
if (current_target !== p.split(":")[0]) { if (current_target !== p.split(":")[0]) {
colorindex++; colorindex++;
current_target = p.split(":")[0]; current_target = p.split(":")[0];
} }
let background = colors[colorindex]; let background = colors[colorindex];
let foreground = "#fff"; let foreground = "#fff";
if (background.includes("_")) { if (background.includes("_")) {
foreground = background.split("_")[1]; foreground = background.split("_")[1];
background = background.split("_")[0]; background = background.split("_")[0];
} }
matched_colors[target] = [background, foreground]; matched_colors[target] = [background, foreground];
}); });
data_loaded = true; data_loaded = true;
original_data = Object.assign(original_data, data); original_data = Object.assign(original_data, data);
editable = Object.assign(editable, original_data); editable = Object.assign(editable, original_data);
}); });
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast($_("updating-group")); Toastify({
UserGroupService.userGroupControllerPut(original_data.id, editable) text: $_('updateing-group'),
.then((resp) => { duration: 2500,
Object.assign(original_data, editable); }).showToast();
original_data = editable; UserGroupService.userGroupControllerPut(original_data.id, editable)
Object.assign(original_data, editable); .then((resp) => {
toast.success($_("group-updated")); Object.assign(original_data, editable);
}) original_data = editable;
.catch((err) => {}); Object.assign(original_data, editable);
} else { Toastify({
} text: $_('group-updated'),
} duration: 2500,
function deleteGroup() { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
UserGroupService.userGroupControllerRemove(original_data.id, true) }).showToast();
.then((resp) => { })
location.replace("./"); .catch((err) => {});
}) } else {
.catch((err) => {}); }
} }
</script> function deleteGroup() {
UserGroupService.userGroupControllerRemove(original_data.id, true)
{#await promise} .then((resp) => {
{$_("loading-group-detail")} location.replace("./");
{:then} })
<section class="container p-5 select-none"> .catch((err) => {});
<div class="flex flex-row mb-4"> }
<div class="w-full"> </script>
<nav class="w-full flex">
<ol class="list-none flex flex-row items-center justify-start"> {#await promise}
<li class="flex items-center"></li> {$_('loading-group-detail')}
<li class="flex items-center"> {:then}
<a class="mr-2" href="../" <section class="container p-5 select-none">
><svg <div class="flex flex-row mb-4">
xmlns="http://www.w3.org/2000/svg" <div class="w-full">
width="24" <nav class="w-full flex">
height="24" <ol class="list-none flex flex-row items-center justify-start">
viewBox="0 0 24 24" <li class="flex items-center">
fill="none" <svg class="flex-shrink-0 w-5 h-5 mr-2" fill="currentColor" width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z"></path></svg>
stroke="currentColor" </li>
stroke-width="2" <li class="flex items-center">
stroke-linecap="round" <a class="mr-2" href="../">{$_('groups')}</a><svg
stroke-linejoin="round" stroke="currentColor"
class="inline-block" fill="none"
><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg stroke-width="2"
> viewBox="0 0 24 24"
{$_("groups")}</a stroke-linecap="round"
> stroke-linejoin="round"
</li> class="h-3 w-3 mr-2 stroke-current"
</ol> height="1em"
</nav> width="1em"
</div> xmlns="http://www.w3.org/2000/svg"><line
</div> x1="5"
<div class="mb-4 text-3xl font-extrabold leading-tight"> y1="12"
{editable.name} x2="19"
<div data-id="group_actions_${editable.id}"> y2="12" />
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:DELETE")} <polyline points="12 5 19 12 12 19" /></svg>
{#if delete_triggered} </li>
<button <li class="flex items-center">
on:click={deleteGroup} <span class="mr-2">{editable.name}</span>
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" </li>
>{$_("confirm-deletion")}</button </ol>
> </nav>
<button </div>
on:click={() => { </div>
delete_triggered = !delete_triggered; <div class="mb-8 text-3xl font-extrabold leading-tight">
}} {original_data.name}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm" <span data-id="group_actions_${editable.id}">
>{$_("cancel")}</button {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:DELETE')}
> {#if delete_triggered}
{/if} <button
{#if !delete_triggered} on:click={deleteGroup}
<button class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('confirm-deletion')}</button>
on:click={() => { <button
delete_triggered = true; on:click={() => {
}} delete_triggered = !delete_triggered;
type="button" }}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm">{$_('cancel')}</button>
>{$_("delete-group")}</button {/if}
> {#if !delete_triggered}
{/if} <button
{/if} on:click={() => {
{#if !delete_triggered} delete_triggered = true;
<button }}
disabled={!save_enabled} type="button"
class:opacity-50={!save_enabled} class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-group')}</button>
type="button" {/if}
on:click={submit} {/if}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" {#if !delete_triggered}
>{$_("save-changes")}</button <button
> disabled={!save_enabled}
{/if} class:opacity-50={!save_enabled}
</div> type="button"
</div> on:click={submit}
<!-- --> class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
<div class="text-sm w-full mt-2"> {/if}
<label for="title" class="font-semibold text-gray-700">{$_("name")}</label </span>
> </div>
<input <!-- -->
autocomplete="off" <div class="text-sm w-full">
placeholder={$_("name")} <label
type="text" for="title"
bind:value={editable.name} class="font-medium text-gray-700">{$_('name')}</label>
class:border-red-500={!isGroupnameValid} <input
class:focus:border-red-500={!isGroupnameValid} autocomplete="off"
class:focus:ring-red-500={!isGroupnameValid} placeholder={$_('name')}
name="title" type="text"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" bind:value={editable.name}
/> class:border-red-500={!isGroupnameValid}
{#if !isGroupnameValid} class:focus:border-red-500={!isGroupnameValid}
<span class:focus:ring-red-500={!isGroupnameValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" name="title"
> class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
{$_("group-name-is-required")} {#if !isGroupnameValid}
</span> <span
{/if} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
</div> {$_('group-name-is-required')}
<div class="text-sm w-full mt-2"> </span>
<label for="groupdescription" class="font-semibold text-gray-700" {/if}
>{$_("description")}</label </div>
> <div class="text-sm w-full">
<input <label
autocomplete="off" for="firstname"
placeholder={$_("description")} class="font-medium text-gray-700">{$_('description')}</label>
type="text" <input
bind:value={editable.description} autocomplete="off"
name="groupdescription" placeholder={$_('description')}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" type="text"
/> bind:value={editable.description}
</div> name="firstname"
<div class="text-sm w-full mt-2"> class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
<p class="font-semibold mb-4"> </div>
{$_("permissions")} <div class="text-sm w-full mt-8">
</p> <p class="font-medium mb-4">
<div> {$_('permissions')}
<a <a
class="px-4 py-2 bg-gray-500 rounded-md text-white" class="px-4 py-2 bg-gray-500 rounded-md text-white"
href="/groups/{params.groupid}/permissions/" href="/groups/{params.groupid}/permissions/">{$_('edit-permissions')}</a>
>{$_("edit-permissions")}</a </p>
> <div class="w-full sm:my-px sm:px-px sm:w-1/2">
</div> <input
<div class="w-full sm:my-px sm:px-px sm:w-1/2"> autocomplete="off"
<input placeholder="{$_('search-for-permission')}"
autocomplete="off" type="text"
placeholder={$_("search-for-permission")} bind:value={search_permission}
type="text" class="mt-4 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 dark:bg-gray-900 dark:text-gray-100 rounded-md p-2" />
bind:value={search_permission} </div>
class="mt-4 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {#each original_data.permissions as p}
/> {#if p.toLowerCase().includes(search_permission.toLowerCase())}
</div> <span
{#each original_data.permissions as p} style="background:{matched_colors[p.split(':')[0]][0]};color:{matched_colors[p.split(':')[0]][1]};"
{#if p.toLowerCase().includes(search_permission.toLowerCase())} class="mt-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-indigo-100 rounded">{p}</span>
<span <!-- -->
style="background:{matched_colors[ {/if}
p.split(':')[0] {/each}
][0]};color:{matched_colors[p.split(':')[0]][1]};" </div>
class="mt-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-indigo-100 rounded" </section>
>{p}</span {:catch error}
> <PromiseError {error} />
<!-- --> {/await}
{/if}
{/each}
</div>
</section>
{:catch error}
<PromiseError {error} />
{/await}

View File

@ -3,10 +3,9 @@
import { import {
PermissionService, PermissionService,
CreatePermission, CreatePermission,
UserGroupService, UserGroupService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import toast from 'svelte-french-toast' import Toastify from "toastify-js";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
export let params; export let params;
let [ let [
@ -21,14 +20,15 @@
$: save_enabled = $: save_enabled =
JSON.stringify(grantedPermissions) === JSON.stringify(grantedPermissions) ===
JSON.stringify(grantedPermissions_initial); JSON.stringify(grantedPermissions_initial);
const group_promise = UserGroupService.userGroupControllerGetOne( const group_promise = UserGroupService.userGroupControllerGetOne(params.groupid);
params.groupid
);
group_promise.then((data) => { group_promise.then((data) => {
original_data = Object.assign(original_data, data); original_data = Object.assign(original_data, data);
}); });
function submit() { function submit() {
toast.loading($_("updating-permissions")); Toastify({
text: $_('updating-permissions'),
duration: 2500,
}).showToast();
to_delete.forEach((d) => { to_delete.forEach((d) => {
promises = promises.concat([ promises = promises.concat([
PermissionService.permissionControllerRemove(d, true), PermissionService.permissionControllerRemove(d, true),
@ -50,7 +50,11 @@
); );
}); });
grantedPermissions_initial = grantedPermissions; grantedPermissions_initial = grantedPermissions;
toast.success($_("permissions-updated")); Toastify({
text: $_("permissions-updated"),
duration: 2500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}); });
} }
Object.values(CreatePermission.target).forEach((t) => { Object.values(CreatePermission.target).forEach((t) => {
@ -58,15 +62,13 @@
allpermissions = allpermissions.concat([{ target: t, action: a }]); allpermissions = allpermissions.concat([{ target: t, action: a }]);
}); });
}); });
UserGroupService.userGroupControllerGetPermissions(params.groupid).then( UserGroupService.userGroupControllerGetPermissions(params.groupid).then((val) => {
(val) => { val.directlyGranted.forEach((p) => {
val.directlyGranted.forEach((p) => { delete p.responseType;
delete p.responseType; grantedPermissions = grantedPermissions.concat([p]);
grantedPermissions = grantedPermissions.concat([p]); });
}); grantedPermissions_initial = grantedPermissions;
grantedPermissions_initial = grantedPermissions; });
}
);
</script> </script>
{#await group_promise} {#await group_promise}
@ -84,15 +86,12 @@
width="24" width="24"
height="24" height="24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512" viewBox="0 0 640 512"><path
><path
fill="currentColor" fill="currentColor"
d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></svg>
/></svg
>
</li> </li>
<li class="flex items-center"> <li class="flex items-center">
<a class="mr-2" href="../../">{$_("user-groups")}</a><svg <a class="mr-2" href="../../">{$_('user-groups')}</a><svg
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
stroke-width="2" stroke-width="2"
@ -102,10 +101,12 @@
class="h-3 w-3 mr-2 stroke-current" class="h-3 w-3 mr-2 stroke-current"
height="1em" height="1em"
width="1em" width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"><line
><line x1="5" y1="12" x2="19" y2="12" /> x1="5"
<polyline points="12 5 19 12 12 19" /></svg y1="12"
> x2="19"
y2="12" />
<polyline points="12 5 19 12 12 19" /></svg>
</li> </li>
<li class="flex items-center"> <li class="flex items-center">
<span class="mr-2"><a href="../">{original_data.name}</a></span> <span class="mr-2"><a href="../">{original_data.name}</a></span>
@ -121,45 +122,45 @@
class="h-3 w-3 mr-2 stroke-current" class="h-3 w-3 mr-2 stroke-current"
height="1em" height="1em"
width="1em" width="1em"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"><line
><line x1="5" y1="12" x2="19" y2="12" /> x1="5"
<polyline points="12 5 19 12 12 19" /></svg y1="12"
> x2="19"
y2="12" />
<polyline points="12 5 19 12 12 19" /></svg>
</li> </li>
<li class="flex items-center"> <li class="flex items-center">
<span class="mr-2">{$_("permissions")}</span> <span class="mr-2">{$_('permissions')}</span>
</li> </li>
</ol> </ol>
</nav> </nav>
</div> </div>
</div> </div>
<div class="mb-4 text-3xl font-extrabold"> <div class="mb-8 text-3xl font-extrabold">
<div> {$_('permissions')}:
{original_data.name}
<span>
{#if promises.length === 0} {#if promises.length === 0}
<button <button
disabled={save_enabled} disabled={save_enabled}
class:opacity-50={save_enabled} class:opacity-50={save_enabled}
type="button" type="button"
on:click={submit} on:click={submit}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
>{$_("save-changes")}</button
>
{:else} {:else}
<button <button
type="button" type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-yellow-600 text-base font-medium text-white hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500 sm:w-auto sm:text-sm" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-yellow-600 text-base font-medium text-white hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('applying-changes')}</button>
>{$_("applying-changes")}</button
>
{/if} {/if}
</div> </span>
</div> </div>
<!-- --> <!-- -->
<div class="flex flex-wrap -mx-1 overflow-hidden"> <div class="flex flex-wrap -mx-1 overflow-hidden">
<div class="my-1 px-1 w-full overflow-hidden sm:w-1/2"> <div class="my-1 px-1 w-full overflow-hidden sm:w-1/2">
{$_("available-permissions")} {$_('verfuegbare')}
</div> </div>
<div class="my-1 px-1 w-full overflow-hidden sm:w-1/2"> <div class="my-1 px-1 w-full overflow-hidden sm:w-1/2">
{$_("granted")} {$_('granted')}
</div> </div>
</div> </div>
<!-- --> <!-- -->
@ -167,14 +168,12 @@
{#if allpermissions.length > 0} {#if allpermissions.length > 0}
<div class="my-1 px-1 w-full overflow-hidden sm:w-1/2"> <div class="my-1 px-1 w-full overflow-hidden sm:w-1/2">
<div <div
class="border-4 border-dashed rounded mb-4 p-5 text-lg text-center" class="border-4 border-dashed rounded mb-4 p-5 text-lg text-center">
>
{#each allpermissions as p} {#each allpermissions as p}
{#if !(grantedPermissions.filter((o) => p.target == o.target && p.action == o.action).length > 0)} {#if !(grantedPermissions.filter((o)=>p.target == o.target && p.action == o.action).length > 0)}
<p <p
class="block w-full mt-1 text-sm bg-gray-200 p-2 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input" class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 bg-gray-200 p-2 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input">
> {p.target + ':' + p.action}
{p.target + ":" + p.action}
<button <button
on:click={() => { on:click={() => {
grantedPermissions = grantedPermissions.concat([p]); grantedPermissions = grantedPermissions.concat([p]);
@ -191,9 +190,7 @@
} }
}} }}
type="button" type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-200 text-base font-medium text-black hover:bg-green-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:w-auto sm:text-sm" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-200 text-base font-medium text-black hover:bg-green-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm">+</button>
>+</button
>
</p> </p>
{/if} {/if}
{/each} {/each}
@ -201,39 +198,22 @@
</div> </div>
<div class="my-1 px-1 w-full overflow-hidden sm:w-1/2"> <div class="my-1 px-1 w-full overflow-hidden sm:w-1/2">
<div <div
class="border-4 border-dashed rounded mb-4 p-5 text-lg text-center" class="border-4 border-dashed rounded mb-4 p-5 text-lg text-center">
>
{#each grantedPermissions as p} {#each grantedPermissions as p}
<p <p
class="block w-full mt-1 text-sm bg-gray-200 p-2 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-input" class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 bg-gray-200 p-2 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input">
> {p.target + ':' + p.action}
{p.target + ":" + p.action}
<button <button
on:click={() => { on:click={() => {
grantedPermissions = grantedPermissions.filter( grantedPermissions = grantedPermissions.filter((o) => o.target + ':' + o.action !== p.target + ':' + p.action);
(o) => if (to_add.some((o) => o.target + ':' + o.action === p.target + ':' + p.action)) {
o.target + ":" + o.action !== p.target + ":" + p.action to_add = to_add.filter((o) => o.target + ':' + o.action !== p.target + ':' + p.action);
);
if (
to_add.some(
(o) =>
o.target + ":" + o.action ===
p.target + ":" + p.action
)
) {
to_add = to_add.filter(
(o) =>
o.target + ":" + o.action !==
p.target + ":" + p.action
);
} else { } else {
to_delete = to_delete.concat([p.id]); to_delete = to_delete.concat([p.id]);
} }
}} }}
type="button" type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-300 text-base font-medium text-black hover:bg-red-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:w-auto sm:text-sm" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-300 text-base font-medium text-black hover:bg-red-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm">-</button>
>-</button
>
</p> </p>
{/each} {/each}
</div> </div>

View File

@ -8,23 +8,22 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_("user-groups")} {$_('user-groups')}
</h4> {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:CREATE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:CREATE")} <button
<button on:click={() => {
on:click={() => { modal_open = true;
modal_open = true; }}
}} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm" {$_('add-user-group')}
> </button>
{$_("add-user-group")} {/if}
</button> </span>
{/if}
<UserGroupsOverview bind:current_groups /> <UserGroupsOverview bind:current_groups />
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:CREATE')}
<AddGroupModal bind:current_groups bind:modal_open /> <AddGroupModal bind:current_groups bind:modal_open />
{/if} {/if}

View File

@ -6,7 +6,7 @@
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="w-full h-44" src={groups_empty} alt="" /> <img class="w-full h-44" src={groups_empty} alt="" />
<span class="font-bold">{$_("there-are-no-groups-yet")}.</span><br /> <span class="font-bold">{$_('there-are-no-groups-yet')}.</span><br />
<span>{$_("add-your-first-group")}</span> <span>{$_('add-your-first-group')}</span>
</p> </p>
</div> </div>

View File

@ -13,14 +13,13 @@
); );
</script> </script>
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:GET')}
{#await groups_promise} {#await groups_promise}
<div <div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert" role="alert">
> <p class="font-bold">{$_('groups-are-being-loaded')}</p>
<p class="font-bold">{$_("groups-are-being-loaded")}</p> <p class="text-sm">{$_('this-might-take-a-moment')}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div> </div>
{:then} {:then}
{#if current_groups.length === 0} {#if current_groups.length === 0}
@ -29,30 +28,26 @@
<input <input
type="search" type="search"
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_('datatable.search')}
aria-label={$_("datatable.search")} aria-label={$_('datatable.search')}
class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" class="gridjs-input gridjs-search-input mb-4" />
/>
<div <div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll" class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
>
<table class="divide-y divide-gray-200 w-full"> <table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr class="odd:bg-white even:bg-gray-100"> <tr>
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
> {$_('name')}
{$_("name")}
</th> </th>
<th <th
scope="col" scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
> {$_('description')}
{$_("description")}
</th> </th>
<th scope="col" class="relative px-6 py-3"> <th scope="col" class="relative px-6 py-3">
<span class="sr-only">{$_("action")}</span> <span class="sr-only">{$_('action')}</span>
</th> </th>
</tr> </tr>
</thead> </thead>
@ -62,10 +57,7 @@
.toString() .toString()
.toLowerCase() .toLowerCase()
.includes(searchvalue)} .includes(searchvalue)}
<tr <tr data-rowid="user_{group.id}">
class="odd:bg-white even:bg-gray-100"
data-rowid="user_{group.id}"
>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center">
<div class="ml-4"> <div class="ml-4">
@ -80,53 +72,39 @@
</td> </td>
{#if active_deletes[group.id] === true} {#if active_deletes[group.id] === true}
<td <td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
>
<button <button
on:click={() => { on:click={() => {
active_deletes[group.id] = false; active_deletes[group.id] = false;
}} }}
tabindex="0" tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer" class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
>{$_("cancel-delete")}</button
>
<button <button
on:click={() => { on:click={() => {
UserGroupService.userGroupControllerRemove( UserGroupService.userGroupControllerRemove(group.id, true)
group.id,
true
)
.then((resp) => { .then((resp) => {
current_groups = current_groups.filter( current_groups = current_groups.filter((obj) => obj.id !== group.id);
(obj) => obj.id !== group.id
);
}) })
.catch((err) => { .catch((err) => {
// error deleting user // error deleting user
}); });
}} }}
tabindex="0" tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
>{$_("confirm-delete")}</button
>
</td> </td>
{:else} {:else}
<td <td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
>
<a <a
href="./{group.id}" href="./{group.id}"
class="text-indigo-600 hover:text-indigo-900">Details</a class="text-indigo-600 hover:text-indigo-900">Details</a>
> {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:DELETE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:DELETE")}
<button <button
on:click={() => { on:click={() => {
active_deletes[group.id] = true; active_deletes[group.id] = true;
}} }}
tabindex="0" tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
>{$_("delete")}</button
>
{/if} {/if}
</td> </td>
{/if} {/if}
@ -140,7 +118,7 @@
{:catch error} {:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"> <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8"> <span class="inline-block align-middle mr-8">
<b>{$_("general_promise_error")}</b> <b class="capitalize">{$_('general_promise_error')}</b>
{error} {error}
</span> </span>
</div> </div>

View File

@ -1,10 +1,9 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerOrganizationService } from "@odit/lfk-client-js"; import { RunnerOrganizationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import toast from "svelte-french-toast";
export let modal_open; export let modal_open;
export let current_organizations; export let current_organizations;
let name_input_dom; let name_input_dom;
@ -25,7 +24,7 @@
$: address_input2_value = ""; $: address_input2_value = "";
$: address_zipcode_value = ""; $: address_zipcode_value = "";
$: address_city_value = ""; $: address_city_value = "";
$: address_checked = false; $: address_checked = true;
let address_input1; let address_input1;
let address_input2; let address_input2;
@ -49,7 +48,10 @@
function submit() { function submit() {
if (processed_last_submit === true) { if (processed_last_submit === true) {
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("organization-is-being-added")); const toast = Toastify({
text: $_("organization-is-being-added"),
duration: -1,
}).showToast();
let address = {}; let address = {};
if (address_checked === true) { if (address_checked === true) {
address = { address = {
@ -68,13 +70,17 @@
.then((result) => { .then((result) => {
name = ""; name = "";
modal_open = false; modal_open = false;
toast.dismiss(); Toastify({
toast.success($_("organization-added")); text: $_("organization-added"),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_organizations = current_organizations.concat([result]); current_organizations = current_organizations.concat([result]);
}) })
.catch((err) => {}) .catch((err) => {})
.finally(() => { .finally(() => {
processed_last_submit = true; processed_last_submit = true;
toast.hideToast();
}); });
} }
} }
@ -82,70 +88,58 @@
{#if modal_open} {#if modal_open}
<div <div
class="fixed z-10 inset-0 overflow-y-hidden" class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
modal_open = false; modal_open = false;
}} }}>
>
<div <div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true"> <div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div <div
class="absolute inset-0 bg-gray-500 opacity-75" class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" data-id="modal_backdrop" />
/>
</div> </div>
<span <span
class="hidden sm:inline-block sm:align-middle sm:h-screen" class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span aria-hidden="true">&#8203;</span>
>
<div <div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10" class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby="modal-headline" aria-labelledby="modal-headline">
> <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl"> <div class="sm:flex sm:items-start">
<div class="">
<div <div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10" class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
>
<svg <svg
class="size-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"><path
><path d="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z" /></svg>
d="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z"
/></svg
>
</div> </div>
<div class="mt-3 sm:text-left"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900"> <h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("create-a-new-organization")} {$_('create-a-new-organization')}
</h3> </h3>
<div class="mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_('please-provide-the-required-information-to-add-a-new-organization')}
"please-provide-the-required-information-to-add-a-new-organization"
)}
</p> </p>
</div> </div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="firstname" for="firstname"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('name')}</label>
>{$_("name")}</label
>
<input <input
use:focus use:focus
autocomplete="off" autocomplete="off"
placeholder={$_("name")} placeholder={$_('name')}
class:border-red-500={!isOrgnameValid} class:border-red-500={!isOrgnameValid}
class:focus:border-red-500={!isOrgnameValid} class:focus:border-red-500={!isOrgnameValid}
class:focus:ring-red-500={!isOrgnameValid} class:focus:ring-red-500={!isOrgnameValid}
@ -153,13 +147,11 @@
bind:this={name_input_dom} bind:this={name_input_dom}
type="text" type="text"
name="firstname" name="firstname"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
/>
{#if !isOrgnameValid} {#if !isOrgnameValid}
<span <span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
> {$_('organization-name-is-required')}
{$_("organization-name-is-required")}
</span> </span>
{/if} {/if}
</div> </div>
@ -170,133 +162,115 @@
id="comments" id="comments"
name="comments" name="comments"
type="checkbox" type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-semibold text-gray-700" <label
>{$_("address")}</label for="comments"
> class="font-medium text-gray-700">{$_('address')}</label>
</div> </div>
</div> </div>
{#if address_checked === true} {#if address_checked === true}
<div class="col-span-6"> <div class="col-span-6">
<label <label
for="address1" for="address1"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700">{$_('address')}</label>
>{$_("address")}</label <input
> autocomplete="off"
<input placeholder="{$_('address')}"
autocomplete="off" class:border-red-500={!isAddress1Valid}
placeholder={$_("address")} class:focus:border-red-500={!isAddress1Valid}
class:border-red-500={!isAddress1Valid} class:focus:ring-red-500={!isAddress1Valid}
class:focus:border-red-500={!isAddress1Valid} bind:value={address_input1_value}
class:focus:ring-red-500={!isAddress1Valid} bind:this={address_input1}
bind:value={address_input1_value} type="text"
bind:this={address_input1} name="address1"
type="text" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
name="address1" {#if !isAddress1Valid}
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <span
/> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{#if !isAddress1Valid} {$_('address-is-required')}
<span </span>
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" {/if}
> </div>
{$_("address-is-required")} <div class="col-span-6">
</span> <label
{/if} for="address2"
</div> class="block text-sm font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
<div class="col-span-6"> <input
<label autocomplete="off"
for="address2" placeholder="{$_('apartment-suite-etc')}"
class="block text-sm font-medium text-gray-700" bind:value={address_input2_value}
>{$_("apartment-suite-etc")}</label bind:this={address_input2}
> type="text"
<input name="address2"
autocomplete="off" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
placeholder={$_("apartment-suite-etc")} </div>
bind:value={address_input2_value} <div class="col-span-6">
bind:this={address_input2} <label
type="text" for="zipcode"
name="address2" class="block text-sm font-medium text-gray-700">{$_('zip-postal-code')}</label>
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <input
/> autocomplete="off"
</div> placeholder="{$_('zip-postal-code')}"
<div class="col-span-2"> class:border-red-500={!iszipcodevalid}
<label class:focus:border-red-500={!iszipcodevalid}
for="zipcode" class:focus:ring-red-500={!iszipcodevalid}
class="block text-sm font-medium text-gray-700" bind:value={address_zipcode_value}
>{$_("zip-postal-code")}</label bind:this={address_zipcode}
> type="text"
<input name="zipcode"
autocomplete="off" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
placeholder={$_("zip-postal-code")} {#if !iszipcodevalid}
class:border-red-500={!iszipcodevalid} <span
class:focus:border-red-500={!iszipcodevalid} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
class:focus:ring-red-500={!iszipcodevalid} {$_('valid-zipcode-postal-code-is-required')}
bind:value={address_zipcode_value} </span>
bind:this={address_zipcode} {/if}
type="text" </div>
name="zipcode" <div class="col-span-6">
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <label
/> for="city"
{#if !iszipcodevalid} class="block text-sm font-medium text-gray-700">{$_('city')}</label>
<span <input
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" autocomplete="off"
> placeholder="{$_('city')}"
{$_("valid-zipcode-postal-code-is-required")} class:border-red-500={!iscityvalid}
</span> class:focus:border-red-500={!iscityvalid}
{/if} class:focus:ring-red-500={!iscityvalid}
</div> bind:value={address_city_value}
<div class="col-span-4"> bind:this={address_city}
<label type="text"
for="city" name="city"
class="block text-sm font-medium text-gray-700" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
>{$_("city")}</label {#if !iscityvalid}
> <span
<input class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
autocomplete="off" {$_('valid-city-is-required')}
placeholder={$_("city")} </span>
class:border-red-500={!iscityvalid} {/if}
class:focus:border-red-500={!iscityvalid} </div>
class:focus:ring-red-500={!iscityvalid} {/if}
bind:value={address_city_value} </div>
bind:this={address_city}
type="text"
name="city"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
/>
{#if !iscityvalid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
>
{$_("valid-city-is-required")}
</span>
{/if}
</div>
{/if}
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10"> <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
disabled={!createbtnenabled} disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled} class:opacity-50={!createbtnenabled}
on:click={submit} on:click={submit}
type="button" type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('create')}
{$_("create")}
</button> </button>
<button <button
on:click={() => { on:click={() => {
modal_open = false; modal_open = false;
}} }}
type="button" type="button"
class="cancel_modal_button" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
> {$_('cancel')}
{$_("cancel")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,102 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { focusTrap } from "svelte-focus-trap";
import { RunnerOrganizationService } from "@odit/lfk-client-js";
import Toastify from "toastify-js";
import { createEventDispatcher } from "svelte";
export let modal_open;
export let delete_org;
const dispatch = createEventDispatcher();
function cancelDelete() {
modal_open = false;
dispatch("cancelDelete", { id: delete_org.id });
}
function deleteOrg() {
RunnerOrganizationService.runnerOrganizationControllerRemove(
delete_org.id,
true
)
.then((resp) => {
Toastify({
text: $_('organization-deleted'),
duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
location.replace("./");
})
.catch((err) => {});
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
on:click_outside={cancelDelete}>
<div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop" />
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span>
<div
class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div
class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
<svg
class="h-6 w-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"><path
fill="currentColor"
d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_('attention')}
</h3>
<div class="mt-2 mb-6">
<p class="text-sm text-gray-500">
{$_(
'do-you-want-to-delete-the-organization-delete_org-name',
{
values: { orgname: delete_org.name },
}
)}<br />
{$_('all-associated-teams-and-runners-will-be-deleted-too')}
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
on:click={deleteOrg}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('confirm-delete-organization-and-associated-teams-runners')}
</button>
<button
on:click={cancelDelete}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
{$_('cancel-keep-organization')}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,104 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { RunnerOrganizationService } from "@odit/lfk-client-js";
import { createEventDispatcher } from "svelte";
export let modal_open;
export let delete_org;
const dispatch = createEventDispatcher();
function cancelDelete() {
modal_open = false;
dispatch("cancelDelete", { id: delete_org.id });
}
function deleteOrg() {
RunnerOrganizationService.runnerOrganizationControllerRemove(
delete_org.id,
true
)
.then((resp) => {
toast.success($_("organization-deleted"));
location.replace("./");
})
.catch((err) => {});
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={cancelDelete}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
class="size-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
><path
fill="currentColor"
d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z"
/></svg
>
</div>
<div class="mt-3 sm:text-left max-h-[75vh] overflow-y-auto">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_("do-you-want-to-delete-the-organization-delete_org-name", {
values: { orgname: delete_org.name },
})}
</h3>
<div class="mb-6">
<p class="text-sm text-gray-500">
{$_("all-associated-teams-and-runners-will-be-deleted-too")}
</p>
</div>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 lg:py-3 sm:px-6 grid gap-2 lg:rounded-b-xl pt-3 pb-10">
<button
on:click={deleteOrg}
type="button"
class="confirm_deletion_button"
>
{$_("confirm-delete-organization-and-associated-teams-runners")}
</button>
<button
on:click={cancelDelete}
type="button"
class="cancel_modal_button"
>
{$_("cancel-keep-organization")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,421 +1,476 @@
<script> <script>
import { import {
GroupContactService, GroupContactService,
RunnerOrganizationService, RunnerOrganizationService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import toast from "svelte-french-toast"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import { _ } from "svelte-i18n"; import Toastify from "toastify-js";
import { tick } from "svelte"; import store from "../../store";
import Select from "svelte-select"; import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte";
import store from "../../store"; import ImportRunnerModal from "../runners/ImportRunnerModal.svelte";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import Select from "svelte-select";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import ImportRunnerModal from "../runners/ImportRunnerModal.svelte"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import ConfirmOrgDeletionModal from "./ConfirmOrgDeletionModal.svelte"; import { tick } from "svelte";
$: address_valid_or_none = $: delete_triggered = false;
(isAddress1Valid && iszipcodevalid && iscityvalid) || $: address_valid_or_none =
editable.address_checked === false; (isAddress1Valid && iszipcodevalid && iscityvalid) ||
$: save_enabled = data_changed && address_valid_or_none; editable.address_checked === false;
let original = ""; $: save_enabled = data_changed && address_valid_or_none;
let original_object = {}; let original = "";
let contacts = []; let original_object = {};
let valueCopy = null; let contacts = [];
let areaDom; let valueCopy = null;
export let params; let areaDom;
$: editable = {}; let copied = false;
$: contact = {}; export let params;
$: data_loaded = false; $: editable = {};
$: data_changed = !(JSON.stringify(editable) === original); $: contact = {};
$: isAddress1Valid = editable.address?.address1?.trim().length !== 0; $: data_loaded = false;
$: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0; $: data_changed = !(JSON.stringify(editable) === original);
$: iscityvalid = editable.address?.city?.trim().length !== 0; $: isAddress1Valid = editable.address?.address1?.trim().length !== 0;
$: sponsoring_contracts_show = true; $: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0;
$: cards_show = true; $: iscityvalid = editable.address?.city?.trim().length !== 0;
$: certificates_show = true; $: sponsoring_contracts_show = true;
$: generate_orgs = [original_object]; $: cards_show = true;
$: registrationLink = `${config.baseurl_selfservice}/register/${editable.registrationKey}`; $: certificates_show = true;
const getContactLabel = (option) => $: generate_orgs = [original_object];
option.firstname + " " + (option.middlename || "") + " " + option.lastname; $: registrationLink = `${config.baseurl}/selfservice/register/${editable.registrationKey}`;
const promise = RunnerOrganizationService.runnerOrganizationControllerGetOne( const getContactLabel = (option) =>
params.orgid option.firstname + " " + (option.middlename || "") + " " + option.lastname;
).then((value) => { const promise = RunnerOrganizationService.runnerOrganizationControllerGetOne(
data_loaded = true; params.orgid
value.address_checked = value.address.address1 !== null; ).then((value) => {
if (value.address_checked === false) { data_loaded = true;
value.address = { value.address_checked = value.address.address1 !== null;
address1: "", if (value.address_checked === false) {
address2: "", value.address = {
city: "", address1: "",
postalcode: "", address2: "",
country: "", city: "",
}; postalcode: "",
} country: "",
editable = Object.assign(editable, value); };
editable = editable; }
original_object = Object.assign(editable, value); editable = Object.assign(editable, value);
original = JSON.stringify(value); editable = editable;
GroupContactService.groupContactControllerGetAll().then((val) => { original_object = Object.assign(editable, value);
contacts = val.map((r) => { original = JSON.stringify(value);
return { label: getContactLabel(r), value: r }; GroupContactService.groupContactControllerGetAll().then((val) => {
}); contacts = val.map((r) => {
if (editable.contact) { return { label: getContactLabel(r), value: r };
contact = contacts.find((g) => g.value.id == editable.contact.id); });
} else { if (editable.contact) {
contact = null; contact = contacts.find((g) => g.value.id == editable.contact.id);
} } else {
}); contact = null;
}); }
let modal_open = false; });
let delete_org = {}; });
function deleteOrganization() { let modal_open = false;
RunnerOrganizationService.runnerOrganizationControllerRemove( let delete_org = {};
original_object.id, function deleteOrganization() {
false RunnerOrganizationService.runnerOrganizationControllerRemove(
) original_object.id,
.then((resp) => { false
toast.success($_("organization-deleted")); )
location.replace("./"); .then((resp) => {
}) Toastify({
.catch((err) => {}); text: $_("organization-deleted"),
} duration: 500,
function submit() { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
if (data_loaded === true && save_enabled) { }).showToast();
toast($_("updating-organization")); location.replace("./");
let postdata = Object.assign({}, editable); })
if (postdata.address_checked === false) { .catch((err) => {
postdata.address = null; modal_open = true;
} delete_org = original_object;
postdata.contact = postdata.contact?.id; });
RunnerOrganizationService.runnerOrganizationControllerPut( }
original_object.id, function submit() {
postdata if (data_loaded === true && save_enabled) {
) Toastify({
.then((resp) => { text: $_("updating-organization"),
editable.registrationKey = resp.registrationKey; duration: 2500,
original_object = Object.assign({}, editable); }).showToast();
original = JSON.stringify(original_object); let postdata = Object.assign({}, editable);
toast.success($_("updated-organization")); if (postdata.address_checked === false) {
}) postdata.address = null;
.catch((err) => {}); }
} else { postdata.contact = postdata.contact?.id;
} RunnerOrganizationService.runnerOrganizationControllerPut(
} original_object.id,
async function copy() { postdata
if (!editable.registrationKey) { )
toast.error($_("you-have-to-save-your-changes-to-generate-a-link")); .then((resp) => {
return; editable.registrationKey = resp.registrationKey;
} original_object = Object.assign({}, editable);
valueCopy = registrationLink; original = JSON.stringify(original_object);
await tick(); Toastify({
areaDom.focus(); text: $_("updated-organization"),
areaDom.select(); duration: 2500,
try { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
const successful = document.execCommand("copy"); }).showToast();
if (!successful) { })
throw new Error(); .catch((err) => {});
} } else {
toast($_("copied-link-to-clipboard")); }
} catch (err) { }
toast.error($_("error-whyile-copying-to-clipboard")); async function copy() {
} if(!editable.registrationKey){
// we can notifi by event or storage about copy status Toastify({
valueCopy = null; text: $_('you-have-to-save-your-changes-to-generate-a-link'),
} duration: 500,
export let import_modal_open = false; backgroundColor:
</script> "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
{#if valueCopy != null}<textarea bind:this={areaDom}>{valueCopy}</textarea>{/if} return;
<ImportRunnerModal }
on:cancelDelete={(event) => { valueCopy = registrationLink;
import_modal_open = false; await tick();
}} areaDom.focus();
current_runners={[]} areaDom.select();
passed_team={{}} try {
passed_orgs={[]} const successful = document.execCommand("copy");
passed_org={editable} if (!successful) {
opened_from="OrgDetail" throw new Error();
bind:import_modal_open }
/> Toastify({
<ConfirmOrgDeletionModal bind:modal_open bind:delete_org /> text: $_("copied-link-to-clipboard"),
{#if data_loaded} duration: 500,
<section class="container p-5"> backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
<div class="flex flex-row mb-4"> }).showToast();
<div class="w-full"> copied = true;
<nav class="w-full flex"> } catch (err) {
<ol class="list-none flex flex-row items-center justify-start"> Toastify({
<li class="flex items-center"> text: $_("error-whyile-copying-to-clipboard"),
<a class="mr-2" href="./" duration: 500,
><svg backgroundColor:
xmlns="http://www.w3.org/2000/svg" "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
width="24" }).showToast();
height="24" }
viewBox="0 0 24 24" // we can notifi by event or storage about copy status
fill="none" valueCopy = null;
stroke="currentColor" }
stroke-width="2" export let import_modal_open = false;
stroke-linecap="round" </script>
stroke-linejoin="round"
class="inline-block" {#if valueCopy != null}<textarea bind:this={areaDom}>{valueCopy}</textarea>{/if}
><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg <ImportRunnerModal
> on:cancelDelete={(event) => {
{$_("organizations")}</a import_modal_open = false;
> }}
</li> current_runners={[]}
</ol> passed_team={{}}
</nav> passed_orgs={[]}
</div> passed_org={editable}
</div> opened_from="OrgDetail"
<div class="mb-4 text-3xl font-extrabold leading-tight"> bind:import_modal_open />
{original_object.name} [#{params.orgid}] <ConfirmOrgDeletion bind:modal_open bind:delete_org />
<div data-id="org_actions_${editable.id}"> {#if data_loaded}
<GenerateSponsoringContracts <section class="container p-5">
bind:sponsoring_contracts_show <div class="mb-8 text-3xl font-extrabold leading-tight">
bind:generate_orgs {original_object.name}
/> <span data-id="org_actions_${editable.id}">
<GenerateRunnerCards bind:cards_show bind:generate_orgs /> <GenerateSponsoringContracts
<GenerateRunnerCertificates bind:certificates_show bind:generate_orgs /> bind:sponsoring_contracts_show
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:IMPORT")} bind:generate_orgs />
<button <GenerateRunnerCards bind:cards_show bind:generate_orgs />
on:click={() => { <GenerateRunnerCertificates bind:certificates_show bind:generate_orgs />
import_modal_open = true; {#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:IMPORT')}
}} <button
type="button" on:click={() => {
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm" import_modal_open = true;
> }}
{$_("import-runners")} type="button"
</button> class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{/if} {$_('import-runners')}
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:DELETE")} </button>
<button {/if}
on:click={() => { {#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:DELETE')}
modal_open = true; {#if delete_triggered}
delete_org = original_object; <button
}} on:click={deleteOrganization}
type="button" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('confirm-delete')}</button>
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm" <button
>{$_("delete-organization")}</button on:click={() => {
> delete_triggered = !delete_triggered;
{/if} }}
<button class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-400 text-base font-medium text-white sm:w-auto sm:text-sm">{$_('cancel')}</button>
on:click={submit} {/if}
disabled={!save_enabled} {#if !delete_triggered}
class:opacity-50={!save_enabled} <button
type="button" on:click={() => {
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" delete_triggered = true;
>{$_("save-changes")}</button }}
> type="button"
</div> class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('delete-organization')}</button>
</div> {/if}
<div class="text-sm w-full mt-2"> {/if}
<label for="name" class="font-semibold text-gray-700">{$_("name")}</label> {#if !delete_triggered}
<input <button
autocomplete="off" on:click={submit}
placeholder={$_("name")} disabled={!save_enabled}
type="text" class:opacity-50={!save_enabled}
bind:value={editable.name} type="button"
name="name" class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">{$_('save-changes')}</button>
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {/if}
/> </span>
</div> </div>
<div class="text-sm w-full mt-2"> <div class="flex flex-row mb-4">
<label for="contact" class="font-semibold text-gray-700" <div class="w-full">
>{$_("contact")}</label <nav class="w-full flex">
> <ol class="list-none flex flex-row items-center justify-start">
<Select <li class="mr-2 flex items-center">
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <svg
itemFilter={(label, filterText, option) => stroke="currentColor"
label.toLowerCase().includes(filterText.toLowerCase()) || fill="none"
option.value.id.toString().startsWith(filterText.toLowerCase())} stroke-width="2"
items={contacts} viewBox="0 0 24 24"
showChevron={true} stroke-linecap="round"
placeholder={$_("no-contact-selected")} stroke-linejoin="round"
noOptionsMessage={$_("no-contact-found")} class="h-3 w-3 stroke-current"
bind:selectedValue={contact} height="1em"
on:select={(selectedValue) => width="1em"
(editable.contact = selectedValue.detail.value)} xmlns="http://www.w3.org/2000/svg"><path
on:clear={() => (editable.contact = null)} d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
/> <polyline points="9 22 9 12 15 12 15 22" /></svg>
</div> </li>
<div> <li class="flex items-center">
<div class="flex items-start mt-2"> <a class="mr-2" href="/">{$_('home')}</a><svg
<div class="flex items-center h-5"> stroke="currentColor"
<input fill="none"
bind:checked={editable.registrationEnabled} stroke-width="2"
id="toggle_selfservice_feature" viewBox="0 0 24 24"
name="toggle_selfservice_feature" stroke-linecap="round"
type="checkbox" stroke-linejoin="round"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" class="h-3 w-3 mr-2 stroke-current"
/> height="1em"
</div> width="1em"
<div class="ml-3 text-sm"> xmlns="http://www.w3.org/2000/svg"><line
<label x1="5"
for="toggle_selfservice_feature" y1="12"
class="font-semibold text-gray-700" x2="19"
>{$_("selfservice-registration")}</label y2="12" />
> <polyline points="12 5 19 12 12 19" /></svg>
</div> </li>
</div> <li class="mr-2 flex items-center">
<div> <svg
{#if editable.registrationEnabled} xmlns="http://www.w3.org/2000/svg"
<div class="text-sm w-full mt-2"> viewBox="0 0 24 24"
<button on:click={copy} class="inline-flex w-full"> width="24"
<p height="24"><path fill="none" d="M0 0h24v24H0z" />
name="token" <path
class="focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 p-2 break-all font-mono text-left" d="M21 20h2v2H1v-2h2V3a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v17zm-2 0V4H5v16h14zM8 11h3v2H8v-2zm0-4h3v2H8V7zm0 8h3v2H8v-2zm5 0h3v2h-3v-2zm0-4h3v2h-3v-2zm0-4h3v2h-3V7z" /></svg>
> </li>
{#if editable.registrationKey} <li class="flex items-center">
{registrationLink} <a class="mr-2" href="./">{$_('organizations')}</a><svg
{:else} stroke="currentColor"
{$_("you-have-to-save-your-changes-to-generate-a-link")} fill="none"
{/if} stroke-width="2"
</p> viewBox="0 0 24 24"
<div stroke-linecap="round"
class="bg-gray-200 border-gray-300 border-t border-b border-r text-black rounded-r-md sm:text-sm p-2 cursor-pointer flex items-center justify-center" stroke-linejoin="round"
> class="h-3 w-3 mr-2 stroke-current"
<svg height="1em"
xmlns="http://www.w3.org/2000/svg" width="1em"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><line
width="24" x1="5"
height="24" y1="12"
><path fill="none" d="M0 0h24v24H0z" /> x2="19"
<path y2="12" />
fill="currentColor" <polyline points="12 5 19 12 12 19" /></svg>
d="M7 4V2h10v2h3l1 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V5l1-1h3zm0 2H5v14h14V6h-2v2H7V6zm2-2v2h6V4H9z" </li>
/></svg <li class="flex items-center">
> <span class="mr-2">Org-Details #{params.orgid}</span>
</div> </li>
</button> </ol>
{#if editable.registrationKey} </nav>
<p class="text-gray-500 text-xs"> </div>
{$_("click-to-copy-the-link-into-your-clipboard")} </div>
</p> <div class="text-sm w-full">
{/if} <label for="name" class="font-medium text-gray-700">{$_('name')}</label>
</div> <input
{/if} autocomplete="off"
<!-- --> placeholder={$_('name')}
<div> type="text"
<div class="flex items-start mt-2"> bind:value={editable.name}
<div class="flex items-center h-5"> name="name"
<input class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
bind:checked={editable.address_checked} </div>
id="toggle_address_checkbox" <div class="text-sm w-full">
name="toggle_address_checkbox" <label
type="checkbox" for="contact"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded" class="font-medium text-gray-700">{$_('contact')}</label>
/> <Select
</div> containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
<div class="ml-3 text-sm"> itemFilter={(label, filterText, option) => label
<label .toLowerCase()
for="toggle_address_checkbox" .includes(
class="font-semibold text-gray-700">{$_("address")}</label filterText.toLowerCase()
> ) || option.value.id
</div> .toString()
</div> .startsWith(filterText.toLowerCase())}
</div> items={contacts}
{#if editable.address_checked === true} showChevron={true}
<div class="col-span-6"> placeholder={$_('no-contact-selected')}
<label noOptionsMessage={$_('no-contact-found')}
for="address1" bind:selectedValue={contact}
class="block text-sm font-medium text-gray-700" on:select={(selectedValue) => (editable.contact = selectedValue.detail.value)}
>{$_("address")}</label on:clear={() => (editable.contact = null)} />
> </div>
<input <div>
autocomplete="off" <div class="flex items-start mt-2">
placeholder="Address" <div class="flex items-center h-5">
class:border-red-500={!isAddress1Valid} <input
class:focus:border-red-500={!isAddress1Valid} bind:checked={editable.registrationEnabled}
class:focus:ring-red-500={!isAddress1Valid} id="toggle_selfservice_feature"
bind:value={editable.address.address1} name="toggle_selfservice_feature"
type="text" type="checkbox"
name="address1" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" </div>
/> <div class="ml-3 text-sm">
{#if !isAddress1Valid} <label
<span for="toggle_selfservice_feature"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class="font-medium text-gray-700">{$_('selfservice-registration')}</label>
> </div>
{$_("address-is-required")} </div>
</span> <div>
{/if} {#if editable.registrationEnabled}
</div> <div class="text-sm w-full">
<div class="col-span-6"> <div on:click={copy} class="inline-flex w-full">
<label <p
for="address2" name="token"
class="block text-sm font-medium text-gray-700" class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 p-2">
>{$_("apartment-suite-etc")}</label {#if editable.registrationKey}
> {registrationLink}
<input {:else}
autocomplete="off" {$_('you-have-to-save-your-changes-to-generate-a-link')}
placeholder={$_("apartment-suite-etc")} {/if}
bind:value={editable.address.address2} </p>
type="text" <div
name="address2" class="bg-gray-200 border-gray-300 border-t border-b border-r text-black rounded-r-md sm:text-sm p-2 mt-1 cursor-pointer">
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" <svg
/> xmlns="http://www.w3.org/2000/svg"
</div> viewBox="0 0 24 24"
<div class="col-span-6"> width="24"
<label for="zipcode" class="block text-sm font-medium text-gray-700" height="24"><path fill="none" d="M0 0h24v24H0z" />
>{$_("zip-postal-code")}</label <path
> fill="currentColor"
<input d="M7 4V2h10v2h3l1 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V5l1-1h3zm0 2H5v14h14V6h-2v2H7V6zm2-2v2h6V4H9z" /></svg>
autocomplete="off" </div>
placeholder={$_("zip-postal-code")} </div>
class:border-red-500={!iszipcodevalid} {#if editable.registrationKey}
class:focus:border-red-500={!iszipcodevalid} <p class="text-gray-500 text-xs">
class:focus:ring-red-500={!iszipcodevalid} {$_('click-to-copy-the-link-into-your-clipboard')}
bind:value={editable.address.postalcode} </p>
type="text" {/if}
name="zipcode" </div>
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" {/if}
/> <!-- -->
{#if !iszipcodevalid} <div>
<span <div class="flex items-start mt-2">
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <div class="flex items-center h-5">
> <input
{$_("valid-zipcode-postal-code-is-required")} bind:checked={editable.address_checked}
</span> id="toggle_address_checkbox"
{/if} name="toggle_address_checkbox"
</div> type="checkbox"
<div class="col-span-6"> class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
<label for="city" class="block text-sm font-medium text-gray-700" </div>
>{$_("city")}</label <div class="ml-3 text-sm">
> <label
<input for="toggle_address_checkbox"
autocomplete="off" class="font-medium text-gray-700">{$_('address')}</label>
placeholder={$_("city")} </div>
class:border-red-500={!iscityvalid} </div>
class:focus:border-red-500={!iscityvalid} </div>
class:focus:ring-red-500={!iscityvalid} {#if editable.address_checked === true}
bind:value={editable.address.city} <div class="col-span-6">
type="text" <label
name="city" for="address1"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2" class="block text-sm font-medium text-gray-700">{$_('address')}</label>
/> <input
{#if !iscityvalid} autocomplete="off"
<span placeholder="Address"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class:border-red-500={!isAddress1Valid}
> class:focus:border-red-500={!isAddress1Valid}
{$_("valid-city-is-required")} class:focus:ring-red-500={!isAddress1Valid}
</span> bind:value={editable.address.address1}
{/if} type="text"
</div> name="address1"
{/if} class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
<div class="text-sm w-full mt-2"> {#if !isAddress1Valid}
<span class="font-semibold text-gray-700">{$_("distance")}</span> <span
<br /> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
<span class="text-gray-700" {$_('address-is-required')}
>{(original_object.total_distance / 1000).toFixed(2)} km</span </span>
> {/if}
</div> </div>
</div> <div class="col-span-6">
</div> <label
</section> for="address2"
{:else} class="block text-sm font-medium text-gray-700">{$_('apartment-suite-etc')}</label>
{#await promise} <input
{$_("organization-detail-is-being-loaded")} autocomplete="off"
{:catch error} placeholder={$_('apartment-suite-etc')}
<PromiseError /> bind:value={editable.address.address2}
{/await} type="text"
{/if} name="address2"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
</div>
<div class="col-span-6">
<label
for="zipcode"
class="block text-sm font-medium text-gray-700">{$_('zip-postal-code')}</label>
<input
autocomplete="off"
placeholder={$_('zip-postal-code')}
class:border-red-500={!iszipcodevalid}
class:focus:border-red-500={!iszipcodevalid}
class:focus:ring-red-500={!iszipcodevalid}
bind:value={editable.address.postalcode}
type="text"
name="zipcode"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
{#if !iszipcodevalid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{$_('valid-zipcode-postal-code-is-required')}
</span>
{/if}
</div>
<div class="col-span-6">
<label
for="city"
class="block text-sm font-medium text-gray-700">{$_('city')}</label>
<input
autocomplete="off"
placeholder={$_('city')}
class:border-red-500={!iscityvalid}
class:focus:border-red-500={!iscityvalid}
class:focus:ring-red-500={!iscityvalid}
bind:value={editable.address.city}
type="text"
name="city"
class="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" />
{#if !iscityvalid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1">
{$_('valid-city-is-required')}
</span>
{/if}
</div>
{/if}
</div>
</div>
</section>
{:else}
{#await promise}
{$_('organization-detail-is-being-loaded')}
{:catch error}
<PromiseError />
{/await}
{/if}

View File

@ -0,0 +1,219 @@
<script>
import { getLocaleFromNavigator, _ } from "svelte-i18n";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
let modal_open = false;
let delete_org = {};
import { RunnerOrganizationService } from "@odit/lfk-client-js";
import store from "../../store";
import OrgsEmptyState from "./OrgsEmptyState.svelte";
import Toastify from "toastify-js";
import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
$: searchvalue = "";
$: active_deletes = [];
$: sponsoring_contracts_show = current_organizations.some((r) => r.is_selected === true);
$: cards_show = current_organizations.some((r) => r.is_selected === true);
$: generate_orgs = current_organizations.filter((r) => r.is_selected === true);
$: certificates_show = current_organizations.some(
(r) => r.is_selected === true
);
export let current_organizations = [];
const promise = RunnerOrganizationService.runnerOrganizationControllerGetAll().then(
(val) => {
current_organizations = val;
}
);
</script>
<ConfirmOrgDeletion
on:cancelDelete={(event) => {
modal_open = false;
active_deletes[event.detail.id] = false;
}}
bind:modal_open
bind:delete_org />
{#if store.state.jwtinfo.userdetails.permissions.includes('ORGANIZATION:GET')}
{#await promise}
<div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert">
<p class="font-bold">{$_('organizations-are-being-loaded')}</p>
<p class="text-sm">{$_('this-might-take-a-moment')}</p>
</div>
{:then}
{#if current_organizations.length === 0}
<OrgsEmptyState />
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_('datatable.search')}
aria-label={$_('datatable.search')}
class="gridjs-input gridjs-search-input mb-4" />
<div class="h-12">
<GenerateSponsoringContracts
bind:sponsoring_contracts_show
bind:generate_orgs />
<GenerateRunnerCards
bind:cards_show
bind:generate_orgs />
<GenerateRunnerCertificates
bind:certificates_show
bind:generate_orgs />
</div>
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll">
<table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50">
<tr>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<span
on:click={() => {
const newstate = !current_organizations.some((r) => r.is_selected === true);
current_organizations = current_organizations.map((r) => {
r.is_selected = newstate;
return r;
});
}}
class="underline cursor-pointer select-none">{#if current_organizations.some((r) => r.is_selected === true)}
{$_('deselect-all')}
{:else}{$_('select-all')}{/if}
</span>
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('name')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('address')}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{$_('contact')}
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">{$_('action')}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each current_organizations as o}
{#if Object.values(o)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr data-rowid="org_{o.id}">
<td class="px-6 py-4 whitespace-nowrap">
<input
bind:checked={o.is_selected}
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{o.name}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{#if o.address.address1 !== null}
{o.address.address1}<br />
{o.address.address2 || ''}<br />
{o.address.postalcode}
{o.address.city}
{o.address.country}
{/if}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
{#if o.contact}
<a
href="../contacts/{o.contact.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">{o.contact.firstname}
{o.contact.middlename || ''}
{o.contact.lastname}</a>
{:else}{$_('no-contact-specified')}{/if}
</div>
</div>
</div>
</td>
{#if active_deletes[o.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button
on:click={() => {
active_deletes[o.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
<button
on:click={() => {
RunnerOrganizationService.runnerOrganizationControllerRemove(o.id, false)
.then((resp) => {
current_organizations = current_organizations.filter((obj) => obj.id !== o.id);
Toastify({
text: 'Organization deleted',
duration: 500,
backgroundColor:
'linear-gradient(to right, #00b09b, #96c93d)',
}).showToast();
})
.catch((err) => {
modal_open = true;
delete_org = o;
});
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
</td>
{:else}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a
href="./{o.id}"
class="text-indigo-600 hover:text-indigo-900">{$_('details')}</a>
{#if store.state.jwtinfo.userdetails.permissions.includes('ORGANIZATION:DELETE')}
<button
on:click={() => {
active_deletes[o.id] = true;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
{/if}
</td>
{/if}
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}
{/if}

View File

@ -1,253 +1,51 @@
<script> <script>
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import { _ } from "svelte-i18n";
let delete_org = {}; import store from "../../store";
import { RunnerOrganizationService } from "@odit/lfk-client-js"; import AddOrgModal from "./AddOrgModal.svelte";
import store from "../../store"; export let modal_open = false;
import OrgsEmptyState from "./OrgsEmptyState.svelte"; import OrgOverview from "./OrgOverview.svelte";
import ConfirmOrgDeletionModal from "./ConfirmOrgDeletionModal.svelte"; import ImportRunnerModal from "../runners/ImportRunnerModal.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; let current_organizations = [];
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; export let import_modal_open = false;
import toast from "svelte-french-toast";
$: searchvalue = "";
$: active_deletes = [];
$: sponsoring_contracts_show = current_organizations.some(
(r) => r.is_selected === true
);
$: cards_show = current_organizations.some((r) => r.is_selected === true);
$: generate_orgs = current_organizations.filter(
(r) => r.is_selected === true
);
$: certificates_show = current_organizations.some(
(r) => r.is_selected === true
);
let current_organizations = [];
const promise =
RunnerOrganizationService.runnerOrganizationControllerGetAll().then(
(val) => {
current_organizations = val;
}
);
import { _ } from "svelte-i18n";
import AddOrgModal from "./AddOrgModal.svelte";
let delete_modal_open = false;
let modal_open = false;
import ImportRunnerModal from "../runners/ImportRunnerModal.svelte";
let import_modal_open = false;
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight"> <span class="mb-1 text-3xl font-extrabold leading-tight">
{$_("organizations")} {$_('organizations')}
</h4> {#if store.state.jwtinfo.userdetails.permissions.includes('ORGANIZATION:CREATE')}
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:CREATE")} <button
<button on:click={() => {
on:click={() => { modal_open = true;
modal_open = true; }}
}} type="button"
type="button" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" {$_('create-organization')}
> </button>
{$_("create-organization")} {/if}
</button> {#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:IMPORT')}
{/if} <button
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:IMPORT")} on:click={() => {
<button import_modal_open = true;
on:click={() => { }}
import_modal_open = true; type="button"
}} class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
type="button" {$_('import-runners')}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" </button>
> {/if}
{$_("import-runners")} </span>
</button> <OrgOverview bind:current_organizations />
{/if}
<ConfirmOrgDeletionModal
on:cancelDelete={(event) => {
delete_modal_open = false;
active_deletes[event.detail.id] = false;
}}
bind:modal_open={delete_modal_open}
bind:delete_org
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")}
{#await promise}
<div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert"
>
<p class="font-bold">{$_("organizations-are-being-loaded")}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div>
{:then}
{#if current_organizations.length === 0}
<OrgsEmptyState />
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")}
class="w-full sm:w-auto sm:mt-0 p-2 rounded-md border mb-1 lg:mb-0"
/>
<GenerateSponsoringContracts
bind:sponsoring_contracts_show
bind:generate_orgs
/>
<GenerateRunnerCards bind:cards_show bind:generate_orgs />
<GenerateRunnerCertificates bind:certificates_show bind:generate_orgs />
<div
class="shadow border-b border-gray-200 sm:rounded-lg overflow-x-scroll"
>
<table class="divide-y divide-gray-200 w-full">
<thead class="bg-gray-50">
<tr class="odd:bg-white even:bg-gray-100">
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<button
on:click={() => {
const newstate = !current_organizations.some(
(r) => r.is_selected === true
);
current_organizations = current_organizations.map((r) => {
r.is_selected = newstate;
return r;
});
}}
class="underline cursor-pointer select-none"
>{#if current_organizations.some((r) => r.is_selected === true)}
{$_("deselect-all")}
{:else}{$_("select-all")}{/if}
</button>
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{$_("name")}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{$_("address")}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{$_("contact")}
</th>
<th scope="col" class="relative px-6 py-3">
<span class="sr-only">{$_("action")}</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{#each current_organizations as o}
{#if Object.values(o)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr
class="odd:bg-white even:bg-gray-100"
data-rowid="org_{o.id}"
>
<td class="px-6 py-4 whitespace-nowrap">
<input
bind:checked={o.is_selected}
type="checkbox"
class="focus:ring-indigo-500 size-4 text-indigo-600 border-gray-300 rounded"
/>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">
{o.name}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">
{#if o.address.address1 !== null}
{o.address.address1}<br />
<!-- {o.address.address2 || ''}<br /> -->
{o.address.postalcode}
{o.address.city}
{o.address.country}
{/if}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">
{#if o.contact}
<a
href="../contacts/{o.contact.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{o.contact.firstname}
{o.contact.middlename || ""}
{o.contact.lastname}</a
>
{:else}{$_("no-contact-specified")}{/if}
</div>
</div>
</td>
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>
<a
href="./{o.id}"
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:DELETE")}
<button
on:click={() => {
active_deletes[o.id] = true;
delete_modal_open = true;
delete_org = o;
}}
tabindex="0"
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
>{$_("delete")}</button
>
{/if}
</td>
</tr>
{/if}
{/each}
</tbody>
</table>
</div>
{/if}
{:catch error}
<div
class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"
>
<span class="inline-block align-middle mr-8">
<b>{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
{/if}
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes('ORGANIZATION:CREATE')}
<AddOrgModal bind:current_organizations bind:modal_open /> <AddOrgModal bind:current_organizations bind:modal_open />
<ImportRunnerModal <ImportRunnerModal
on:cancelDelete={(event) => { on:cancelDelete={(event) => {
import_modal_open = false; import_modal_open = false;
}} }}
passed_team={{}} passed_team={{}}
passed_org={{}} passed_org={{}}
passed_orgs={current_organizations} passed_orgs={current_organizations}
opened_from="OrgOverview" opened_from="OrgOverview"
bind:import_modal_open current_runners={[]}
/> bind:import_modal_open />
{/if} {/if}

View File

@ -9,9 +9,9 @@
<div class="text-center items-center justify-center"> <div class="text-center items-center justify-center">
<p class="mb-16 text-lg text-gray-500"> <p class="mb-16 text-lg text-gray-500">
<img class="w-full h-44" src={org_empty} alt="" /> <img class="w-full h-44" src={org_empty} alt="" />
<span class="font-bold">{$_("there-are-no-organizations-added-yet")}</span <span
><br /> class="font-bold">{$_('there-are-no-organizations-added-yet')}</span><br />
<span>{$_("add-your-first-organization")}</span> <span>{$_('add-your-first-organization')}</span>
</p> </p>
</div> </div>

View File

@ -1,152 +0,0 @@
class DocumentServer {
baseUrl: string;
apiKey: string;
constructor(baseUrl: string, apiKey: string) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
async generateCards(cards: any[], locale: string) {
const generateCards = new Array<any>();
for (let i = 0; i < cards.length; i++) {
const card = {
id: parseInt(cards[i].id),
enabled: cards[i].enabled,
code: cards[i].code,
runner: {
id: parseInt(cards[i]?.runner?.id),
first_name: cards[i]?.runner?.firstname,
middle_name: cards[i]?.runner?.middlename,
last_name: cards[i]?.runner?.lastname,
group: {
id: parseInt(cards[i]?.runner?.group?.id),
name: cards[i]?.runner?.group.name,
parent_group: {
id: parseInt(cards[i]?.runner?.group?.parentGroup?.id),
name: cards[i]?.runner?.group?.parentGroup?.name,
},
},
},
};
generateCards.push(card);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/cards?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
cards: generateCards,
}),
},
);
const blob = await response.blob();
return blob;
}
async generateContracts(runners: any[], locale: string) {
const generateRunners = new Array<any>();
for (let i = 0; i < runners.length; i++) {
console.log(runners[i]);
const card = {
id: parseInt(runners[i].id),
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
group: {
id: parseInt(runners[i].group.id),
name: runners[i].group.name,
parent_group: {
id: parseInt(runners[i]?.group?.parentGroup?.id),
name: runners[i]?.group?.parentGroup?.name,
},
},
};
generateRunners.push(card);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/contracts?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
runners: generateRunners,
}),
},
);
const blob = await response.blob();
return blob;
}
async generateCertificates(runners: any[], locale: string) {
const generateRunners = new Array<any>();
for (let i = 0; i < runners.length; i++) {
const certificate = {
id: parseInt(runners[i].id),
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
self_service_link: runners[i].selfserviceLink,
group: {
id: parseInt(runners[i].group.id),
name: runners[i].group.name,
parent_group: {
id: parseInt(runners[i]?.group?.parentGroup?.id),
name: runners[i]?.group?.parentGroup?.name,
},
},
distance: parseInt(runners[i].distance),
distance_donations: runners[i].distanceDonations.map(
(distanceDonation: any) => {
return {
id: distanceDonation.id,
amount: parseInt(distanceDonation.amount),
amount_per_distance: parseInt(distanceDonation.amountPerDistance),
donor: {
id: parseInt(distanceDonation.donor.id),
first_name: distanceDonation.donor.firstname,
middle_name: distanceDonation.donor.middlename,
last_name: distanceDonation.donor.lastname,
},
};
},
),
};
generateRunners.push(certificate);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/certificates?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
runners: generateRunners,
}),
},
);
const blob = await response.blob();
return blob;
}
}
export default DocumentServer;

View File

@ -1,81 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { onMount } from "svelte";
export let download_details = "";
export let modal_open;
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw] relative z-10"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 rounded-t-xl">
<div class="">
<div
class="flex-shrink-0 flex items-center justify-center size-12 rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="size-6 text-blue-600"
fill="currentColor"
width="24"
height="24"
><path fill="none" d="M0 0h24v24H0z" />
<path
d="M9.83 8.79L8 9.456V13H6V8.05h.015l5.268-1.918c.244-.093.51-.14.782-.131a2.616 2.616 0 0 1 2.427 1.82c.186.583.356.977.51 1.182A4.992 4.992 0 0 0 19 11v2a6.986 6.986 0 0 1-5.402-2.547l-.581 3.297L15 15.67V23h-2v-5.986l-2.05-1.987-.947 4.298-6.894-1.215.348-1.97 4.924.868L9.83 8.79zM13.5 5.5a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"
/></svg
>
</div>
<div class="mt-3 sm:text-left text-base">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{$_('download_laeuft')}
</h3>
<div class="w-full">
{download_details}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}

View File

@ -1,256 +1,418 @@
<script> <script>
import { _ } from "svelte-i18n"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import { import {
RunnerCardService, RunnerCardService,
RunnerOrganizationService, RunnerOrganizationService,
RunnerTeamService, RunnerTeamService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import toast from "svelte-french-toast"; import Toastify from "toastify-js";
import DocumentServer from "./DocumentServer.ts"; import { init } from "@paralleldrive/cuid2";
const createId = init({ length: 10, fingerprint: "lfk-frontend" });
import { init } from "@paralleldrive/cuid2";
const createId = init({ length: 10, fingerprint: "lfk-frontend" }); export let cards_show = false;
const documentServer = new DocumentServer( export let generate_cards = [];
config.baseurl_documentserver, export let generate_runners = [];
config.documentserver_key export let generate_orgs = [];
); export let generate_teams = [];
$: cards_dropdown_open = false;
export let cards_show = false; document.addEventListener("click", function (e) {
export let generate_cards = []; if (
export let generate_runners = []; e.target.parentNode?.parentNode?.id != "cards:dropdown" &&
export let generate_orgs = []; e.target.parentNode?.parentNode?.id != "cards:dropdown:menu"
export let generate_teams = []; ) {
cards_dropdown_open = false;
function download(blob, fileName) { }
const url = window.URL.createObjectURL(blob); });
let a = document.createElement("a");
a.href = url; function generateRunnerCards(locale) {
a.download = fileName; cards_dropdown_open = false;
document.body.appendChild(a);
a.click(); if (generate_orgs.length > 0) {
a.remove(); generateOrgCards(locale);
toast.dismiss(); } else if (generate_teams.length > 0) {
toast.success($_("pdf-successfully-generated")); generateTeamCards(locale);
} } else if (generate_runners.length > 0) {
generateRunnersCards(locale);
function generateRunnerCards(locale, useCombined = false) { } else {
if (generate_orgs.length > 0) { generateCards(locale);
if(useCombined){ }
generateOrgCardsCombined(locale); }
} else {
generateOrgCards(locale) function generateCards(locale) {
} const toast = Toastify({
} else if (generate_teams.length > 0) { text: $_("generating-pdf"),
generateTeamCards(locale); duration: -1,
} else if (generate_runners.length > 0) { }).showToast();
generateRunnersCards(locale); fetch(
} else { `${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`,
generateCards(locale); {
} method: "POST",
} headers: {
"Content-Type": "application/json",
function generateCards(locale) { },
toast.loading($_("generating-pdf")); body: JSON.stringify(generate_cards),
documentServer }
.generateCards(generate_cards, locale) )
.then((blob) => { .then((response) => {
download(blob, `${$_("runnercards")}-${locale}-${createId()}.pdf`); if (response.status != "200") {
}) toast.hideToast();
.catch((err) => { Toastify({
console.error(err); text: $_("pdf-generation-failed"),
}); duration: 3500,
} backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
async function generateRunnersCards(locale) { }).showToast();
toast.loading($_("generating-pdf")); } else {
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); return response.blob();
let cards = []; }
for (let runner of generate_runners) { })
let card = current_cards.find((c) => c.runner?.id == runner.id); .then((blob) => {
if (!card) { const url = window.URL.createObjectURL(blob);
card = await RunnerCardService.runnerCardControllerPost({ let a = document.createElement("a");
runner: runner.id, a.href = url;
}); a.download = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
} document.body.appendChild(a);
cards.push(card); a.click();
} a.remove();
documentServer toast.hideToast();
.generateCards(cards, locale) Toastify({
.then((blob) => { text: $_("pdf-successfully-generated"),
let fileName = `${$_("runnercards")}-${locale}-${createId()}.pdf`; duration: 3500,
if (generate_runners.length == 1) { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
fileName = `${$_("runnercards")}_${generate_runners[0].firstname}_${ }).showToast();
generate_runners[0].lastname })
}-${locale}-${createId()}.pdf`; .catch((err) => {
} console.error(err);
download(blob, fileName); });
}) }
.catch((err) => {});
} async function generateRunnersCards(locale) {
const toast = Toastify({
async function generateTeamCards(locale) { text: $_("generating-pdf"),
toast.loading($_("generating-pdfs")); duration: -1,
let count = 0; }).showToast();
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); const current_cards = await RunnerCardService.runnerCardControllerGetAll();
for (const t of generate_teams) { let cards = [];
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( for (let runner of generate_runners) {
t.id let card = current_cards.find((c) => c.runner?.id == runner.id);
); if (!card) {
let cards = []; card = await RunnerCardService.runnerCardControllerPost({
for (let runner of runners) { runner: runner.id,
let card = current_cards.find((c) => c.runner?.id == runner.id); });
if (!card) { }
card = await RunnerCardService.runnerCardControllerPost({ cards.push(card);
runner: runner.id, }
}); fetch(
} `${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`,
cards.push(card); {
} method: "POST",
documentServer headers: {
.generateCards(cards, locale) "Content-Type": "application/json",
.then((blob) => { },
download( body: JSON.stringify(cards),
blob, }
`${$_("runnercards")}_${t.name}-${locale}-${createId()}.pdf` )
); .then((response) => {
}) if (response.status != "200") {
.catch((err) => {}); toast.hideToast();
} Toastify({
} text: $_("pdf-generation-failed"),
duration: 3500,
async function generateOrgCards(locale) { backgroundColor:
toast.loading($_("generating-pdfs")); "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); }).showToast();
let count = 0; } else {
let count_orgs = 0; return response.blob();
for (const o of generate_orgs) { }
count_orgs++; })
let count = 0; .then((blob) => {
let runners = const url = window.URL.createObjectURL(blob);
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( let a = document.createElement("a");
o.id, a.href = url;
true if (generate_runners.length == 1) {
); a.download = `${$_("runnercards")}_${generate_runners[0].firstname}_${
let cards = []; generate_runners[0].lastname
for (let runner of runners) { }-${locale}-${createId()}.pdf`;
let card = current_cards.find((c) => c.runner?.id == runner.id); } else {
if (!card) { a.download = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
card = await RunnerCardService.runnerCardControllerPost({ }
runner: runner.id, document.body.appendChild(a);
}); a.click();
} a.remove();
cards.push(card); toast.hideToast();
} Toastify({
await documentServer text: $_("pdf-successfully-generated"),
.generateCards(cards, locale) duration: 3500,
.then((blob) => { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
download( }).showToast();
blob, })
`${$_("runnercards")}_${o.name}_direct-${locale}-${createId()}.pdf` .catch((err) => {});
); }
})
.catch((err) => {}); async function generateTeamCards(locale) {
for (const t of o.teams) { const toast = Toastify({
count++; text: $_("generating-pdfs"),
let runners = await RunnerTeamService.runnerTeamControllerGetRunners( duration: -1,
t.id }).showToast();
); let count = 0;
let cards = []; const current_cards = await RunnerCardService.runnerCardControllerGetAll();
for (let runner of runners) { for (const t of generate_teams) {
let card = current_cards.find((c) => c.runner?.id == runner.id); const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
if (!card) { t.id
card = await RunnerCardService.runnerCardControllerPost({ );
runner: runner.id, let cards = [];
}); for (let runner of runners) {
} let card = current_cards.find((c) => c.runner?.id == runner.id);
cards.push(card); if (!card) {
} card = await RunnerCardService.runnerCardControllerPost({
await documentServer runner: runner.id,
.generateCards(cards, locale) });
.then((blob) => { }
download( cards.push(card);
blob, }
`${$_("runnercards")}_${o.name}_${ fetch(
t.name `${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`,
}-${locale}-${createId()}.pdf` {
); method: "POST",
}) headers: {
.catch((err) => {}); "Content-Type": "application/json",
} },
} body: JSON.stringify(cards),
} }
async function generateOrgCardsCombined(locale) { )
toast.loading($_("generating-pdfs")); .then((response) => {
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); if (response.status != "200") {
let count = 0; toast.hideToast();
let count_orgs = 0; Toastify({
for (const o of generate_orgs) { text: $_("pdf-generation-failed"),
count_orgs++; duration: 3500,
let cards = []; backgroundColor:
let count = 0; "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
let runners = }).showToast();
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( } else {
o.id, return response.blob();
true }
); })
for (let runner of runners) { .then((blob) => {
let card = current_cards.find((c) => c.runner?.id == runner.id); count++;
if (!card) { const url = window.URL.createObjectURL(blob);
card = await RunnerCardService.runnerCardControllerPost({ let a = document.createElement("a");
runner: runner.id, a.href = url;
}); a.download = `${$_("runnercards")}_${t.name}-${locale}-${createId()}.pdf`;
} document.body.appendChild(a);
cards.push(card); a.click();
} a.remove();
for (const t of o.teams) { if (count === generate_teams.length) {
count++; toast.hideToast();
let runners = await RunnerTeamService.runnerTeamControllerGetRunners( Toastify({
t.id text: $_("pdfs-successfully-generated"),
); duration: 3500,
for (let runner of runners) { backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
let card = current_cards.find((c) => c.runner?.id == runner.id); }).showToast();
if (!card) { }
card = await RunnerCardService.runnerCardControllerPost({ })
runner: runner.id, .catch((err) => {});
}); }
} }
cards.push(card);
} async function generateOrgCards(locale) {
} const toast = Toastify({
await documentServer text: $_("generating-pdfs"),
.generateCards(cards, locale) duration: -1,
.then((blob) => { }).showToast();
download( const current_cards = await RunnerCardService.runnerCardControllerGetAll();
blob, let count = 0;
`${$_("runnercards")}_${o.name}-${locale}-${createId()}.pdf` let count_orgs = 0;
); for (const o of generate_orgs) {
}) count_orgs++;
.catch((err) => {}); let count = 0;
} let runners =
} await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
</script> o.id,
true
{#if cards_show} );
<button let cards = [];
on:click={() => { for (let runner of runners) {
generateRunnerCards("de"); let card = current_cards.find((c) => c.runner?.id == runner.id);
}} if (!card) {
on:contextmenu|preventDefault={() => { card = await RunnerCardService.runnerCardControllerPost({
generateRunnerCards("de", true); runner: runner.id,
}} });
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" }
> cards.push(card);
{$_("generate-runnercards")}: DE }
</button> await fetch(
<button `${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`,
on:click={() => { {
generateRunnerCards("en"); method: "POST",
}} headers: {
on:contextmenu|preventDefault={() => { "Content-Type": "application/json",
generateRunnerCards("en", true); },
}} body: JSON.stringify(cards),
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" }
> )
{$_("generate-runnercards")}: EN .then((response) => {
</button> if (response.status != "200") {
{/if} toast.hideToast();
Toastify({
text: $_("pdf-generation-failed"),
duration: 3500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("runnercards")}_${o.name}_direct-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (count === o.teams.length && count_orgs === generate_orgs.length) {
toast.hideToast();
console.log("here");
Toastify({
text: $_("pdfs-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
})
.catch((err) => {});
for (const t of o.teams) {
count++;
let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
let cards = [];
for (let runner of runners) {
let card = current_cards.find((c) => c.runner?.id == runner.id);
if (!card) {
card = await RunnerCardService.runnerCardControllerPost({
runner: runner.id,
});
}
cards.push(card);
}
await fetch(
`${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(cards),
}
)
.then((response) => {
if (response.status != "200") {
toast.hideToast();
Toastify({
text: $_("pdf-generation-failed"),
duration: 3500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("runnercards")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (
count === o.teams.length &&
count_orgs === generate_orgs.length
) {
toast.hideToast();
Toastify({
text: $_("pdfs-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
})
.catch((err) => {});
}
}
}
</script>
{#if cards_show}
<div id="cards:dropdown" class="relative inline-block">
<div>
<button
on:click={() => {
cards_dropdown_open = !cards_dropdown_open;
}}
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{$_("generate-runnercards")}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="-mr-1 ml-2 h-5 w-5"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M3 19h18v2H3v-2zm10-5.83l6.07-6.07 1.42 1.41L12 17 3.52 8.52l1.4-1.42L11 13.17V2h2v11.17z"
/></svg
>
</button>
</div>
{#if cards_dropdown_open}
<div
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10"
id="cards:dropdown:menu"
>
<div
class="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<span class="block w-full text-left px-4 py-2 text-sm text-gray-700"
>{$_("select-language")}</span
>
<button
on:click={() => {
generateRunnerCards("de");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("german")}
</button>
<button
on:click={() => {
generateRunnerCards("en");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("english")}
</button>
</div>
</div>
{/if}
</div>
{/if}

View File

@ -1,220 +1,355 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { import {
DonationService, DonationService,
RunnerTeamService, RunnerTeamService,
RunnerOrganizationService, RunnerOrganizationService,
RunnerService } from "@odit/lfk-client-js";
} from "@odit/lfk-client-js"; import Toastify from "toastify-js";
import { init } from "@paralleldrive/cuid2"; import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast"; const createId = init({ length: 10, fingerprint: "lfk-frontend" });
import DocumentServer from "./DocumentServer";
const createId = init({ length: 10, fingerprint: "lfk-frontend" }); export let certificates_show = false;
const documentServer = new DocumentServer( export let generate_runners = [];
config.baseurl_documentserver, export let generate_orgs = [];
config.documentserver_key export let generate_teams = [];
); $: certificates_dropdown_open = false;
document.addEventListener("click", function (e) {
export let certificates_show = false; if (
export let generate_runners = []; e.target.parentNode?.parentNode?.id != "certificates:dropdown" &&
export let generate_orgs = []; e.target.parentNode?.parentNode?.id != "certificates:dropdown:menu"
export let generate_teams = []; ) {
certificates_dropdown_open = false;
function generateCertificates(locale, include0runners = false) { }
if (generate_orgs.length > 0) { });
generateOrgCertificates(locale, include0runners = false);
} else if (generate_teams.length > 0) { function generateCertificates(locale) {
generateTeamCertificates(locale, include0runners = false); certificates_dropdown_open = false;
} else {
generateRunnerCertificates(locale, include0runners = false); if (generate_orgs.length > 0) {
} generateOrgCertificates(locale);
} } else if (generate_teams.length > 0) {
function download(blob, fileName) { generateTeamCertificates(locale);
const url = window.URL.createObjectURL(blob); } else {
let a = document.createElement("a"); generateRunnerCertificates(locale);
a.href = url; }
a.download = fileName; }
document.body.appendChild(a);
a.click(); async function generateRunnerCertificates(locale) {
a.remove(); const toast = Toastify({
toast.dismiss(); text: $_("generating-pdf"),
toast.success($_("pdf-successfully-generated")); duration: -1,
} }).showToast();
const current_donations =
async function generateRunnerCertificates(locale, include0runners = false) { (await DonationService.donationControllerGetAll()) || [];
toast.loading($_("generating-pdf")); let certificateRunners = [];
const current_donations = for (let runner of generate_runners) {
(await DonationService.donationControllerGetAll()) || []; runner.distanceDonations =
let certificateRunners = []; current_donations.filter((d) => d.runner?.id == runner.id) || [];
for (let runner of generate_runners) { console.log(runner.distanceDonations);
const linkRunner = await RunnerService.runnerControllerGetOne(runner.id) certificateRunners.push(runner);
linkRunner.distanceDonations = }
current_donations.filter((d) => d.runner?.id == runner.id) || []; fetch(
// check if linkRunner.distance is 0, if so, and include0runners is false, skip this runner `${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`,
if ( {
!include0runners && method: "POST",
(linkRunner.distance === 0 || linkRunner.distance === null) headers: {
) { "Content-Type": "application/json",
continue; },
} else { body: JSON.stringify(certificateRunners),
certificateRunners.push(linkRunner); }
} )
} .then((response) => {
documentServer if (response.status != "200") {
.generateCertificates(certificateRunners, locale) toast.hideToast();
.then((blob) => { Toastify({
let fileName = `${$_("certificates")}-${locale}.pdf`; text: $_("pdf-generation-failed"),
if (generate_runners.length == 1) { duration: 3500,
fileName = `${$_("certificates")}_${ backgroundColor:
generate_runners[0].firstname "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`; }).showToast();
} } else {
download(blob, fileName); return response.blob();
}) }
.catch((err) => {}); })
} .then((blob) => {
const url = window.URL.createObjectURL(blob);
async function generateTeamCertificates(locale, include0runners = false) { let a = document.createElement("a");
toast.loading($_("generating-pdfs")); a.href = url;
let count = 0; if (generate_runners.length == 1) {
const current_donations = a.download = `${$_("certificates")}_${
(await DonationService.donationControllerGetAll()) || []; generate_runners[0].firstname
for (const t of generate_teams) { }_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`;
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( } else {
t.id, a.download = `${$_("certificates")}-${locale}.pdf`;
true }
); document.body.appendChild(a);
let certificateRunners = []; a.click();
for (let runner of runners) { a.remove();
runner.distanceDonations = toast.hideToast();
current_donations.filter((d) => d.runner?.id == runner.id) || []; Toastify({
// check if runner.distance is 0, if so, and include0runners is false, skip this runner text: $_("pdf-successfully-generated"),
if ( duration: 3500,
!include0runners && backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
(runner.distance === 0 || runner.distance === null) }).showToast();
) { })
continue; .catch((err) => {});
} else { }
certificateRunners.push(runner);
} async function generateTeamCertificates(locale) {
} const toast = Toastify({
documentServer text: $_("generating-pdfs"),
.generateCertificates(certificateRunners, locale) duration: -1,
.then((blob) => { }).showToast();
count++; let count = 0;
download( const current_donations =
blob, (await DonationService.donationControllerGetAll()) || [];
`${$_("certificates")}_${t.name}-${locale}-${createId()}.pdf` for (const t of generate_teams) {
); const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
}) t.id
.catch((err) => {}); );
} let certificateRunners = [];
} for (let runner of runners) {
runner.distanceDonations =
async function generateOrgCertificates(locale, include0runners = false) { current_donations.filter((d) => d.runner?.id == runner.id) || [];
toast.loading($_("generating-pdfs")); certificateRunners.push(runner);
const current_donations = }
(await DonationService.donationControllerGetAll()) || []; fetch(
let count = 0; `${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`,
let count_orgs = 0; {
for (const o of generate_orgs) { method: "POST",
count_orgs++; headers: {
let count = 0; "Content-Type": "application/json",
let runners = },
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( body: JSON.stringify(certificateRunners),
o.id, }
true, )
true .then((response) => {
); if (response.status != "200") {
let certificateRunners = []; toast.hideToast();
for (let runner of runners) { Toastify({
runner.distanceDonations = text: $_("pdf-generation-failed"),
current_donations.filter((d) => d.runner?.id == runner.id) || []; duration: 3500,
// check if runner.distance is 0, if so, and include0runners is false, skip this runner backgroundColor:
if ( "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
!include0runners && }).showToast();
(runner.distance === 0 || runner.distance === null) } else {
) { return response.blob();
continue; }
} else { })
certificateRunners.push(runner); .then((blob) => {
} count++;
} const url = window.URL.createObjectURL(blob);
await documentServer let a = document.createElement("a");
.generateCertificates(certificateRunners, locale) a.href = url;
.then((blob) => { a.download = `${$_("certificates")}_${t.name}-${locale}-${createId()}.pdf`;
download( document.body.appendChild(a);
blob, a.click();
`${$_("certificates")}_${o.name}-${locale}-${createId()}.pdf` a.remove();
); if (count === generate_teams.length) {
}) toast.hideToast();
.catch((err) => {}); Toastify({
for (const t of o.teams) { text: $_("pdfs-successfully-generated"),
count++; duration: 3500,
let runners = await RunnerTeamService.runnerTeamControllerGetRunners( backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
t.id, }).showToast();
true }
); })
let certificateRunners = []; .catch((err) => {});
for (let runner of runners) { }
runner.distanceDonations = }
current_donations.filter((d) => d.runner?.id == runner.id) || [];
certificateRunners.push(runner); async function generateOrgCertificates(locale) {
} const toast = Toastify({
await documentServer text: $_("generating-pdfs"),
.generateCertificates(certificateRunners, locale) duration: -1,
.then((blob) => { }).showToast();
download( const current_donations =
blob, (await DonationService.donationControllerGetAll()) || [];
`${$_("certificates")}_${o.name}_${ let count = 0;
t.name let count_orgs = 0;
}-${locale}-${createId()}.pdf` for (const o of generate_orgs) {
); count_orgs++;
if ( let count = 0;
count === o.teams.length && let runners =
count_orgs === generate_orgs.length await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
) { o.id,
toast.dismiss(); true
toast.success($_("pdfs-successfully-generated")); );
} let certificateRunners = [];
}) for (let runner of runners) {
.catch((err) => {}); runner.distanceDonations =
} current_donations.filter((d) => d.runner?.id == runner.id) || [];
} certificateRunners.push(runner);
} }
</script> await fetch(
`${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`,
{#if certificates_show} {
<button method: "POST",
on:click={() => { headers: {
generateCertificates("de", true); "Content-Type": "application/json",
}} },
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" body: JSON.stringify(certificateRunners),
> }
{$_("generate-runner-certificates")}: DE )
</button> .then((response) => {
<button if (response.status != "200") {
on:click={() => { toast.hideToast();
generateCertificates("de", false); Toastify({
}} text: $_("pdf-generation-failed"),
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" duration: 3500,
> backgroundColor:
{$_("generate-runner-certificates")}: DE [{$_('exclude_0m_runners_certificate')}] "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
</button> }).showToast();
<button } else {
on:click={() => { return response.blob();
generateCertificates("en", true); }
}} })
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" .then((blob) => {
> const url = window.URL.createObjectURL(blob);
{$_("generate-runner-certificates")}: EN let a = document.createElement("a");
</button> a.href = url;
<button a.download = `${$_("certificates")}_${o.name}-${locale}-${createId()}.pdf`;
on:click={() => { document.body.appendChild(a);
generateCertificates("en", false); a.click();
}} a.remove();
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" if (count === o.teams.length && count_orgs === generate_orgs.length) {
> toast.hideToast();
{$_("generate-runner-certificates")}: EN [{$_('exclude_0m_runners_certificate')}] console.log("here");
</button> Toastify({
{/if} text: $_("pdfs-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
})
.catch((err) => {});
for (const t of o.teams) {
count++;
let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
let certificateRunners = [];
for (let runner of runners) {
runner.distanceDonations =
current_donations.filter((d) => d.runner?.id == runner.id) || [];
certificateRunners.push(runner);
}
await fetch(
`${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(certificateRunners),
}
)
.then((response) => {
if (response.status != "200") {
toast.hideToast();
Toastify({
text: $_("pdf-generation-failed"),
duration: 3500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("certificates")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (
count === o.teams.length &&
count_orgs === generate_orgs.length
) {
toast.hideToast();
Toastify({
text: $_("pdfs-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
})
.catch((err) => {});
}
}
}
</script>
{#if certificates_show}
<div id="certificates:dropdown" class="relative inline-block">
<div>
<button
on:click={() => {
certificates_dropdown_open = !certificates_dropdown_open;
}}
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{$_("generate-runner-certificates")}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="-mr-1 ml-2 h-5 w-5"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M3 19h18v2H3v-2zm10-5.83l6.07-6.07 1.42 1.41L12 17 3.52 8.52l1.4-1.42L11 13.17V2h2v11.17z"
/></svg
>
</button>
</div>
{#if certificates_dropdown_open}
<div
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10"
id="certificates:dropdown:menu"
>
<div
class="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<span class="block w-full text-left px-4 py-2 text-sm text-gray-700"
>{$_("select-language")}</span
>
<button
on:click={() => {
generateCertificates("de");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("german")}
</button>
<button
on:click={() => {
generateCertificates("en");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("english")}
</button>
</div>
</div>
{/if}
</div>
{/if}

View File

@ -1,163 +1,324 @@
<script> <script>
import { _ } from "svelte-i18n"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import { import {
RunnerOrganizationService, RunnerOrganizationService,
RunnerTeamService, RunnerTeamService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import DocumentServer from "./DocumentServer"; import Toastify from "toastify-js";
import { init } from "@paralleldrive/cuid2"; import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast"; const createId = init({ length: 10, fingerprint: "lfk-frontend" });
import DownloadProgressModal from "./DownloadProgressModal.svelte";
const createId = init({ length: 10, fingerprint: "lfk-frontend" }); export let sponsoring_contracts_show = false;
const documentServer = new DocumentServer( export let generate_runners = [];
config.baseurl_documentserver, export let generate_orgs = [];
config.documentserver_key export let generate_teams = [];
); $: sponsoring_contracts_download_open = false;
document.addEventListener("click", function (e) {
export let sponsoring_contracts_show = false; if (
export let generate_runners = []; e.target.parentNode?.parentNode?.id != "sponsoring:dropdown" &&
export let generate_orgs = []; e.target.parentNode?.parentNode?.id != "sponsoring:dropdown:menu"
export let generate_teams = []; ) {
// sponsoring_contracts_download_open = false;
export let download_modal_open = false; }
export let download_details = ""; });
function generateSponsoringContract(locale) { function generateSponsoringContract(locale) {
download_modal_open = true; sponsoring_contracts_download_open = false;
if (generate_orgs.length > 0) {
generateOrgContracts(locale); if (generate_orgs.length > 0) {
} else if (generate_teams.length > 0) { generateOrgContracts(locale);
generateTeamContracts(locale); } else if (generate_teams.length > 0) {
} else { generateTeamContracts(locale);
generateRunnerContracts(locale); } else {
} generateRunnerContracts(locale);
} }
function download(blob, fileName) { }
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a"); async function generateTeamContracts(locale) {
a.href = url; const toast = Toastify({
a.download = fileName; text: $_("generating-pdfs"),
document.body.appendChild(a); duration: -1,
a.click(); }).showToast();
a.remove(); let count = 0;
toast.dismiss(); for (const t of generate_teams) {
toast.success($_("pdf-successfully-generated")); count++;
} const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
async function generateTeamContracts(locale) { );
toast.loading($_("generating-pdfs")); fetch(
let totalCount = generate_teams.length; `${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
let count = 0; {
for (const t of generate_teams) { method: "POST",
count++; headers: {
download_details = `${t.parentGroup.name} > ${t.name}`; "Content-Type": "application/json",
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( },
t.id body: JSON.stringify(runners),
); }
await documentServer )
.generateContracts(runners, locale) .then((response) => {
.then((blob) => { if (response.status != "200") {
download( toast.hideToast();
blob, Toastify({
`${$_("sponsorings")}_${t.name}-${locale}-${createId()}.pdf` text: $_("pdf-generation-failed"),
); duration: 3500,
if (count === totalCount) { backgroundColor:
download_modal_open = false; "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
} }).showToast();
}) } else {
.catch((err) => {}); return response.blob();
} }
} })
.then((blob) => {
async function generateOrgContracts(locale) { const url = window.URL.createObjectURL(blob);
toast.loading($_("generating-pdf")); let a = document.createElement("a");
let totalCount = 0; a.href = url;
for (const o of generate_orgs) { a.download = `${$_("sponsorings")}_${t.name}-${locale}-${createId()}.pdf`;
totalCount++; document.body.appendChild(a);
for (const t of o.teams) { a.click();
totalCount++; a.remove();
} if (count === generate_teams.length) {
} toast.hideToast();
console.log({ totalCount }); Toastify({
let count = 0; text: $_("pdfs-successfully-generated"),
for (const o of generate_orgs) { duration: 3500,
count++; backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
let runners = }).showToast();
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( }
o.id, })
true .catch((err) => {});
); }
download_details = o.name; }
await documentServer
.generateContracts(runners, locale) async function generateOrgContracts(locale) {
.then((blob) => { const toast = Toastify({
download( text: $_("generating-pdf"),
blob, duration: -1,
`${$_("sponsorings")}_${o.name}_direct-${locale}-${createId()}.pdf` }).showToast();
); let count_orgs = 0;
}) for (const o of generate_orgs) {
.catch((err) => {}); count_orgs++;
for (const t of o.teams) { let count = 0;
count++; let runners =
let runners = await RunnerTeamService.runnerTeamControllerGetRunners( await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
t.id o.id,
); true
download_details = `${o.name} > ${t.name}`; );
await documentServer await fetch(
.generateContracts(runners, locale) `${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
.then((blob) => { {
download( method: "POST",
blob, headers: {
`${$_("sponsorings")}_${o.name}_${ "Content-Type": "application/json",
t.name },
}-${locale}-${createId()}.pdf` body: JSON.stringify(runners),
); }
console.log({ count }); )
if (count === totalCount) { .then((response) => {
download_modal_open = false; if (response.status != "200") {
} toast.hideToast();
}) Toastify({
.catch((err) => {}); text: $_("pdf-generation-failed"),
} duration: 3500,
} backgroundColor:
} "linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
async function generateRunnerContracts(locale) { } else {
toast.loading($_("generating-pdf")); return response.blob();
await documentServer }
.generateContracts(generate_runners, locale) })
.then((blob) => { .then((blob) => {
let fileName = `${$_("sponsorings")}-${locale}-${createId()}.pdf`; const url = window.URL.createObjectURL(blob);
if (generate_runners.length == 1) { let a = document.createElement("a");
fileName = `${$_("sponsorings")}_${generate_runners[0].firstname}_${ a.href = url;
generate_runners[0].lastname a.download = `${$_("sponsorings")}_${o.name}_direct-${locale}-${createId()}.pdf`;
}-${locale}-${createId()}.pdf`; document.body.appendChild(a);
} a.click();
download(blob, fileName); a.remove();
download_modal_open = false; if (count === o.teams.length && count_orgs === generate_orgs.length) {
}) toast.hideToast();
.catch((err) => { console.log("here");
console.error(err); Toastify({
}); text: $_("pdfs-successfully-generated"),
} duration: 3500,
</script> backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
{#if sponsoring_contracts_show} }
<DownloadProgressModal {download_details} modal_open={download_modal_open} /> })
<button .catch((err) => {});
on:click={() => { for (const t of o.teams) {
generateSponsoringContract("de"); count++;
}} let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" t.id
> );
{$_("generate-sponsoring-contracts")}: DE await fetch(
</button> `${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
<button {
on:click={() => { method: "POST",
generateSponsoringContract("en"); headers: {
}} "Content-Type": "application/json",
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0" },
> body: JSON.stringify(runners),
{$_("generate-sponsoring-contracts")}: EN }
</button> )
{/if} .then((response) => {
if (response.status != "200") {
toast.hideToast();
Toastify({
text: $_("pdf-generation-failed"),
duration: 3500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("sponsorings")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (
count === o.teams.length &&
count_orgs === generate_orgs.length
) {
toast.hideToast();
Toastify({
text: $_("pdfs-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
}
})
.catch((err) => {});
}
}
}
function generateRunnerContracts(locale) {
const toast = Toastify({
text: $_("generating-pdf"),
duration: -1,
}).showToast();
fetch(
`${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(generate_runners),
}
)
.then((response) => {
if (response.status != "200") {
toast.hideToast();
Toastify({
text: $_("pdf-generation-failed"),
duration: 3500,
backgroundColor:
"linear-gradient(90deg, hsla(281, 37%, 45%, 1) 0%, hsla(1, 62%, 48%, 1) 100%)",
}).showToast();
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
if (generate_runners.length == 1) {
a.download = `${$_("sponsorings")}_${generate_runners[0].firstname}_${
generate_runners[0].lastname
}-${locale}-${createId()}.pdf`;
}
a.download = `${$_("sponsorings")}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
toast.hideToast();
Toastify({
text: $_("pdf-successfully-generated"),
duration: 3500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
})
.catch((err) => {
console.error(err);
});
}
</script>
{#if sponsoring_contracts_show}
<div id="sponsoring:dropdown" class="relative inline-block">
<div>
<button
on:click={() => {
sponsoring_contracts_download_open =
!sponsoring_contracts_download_open;
}}
type="button"
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-gray-600 text-base font-medium text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 sm:ml-3 sm:w-auto sm:text-sm inline-flex"
id="options-menu"
aria-haspopup="true"
aria-expanded="true"
>
{$_("generate-sponsoring-contracts")}
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="-mr-1 ml-2 h-5 w-5"
><path fill="none" d="M0 0h24v24H0z" />
<path
fill="currentColor"
d="M3 19h18v2H3v-2zm10-5.83l6.07-6.07 1.42 1.41L12 17 3.52 8.52l1.4-1.42L11 13.17V2h2v11.17z"
/></svg
>
</button>
</div>
{#if sponsoring_contracts_download_open}
<div
class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10"
id="sponsoring:dropdown:menu"
>
<div
class="py-1"
role="menu"
aria-orientation="vertical"
aria-labelledby="options-menu"
>
<span class="block w-full text-left px-4 py-2 text-sm text-gray-700"
>{$_("select-language")}</span
>
<button
on:click={() => {
generateSponsoringContract("de");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("german")}
</button>
<button
on:click={() => {
generateSponsoringContract("en");
}}
type="submit"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900"
role="menuitem"
>
{$_("english")}
</button>
</div>
</div>
{/if}
</div>
{/if}

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