Compare commits

..

101 Commits
1.4.2 ... 1.8.1

Author SHA1 Message Date
b8e6b24bf3 🚀RELEASE v1.8.1
All checks were successful
Build release images / build-container (push) Successful in 1m18s
Build Latest and dev images / build-container (push) Successful in 1m21s
2025-03-26 19:12:05 +01:00
97b7ca931f fix(pdf_generation): Only load direct runners for direct calls 2025-03-26 19:10:37 +01:00
48dd9acde5 🚀RELEASE v1.8.0
All checks were successful
Build release images / build-container (push) Successful in 1m13s
Build Latest and dev images / build-container (push) Successful in 1m15s
2025-03-26 16:03:19 +01:00
5147a20b3c fix(DonorDetail): donor deletion
All checks were successful
Build Latest and dev images / build-container (push) Successful in 1m20s
2025-03-26 15:57:44 +01:00
bb2319a78d chore(deps): bump
Some checks failed
Build Latest and dev images / build-container (push) Failing after 23s
2025-03-26 15:35:53 +01:00
7c10d95c1c feat(RunnerOrganizationService.runnerOrganizationControllerGetRunners): load all runners in org
All checks were successful
Build Latest and dev images / build-container (push) Successful in 1m7s
2025-03-26 15:34:38 +01:00
f734d1e3f6 feat: cleanup TeamDetail + OrgDetail 2025-03-26 15:33:34 +01:00
e567bb35c3 fix(ci): Correct tag pattern syntax in release workflow
All checks were successful
Build Latest and dev images / build-container (push) Successful in 58s
2025-03-22 22:39:20 +01:00
3ec18a6964 refactor(ci): Add Gitea workflow for building release images and remove Woodpecker configuration
All checks were successful
Build Latest and dev images / build-container (push) Successful in 1m53s
2025-03-22 22:38:43 +01:00
847fa288f1 refactor(ci): Switch to actions for dev 2025-03-22 22:33:59 +01:00
824ecfab2e wip 2025-03-20 22:29:36 +01:00
5f5d8277b9 wip 2025-03-20 22:15:36 +01:00
0a6cf619b0 wip 2025-03-20 22:13:41 +01:00
050a146ae0 wip 2025-03-20 21:52:39 +01:00
1bc53146b9 wip 2025-03-20 21:51:00 +01:00
e82350df4a wip 2025-03-20 21:42:02 +01:00
3d3ce2918b wip 2025-03-20 21:34:51 +01:00
79e6a4212d feat: improve input readability 2025-03-20 21:31:33 +01:00
37cdbba0a3 wip 2025-03-20 21:31:03 +01:00
c37fb98bed feat: improve fonts + button positions 2025-03-20 21:29:10 +01:00
975f145444 feat(dashboard): full width for sidebar items 2025-03-18 00:46:29 +01:00
391186d01f feat: athiti font 2025-03-18 00:46:10 +01:00
ae056cd88c 🚀RELEASE v1.7.0
All checks were successful
ci/woodpecker/push/build Pipeline was successful
ci/woodpecker/tag/release Pipeline was successful
2024-12-17 17:41:11 +01:00
7f989b206b fix(pdfgeneration): Added parent_group
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-17 17:40:19 +01:00
65ce02e777 refactor(cards): Switched over to new document-server api
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 17:32:33 +01:00
878d9714cb refactor(pdfgeneration): Switch to new document-server api
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-12-17 17:29:42 +01:00
f99b7f4bb8 refactor(pdfgeneration): Switched contract generation over to new document-server
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-12-17 17:19:20 +01:00
e23098410c refactor(pdfgeneration): Switch cards over to new service 2024-12-17 17:09:55 +01:00
04494d2a2a chore: bump
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-12-11 17:30:49 +01:00
e2d6fbb513 refactor(orgs): Swtich to new selfservice baseurl 2024-12-11 17:29:14 +01:00
477c650f3f 🚀RELEASE v1.5.3
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
ci/woodpecker/push/build Pipeline was successful
2024-11-26 19:20:04 +01:00
fc15c68cba feat(dx): Yarn support
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-11-26 19:19:31 +01:00
32b5f5420b refactor(ci): Only build licences, don't export
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2024-11-26 19:11:14 +01:00
ee87f82799 fix(ci): Update git pushb settings
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-11-26 19:06:32 +01:00
7c08f522aa fix(ci): Update relase machanism 2024-11-26 19:06:16 +01:00
e211554579 fix(ci): Install pnpm
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-11-26 19:01:51 +01:00
7ba890dfd7 fix(ci): Switch over to new woodpecker version
Some checks failed
ci/woodpecker/push/build Pipeline failed
2024-11-26 18:59:42 +01:00
68b4309164 chore(deps): bump some 2024-11-21 18:05:11 +01:00
d803f3d490 fix: unexpected/ missing props 2024-11-21 17:05:06 +01:00
5468766d87 fix(orgs): ImportRunnerModal props 2024-11-21 17:00:03 +01:00
e967d8d20c feat(dashboard): reorder menu items 2024-11-21 16:59:48 +01:00
ad4db882f0 feat: cleanup random page toasts 2024-11-21 16:56:10 +01:00
84aa846b87 feat(about): cleanup ui 2024-11-21 16:55:58 +01:00
3532968b33 🚀RELEASE v1.5.2 2024-11-21 10:33:56 +01:00
6bb49db4ee feat(dashboard): add lfk icon and app name to mobile nav bar 2024-11-21 10:24:23 +01:00
38fb111f7a feat: improved mobile buttons + search ui 2024-11-21 10:22:06 +01:00
a50447f457 feat(dashboard): improved a11y of active sidebar menu item 2024-11-21 10:14:22 +01:00
b1a2044631 feat(dashboard): match greeting style with rest of titles 2024-11-21 10:11:03 +01:00
c883920caa feat: improved dashboard titles ui + a11y 2024-11-21 10:09:14 +01:00
21453ef272 feat: improved dashboard titles ui + a11y 2024-11-21 10:07:20 +01:00
10182433f8 feat(i18n/de): rename "Track" to "Laufstrecke" 2024-11-21 10:04:38 +01:00
b338f33a63 feat(dashboard): improved mobile ui hamburger button 2024-11-21 09:58:15 +01:00
cb82200481 feat(users/UsersOverview): improve ui by adding borders to badges 2024-11-21 09:57:50 +01:00
9abf74d6d2 🚀RELEASE v1.5.1 2024-11-21 09:46:27 +01:00
50e81a6cb5 fix(dockerfile): AS casing 2024-11-21 09:45:51 +01:00
8fae1fb6b3 chore(deps): bump some 2024-11-21 09:45:39 +01:00
372fa110ec fix(scanstations): CopyScanStationTokenModal open after create 2024-11-21 09:40:32 +01:00
35bec9fe58 chore(deps): pnpm@9 2024-11-21 09:33:22 +01:00
93d67bdba9 chore(deps): node:23.2.0 2024-11-21 09:32:58 +01:00
a5e72a18e3 refactor(scanstations/CopyScanStationTokenModal): drop dispatch 2024-11-21 09:31:45 +01:00
91d2f46b93 fix(config): add explicit window.config 2024-11-21 09:31:09 +01:00
c60bae4533 fix(tracks/AddTrackModal): i18n 2024-11-21 09:30:52 +01:00
43ac878d44 fix: tailwind config 2024-11-21 09:30:24 +01:00
ceabd06a43 🚀RELEASE v1.5.0 2024-11-20 19:29:18 +01:00
9bfc0c5338 fix(components): Add missing toast imports 2024-11-20 19:28:07 +01:00
fb8206ff13 feat(ci)!: Switch to woodpecker
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2023-11-06 19:51:01 +01:00
dceb0ef461 🚀RELEASE v1.4.13
Some checks failed
continuous-integration/drone/push Build encountered an error
2023-07-31 18:55:53 +02:00
88bc1982ca Show donations as euro in export 2023-07-31 18:55:09 +02:00
e741a9d7e7 Merge branch 'dev' of git.odit.services:lfk/frontend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-18 21:35:28 +02:00
65f1d22205 🚀RELEASE v1.4.12 2023-05-18 21:35:16 +02:00
d867c08aba fix(donation/payment): Funny javascript number to float conversion where integers were needed 2023-05-18 21:34:41 +02:00
6193eff38e new license file version [CI SKIP] 2023-05-10 11:29:03 +00:00
f1929e7cf9 Merge branch 'dev' of git.odit.services:lfk/frontend into dev
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:28:43 +02:00
373484c242 🚀RELEASE v1.4.11 2023-05-10 13:28:34 +02:00
f77460bb0c chore(deps): Lockfile 2023-05-10 13:28:21 +02:00
574e0dcb05 feat(orgs): Show total distance 2023-05-10 13:27:47 +02:00
7b19a0aa08 chore(deps): More bumps 2023-05-10 13:22:11 +02:00
08642d7618 new license file version [CI SKIP] 2023-05-10 11:21:43 +00:00
c3e9c27cd3 🚀RELEASE v1.4.10
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-10 13:21:10 +02:00
29a2854671 chore(deps): Bumped svelte-table 2023-05-10 13:20:44 +02:00
8e6786e722 chore(deps): Pin and bump 2023-05-10 13:19:25 +02:00
6ad40564e3 chore(deps): Bumped scanclientjs 2023-05-10 13:18:44 +02:00
776973bfe9 🚀RELEASE v1.4.9
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-09 16:32:00 +02:00
6025e43baa Fixed empty return 2023-05-09 16:29:41 +02:00
d9a47f882c Changed the in table replacement method 2023-05-09 16:29:25 +02:00
4235758a6d 🚀RELEASE v1.4.8
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-09 10:44:04 +02:00
59fe2dfabb Switched donor loading to non-paginated 2023-05-09 10:43:33 +02:00
6364536dcd 🚀RELEASE v1.4.7
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 20:30:20 +02:00
a8a771114d Paginated modal data loading 2023-05-04 20:28:21 +02:00
4e0a2c8301 Moved loading to onmount 2023-05-04 20:17:29 +02:00
b6fed92a17 🚀RELEASE v1.4.6
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 20:10:57 +02:00
97b57aeb0c fix(donor/detail): Set email to null to avoid vaidation errors 2023-05-04 20:10:21 +02:00
e25ed1fff9 fix(donor/detail): Set phone to null to avoid vaidation errors 2023-05-04 20:09:43 +02:00
a2ff5b8a14 fix(donor/details): don't load donations 2023-05-04 20:07:54 +02:00
0284f18beb 🚀RELEASE v1.4.5
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 20:04:02 +02:00
803d64c78c fix: Removed dynamic pagesize adjustments
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 20:03:23 +02:00
dacb2f8ace Revert "revert: buggy pagination"
This reverts commit b2648645e8.
2023-05-04 19:57:46 +02:00
b7a53960e5 🚀RELEASE v1.4.4
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 14:44:31 +02:00
66f1e6b4fe fix(AddDonationModal): missing toast dismiss on success distance donation
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-04 14:44:17 +02:00
33166bfafc 🚀RELEASE v1.4.3
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-04 14:32:56 +02:00
b2648645e8 revert: buggy pagination
Some checks failed
continuous-integration/drone/push Build is failing
2023-05-04 14:32:37 +02:00
97 changed files with 15783 additions and 10074 deletions

View File

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

View File

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

34
.gitea/workflows/dev.yaml Normal file
View File

@@ -0,0 +1,34 @@
name: Build Latest and dev images
on:
push:
branches:
- dev
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@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:dev
${{ vars.REGISTRY }}/lfk/frontend:latest
platforms: linux/amd64,linux/arm64

View File

@@ -0,0 +1,33 @@
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@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,4 +3,6 @@ node_modules
public/env.js public/env.js
public/index.html public/index.html
/dist /dist
.pnpm-store .pnpm-store
.yarn
.pnp.*

View File

@@ -2,8 +2,202 @@
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.8.1](https://git.odit.services/lfk/frontend/compare/1.8.0...1.8.1)
- 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) #### [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(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) - fix(DonorDetail): missing toast import [`edc2dca`](https://git.odit.services/lfk/frontend/commit/edc2dcab92c3cace05335a283a849c3c978ec8ec)

View File

@@ -1,9 +1,9 @@
FROM registry.odit.services/hub/library/node:20.0.0-alpine3.17 as build FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 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 tailwind.config.js postcss.config.cjs index.html ./ COPY package.json pnpm-lock.yaml vite.config.js tailwind.config.cjs postcss.config.cjs index.html ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@8 RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@9
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i
COPY src ./src COPY src ./src
@@ -11,6 +11,6 @@ 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,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
@@ -13,7 +13,7 @@
<body> <body>
<span style="display: none; visibility: hidden" id="buildinfo" <span style="display: none; visibility: hidden" id="buildinfo"
>RELEASE_INFO-1.4.2-RELEASE_INFO</span >RELEASE_INFO-1.8.1-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>

View File

@@ -1,6 +1,6 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.4.2", "version": "1.8.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"i18n-order": "node order.js", "i18n-order": "node order.js",
@@ -12,16 +12,16 @@
}, },
"license": "CC-BY-NC-SA-4.0", "license": "CC-BY-NC-SA-4.0",
"devDependencies": { "devDependencies": {
"@odit/license-exporter": "0.0.12", "@odit/license-exporter": "0.2.0",
"@sveltejs/vite-plugin-svelte": "2.1.1", "@sveltejs/vite-plugin-svelte": "2.1.1",
"auto-changelog": "2.4.0", "auto-changelog": "2.5.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.21",
"postcss": "8.4.23", "postcss": "8.5.3",
"prettier": "^2.8.8", "prettier": "3.5.3",
"prettier-plugin-svelte": "^2.10.0", "prettier-plugin-svelte": "3.3.3",
"release-it": "15.10.1", "release-it": "17.10.0",
"svelte-select": "3.17.0", "svelte-select": "3.17.0",
"tailwindcss": "3.3.2", "tailwindcss": "3.4.15",
"vite": "4.3.3" "vite": "4.3.3"
}, },
"release-it": { "release-it": {
@@ -42,19 +42,20 @@
} }
}, },
"dependencies": { "dependencies": {
"@odit/lfk-client-js": "1.1.1", "@fontsource/athiti": "^5.2.5",
"@paralleldrive/cuid2": "^2.2.0", "@odit/lfk-client-js": "1.1.3",
"@tanstack/svelte-table": "^8.8.6", "@paralleldrive/cuid2": "2.2.2",
"bwip-js": "^3.4.0", "@tanstack/svelte-table": "8.9.1",
"check-password-strength": "2.0.7", "bwip-js": "3.4.0",
"check-password-strength": "2.0.10",
"csvtojson": "2.0.10", "csvtojson": "2.0.10",
"localforage": "1.10.0", "localforage": "1.10.0",
"marked": "4.3.0", "marked": "4.3.0",
"svelte": "3.58.0", "svelte": "3.58.0",
"svelte-french-toast": "1.0.4-beta.0", "svelte-french-toast": "1.2.0",
"svelte-i18n": "3.6.0", "svelte-i18n": "3.6.0",
"tinro": "0.6.12", "tinro": "0.6.12",
"validator": "13.9.0", "validator": "13.15.0",
"xlsx": "0.18.5" "xlsx": "0.18.5"
}, },
"volta": { "volta": {

7981
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
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: documentserver_key:
"NqZSYTy5AFQ7MppbLW5moqpTk7u7YrNUHKYhKYuThnnya2WpCOIU694hIZT1FzYe", "NqZSYTy5AFQ7MppbLW5moqpTk7u7YrNUHKYhKYuThnnya2WpCOIU694hIZT1FzYe",
@@ -8,3 +9,4 @@ const config = {
default_password: "demo", default_password: "demo",
prefersHashRouting: true, prefersHashRouting: true,
}; };
window.config = config;

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,6 @@
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);
toast($_("welcome_wavinghand"));
} }
} }
}); });
@@ -50,7 +49,6 @@
store.login(result.access_token, jwtinfo); store.login(result.access_token, jwtinfo);
location.replace("/"); location.replace("/");
toast.dismiss(); toast.dismiss();
toast($_("welcome_wavinghand"));
}) })
.catch((err) => { .catch((err) => {
toast.dismiss(); toast.dismiss();

View File

@@ -5,8 +5,11 @@
import { RunnerCardService } from "@odit/lfk-client-js"; import { RunnerCardService } from "@odit/lfk-client-js";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
import DocumentServer from "../pdf_generation/DocumentServer";
export let bulk_modal_open; export let bulk_modal_open;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const documentServer = new DocumentServer(config.baseurl_documentserver,config.documentserver_key);
$: card_count = 0; $: card_count = 0;
$: is_card_count_valid = card_count > 0; $: is_card_count_valid = card_count > 0;
@@ -60,24 +63,7 @@
toast.success($_("created-blanco-cards")); toast.success($_("created-blanco-cards"));
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdf"));
dispatch("created", { cards: result }); dispatch("created", { cards: result });
fetch( documentServer.generateCards(result, "de")
`${config.baseurl_documentserver}/cards?&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(result),
}
)
.then((response) => {
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} else {
return response.blob();
}
})
.then((blob) => { .then((blob) => {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
let a = document.createElement("a"); let a = document.createElement("a");
@@ -177,7 +163,7 @@
type="number" type="number"
step="1" step="1"
name="amount" name="amount"
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" 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"
placeholder="400" placeholder="400"
/> />
<span <span

View File

@@ -148,7 +148,7 @@
>{$_("runner")}</label >{$_("runner")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
filterRunners(label, filterText, option)} filterRunners(label, filterText, option)}
items={runners} items={runners}

View File

@@ -139,7 +139,7 @@
>{$_("runner")}</label >{$_("runner")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
filterRunners(label, filterText, option)} filterRunners(label, filterText, option)}
items={runners} items={runners}

View File

@@ -5,12 +5,12 @@
{#if enabled} {#if enabled}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("enabled")}</span >{$_("enabled")}</span
> >
{:else} {:else}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
>{$_("disabled")}</span >{$_("disabled")}</span
> >
{/if} {/if}

View File

@@ -11,29 +11,29 @@
</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"
> 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 mt-1 sm:mt-0"
{$_("add-card")} >
</button> {$_("add-card")}
<button </button>
on:click={() => { <button
bulk_modal_open = true; on:click={() => {
}} bulk_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"
> 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 mt-1 sm:mt-0"
{$_("create-bulk-cards")} >
</button> {$_("create-bulk-cards")}
{/if} </button>
</span> {/if}
<CardsOverview bind:current_cards bind:addCards /> <CardsOverview bind:current_cards bind:addCards />
</section> </section>

View File

@@ -5,7 +5,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="m-auto" style="height:15rem" src={cards_empty} alt="" /> <img class="m-auto mt-2" 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>

View File

@@ -151,9 +151,8 @@
} }
onMount(async () => { onMount(async () => {
toast.loading($_("loading-cards"));
let page = 0; let page = 0;
let pagesize = 100; let pagesize = 500;
while (page >= 0) { while (page >= 0) {
const cards = await RunnerCardService.runnerCardControllerGetAll( const cards = await RunnerCardService.runnerCardControllerGetAll(
page, page,
@@ -171,10 +170,7 @@
dataLoaded = true; dataLoaded = true;
page++; page++;
pagesize += 100;
} }
toast.dismiss();
toast.success($_("all-cards-loaded"));
}); });
</script> </script>
@@ -220,7 +216,7 @@
{#if selected.length > 0} {#if selected.length > 0}
<button <button
type="button" 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 inline-flex" 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"
id="options-menu" id="options-menu"
on:click={async () => { on:click={async () => {
const prom = []; const prom = [];

View File

@@ -208,7 +208,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
@@ -231,7 +231,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -250,7 +250,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
@@ -270,7 +270,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
> >
{#each teams as team} {#each teams as team}
<option value={team.id}> <option value={team.id}>
@@ -300,7 +300,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isPhoneValidOrEmpty} {#if !isPhoneValidOrEmpty}
<span <span
@@ -328,7 +328,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isEmailValidOrEmpty} {#if !isEmailValidOrEmpty}
<span <span
@@ -349,7 +349,7 @@
/> />
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-medium text-gray-700" <label for="comments" class="font-semibold text-gray-700"
>{$_("address")}</label >{$_("address")}</label
> >
</div> </div>
@@ -371,7 +371,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isAddress1Valid} {#if !isAddress1Valid}
<span <span
@@ -394,7 +394,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -413,7 +413,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iszipcodevalid} {#if !iszipcodevalid}
<span <span
@@ -439,7 +439,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iscityvalid} {#if !iscityvalid}
<span <span

View File

@@ -1,417 +1,399 @@
<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 PromiseError from "../base/PromiseError.svelte";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
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")); toast.loading($_("contact-is-being-updated"));
editable.address.country = "DE"; editable.address.country = "DE";
if (editable.address_checked === false) { if (editable.address_checked === false) {
editable.address = null; editable.address = null;
} }
if (editable.email) editable.email = editable.email; if (editable.email) editable.email = editable.email;
if (editable.phone) editable.phone = editable.phone; if (editable.phone) editable.phone = editable.phone;
if (editable.middlename) editable.middlename = editable.middlename; if (editable.middlename) editable.middlename = editable.middlename;
GroupContactService.groupContactControllerPut(original_data.id, editable) GroupContactService.groupContactControllerPut(original_data.id, editable)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.dismiss(); toast.dismiss();
toast.success($_("updated-contact")); toast.success($_("updated-contact"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
} }
} }
function deleteContact() { function deleteContact() {
GroupContactService.groupContactControllerRemove(original_data.id, true) GroupContactService.groupContactControllerRemove(original_data.id, true)
.then((resp) => { .then((resp) => {
location.replace("./"); location.replace("./");
}) })
.catch((err) => {}); .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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 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" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">{$_("contacts")}</a><svg >
stroke="currentColor" {$_("contacts")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {original_data.firstname}
xmlns="http://www.w3.org/2000/svg" {original_data.middlename || ""}
><line x1="5" y1="12" x2="19" y2="12" /> {original_data.lastname}
<polyline points="12 5 19 12 12 19" /></svg <div data-id="contact_actions_${editable.id}">
> {#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:DELETE")}
</li> {#if delete_triggered}
<li class="flex items-center"> <button
<span class="mr-2" on:click={deleteContact}
>{original_data.firstname} 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.middlename || ""} >{$_("confirm-deletion")}</button
{original_data.lastname}</span >
> <button
</li> on:click={() => {
</ol> delete_triggered = !delete_triggered;
</nav> }}
</div> 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"
</div> >{$_("cancel")}</button
<div class="mb-8 text-3xl font-extrabold leading-tight"> >
{original_data.firstname} {/if}
{original_data.middlename || ""} {#if !delete_triggered}
{original_data.lastname} <button
<span data-id="contact_actions_${editable.id}"> on:click={() => {
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:DELETE")} delete_triggered = true;
{#if delete_triggered} }}
<button type="button"
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:w-auto sm:text-sm"
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
>{$_("confirm-deletion")}</button >
> {/if}
<button {/if}
on:click={() => { {#if !delete_triggered}
delete_triggered = !delete_triggered; <button
}} disabled={!save_enabled}
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" class:opacity-50={!save_enabled}
>{$_("cancel")}</button type="button"
> 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"
{#if !delete_triggered} >{$_("save-changes")}</button
<button >
on:click={() => { {/if}
delete_triggered = true; </div>
}} </div>
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" <div class="text-sm w-full mt-2">
>{$_("delete-contact")}</button <label for="firstname" class="font-semibold text-gray-700"
> >{$_("first-name")}</label
{/if} >
{/if} <input
{#if !delete_triggered} autocomplete="off"
<button placeholder={$_("first-name")}
disabled={!save_enabled} type="text"
class:opacity-50={!save_enabled} class:border-red-500={!isFirstnameValid}
type="button" class:focus:border-red-500={!isFirstnameValid}
on:click={submit} class:focus:ring-red-500={!isFirstnameValid}
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" bind:value={editable.firstname}
>{$_("save-changes")}</button 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"
{/if} />
</span> {#if !isFirstnameValid}
</div> <span
<!-- --> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<div class="text-sm w-full"> >
<label for="firstname" class="font-medium text-gray-700" {$_("first-name-is-required")}
>{$_("first-name")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="text-sm w-full mt-2">
placeholder={$_("first-name")} <label for="middlename" class="font-semibold text-gray-700"
type="text" >{$_("middle-name")}</label
class:border-red-500={!isFirstnameValid} >
class:focus:border-red-500={!isFirstnameValid} <input
class:focus:ring-red-500={!isFirstnameValid} autocomplete="off"
bind:value={editable.firstname} placeholder={$_("middle-name")}
name="firstname" 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" bind:value={editable.middlename}
/> name="middlename"
{#if !isFirstnameValid} 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" </div>
> <div class="text-sm w-full mt-2">
{$_("first-name-is-required")} <label for="lastname" class="font-semibold text-gray-700"
</span> >{$_("last-name")}</label
{/if} >
</div> <input
<div class="text-sm w-full"> autocomplete="off"
<label for="middlename" class="font-medium text-gray-700" placeholder={$_("last-name")}
>{$_("middle-name")}</label type="text"
> bind:value={editable.lastname}
<input class:border-red-500={!isLastnameValid}
autocomplete="off" class:focus:border-red-500={!isLastnameValid}
placeholder={$_("middle-name")} class:focus:ring-red-500={!isLastnameValid}
type="text" name="lastname"
bind:value={editable.middlename} 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"
name="middlename" />
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}
/> <span
</div> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<div class="text-sm w-full"> >
<label for="lastname" class="font-medium text-gray-700" {$_("last-name-is-required")}
>{$_("last-name")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="text-sm w-full mt-2">
placeholder={$_("last-name")} <label for="email" class="font-semibold text-gray-700"
type="text" >{$_("e-mail-adress")}</label
bind:value={editable.lastname} >
class:border-red-500={!isLastnameValid} <input
class:focus:border-red-500={!isLastnameValid} autocomplete="off"
class:focus:ring-red-500={!isLastnameValid} placeholder={$_("e-mail-adress")}
name="lastname" type="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-gray-500 rounded-md p-2" bind:value={editable.email}
/> class:border-red-500={!isEmailValid}
{#if !isLastnameValid} class:focus:border-red-500={!isEmailValid}
<span class:focus:ring-red-500={!isEmailValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" 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"
{$_("last-name-is-required")} />
</span> {#if !isEmailValid}
{/if} <span
</div> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<div class="text-sm w-full"> >
<label for="email" class="font-medium text-gray-700" {$_("valid-email-is-required")}
>{$_("e-mail-adress")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="text-sm w-full mt-2">
placeholder={$_("e-mail-adress")} <label for="phone" class="font-semibold text-gray-700">{$_("phone")}</label>
type="email" <input
bind:value={editable.email} autocomplete="off"
class:border-red-500={!isEmailValid} placeholder={$_("phone")}
class:focus:border-red-500={!isEmailValid} type="tel"
class:focus:ring-red-500={!isEmailValid} class:border-red-500={!isPhoneValidOrEmpty}
name="email" class:focus:border-red-500={!isPhoneValidOrEmpty}
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:focus:ring-red-500={!isPhoneValidOrEmpty}
/> bind:value={editable.phone}
{#if !isEmailValid} name="phone"
<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-neutral-800 rounded-md p-2"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" />
> {#if !isPhoneValidOrEmpty}
{$_("valid-email-is-required")} <span
</span> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
{/if} >
</div> {$_("valid-international-phone-number-is-required")}
<div class="text-sm w-full"> </span>
<label for="phone" class="font-medium text-gray-700">{$_("phone")}</label> {/if}
<input </div>
autocomplete="off" <div class="text-sm w-full mt-2">
placeholder={$_("phone")} <span class="font-semibold text-gray-700">{$_("groups")}</span>
type="tel" <select
class:border-red-500={!isPhoneValidOrEmpty} bind:value={editable.groups}
class:focus:border-red-500={!isPhoneValidOrEmpty} name="team"
class:focus:ring-red-500={!isPhoneValidOrEmpty} multiple
bind:value={editable.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"
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-gray-500 rounded-md p-2" {#each teams as team}
/> <option value={team.id}>
{#if !isPhoneValidOrEmpty} {team.parentGroup.name}
<span &gt;
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" {team.name}
> </option>
{$_("valid-international-phone-number-is-required")} {/each}
</span> {#each orgs as org}
{/if} <option value={org.id}>{org.name}</option>
</div> {/each}
<div class="text-sm w-full"> </select>
<span class="font-medium text-gray-700">{$_("groups")}</span> </div>
<select <!-- -->
bind:value={editable.groups} <div class="flex items-start mt-2">
name="team" <div class="flex items-center h-5">
multiple <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}
> id="comments"
{#each teams as team} name="comments"
<option value={team.id}> type="checkbox"
{team.parentGroup.name} class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
&gt; />
{team.name} </div>
</option> <div class="ml-3 text-sm">
{/each} <label for="comments" class="font-semibold text-gray-700"
{#each orgs as org} >{$_("address")}</label
<option value={org.id}>{org.name}</option> >
{/each} </div>
</select> </div>
</div> {#if editable.address_checked === true}
<!-- --> <div class="col-span-6">
<div class="flex items-start mt-2"> <label for="address1" class="block text-sm font-medium text-gray-700"
<div class="flex items-center h-5"> >{$_("address")}</label
<input >
bind:checked={editable.address_checked} <input
id="comments" autocomplete="off"
name="comments" placeholder="Address"
type="checkbox" class:border-red-500={!isAddress1Valid}
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" class:focus:border-red-500={!isAddress1Valid}
/> class:focus:ring-red-500={!isAddress1Valid}
</div> bind:value={editable.address.address1}
<div class="ml-3 text-sm"> type="text"
<label for="comments" class="font-medium text-gray-700" name="address1"
>{$_("address")}</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"
> />
</div> {#if !isAddress1Valid}
</div> <span
{#if editable.address_checked === true} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<div class="col-span-6"> >
<label for="address1" class="block text-sm font-medium text-gray-700" {$_("address-is-required")}
>{$_("address")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="col-span-6">
placeholder="Address" <label for="address2" class="block text-sm font-medium text-gray-700"
class:border-red-500={!isAddress1Valid} >{$_("apartment-suite-etc")}</label
class:focus:border-red-500={!isAddress1Valid} >
class:focus:ring-red-500={!isAddress1Valid} <input
bind:value={editable.address.address1} autocomplete="off"
type="text" placeholder={$_("apartment-suite-etc")}
name="address1" bind:value={editable.address.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" type="text"
/> name="address2"
{#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" </div>
> <div class="col-span-6">
{$_("address-is-required")} <label for="zipcode" class="block text-sm font-medium text-gray-700"
</span> >{$_("zip-postal-code")}</label
{/if} >
</div> <input
<div class="col-span-6"> autocomplete="off"
<label for="address2" class="block text-sm font-medium text-gray-700" placeholder={$_("zip-postal-code")}
>{$_("apartment-suite-etc")}</label class:border-red-500={!iszipcodevalid}
> class:focus:border-red-500={!iszipcodevalid}
<input class:focus:ring-red-500={!iszipcodevalid}
autocomplete="off" bind:value={editable.address.postalcode}
placeholder={$_("apartment-suite-etc")} type="text"
bind:value={editable.address.address2} name="zipcode"
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"
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" {#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"> >
<label for="zipcode" class="block text-sm font-medium text-gray-700" {$_("valid-zipcode-postal-code-is-required")}
>{$_("zip-postal-code")}</label </span>
> {/if}
<input </div>
autocomplete="off" <div class="col-span-6">
placeholder={$_("zip-postal-code")} <label for="city" class="block text-sm font-medium text-gray-700"
class:border-red-500={!iszipcodevalid} >{$_("city")}</label
class:focus:border-red-500={!iszipcodevalid} >
class:focus:ring-red-500={!iszipcodevalid} <input
bind:value={editable.address.postalcode} autocomplete="off"
type="text" placeholder={$_("city")}
name="zipcode" class:border-red-500={!iscityvalid}
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:focus:border-red-500={!iscityvalid}
/> class:focus:ring-red-500={!iscityvalid}
{#if !iszipcodevalid} bind:value={editable.address.city}
<span type="text"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" 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"
{$_("valid-zipcode-postal-code-is-required")} />
</span> {#if !iscityvalid}
{/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 for="city" class="block text-sm font-medium text-gray-700" {$_("valid-city-is-required")}
>{$_("city")}</label </span>
> {/if}
<input </div>
autocomplete="off" {/if}
placeholder={$_("city")} </section>
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}
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -8,20 +8,20 @@
</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">
{$_("contacts")} {$_("contacts")}
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT: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"
> 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>

View File

@@ -1,198 +1,198 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { GroupContactService } from "@odit/lfk-client-js"; import { GroupContactService } from "@odit/lfk-client-js";
const promise = GroupContactService.groupContactControllerGetAll().then( const promise = GroupContactService.groupContactControllerGetAll().then(
(result) => { (result) => {
current_contacts = result; current_contacts = result;
} }
); );
import store from "../../store"; import store from "../../store";
import ContactsEmptyState from "./ContactsEmptyState.svelte"; import ContactsEmptyState from "./ContactsEmptyState.svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
$: 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="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<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 class="odd:bg-white even:bg-gray-100">
<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"
> >
{$_("groups")} {$_("groups")}
</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"
> >
{$_("address")} {$_("address")}
</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>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
{#each current_contacts as t} {#each current_contacts as t}
{#if Object.values(t) {#if Object.values(t)
.toString() .toString()
.toLowerCase() .toLowerCase()
.includes(searchvalue)} .includes(searchvalue)}
<tr <tr
class="odd:bg-white even:bg-gray-100" class="odd:bg-white even:bg-gray-100"
data-rowid="team_{t.id}" data-rowid="team_{t.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">
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900">
{t.firstname} {t.firstname}
{t.middlename || ""} {t.middlename || ""}
{t.lastname} {t.lastname}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<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
<div class="text-sm font-medium text-gray-900"> class="text-sm font-medium text-gray-900 gap-0.5 flex flex-wrap"
{#if t.groups.length > 0} >
{#each t.groups as g} {#if t.groups.length > 0}
{#if g.responseType === "RUNNERORGANIZATION"} {#each t.groups as g}
<a {#if g.responseType === "RUNNERORGANIZATION"}
href="../orgs/{g.id}" <a
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" href="../orgs/{g.id}"
>{g.name}</a class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
> >{g.name}</a
{:else} >
<a {:else}
href="../teams/{g.id}" <a
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" href="../teams/{g.id}"
>{g.parentGroup.name} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
&gt; >{g.parentGroup.name}
{g.name}</a &gt;
> {g.name}</a
{/if} >
{/each} {/if}
{:else} {/each}
{$_("contact-is-not-a-member-in-any-group")} {:else}
{/if} {$_("contact-is-not-a-member-in-any-group")}
</div> {/if}
</div> </div>
</div> </div>
</td> </td>
<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">
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900">
{#if t.address.address1 !== null} {#if t.address.address1 !== null}
{t.address.address1}<br /> {t.address.address1}<br />
{t.address.address2 || ""}<br /> {t.address.address2 || ""}<br />
{t.address.postalcode} {t.address.postalcode}
{t.address.city} {t.address.city}
{t.address.country} {t.address.country}
{/if} {/if}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
{#if active_deletes[t.id] === true} {#if active_deletes[t.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[t.id] = false; active_deletes[t.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={() => {
toast.loading($_("deleting-contact")); toast.loading($_("deleting-contact"));
GroupContactService.groupContactControllerRemove( GroupContactService.groupContactControllerRemove(
t.id, t.id,
false false
).then((resp) => { ).then((resp) => {
current_contacts = current_contacts.filter( current_contacts = current_contacts.filter(
(obj) => obj.id !== t.id (obj) => obj.id !== t.id
); );
toast.dismiss(); toast.dismiss();
toast($_("contact-deleted")); toast($_("contact-deleted"));
}); });
}} }}
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="./{t.id}" href="./{t.id}"
class="text-indigo-600 hover:text-indigo-900" class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a >{$_("details")}</a
> >
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")}
<button <button
on:click={() => { on:click={() => {
active_deletes[t.id] = true; 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 >{$_("delete")}</button
> >
{/if} {/if}
</td> </td>
{/if} {/if}
</tr> </tr>
{/if} {/if}
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
{/if} {/if}
{: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 class="capitalize">{$_("general_promise_error")}</b> <b class="capitalize">{$_("general_promise_error")}</b>
{error} {error}
</span> </span>
</div> </div>
{/await} {/await}
{/if} {/if}

View File

@@ -1,431 +1,438 @@
<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"; import { Toaster } from "svelte-french-toast";
$: navOpen = false; $: navOpen = false;
function logout() { function logout() {
localForage.clear(); localForage.clear();
location.replace("/"); location.replace("/");
} }
</script> </script>
<section class="min-h-screen bg-gray-50"> <section class="min-h-screen bg-gray-50">
<div <div
class:collapsed_navigation={!navOpen} class:collapsed_navigation={!navOpen}
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" 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"
> >
<a href="/" class="flex items-center px-4 py-5"> <a href="/" class="flex items-center px-4 py-5">
<img src="/lfk-logo.png" alt="Logo" class="h-10" /> <img src="/lfk-logo.png" alt="Logo" class="h-10" />
<h3 class="text-lg font-bold">LfK!Admin</h3> <h3 class="text-lg font-bold">LfK!Admin</h3>
</a> </a>
<nav class="text-sm font-medium text-gray-600" aria-label="Main Navigation"> <nav class="text-sm font-medium text-gray-600" aria-label="Main Navigation">
<a <a
class:bg-gray-100={$router.path === "/"} class:activenav={$router.path === "/"}
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="/" href="/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
> >
<path <path
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" 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> </svg>
<span>{$_("dashboard-title")}</span> <span>{$_("dashboard-title")}</span>
</a> </a>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET")}
<a <a
class:bg-gray-100={$router.path.includes("/orgs/")} class:activenav={$router.path === "/runners/"}
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="/orgs/" href="/runners/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
viewBox="0 0 24 24" 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="M17 19h2v-8h-6v8h2v-6h2v6zM3 19V4a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v5h2v10h1v2H2v-2h1zm4-8v2h2v-2H7zm0 4v2h2v-2H7zm0-8v2h2V7H7z" 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 /></svg
> >
<span>{$_("orgs")}</span> <span>{$_("runners")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")}
<a <a
class:bg-gray-100={$router.path === "/users/"} class:activenav={$router.path === "/teams/"}
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="/teams/"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
width="24" fill="currentColor"
height="24" 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" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 640 512"
><path fill="none" d="M0 0h24v24H0z" /> ><path
<path fill="currentColor"
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" 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 /></svg
> >
<span>{$_("users")}</span> <span>{$_("teams")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")}
<a <a
class:bg-gray-100={$router.path === "/groups/"} class:activenav={$router.path.includes("/orgs/")}
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="/groups/" href="/orgs/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 640 512" height="24"
><path ><path fill="none" d="M0 0h24v24H0z" />
fill="currentColor" <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="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 /></svg
> >
<span>{$_("user-groups")}</span> <span>{$_("orgs")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")}
<a <a
class:bg-gray-100={$router.path === "/runners/"} class:activenav={$router.path.includes("/donors/")}
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="/runners/" href="/donors/"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
viewBox="0 0 24 24" fill="currentColor"
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" 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.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" 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
> >
<span>{$_("runners")}</span> <span>{$_("donors")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")}
<a <a
class:bg-gray-100={$router.path === "/teams/"} class:activenav={$router.path.includes("/donations/")}
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="/teams/" href="/donations/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 640 512" height="24"
><path ><path fill="none" d="M0 0h24v24H0z" />
fill="currentColor" <path
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" 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
> >
<span>{$_("teams")}</span> <span>{$_("donations")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")}
<a <a
class:bg-gray-100={$router.path.includes("/donors/")} class:activenav={$router.path === "/tracks/"}
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="/donors/" href="/tracks/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 24 24" height="24"
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" viewBox="0 0 640 512"
><path fill="none" d="M0 0h24v24H0z" /> ><path
<path fill="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" 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 /></svg
> >
<span>{$_("donors")}</span> <span>{$_("tracks")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")}
<a <a
class:bg-gray-100={$router.path.includes("/donations/")} class:activenav={$router.path === "/cards/"}
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="/donations/" href="/cards/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 24 24" height="24"
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> >
<path <path fill="none" d="M0 0h24v24H0z" />
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" <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"
<span>{$_("donations")}</span> /></svg
</a> >
{/if} <span>{$_("cards")}</span>
{#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")} </a>
<a {/if}
class:bg-gray-100={$router.path === "/tracks/"} {#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900" <a
href="/tracks/" class:activenav={$router.path.includes("/scans/")}
> 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="/scans/"
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"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 640 512" height="24"
><path xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 24 24"
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" ><path fill="none" d="M0 0h24v24H0z" />
/></svg <path
> fill="currentColor"
<span>{$_("tracks")}</span> d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z"
</a> /></svg
{/if} >
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} <span>Scans</span>
<a </a>
class:bg-gray-100={$router.path === "/cards/"} {/if}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900" {#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")}
href="/cards/" <a
> class:activenav={$router.path.includes("/contacts/")}
<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="/contacts/"
fill="currentColor" >
width="24" <svg
height="24" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24"
<path fill="none" d="M0 0h24v24H0z" /> width="24"
<path height="24"
fill="currentColor" ><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 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
<span>{$_("cards")}</span> >
</a> <span>{$_("contacts")}</span>
{/if} </a>
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")} {/if}
<a {#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")}
class:bg-gray-100={$router.path === "/scans/"} <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("/scanstations/")}
href="/scans/" 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="/scanstations/"
<svg >
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" <svg
fill="currentColor" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
width="24" fill="currentColor"
height="24" width="24"
xmlns="http://www.w3.org/2000/svg" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> xmlns="http://www.w3.org/2000/svg"
<path ><path fill="none" d="M0 0h24v24H0z" />
fill="currentColor" <path
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" fill="currentColor"
/></svg d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z"
> /></svg
<span>Scans</span> >
</a> <span>{$_("scanstations")}</span>
{/if} </a>
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")} {/if}
<a {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")}
class:bg-gray-100={$router.path === "/contacts/"} <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("/statsclients/")}
href="/contacts/" 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="/statsclients/"
<svg >
fill="currentColor" <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> xmlns="http://www.w3.org/2000/svg"
<path ><path fill="none" d="M0 0h24v24H0z" />
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" <path
/></svg fill="currentColor"
> d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z"
<span>{$_("contacts")}</span> /></svg
</a> >
{/if} <span>{$_("statsclients")}</span>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")} </a>
<a {/if}
class:bg-gray-100={$router.path === "/scanstations/"} {#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900" <a
href="/scanstations/" 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"
<svg href="/users/"
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" >
fill="currentColor" <svg
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" width="24"
viewBox="0 0 24 24" height="24"
xmlns="http://www.w3.org/2000/svg" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
><path fill="none" d="M0 0h24v24H0z" /> fill="currentColor"
<path viewBox="0 0 24 24"
fill="currentColor" ><path fill="none" d="M0 0h24v24H0z" />
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" <path
/></svg 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>{$_("scanstations")}</span> >
</a> <span>{$_("users")}</span>
{/if} </a>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")} {/if}
<a {#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")}
class:bg-gray-100={$router.path === "/statsclients/"} <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("/groups/")}
href="/statsclients/" 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 text-gray-400 transition group-hover:text-gray-600" <svg
fill="currentColor" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
width="24" fill="currentColor"
height="24" width="24"
viewBox="0 0 24 24" height="24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path fill="none" 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" 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
> >
<span>{$_("statsclients")}</span> <span>{$_("user-groups")}</span>
</a> </a>
{/if} {/if}
<a <a
class:bg-gray-100={$router.path === "/settings/"} class:activenav={$router.path === "/settings/"}
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="/settings/" href="/settings/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
> >
<path <path
fill-rule="evenodd" 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" 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" clip-rule="evenodd"
/> />
</svg> </svg>
<span>{$_("settings")}</span> <span>{$_("settings")}</span>
</a> </a>
<a <a
class:bg-gray-100={$router.path === "/about/"} class:activenav={$router.path === "/about/"}
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="/about/" href="/about/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="2"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
viewBox="0 0 24 24" viewBox="0 0 24 24"
><circle cx="12" cy="12" r="10" /> ><circle cx="12" cy="12" r="10" />
<path d="M12 16v-4M12 8h.01" /></svg <path d="M12 16v-4M12 8h.01" /></svg
> >
<span>{$_("about")}</span> <span>{$_("about")}</span>
</a> </a>
<button <button
tabindex="0" tabindex="0"
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"
on:click={() => { on:click={() => {
AuthService.authControllerLogout(); AuthService.authControllerLogout();
logout(); logout();
}} }}
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
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 24 24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <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" 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 /></svg
> >
<span>{$_("logout")}</span> <span>{$_("logout")}</span>
</button> </button>
</nav> </nav>
</div> </div>
<div class="ml-0 transition md:ml-60"> <div class="ml-0 transition md:ml-60">
<header <header
class="flex items-center justify-between w-full px-4 bg-white border-b h-14 md:hidden" class="flex items-center w-full px-4 bg-white border-b h-14 md:hidden"
> >
<button <button
on:click={() => { on:click={() => {
navOpen = true; navOpen = true;
}} }}
class="block btn btn-light md:hidden" class="block btn btn-light md:hidden"
> >
<span class="sr-only">Menu</span><svg <span class="sr-only">Menu</span><svg
class="w-4 h-4" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 20 20" viewBox="0 0 24 24"
fill="currentcolor" stroke-width="1.5"
><path stroke="currentColor"
fill-rule="evenodd" class="size-6"
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" <path
/></svg stroke-linecap="round"
></button stroke-linejoin="round"
> d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
</header> />
<Toaster position="top-right" /> </svg>
<slot> </button>
<NoComponentLoaded /> <span class="inline-block">
</slot> <img src="/lfk-logo.png" alt="Logo" class="h-8 inline-block" />
</div> <span class="text-lg font-bold">LfK!Admin</span>
{#if navOpen === true} </span>
<button </header>
on:click={() => { <Toaster position="top-right" />
navOpen = false; <slot>
}} <NoComponentLoaded />
class:hidden={!navOpen} </slot>
class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden" </div>
/> {#if navOpen === true}
{/if} <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> </section>
<style> <style>
.collapsed_navigation { .collapsed_navigation {
transform: translateX(-100%); transform: translateX(-100%);
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.collapsed_navigation { .collapsed_navigation {
transform: translateX(0px); transform: translateX(0px);
} }
} }
</style> </style>

View File

@@ -3,18 +3,16 @@
import { StatsService } from "@odit/lfk-client-js"; import { StatsService } from "@odit/lfk-client-js";
import store from "../../store"; import store from "../../store";
import StatCard from "./StatCard.svelte"; import StatCard from "./StatCard.svelte";
let navOpen = false;
const stats_promise = StatsService.statsControllerGet(); const stats_promise = StatsService.statsControllerGet();
</script> </script>
<div class="p-2 md:p-5 overflow-x-hidden"> <div class="p-2 md:p-5 overflow-x-hidden">
<h1 class="text-3xl leading-tight mb-4"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("dashboard-greeting")}, {$_("dashboard-greeting")} <span class="text-blue-500"
<span class="text-blue-500"
>{store.state.jwtinfo.userdetails.firstname} >{store.state.jwtinfo.userdetails.firstname}
{store.state.jwtinfo.userdetails.lastname}</span {store.state.jwtinfo.userdetails.lastname}</span
> >
</h1> </h4>
{#await stats_promise} {#await stats_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"
@@ -25,7 +23,7 @@
</div> </div>
{:then stats} {:then stats}
<div <div
class="grid gap-2 grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6 sm:gap-4" class="grid gap-1 grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 2xl:grid-cols-6 sm:gap-4"
> >
<StatCard <StatCard
title={$_("runners")} title={$_("runners")}

View File

@@ -7,13 +7,13 @@
</script> </script>
<a {href}> <a {href}>
<div class="p-4 rounded-lg bg-white border border-grey-100"> <div class="p-3 py-4 sm: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-xs uppercase font-normal text-grey-500"> <div class="text-md sm:text-xs uppercase font-normal text-grey-500">
{title} {title}
</div> </div>
<div class="text-xl font-bold font-mono">{value}</div> <div class="text-2xl sm:text-xl font-bold font-mono">{value}</div>
</div> </div>
<slot /> <slot />
</div> </div>

View File

@@ -8,7 +8,7 @@
RunnerService, RunnerService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import Select from "svelte-select"; import Select from "svelte-select";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
export let modal_open; export let modal_open;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -23,16 +23,6 @@
$: runners = []; $: runners = [];
$: is_fixed = false; $: is_fixed = false;
$: is_paid = false; $: is_paid = false;
DonorService.donorControllerGetAll().then((val) => {
donors = val.map((r) => {
return { label: getDonorLabel(r), value: r };
});
});
RunnerService.runnerControllerGetAll().then((val) => {
runners = val.map((r) => {
return { label: getDonorLabel(r), value: r };
});
});
$: amount_input = 0; $: amount_input = 0;
$: processed_last_submit = true; $: processed_last_submit = true;
$: is_amount_valid = amount_input > 0; $: is_amount_valid = amount_input > 0;
@@ -95,6 +85,7 @@
amount_input = 0; amount_input = 0;
modal_open = false; modal_open = false;
// //
toast.dismiss();
toast.success($_("donation_added")); toast.success($_("donation_added"));
dispatch("created", { donations: [result] }); dispatch("created", { donations: [result] });
}) })
@@ -107,6 +98,19 @@
} }
} }
} }
onMount(async () => {
donors = (await DonorService.donorControllerGetAll()).map(
(r) => {
return { label: getDonorLabel(r), value: r };
}
);
runners = (await RunnerService.runnerControllerGetAll()).map(
(r) => {
return { label: getDonorLabel(r), value: r };
}
);
});
</script> </script>
{#if modal_open} {#if modal_open}
@@ -188,7 +192,7 @@
>{$_("donor")}</label >{$_("donor")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
filterDonors(label, filterText, option)} filterDonors(label, filterText, option)}
items={donors} items={donors}
@@ -208,7 +212,7 @@
>{$_("runner")}</label >{$_("runner")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
filterDonors(label, filterText, option)} filterDonors(label, filterText, option)}
items={runners} items={runners}
@@ -240,7 +244,7 @@
type="number" type="number"
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 border-gray-300 border bg-gray-50 text-gray-500 p-2" 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"
placeholder="2.00" placeholder="2.00"
/> />
<span <span

View File

@@ -33,7 +33,7 @@
toast.loading($_("updating-donation")); toast.loading($_("updating-donation"));
const editable = Object.assign({}, original_data); const editable = Object.assign({}, original_data);
editable.donor = editable.donor.id; editable.donor = editable.donor.id;
editable.paidAmount = paid_amount_input * 100; editable.paidAmount = Math.round(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(
@@ -46,7 +46,7 @@
toast.dismiss(); toast.dismiss();
toast.success($_("donation-updated")); toast.success($_("donation-updated"));
dispatch("created", { donation: response }); dispatch("created", { donation: result });
}) })
.catch((err) => { .catch((err) => {
// //
@@ -61,7 +61,7 @@
// //
toast.dismiss(); toast.dismiss();
toast.success($_("donation-updated")); toast.success($_("donation-updated"));
dispatch("created", { donation: response }); dispatch("created", { donation: result });
}) })
.catch((err) => { .catch((err) => {
// //

View File

@@ -1,364 +1,352 @@
<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 PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import Select from "svelte-select"; import Select from "svelte-select";
let data_loaded = false; let data_loaded = false;
export let params; export let params;
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: donor = {}; $: donor = {};
$: runner = {}; $: runner = {};
$: current_donors = []; $: current_donors = [];
$: current_runners = []; $: current_runners = [];
$: amount_input = 0; $: amount_input = 0;
$: is_amount_valid = amount_input > 0; $: is_amount_valid = amount_input > 0;
$: paid_amount_input = 0; $: paid_amount_input = 0;
$: is_paid_amount_valid = paid_amount_input > 0; $: is_paid_amount_valid = paid_amount_input > 0;
$: is_everything_set = $: is_everything_set =
editable.donor != null && editable.donor != null &&
((original_data.responseType == "DISTANCEDONATION" && ((original_data.responseType == "DISTANCEDONATION" &&
editable?.runner != null) || editable?.runner != null) ||
original_data.responseType !== "DISTANCEDONATION"); original_data.responseType !== "DISTANCEDONATION");
$: changes_performed = $: changes_performed =
!(JSON.stringify(original_data) === JSON.stringify(editable)) || !(JSON.stringify(original_data) === JSON.stringify(editable)) ||
(original_data.responseType == "DISTANCEDONATION" && (original_data.responseType == "DISTANCEDONATION" &&
!(Math.floor(amount_input * 100) === original_data.amountPerDistance)) || !(Math.floor(amount_input * 100) === original_data.amountPerDistance)) ||
(original_data.responseType !== "DISTANCEDONATION" && (original_data.responseType !== "DISTANCEDONATION" &&
!(Math.floor(amount_input * 100) === original_data.amount)) || !(Math.floor(amount_input * 100) === original_data.amount)) ||
!(Math.floor(paid_amount_input * 100) === original_data.paidAmount); !(Math.floor(paid_amount_input * 100) === original_data.paidAmount);
$: save_enabled = changes_performed && is_amount_valid && is_everything_set; $: save_enabled = changes_performed && is_amount_valid && is_everything_set;
const promise = DonationService.donationControllerGetOne( const promise = DonationService.donationControllerGetOne(
params.donationid params.donationid
).then((data) => { ).then((data) => {
data_loaded = true; data_loaded = true;
original_data = Object.assign({}, data); original_data = Object.assign({}, data);
editable = Object.assign({}, original_data); editable = Object.assign({}, original_data);
paid_amount_input = data.paidAmount / 100; paid_amount_input = data.paidAmount / 100;
if (data.responseType == "DISTANCEDONATION") { if (data.responseType == "DISTANCEDONATION") {
amount_input = data.amountPerDistance / 100; amount_input = data.amountPerDistance / 100;
RunnerService.runnerControllerGetAll().then((val) => { RunnerService.runnerControllerGetAll().then((val) => {
current_runners = val.map((r) => { current_runners = val.map((r) => {
return { label: getDonorLabel(r), value: r }; return { label: getDonorLabel(r), value: r };
}); });
runner = current_runners.find((g) => g.value.id == editable.runner.id); runner = current_runners.find((g) => g.value.id == editable.runner.id);
}); });
} else { } else {
amount_input = data.amount / 100; amount_input = data.amount / 100;
} }
DonorService.donorControllerGetAll().then((val) => { DonorService.donorControllerGetAll().then((val) => {
current_donors = val.map((r) => { current_donors = val.map((r) => {
return { label: getDonorLabel(r), value: r }; return { label: getDonorLabel(r), value: r };
}); });
donor = current_donors.find((g) => g.value.id == editable.donor.id); donor = current_donors.find((g) => g.value.id == editable.donor.id);
}); });
}); });
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());
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast($_("updating-donation")); toast($_("updating-donation"));
let postdata = {}; let postdata = {};
editable.paidAmount = paid_amount_input * 100; editable.paidAmount = paid_amount_input * 100;
if (original_data.responseType === "DISTANCEDONATION") { if (original_data.responseType === "DISTANCEDONATION") {
editable.amountPerDistance = Math.floor(amount_input * 100); editable.amountPerDistance = Math.floor(amount_input * 100);
postdata = Object.assign(postdata, editable); postdata = Object.assign(postdata, editable);
postdata.runner = postdata.runner.id; postdata.runner = postdata.runner.id;
postdata.donor = postdata.donor.id; postdata.donor = postdata.donor.id;
DonationService.donationControllerPutDistance( DonationService.donationControllerPutDistance(
original_data.id, original_data.id,
postdata postdata
) )
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.success($_("donation-updated")); toast.success($_("donation-updated"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
editable.amount = Math.floor(amount_input * 100); editable.amount = Math.floor(amount_input * 100);
postdata = Object.assign(postdata, editable); postdata = Object.assign(postdata, editable);
postdata.donor = postdata.donor.id; postdata.donor = postdata.donor.id;
DonationService.donationControllerPutFixed(original_data.id, postdata) DonationService.donationControllerPutFixed(original_data.id, postdata)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.success($_("donation-updated")); toast.success($_("donation-updated"));
}) })
.catch((err) => {}); .catch((err) => {});
} }
} else { } else {
} }
} }
function deleteDonation() { function deleteDonation() {
DonationService.donationControllerRemove(original_data.id, false) DonationService.donationControllerRemove(original_data.id, false)
.then((resp) => { .then((resp) => {
toast($_("donation-deleted")); toast($_("donation-deleted"));
location.replace("./"); location.replace("./");
}) })
.catch((err) => { .catch((err) => {
modal_open = true; modal_open = true;
delete_donor = original_data; 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="w-full"> <div class="mt-2 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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 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" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">{$_("donations")}</a><svg >
stroke="currentColor" {$_("donations")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {original_data.donor.firstname}
xmlns="http://www.w3.org/2000/svg" {original_data.donor.middlename || ""}
><line x1="5" y1="12" x2="19" y2="12" /> {original_data.donor.lastname}
<polyline points="12 5 19 12 12 19" /></svg &gt;
> {#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:" on:click={() => {
>{$_("confirm-deletion")}</button delete_triggered = true;
> }}
<button type="button"
on:click={() => { 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"
delete_triggered = !delete_triggered; >{$_("delete-donation")}</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:" {/if}
>{$_("cancel")}</button {/if}
> {#if !delete_triggered}
{/if} <button
{#if !delete_triggered} disabled={!save_enabled}
<button class:opacity-50={!save_enabled}
on:click={() => { type="button"
delete_triggered = true; 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"
type="button" >{$_("save-changes")}</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:" >
>{$_("delete-donation")}</button {/if}
> </div>
{/if} </div>
{/if} <!-- -->
{#if !delete_triggered} <div>
<button <span class="font-semibold text-gray-700"
disabled={!save_enabled} >{$_("total-donation-amount")}:</span
class:opacity-50={!save_enabled} >
type="button" <span
on:click={submit} >{(editable.amount / 100)
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:" .toFixed(2)
>{$_("save-changes")}</button .toLocaleString("de-DE", { valute: "EUR" })}</span
> >
{/if} |
</span> <span class="font-semibold text-gray-700">{$_("paid-amount")}:</span>
</div> <span
<!-- --> >{(editable.paidAmount / 100)
<div> .toFixed(2)
<span class="font-medium text-gray-700" .toLocaleString("de-DE", { valute: "EUR" })}</span
>{$_("total-donation-amount")}:</span >
> |
<span <span class="font-semibold text-gray-700">{$_("status")}:</span>
>{(editable.amount / 100) {#if editable.status == "PAID"}
.toFixed(2) <span
.toLocaleString("de-DE", { valute: "EUR" })}</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
| >
<span class="font-medium text-gray-700">{$_("paid-amount")}:</span> {:else}
<span <span
>{(editable.paidAmount / 100) class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
.toFixed(2) >{$_("open")}</span
.toLocaleString("de-DE", { valute: "EUR" })}</span >
> {/if}
| </div>
<span class="font-medium text-gray-700">{$_("status")}:</span> <br />
{#if editable.status == "PAID"} <div class=" mt-2 w-full">
<span <label for="donor" class="block font-semibold text-gray-700"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" >{$_("donor")}</label
>{$_("paid")}</span >
> <Select
{:else} 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-neutral-800 rounded-md p-2"
<span itemFilter={(label, filterText, option) =>
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800" filterDonors(label, filterText, option)}
>{$_("open")}</span items={current_donors}
> showChevron={true}
{/if} placeholder={$_("search-for-donor-name-or-id")}
</div> noOptionsMessage={$_("no-donors-found")}
<br /> bind:selectedValue={donor}
<div class=" w-full"> on:select={(selectedValue) => {
<label for="donor" class="block font-medium text-gray-700" editable.donor = selectedValue.detail.value;
>{$_("donor")}</label editable.donor.donationAmount = original_data.donor.donationAmount;
> editable.donor.paidDonationAmount =
<Select original_data.donor.paidDonationAmount;
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) => on:clear={() => (editable.donor = null)}
filterDonors(label, filterText, option)} />
items={current_donors} </div>
showChevron={true} {#if original_data.responseType == "DISTANCEDONATION"}
placeholder={$_("search-for-donor-name-or-id")} <div class=" mt-2 w-full">
noOptionsMessage={$_("no-donors-found")} <label for="donor" class="block font-semibold text-gray-700"
bind:selectedValue={donor} >{$_("runner")}</label
on:select={(selectedValue) => { >
editable.donor = selectedValue.detail.value; <Select
editable.donor.donationAmount = original_data.donor.donationAmount; 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-neutral-800 rounded-md p-2"
editable.donor.paidDonationAmount = itemFilter={(label, filterText, option) =>
original_data.donor.paidDonationAmount; filterDonors(label, filterText, option)}
}} items={current_runners}
on:clear={() => (editable.donor = null)} showChevron={true}
/> placeholder={$_("search-for-runner-by-name-or-id")}
</div> noOptionsMessage={$_("no-runners-found")}
{#if original_data.responseType == "DISTANCEDONATION"} bind:selectedValue={runner}
<div class=" w-full"> on:select={(selectedValue) =>
<label for="donor" class="block font-medium text-gray-700" (editable.runner = selectedValue.detail.value)}
>{$_("runner")}</label 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 rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" {/if}
itemFilter={(label, filterText, option) => <div class=" mt-2 w-full">
filterDonors(label, filterText, option)} <label for="lastname" class="font-semibold 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=" w-full"> type="number"
<label for="lastname" class="font-medium 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-neutral-800 p-2"
{:else}{$_("donation-amount")}{/if} placeholder="2.00"
</label> />
<div class="mt-1 flex rounded-md shadow-sm"> <span
<input class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500"
autocomplete="off" ></span
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 !is_amount_valid}
bind:value={amount_input} <span
type="number" class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
step="0.01" >
name="donation_amount_eur" {$_("donation-amount-must-be-greater-that-0-00eur")}
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" </span>
placeholder="2.00" {/if}
/> </div>
<span <div class="mt-2 w-full">
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500" <label for="token" class="block font-semibold text-gray-700"
></span >{$_("paid-amount")}</label
> >
</div> <div
{#if !is_amount_valid} class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full"
<span >
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <input
> autocomplete="off"
{$_("donation-amount-must-be-greater-that-0-00eur")} class:border-red-500={!is_amount_valid}
</span> class:focus:border-red-500={!is_amount_valid}
{/if} class:focus:ring-red-500={!is_amount_valid}
</div> bind:value={paid_amount_input}
<div class="w-full"> type="number"
<label for="token" class="block text-sm font-medium text-gray-700" step="0.01"
>{$_("paid-amount")}</label 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"
<div placeholder="2.00"
class="inline-flex border-gray-300 border rounded-l-md rounded-r-md bg-gray-50 text-gray-500 w-full" />
> <button
<input on:click={() => {
autocomplete="off" paid_amount_input = paid_amount_input = (
class:border-red-500={!is_amount_valid} original_data.amount / 100
class:focus:border-red-500={!is_amount_valid} ).toFixed(2);
class:focus:ring-red-500={!is_amount_valid} }}
bind:value={paid_amount_input} class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm"
type="number" >MAX</button
step="0.01" >
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" class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm"
placeholder="2.00" ></span
/> >
<button </div>
on:click={() => { {#if !is_paid_amount_valid}
paid_amount_input = paid_amount_input = ( <span
original_data.amount / 100 class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
).toFixed(2); >
}} {$_("payment-amount-must-be-greater-than-0-00eur")}
class="inline-flex items-center p-r-2 text-indigo-300 hover:text-indigo-700 text-sm" </span>
>MAX</button {/if}
> </div>
<span </section>
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

@@ -9,7 +9,7 @@
<div class="flex items-center"> <div class="flex items-center">
<a <a
href="../donors/{donor.id}" href="../donors/{donor.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{donor.firstname} >{donor.firstname}
{#if donor.middlename}{donor.middlename}{/if} {#if donor.middlename}{donor.middlename}{/if}
{donor.lastname}</a {donor.lastname}</a

View File

@@ -9,7 +9,7 @@
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900">
<a <a
href="../runners/{runner.id}" href="../runners/{runner.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>{runner.firstname} >{runner.firstname}
{#if runner.middlename}{runner.middlename}{/if} {#if runner.middlename}{runner.middlename}{/if}
{runner.lastname}</a {runner.lastname}</a

View File

@@ -5,12 +5,12 @@
{#if status == "PAID"} {#if status == "PAID"}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("paid")}</span >{$_("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" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
>{$_("open")}</span >{$_("open")}</span
> >
{/if} {/if}

View File

@@ -9,20 +9,20 @@
</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">
{$_("donations")} {$_("donations")}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("DONATION: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"
> 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 bind:addDonations /> <DonationsOverview bind:current_donations bind:addDonations />
</section> </section>

View File

@@ -5,7 +5,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="m-auto" style="height:15rem" src={donations_empty} alt="" /> <img class="m-auto mt-2" 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>

View File

@@ -168,7 +168,7 @@
onMount(async () => { onMount(async () => {
let page = 0; let page = 0;
let pagesize = 100; let pagesize = 300;
while (page >= 0) { while (page >= 0) {
const donations = await DonationService.donationControllerGetAll( const donations = await DonationService.donationControllerGetAll(
page, page,
@@ -186,7 +186,6 @@
dataLoaded = true; dataLoaded = true;
page++; page++;
pagesize += 100;
} }
}); });
</script> </script>
@@ -196,9 +195,12 @@
payment_modal_open={active_edits.length > 0} payment_modal_open={active_edits.length > 0}
paid_amount_input={(active_edits[0]?.paidAmount || 0) / 100} paid_amount_input={(active_edits[0]?.paidAmount || 0) / 100}
on:created={(event) => { on:created={(event) => {
current_donations[ current_donations = current_donations.map((d)=>{
current_donations.findIndex((d) => d.id === event.detail.donation.id) if(d.id === event.detail.donation.id){
].paidAmount = event.detail.donation.paidAmount; d.paidAmount = event.detail.donation.paidAmount;
}
return d;
})
options.update((options) => ({ options.update((options) => ({
...options, ...options,
data: current_donations, data: current_donations,
@@ -229,7 +231,7 @@
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")} aria-label={$_("datatable.search")}
class="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<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"

View File

@@ -196,7 +196,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
@@ -219,7 +219,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -238,7 +238,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
@@ -264,7 +264,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isPhoneValidOrEmpty} {#if !isPhoneValidOrEmpty}
<span <span
@@ -292,7 +292,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isEmailValidOrEmpty} {#if !isEmailValidOrEmpty}
<span <span
@@ -313,7 +313,7 @@
/> />
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-medium text-gray-700" <label for="comments" class="font-semibold text-gray-700"
>{$_("receipt-needed")}</label >{$_("receipt-needed")}</label
> >
</div> </div>
@@ -335,7 +335,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isAddress1Valid} {#if !isAddress1Valid}
<span <span
@@ -358,7 +358,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -377,7 +377,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iszipcodevalid} {#if !iszipcodevalid}
<span <span
@@ -403,7 +403,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iscityvalid} {#if !iscityvalid}
<span <span

View File

@@ -1,438 +1,413 @@
<script> <script>
import { _ } from "svelte-i18n"; import { DonorService } from "@odit/lfk-client-js";
import store from "../../store"; import { _ } from "svelte-i18n";
import { DonorService, DonationService } from "@odit/lfk-client-js"; import store from "../../store";
import toast from "svelte-french-toast";
import PromiseError from "../base/PromiseError.svelte"; import isEmail from "validator/es/lib/isEmail";
import isEmail from "validator/es/lib/isEmail"; import PromiseError from "../base/PromiseError.svelte";
import ConfirmDonorDeletion from "./ConfirmDonorDeletion.svelte"; let data_loaded = false;
import toast from "svelte-french-toast"; 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;
toast($_("donor-is-being-updated")); editable.receiptNeeded = editable.address_checked;
editable.address.country = "DE"; DonorService.donorControllerPut(original_data.id, editable)
if (editable.address_checked === false) { .then((resp) => {
editable.address = null; Object.assign(original_data, editable);
} original_data = original_data;
if (editable.email) editable.email = editable.email; toast.success($_("updated-donor"));
if (editable.phone) editable.phone = editable.phone; })
if (editable.middlename) editable.middlename = editable.middlename; .catch((err) => {});
editable.receiptNeeded = editable.address_checked; } else {
DonorService.donorControllerPut(original_data.id, editable) }
.then((resp) => { }
Object.assign(original_data, editable); function deleteDonor() {
original_data = original_data; DonorService.donorControllerRemove(original_data.id, true)
toast.success($_("updated-donor")); .then((resp) => {
}) toast($_("donor-deleted"));
.catch((err) => {}); location.replace("./");
} else { })
} .catch((err) => {
} console.log(err);
function deleteDonor() { });
DonorService.donorControllerRemove(original_data.id, false) }
.then((resp) => {
toast($_("donor-deleted"));
location.replace("./");
})
.catch((err) => {
modal_open = true;
delete_donor = original_data;
});
}
</script> </script>
<ConfirmDonorDeletion bind:modal_open bind:delete_donor /> {#await promise}
{#await promise && donation_promise} {$_("loading-donor-details")}
{$_("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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 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" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">{$_("donors")}</a><svg >
stroke="currentColor" {$_("donors")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {original_data.firstname}
xmlns="http://www.w3.org/2000/svg" {original_data.middlename || ""}
><line x1="5" y1="12" x2="19" y2="12" /> {original_data.lastname}
<polyline points="12 5 19 12 12 19" /></svg <div data-id="donor_actions_${editable.id}">
> {#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:DELETE")}
</li> {#if delete_triggered}
<li class="flex items-center"> <button
<span class="mr-2" on:click={deleteDonor}
>{original_data.firstname} 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.middlename || ""} >{$_("confirm-deletion")}</button
{original_data.lastname}</span >
> <button
</li> on:click={() => {
</ol> delete_triggered = !delete_triggered;
</nav> }}
</div> 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"
</div> >{$_("cancel")}</button
<div class="mb-8 text-3xl font-extrabold leading-tight"> >
{original_data.firstname} {/if}
{original_data.middlename || ""} {#if !delete_triggered}
{original_data.lastname} <button
<span data-id="donor_actions_${editable.id}"> on:click={() => {
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:DELETE")} delete_triggered = true;
{#if delete_triggered} }}
<button type="button"
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:w-auto sm:text-sm"
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
>{$_("confirm-deletion")}</button >
> {/if}
<button {/if}
on:click={() => { {#if !delete_triggered}
delete_triggered = !delete_triggered; <button
}} disabled={!save_enabled}
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:" class:opacity-50={!save_enabled}
>{$_("cancel")}</button type="button"
> 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"
{#if !delete_triggered} >{$_("save-changes")}</button
<button >
on:click={() => { {/if}
delete_triggered = true; </div>
}} </div>
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:" <div>
>{$_("delete-donor")}</button <span class="font-semibold text-gray-700"
> >{$_("total-donation-amount")}:</span
{/if} >
{/if} <span
{#if !delete_triggered} >{(editable.donationAmount / 100)
<button .toFixed(2)
disabled={!save_enabled} .toLocaleString("de-DE", { valute: "EUR" })}</span
class:opacity-50={!save_enabled} >
type="button" |
on:click={submit} <span class="font-semibold text-gray-700">{$_("total-paid-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 <span
> >{(editable.paidDonationAmount / 100)
{/if} .toFixed(2)
</span> .toLocaleString("de-DE", { valute: "EUR" })}</span
</div> >
<!-- --> <br />
<div> <span class="font-semibold text-gray-700">{$_("donations")}:</span>
<span class="font-medium text-gray-700" {#if original_data.donations.length > 0}
>{$_("total-donation-amount")}:</span {#each original_data.donations as d}
> {#if d.responseType === "DISTANCEDONATION"}
<span <a
>{(editable.donationAmount / 100) href="../donations/{d.id}"
.toFixed(2) class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1"
.toLocaleString("de-DE", { valute: "EUR" })}</span >{d.runner.firstname}
> {d.runner.middlename || ""}
| {d.runner.lastname}</a
<span class="font-medium text-gray-700">{$_("total-paid-amount")}:</span> >
<span {:else}
>{(editable.paidDonationAmount / 100) <a
.toFixed(2) href="../donations/{d.id}"
.toLocaleString("de-DE", { valute: "EUR" })}</span 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")}:
<br /> {(d.amount / 100)
<span class="font-medium text-gray-700">{$_("donations")}:</span> .toFixed(2)
{#if current_donations.filter((d) => d.donor.id == editable.id).length > 0} .toLocaleString("de-DE", { valute: "EUR" })}</a
{#each current_donations.filter((o) => o.donor.id == editable.id) as d} >
{#if d.responseType === "DISTANCEDONATION"} {/if}
<a {/each}
href="../donations/{d.id}" {:else}{$_("donor-has-no-associated-donations")}{/if}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-600 text-white mr-1" </div>
>{d.runner.firstname} <div class="mt-2 w-full">
{d.runner.middlename || ""} <label for="firstname" class="font-semibold text-gray-700"
{d.runner.lastname}</a >{$_("first-name")}</label
> >
{:else} <input
<a autocomplete="off"
href="../donations/{d.id}" placeholder={$_("first-name")}
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-700 text-white mr-1" type="text"
>{$_("fixed-donation")}: class:border-red-500={!isFirstnameValid}
{(d.amount / 100) class:focus:border-red-500={!isFirstnameValid}
.toFixed(2) class:focus:ring-red-500={!isFirstnameValid}
.toLocaleString("de-DE", { valute: "EUR" })}</a bind:value={editable.firstname}
> name="firstname"
{/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"
{/each} />
{:else}{$_("donor-has-no-associated-donations")}{/if} {#if !isFirstnameValid}
</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="firstname" class="font-medium text-gray-700" >
>{$_("first-name")}</label {$_("first-name-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("first-name")} <div class="mt-2 w-full">
type="text" <label for="middlename" class="font-semibold text-gray-700"
class:border-red-500={!isFirstnameValid} >{$_("middle-name")}</label
class:focus:border-red-500={!isFirstnameValid} >
class:focus:ring-red-500={!isFirstnameValid} <input
bind:value={editable.firstname} autocomplete="off"
name="firstname" 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"
/> bind:value={editable.middlename}
{#if !isFirstnameValid} name="middlename"
<span 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="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" />
> </div>
{$_("first-name-is-required")} <div class="mt-2 w-full">
</span> <label for="lastname" class="font-semibold text-gray-700"
{/if} >{$_("last-name")}</label
</div> >
<div class=" w-full"> <input
<label for="middlename" class="font-medium text-gray-700" autocomplete="off"
>{$_("middle-name")}</label placeholder={$_("last-name")}
> type="text"
<input bind:value={editable.lastname}
autocomplete="off" class:border-red-500={!isLastnameValid}
placeholder={$_("middle-name")} class:focus:border-red-500={!isLastnameValid}
type="text" class:focus:ring-red-500={!isLastnameValid}
bind:value={editable.middlename} name="lastname"
name="middlename" 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="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 !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="lastname" class="font-medium text-gray-700" >
>{$_("last-name")}</label {$_("last-name-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("last-name")} <div class="mt-2 w-full">
type="text" <label for="email" class="font-semibold text-gray-700"
bind:value={editable.lastname} >{$_("e-mail-adress")}</label
class:border-red-500={!isLastnameValid} >
class:focus:border-red-500={!isLastnameValid} <input
class:focus:ring-red-500={!isLastnameValid} autocomplete="off"
name="lastname" 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"
/> bind:value={editable.email}
{#if !isLastnameValid} class:border-red-500={!isEmailValid}
<span class:focus:border-red-500={!isEmailValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class:focus:ring-red-500={!isEmailValid}
> name="email"
{$_("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 !isEmailValid}
</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" class="font-medium text-gray-700" >
>{$_("e-mail-adress")}</label {$_("valid-email-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("e-mail-adress")} <div class="mt-2 w-full">
type="email" <label for="phone" class="font-semibold text-gray-700"
bind:value={editable.email} >{$_("phone")}</label
class:border-red-500={!isEmailValid} >
class:focus:border-red-500={!isEmailValid} <input
class:focus:ring-red-500={!isEmailValid} autocomplete="off"
name="email" placeholder={$_("phone")}
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="tel"
/> class:border-red-500={!isPhoneValidOrEmpty}
{#if !isEmailValid} class:focus:border-red-500={!isPhoneValidOrEmpty}
<span class:focus:ring-red-500={!isPhoneValidOrEmpty}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" bind:value={editable.phone}
> name="phone"
{$_("valid-email-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 !isPhoneValidOrEmpty}
</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="phone" class="font-medium text-gray-700">{$_("phone")}</label> >
<input {$_("valid-international-phone-number-is-required")}
autocomplete="off" </span>
placeholder={$_("phone")} {/if}
type="tel" </div>
class:border-red-500={!isPhoneValidOrEmpty} <div class="flex items-start mt-2">
class:focus:border-red-500={!isPhoneValidOrEmpty} <div class="flex items-center h-5">
class:focus:ring-red-500={!isPhoneValidOrEmpty} <input
bind:value={editable.phone} bind:checked={editable.address_checked}
name="phone" id="comments"
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="comments"
/> type="checkbox"
{#if !isPhoneValidOrEmpty} class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
<span />
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" </div>
> <div class="ml-3">
{$_("valid-international-phone-number-is-required")} <label for="comments" class="font-semibold text-gray-700"
</span> >{$_("receipt-needed")}</label
{/if} >
</div> </div>
<div class="flex items-start mt-2"> </div>
<div class="flex items-center h-5"> {#if editable.address_checked === true}
<input <div class="col-span-6">
bind:checked={editable.address_checked} <label for="address1" class="block font-medium text-gray-700"
id="comments" >{$_("address")}</label
name="comments" >
type="checkbox" <input
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" autocomplete="off"
/> placeholder="Address"
</div> class:border-red-500={!isAddress1Valid}
<div class="ml-3"> class:focus:border-red-500={!isAddress1Valid}
<label for="comments" class="font-medium text-gray-700" class:focus:ring-red-500={!isAddress1Valid}
>{$_("receipt-needed")}</label bind:value={editable.address.address1}
> type="text"
</div> name="address1"
</div> 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 editable.address_checked === true} />
<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"
> >
<input {$_("address-is-required")}
autocomplete="off" </span>
placeholder="Address" {/if}
class:border-red-500={!isAddress1Valid} </div>
class:focus:border-red-500={!isAddress1Valid} <div class="col-span-6">
class:focus:ring-red-500={!isAddress1Valid} <label for="address2" class="block font-medium text-gray-700"
bind:value={editable.address.address1} >{$_("apartment-suite-etc")}</label
type="text" >
name="address1" <input
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" autocomplete="off"
/> placeholder={$_("apartment-suite-etc")}
{#if !isAddress1Valid} bind:value={editable.address.address2}
<span type="text"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" name="address2"
> 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"
{$_("address-is-required")} />
</span> </div>
{/if} <div class="col-span-6">
</div> <label for="zipcode" class="block font-medium text-gray-700"
<div class="col-span-6"> >{$_("zip-postal-code")}</label
<label for="address2" class="block font-medium text-gray-700" >
>{$_("apartment-suite-etc")}</label <input
> autocomplete="off"
<input placeholder={$_("zip-postal-code")}
autocomplete="off" class:border-red-500={!iszipcodevalid}
placeholder={$_("apartment-suite-etc")} class:focus:border-red-500={!iszipcodevalid}
bind:value={editable.address.address2} class:focus:ring-red-500={!iszipcodevalid}
type="text" bind:value={editable.address.postalcode}
name="address2" type="text"
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="zipcode"
/> 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="col-span-6"> {#if !iszipcodevalid}
<label for="zipcode" class="block font-medium text-gray-700" <span
>{$_("zip-postal-code")}</label class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
> >
<input {$_("valid-zipcode-postal-code-is-required")}
autocomplete="off" </span>
placeholder={$_("zip-postal-code")} {/if}
class:border-red-500={!iszipcodevalid} </div>
class:focus:border-red-500={!iszipcodevalid} <div class="col-span-6">
class:focus:ring-red-500={!iszipcodevalid} <label for="city" class="block font-medium text-gray-700"
bind:value={editable.address.postalcode} >{$_("city")}</label
type="text" >
name="zipcode" <input
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" autocomplete="off"
/> placeholder={$_("city")}
{#if !iszipcodevalid} class:border-red-500={!iscityvalid}
<span class:focus:border-red-500={!iscityvalid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class:focus:ring-red-500={!iscityvalid}
> bind:value={editable.address.city}
{$_("valid-zipcode-postal-code-is-required")} type="text"
</span> name="city"
{/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="col-span-6"> {#if !iscityvalid}
<label for="city" class="block font-medium text-gray-700" <span
>{$_("city")}</label class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
> >
<input {$_("valid-city-is-required")}
autocomplete="off" </span>
placeholder={$_("city")} {/if}
class:border-red-500={!iscityvalid} </div>
class:focus:border-red-500={!iscityvalid} {/if}
class:focus:ring-red-500={!iscityvalid} </section>
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: 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}
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -18,7 +18,7 @@
{:else} {:else}
<a <a
href="../donations/{donation.id}" href="../donations/{donation.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-700 text-white mr-1" 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")}: >{$_("fixed-donation")}:
{(donation.amount / 100) {(donation.amount / 100)
.toFixed(2) .toFixed(2)

View File

@@ -9,61 +9,61 @@
</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">
{$_("donors")} {$_("donors")}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("DONOR: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"
> 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 mt-1 sm:mt-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 on:click={() => {
.filter((d) => d.receiptNeeded === true) const data = current_donors
.map(function (d) { .filter((d) => d.receiptNeeded === true)
d.address.address2 = .map(function (d) {
d.address.address2 === "" ? "" : " " + d.address.address2; d.address.address2 =
const address = `${d.address.address1}${d.address.address2}, ${d.address.postalcode} ${d.address.city}, ${d.address.country}`; d.address.address2 === "" ? "" : " " + d.address.address2;
return [ const address = `${d.address.address1}${d.address.address2}, ${d.address.postalcode} ${d.address.city}, ${d.address.country}`;
d.firstname, return [
d.middlename, d.firstname,
d.lastname, d.middlename,
d.paidDonationAmount, d.lastname,
address, (d.paidDonationAmount/100).toFixed(2),
]; address,
}); ];
let csv = `${$_("csv_import__firstname")};${$_(
"csv_import__middlename"
)};${$_("csv_import__lastname")};${$_(
"total_donation_amount_in_eur"
)};${$_("address")}\n`;
data.forEach(function (row) {
csv += row.join(";");
csv += "\n";
}); });
let hiddenElement = document.createElement("a"); let csv = `${$_("csv_import__firstname")};${$_(
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv); "csv_import__middlename"
hiddenElement.target = "_blank"; )};${$_("csv_import__lastname")};${$_(
hiddenElement.download = `${$_( "total_donation_amount_in_eur"
"filename_sponsoringquittungsliste" )};${$_("address")}\n`;
)}.csv`; data.forEach(function (row) {
hiddenElement.click(); csv += row.join(";");
hiddenElement.remove(); csv += "\n";
}} });
type="button" let hiddenElement = document.createElement("a");
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" hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv);
> hiddenElement.target = "_blank";
{$_("sponsoring-quittungs-liste_herunterladen")} hiddenElement.download = `${$_(
</button> "filename_sponsoringquittungsliste"
{/if} )}.csv`;
</span> 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 mt-1 sm:mt-0"
>
{$_("sponsoring-quittungs-liste_herunterladen")}
</button>
{/if}
<DonorsOverview bind:current_donors bind:addDonors /> <DonorsOverview bind:current_donors bind:addDonors />
</section> </section>

View File

@@ -147,7 +147,7 @@
onMount(async () => { onMount(async () => {
let page = 0; let page = 0;
let pagesize = 100; let pagesize = 300;
while (page >= 0) { while (page >= 0) {
const donors = await DonorService.donorControllerGetAll(page, pagesize); const donors = await DonorService.donorControllerGetAll(page, pagesize);
if (donors.length == 0) { if (donors.length == 0) {
@@ -162,7 +162,6 @@
dataLoaded = true; dataLoaded = true;
page++; page++;
pagesize += 100;
} }
}); });
</script> </script>
@@ -203,7 +202,7 @@
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")} aria-label={$_("datatable.search")}
class="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<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"

View File

@@ -1,210 +1,196 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick"; import { clickOutside } from "../base/outsideclick";
export let modal_open; let modal_open = false;
(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-auto" class="fixed z-10 inset-0 overflow-y-auto"
use:clickOutside use:clickOutside
on:click_outside={() => { on:click_outside={() => {
modal_open = false; modal_open = false;
}} }}
> >
<div <div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0" class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 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 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" 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">
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div <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" 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
fill="currentColor" fill="currentColor"
class="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
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="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" 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 /></svg
> >
</div> </div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"> <div class="mt-3 sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium"> <h3 class="text-lg leading-6 font-medium">
{$_("read-license")} {$_("read-license")}
</h3> </h3>
<div class="mt-2 mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500">{currentlicense}</p> <p class="text-sm text-gray-500">{currentlicense}</p>
</div> </div>
<div class="mt-2 mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500">{licensetext}</p> <p class="text-sm text-gray-500">{licensetext}</p>
</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 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
on:click={() => { on:click={() => {
modal_open = false; modal_open = false;
}} }}
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"
> >
{$_("close")} {$_("close")}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
<!-- /// --> <!-- /// -->
<div class="pt-12 px-4 sm:px-6 lg:px-8 lg:pt-20 bg-gray-900 pb-12"> <section class="container p-5">
<div class="text-center mb-8"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
<h1 {$_("about")}
class="mt-9 font-display text-4xl leading-none font-semibold text-white sm:text-5xl lg:text-6xl" </h4>
> <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>
> <br />
Lauf für Kaya! <span>{$_("lfk-is-os")}</span>
<strong class="text-white font-medium"> </p>
{$_("by")} <h4 class="mb-1 text-3xl font-extrabold leading-tight">
<a href="https://odit.services" class="underline">ODIT.Services</a> {$_("credits")}
</strong> </h4>
<br /> <p class="text-left">{$_("oss_credit_description")}</p>
<span class="text-lg">{$_("lfk-is-os")}</span> <div class="mt-5 overflow-x-auto">
</p> {#await license_promise}
</div> <p>{$_("licenses-are-being-loaded")}</p>
</div> {:then}
<table class="font-mono">
<div class="pt-0 pb-16 overflow-hidden lg:pt-12 lg:py-24"> <thead class="border-b border-gray-400">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8"> <tr class="odd:bg-white even:bg-gray-100">
<h2 class="text-4xl font-display font-semibold md:text-5xl"> <th>{$_("dependency_name")}</th>
{$_("credits")} <th>{$_("license")}</th>
</h2> <th>{$_("repo_link")}</th>
<div class="max-w-3xl mx-auto text-xl leading-8 font-medium mt-8"> <th>{$_("installed-version")}</th>
<p class="text-center">{$_("oss_credit_description")}</p> <th>{$_("author")}</th>
</div> </tr>
<div class="w-screen leading-8 pl-5 mt-5"> </thead>
{#await license_promise} <tbody>
<p class="text-center w-full">{$_("licenses-are-being-loaded")}</p> {#each licenses as l}
{:then} <tr class="odd:bg-white even:bg-gray-100 *:p-2">
<table> <td>{l.name}</td>
<thead class="border-b border-gray-400"> <td>
<tr class="odd:bg-white even:bg-gray-100"> <button
<th>{$_("dependency_name")}</th> class="underline cursor-pointer"
<th>{$_("license")}</th> on:click={() => {
<th>{$_("repo_link")}</th> modal_open = true;
<th>{$_("installed-version")}</th> currentlicense = l.name + "@" + l.version;
<th>{$_("author")}</th> licensetext =
</tr> l.licensetext || $_("no-license-text-could-be-found");
</thead> }}>{l.license || "?"}</button
<tbody> >
{#each licenses as l} </td>
<tr class="odd:bg-white even:bg-gray-100"> <td>
<td>{l.name}</td> {(l.repo?.url || l.repo)
<td> .replace("git+", "")
{l.license || "?"}<br /><button .replace("git://", "")}
class="underline cursor-pointer" </td>
on:click={() => { <td>{l.version || "?"}</td>
modal_open = true; <td>{l.author?.name || l.author || "?"}</td>
currentlicense = l.name + "@" + l.version; </tr>
licensetext = {/each}
l.licensetext || $_("no-license-text-could-be-found"); </tbody>
}}>{$_("read-license")}</button </table>
> {:catch error}
</td> <div
<td> class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"
{(l.repo?.url || l.repo) >
.replace("git+", "") <span class="inline-block align-middle mr-8">
.replace("git://", "")} <b class="capitalize">{$_("general_promise_error")}</b>
</td> {error}
<td>{l.version || "?"}</td> </span>
<td>{l.author?.name || l.author || "?"}</td> </div>
</tr> {/await}
{/each} </div>
</tbody> <div class="w-full mt-8">
</table> <p class="font-medium">{$_("icon-image-credits")}</p>
{:catch error} <ul class="list-disc ml-6">
<div <li>
class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500" <a
> class="underline"
<span class="inline-block align-middle mr-8"> target="_blank"
<b class="capitalize">{$_("general_promise_error")}</b> rel="noopener noreferrer"
{error} href="https://storyset.com">https://storyset.com</a
</span> >
</div> </li>
{/await} <li>
</div> <a
<div class="w-full leading-8 mt-8"> class="underline"
<p class="text-xl font-medium">{$_("icon-image-credits")}</p> target="_blank"
<ul class="list-disc"> rel="noopener noreferrer"
<li> href="https://undraw.co">https://undraw.co</a
<a >
class="underline" </li>
target="_blank" <li>
rel="noopener noreferrer" <a
href="https://storyset.com">https://storyset.com</a class="underline"
> target="_blank"
</li> rel="noopener noreferrer"
<li> href="https://remixicon.com">https://remixicon.com</a
<a >
class="underline" </li>
target="_blank" </ul>
rel="noopener noreferrer" </div>
href="https://undraw.co">https://undraw.co</a </section>
>
</li>
<li>
<a
class="underline"
target="_blank"
rel="noopener noreferrer"
href="https://remixicon.com">https://remixicon.com</a
>
</li>
</ul>
</div>
</div>
</div>

View File

@@ -130,7 +130,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isNameValid} {#if !isNameValid}
<span <span
@@ -152,7 +152,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
</div> </div>

View File

@@ -1,237 +1,227 @@
<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 { UserGroupService } from "@odit/lfk-client-js";
import toast from "svelte-french-toast";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
let data_loaded = false; let data_loaded = false;
export let params; export let params;
const promise = UserGroupService.userGroupControllerGetOne(params.groupid); const promise = UserGroupService.userGroupControllerGetOne(params.groupid);
const colors = [ const colors = [
"#f3558e", "#f3558e",
"#17b978", "#17b978",
"#3498db", "#3498db",
"#3f3b3b", "#3f3b3b",
"#775ada", "#775ada",
"#7ed6df_#000000", "#7ed6df_#000000",
"#000000", "#000000",
"#21e6c1_#000000", "#21e6c1_#000000",
"#c0392b", "#c0392b",
"#d35400", "#d35400",
"#7f8c8d", "#7f8c8d",
"#6ab04c", "#6ab04c",
"#4834d4", "#4834d4",
"#ff1f5a", "#ff1f5a",
"#eac100", "#eac100",
]; ];
let matched_colors = []; let matched_colors = [];
$: delete_triggered = false; $: delete_triggered = false;
$: search_permission = ""; $: search_permission = "";
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: changes_performed = !( $: changes_performed = !(
JSON.stringify(original_data) == JSON.stringify(editable) JSON.stringify(original_data) == JSON.stringify(editable)
); );
$: isGroupnameValid = editable.name !== ""; $: isGroupnameValid = editable.name !== "";
$: save_enabled = changes_performed && isGroupnameValid; $: save_enabled = 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")); toast($_("updating-group"));
UserGroupService.userGroupControllerPut(original_data.id, editable) UserGroupService.userGroupControllerPut(original_data.id, editable)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = editable; original_data = editable;
Object.assign(original_data, editable); Object.assign(original_data, editable);
toast.success($_("group-updated")); toast.success($_("group-updated"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
} }
} }
function deleteGroup() { function deleteGroup() {
UserGroupService.userGroupControllerRemove(original_data.id, true) UserGroupService.userGroupControllerRemove(original_data.id, true)
.then((resp) => { .then((resp) => {
location.replace("./"); location.replace("./");
}) })
.catch((err) => {}); .catch((err) => {});
} }
</script> </script>
{#await promise} {#await promise}
{$_("loading-group-detail")} {$_("loading-group-detail")}
{: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"></li>
<svg <li class="flex items-center">
class="flex-shrink-0 w-5 h-5 mr-2" <a class="mr-2" href="../"
fill="currentColor" ><svg
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" width="24"
xmlns="http://www.w3.org/2000/svg" height="24"
viewBox="0 0 640 512" viewBox="0 0 24 24"
><path fill="none"
fill="currentColor" stroke="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" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="../">{$_("groups")}</a><svg >
stroke="currentColor" {$_("groups")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {editable.name}
xmlns="http://www.w3.org/2000/svg" <div data-id="group_actions_${editable.id}">
><line x1="5" y1="12" x2="19" y2="12" /> {#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:DELETE")}
<polyline points="12 5 19 12 12 19" /></svg {#if delete_triggered}
> <button
</li> on:click={deleteGroup}
<li class="flex items-center"> 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"
<span class="mr-2">{editable.name}</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.name} >{$_("cancel")}</button
<span data-id="group_actions_${editable.id}"> >
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:DELETE")} {/if}
{#if delete_triggered} {#if !delete_triggered}
<button <button
on:click={deleteGroup} on:click={() => {
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_triggered = true;
>{$_("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-group")}</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" {/if}
>{$_("cancel")}</button {#if !delete_triggered}
> <button
{/if} disabled={!save_enabled}
{#if !delete_triggered} class:opacity-50={!save_enabled}
<button type="button"
on:click={() => { on:click={submit}
delete_triggered = 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"
}} >{$_("save-changes")}</button
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" {/if}
>{$_("delete-group")}</button </div>
> </div>
{/if} <!-- -->
{/if} <div class="text-sm w-full mt-2">
{#if !delete_triggered} <label for="title" class="font-semibold text-gray-700">{$_("name")}</label
<button >
disabled={!save_enabled} <input
class:opacity-50={!save_enabled} autocomplete="off"
type="button" placeholder={$_("name")}
on:click={submit} type="text"
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" bind:value={editable.name}
>{$_("save-changes")}</button class:border-red-500={!isGroupnameValid}
> class:focus:border-red-500={!isGroupnameValid}
{/if} class:focus:ring-red-500={!isGroupnameValid}
</span> name="title"
</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"
<!-- --> />
<div class="text-sm w-full"> {#if !isGroupnameValid}
<label for="title" class="font-medium text-gray-700">{$_("name")}</label> <span
<input class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
autocomplete="off" >
placeholder={$_("name")} {$_("group-name-is-required")}
type="text" </span>
bind:value={editable.name} {/if}
class:border-red-500={!isGroupnameValid} </div>
class:focus:border-red-500={!isGroupnameValid} <div class="text-sm w-full mt-2">
class:focus:ring-red-500={!isGroupnameValid} <label for="groupdescription" class="font-semibold text-gray-700"
name="title" >{$_("description")}</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" >
/> <input
{#if !isGroupnameValid} autocomplete="off"
<span placeholder={$_("description")}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" type="text"
> bind:value={editable.description}
{$_("group-name-is-required")} name="groupdescription"
</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-neutral-800 rounded-md p-2"
{/if} />
</div> </div>
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="firstname" class="font-medium text-gray-700" <p class="font-semibold mb-4">
>{$_("description")}</label {$_("permissions")}
> </p>
<input <div>
autocomplete="off" <a
placeholder={$_("description")} class="px-4 py-2 bg-gray-500 rounded-md text-white"
type="text" href="/groups/{params.groupid}/permissions/"
bind:value={editable.description} >{$_("edit-permissions")}</a
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-gray-500 rounded-md p-2" </div>
/> <div class="w-full sm:my-px sm:px-px sm:w-1/2">
</div> <input
<div class="text-sm w-full mt-8"> autocomplete="off"
<p class="font-medium mb-4"> placeholder={$_("search-for-permission")}
{$_("permissions")} type="text"
<a bind:value={search_permission}
class="px-4 py-2 bg-gray-500 rounded-md text-white" 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"
href="/groups/{params.groupid}/permissions/" />
>{$_("edit-permissions")}</a </div>
> {#each original_data.permissions as p}
</p> {#if p.toLowerCase().includes(search_permission.toLowerCase())}
<div class="w-full sm:my-px sm:px-px sm:w-1/2"> <span
<input style="background:{matched_colors[
autocomplete="off" p.split(':')[0]
placeholder={$_("search-for-permission")} ][0]};color:{matched_colors[p.split(':')[0]][1]};"
type="text" class="mt-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-indigo-100 rounded"
bind:value={search_permission} >{p}</span
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 rounded-md p-2" >
/> <!-- -->
</div> {/if}
{#each original_data.permissions as p} {/each}
{#if p.toLowerCase().includes(search_permission.toLowerCase())} </div>
<span </section>
style="background:{matched_colors[
p.split(':')[0]
][0]};color:{matched_colors[p.split(':')[0]][1]};"
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
>
<!-- -->
{/if}
{/each}
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -5,6 +5,7 @@
CreatePermission, CreatePermission,
UserGroupService, UserGroupService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import toast from 'svelte-french-toast'
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
export let params; export let params;
@@ -132,27 +133,25 @@
</nav> </nav>
</div> </div>
</div> </div>
<div class="mb-8 text-3xl font-extrabold"> <div class="mb-4 text-3xl font-extrabold">
{$_("permissions")}: <div>
{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: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"
>{$_("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: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-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"
>{$_("applying-changes")}</button >{$_("applying-changes")}</button
> >
{/if} {/if}
</span> </div>
</div> </div>
<!-- --> <!-- -->
<div class="flex flex-wrap -mx-1 overflow-hidden"> <div class="flex flex-wrap -mx-1 overflow-hidden">
@@ -192,7 +191,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: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-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"
>+</button >+</button
> >
</p> </p>
@@ -232,7 +231,7 @@
} }
}} }}
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: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-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"
>-</button >-</button
> >
</p> </p>

View File

@@ -8,20 +8,20 @@
</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">
{$_("user-groups")} {$_("user-groups")}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP: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"
> 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>

View File

@@ -31,7 +31,7 @@
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")} aria-label={$_("datatable.search")}
class="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<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"

View File

@@ -153,7 +153,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isOrgnameValid} {#if !isOrgnameValid}
<span <span
@@ -174,7 +174,7 @@
/> />
</div> </div>
<div class="ml-3 text-sm"> <div class="ml-3 text-sm">
<label for="comments" class="font-medium text-gray-700" <label for="comments" class="font-semibold text-gray-700"
>{$_("address")}</label >{$_("address")}</label
> >
</div> </div>
@@ -196,7 +196,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isAddress1Valid} {#if !isAddress1Valid}
<span <span
@@ -219,7 +219,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -238,7 +238,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iszipcodevalid} {#if !iszipcodevalid}
<span <span
@@ -264,7 +264,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !iscityvalid} {#if !iscityvalid}
<span <span

View File

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

View File

@@ -1,250 +1,244 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
let modal_open = false; let modal_open = false;
let delete_org = {}; let delete_org = {};
import { RunnerOrganizationService } from "@odit/lfk-client-js"; import { RunnerOrganizationService } from "@odit/lfk-client-js";
import store from "../../store"; import store from "../../store";
import OrgsEmptyState from "./OrgsEmptyState.svelte"; import OrgsEmptyState from "./OrgsEmptyState.svelte";
import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte"; import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
$: sponsoring_contracts_show = current_organizations.some( $: sponsoring_contracts_show = current_organizations.some(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
$: cards_show = current_organizations.some((r) => r.is_selected === true); $: cards_show = current_organizations.some((r) => r.is_selected === true);
$: generate_orgs = current_organizations.filter( $: generate_orgs = current_organizations.filter(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
$: certificates_show = current_organizations.some( $: certificates_show = current_organizations.some(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
export let current_organizations = []; export let current_organizations = [];
const promise = const promise =
RunnerOrganizationService.runnerOrganizationControllerGetAll().then( RunnerOrganizationService.runnerOrganizationControllerGetAll().then(
(val) => { (val) => {
current_organizations = val; current_organizations = val;
} }
); );
</script> </script>
<ConfirmOrgDeletion <ConfirmOrgDeletion
on:cancelDelete={(event) => { on:cancelDelete={(event) => {
modal_open = false; modal_open = false;
active_deletes[event.detail.id] = false; active_deletes[event.detail.id] = false;
}} }}
bind:modal_open bind:modal_open
bind:delete_org bind:delete_org
/> />
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION: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">{$_("organizations-are-being-loaded")}</p> <p class="font-bold">{$_("organizations-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_organizations.length === 0} {#if current_organizations.length === 0}
<OrgsEmptyState /> <OrgsEmptyState />
{: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="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<div class="h-12"> <div class="h-12">
<GenerateSponsoringContracts <GenerateSponsoringContracts
bind:sponsoring_contracts_show bind:sponsoring_contracts_show
bind:generate_orgs bind:generate_orgs
/> />
<GenerateRunnerCards bind:cards_show bind:generate_orgs /> <GenerateRunnerCards bind:cards_show bind:generate_orgs />
<GenerateRunnerCertificates bind:certificates_show bind:generate_orgs /> <GenerateRunnerCertificates bind:certificates_show bind:generate_orgs />
</div> </div>
<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 class="odd:bg-white even:bg-gray-100">
<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"
> >
<button <button
on:click={() => { on:click={() => {
const newstate = !current_organizations.some( const newstate = !current_organizations.some(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
current_organizations = current_organizations.map((r) => { current_organizations = current_organizations.map((r) => {
r.is_selected = newstate; r.is_selected = newstate;
return r; return r;
}); });
}} }}
class="underline cursor-pointer select-none" class="underline cursor-pointer select-none"
>{#if current_organizations.some((r) => r.is_selected === true)} >{#if current_organizations.some((r) => r.is_selected === true)}
{$_("deselect-all")} {$_("deselect-all")}
{:else}{$_("select-all")}{/if} {:else}{$_("select-all")}{/if}
</button> </button>
</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"
> >
{$_("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"
> >
{$_("address")} {$_("address")}
</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"
> >
{$_("contact")} {$_("contact")}
</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>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
{#each current_organizations as o} {#each current_organizations as o}
{#if Object.values(o) {#if Object.values(o)
.toString() .toString()
.toLowerCase() .toLowerCase()
.includes(searchvalue)} .includes(searchvalue)}
<tr <tr
class="odd:bg-white even:bg-gray-100" class="odd:bg-white even:bg-gray-100"
data-rowid="org_{o.id}" data-rowid="org_{o.id}"
> >
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<input <input
bind:checked={o.is_selected} bind:checked={o.is_selected}
type="checkbox" type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/> />
</td> </td>
<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="text-sm font-medium text-gray-900">
<div class="text-sm font-medium text-gray-900"> {o.name}
{o.name} </div>
</div> </div>
</div> </td>
</div> <td class="px-6 py-4 whitespace-nowrap">
</td> <div class="flex items-center">
<td class="px-6 py-4 whitespace-nowrap"> <div class="text-sm font-medium text-gray-900">
<div class="flex items-center"> {#if o.address.address1 !== null}
<div class="ml-4"> {o.address.address1}<br />
<div class="text-sm font-medium text-gray-900"> <!-- {o.address.address2 || ''}<br /> -->
{#if o.address.address1 !== null} {o.address.postalcode}
{o.address.address1}<br /> {o.address.city}
<!-- {o.address.address2 || ''}<br /> --> {o.address.country}
{o.address.postalcode} {/if}
{o.address.city} </div>
{o.address.country} </div>
{/if} </td>
</div> <td class="px-6 py-4 whitespace-nowrap">
</div> <div class="flex items-center">
</div> <div class="text-sm font-medium text-gray-900">
</td> {#if o.contact}
<td class="px-6 py-4 whitespace-nowrap"> <a
<div class="flex items-center"> href="../contacts/{o.contact.id}"
<div class="ml-4"> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
<div class="text-sm font-medium text-gray-900"> >{o.contact.firstname}
{#if o.contact} {o.contact.middlename || ""}
<a {o.contact.lastname}</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" {:else}{$_("no-contact-specified")}{/if}
>{o.contact.firstname} </div>
{o.contact.middlename || ""} </div>
{o.contact.lastname}</a </td>
> {#if active_deletes[o.id] === true}
{:else}{$_("no-contact-specified")}{/if} <td
</div> class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
</div> >
</div> <button
</td> on:click={() => {
{#if active_deletes[o.id] === true} active_deletes[o.id] = false;
<td }}
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" tabindex="0"
> class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
<button >{$_("cancel-delete")}</button
on:click={() => { >
active_deletes[o.id] = false; <button
}} on:click={() => {
tabindex="0" toast.loading($_("deleting-organization"));
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer" RunnerOrganizationService.runnerOrganizationControllerRemove(
>{$_("cancel-delete")}</button o.id,
> false
<button )
on:click={() => { .then((resp) => {
toast.loading($_("deleting-organization")); current_organizations =
RunnerOrganizationService.runnerOrganizationControllerRemove( current_organizations.filter(
o.id, (obj) => obj.id !== o.id
false );
) toast($_("organization-deleted"));
.then((resp) => { })
current_organizations = .catch((err) => {
current_organizations.filter( modal_open = true;
(obj) => obj.id !== o.id delete_org = o;
); });
toast($_("organization-deleted")); }}
}) tabindex="0"
.catch((err) => { class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
modal_open = true; >{$_("confirm-delete")}</button
delete_org = o; >
}); </td>
}} {:else}
tabindex="0" <td
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>{$_("confirm-delete")}</button >
> <a
</td> href="./{o.id}"
{:else} class="text-indigo-600 hover:text-indigo-900"
<td >{$_("details")}</a
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" >
> {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:DELETE")}
<a <button
href="./{o.id}" on:click={() => {
class="text-indigo-600 hover:text-indigo-900" active_deletes[o.id] = true;
>{$_("details")}</a }}
> tabindex="0"
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:DELETE")} class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
<button >{$_("delete")}</button
on:click={() => { >
active_deletes[o.id] = true; {/if}
}} </td>
tabindex="0" {/if}
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" </tr>
>{$_("delete")}</button {/if}
> {/each}
{/if} </tbody>
</td> </table>
{/if} </div>
</tr> {/if}
{/if} {:catch error}
{/each} <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
</tbody> <span class="inline-block align-middle mr-8">
</table> <b class="capitalize">{$_("general_promise_error")}</b>
</div> {error}
{/if} </span>
{:catch error} </div>
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"> {/await}
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
{/if} {/if}

View File

@@ -10,31 +10,31 @@
</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">
{$_("organizations")} {$_("organizations")}
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION: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"
> 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 mt-1 sm:mt-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"
> 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 mt-1 sm:mt-0"
{$_("import-runners")} >
</button> {$_("import-runners")}
{/if} </button>
</span> {/if}
<OrgOverview bind:current_organizations /> <OrgOverview bind:current_organizations />
</section> </section>
@@ -48,7 +48,6 @@
passed_org={{}} passed_org={{}}
passed_orgs={current_organizations} passed_orgs={current_organizations}
opened_from="OrgOverview" opened_from="OrgOverview"
current_runners={[]}
bind:import_modal_open bind:import_modal_open
/> />
{/if} {/if}

View File

@@ -0,0 +1,150 @@
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: cards[i].id,
enabled: cards[i].enabled,
code: cards[i].code,
runner: {
id: cards[i]?.runner?.id,
first_name: cards[i]?.runner?.firstname,
middle_name: cards[i]?.runner?.middlename,
last_name: cards[i]?.runner?.lastname,
group: {
id: cards[i]?.runner?.group.id,
name: cards[i]?.runner?.group.name,
parent_group: {
id: 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: runners[i].id,
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
group: {
id: runners[i].group.id,
name: runners[i].group.name,
parent_group: {
id: 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: runners[i].id,
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
group: {
id: runners[i].group.id,
name: runners[i].group.name,
parent_group: {
id: runners[i]?.group?.parentGroup?.id,
name: runners[i]?.group?.parentGroup?.name,
},
},
distance: runners[i].distance,
distance_donations: runners[i].distanceDonations.map(
(distanceDonation: any) => {
return {
id: distanceDonation.id,
amount: distanceDonation.amount,
amount_per_distance: distanceDonation.amountPerDistance,
donor: {
id: 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,364 +1,202 @@
<script> <script>
import { getLocaleFromNavigator, _ } from "svelte-i18n"; import { _ } 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 DocumentServer from "./DocumentServer.ts";
import { init } from "@paralleldrive/cuid2"; import { init } from "@paralleldrive/cuid2";
const createId = init({ length: 10, fingerprint: "lfk-frontend" }); const createId = init({ length: 10, fingerprint: "lfk-frontend" });
const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
export let cards_show = false; export let cards_show = false;
export let generate_cards = []; export let generate_cards = [];
export let generate_runners = []; export let generate_runners = [];
export let generate_orgs = []; export let generate_orgs = [];
export let generate_teams = []; export let generate_teams = [];
$: cards_dropdown_open = false;
document.addEventListener("click", function (e) {
if (
e.target.parentNode?.parentNode?.id != "cards:dropdown" &&
e.target.parentNode?.parentNode?.id != "cards:dropdown:menu"
) {
cards_dropdown_open = false;
}
});
function generateRunnerCards(locale) { function download(blob, fileName) {
cards_dropdown_open = false; const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
}
if (generate_orgs.length > 0) { function generateRunnerCards(locale) {
generateOrgCards(locale); if (generate_orgs.length > 0) {
} else if (generate_teams.length > 0) { generateOrgCards(locale);
generateTeamCards(locale); } else if (generate_teams.length > 0) {
} else if (generate_runners.length > 0) { generateTeamCards(locale);
generateRunnersCards(locale); } else if (generate_runners.length > 0) {
} else { generateRunnersCards(locale);
generateCards(locale); } else {
} generateCards(locale);
} }
}
function generateCards(locale) { function generateCards(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdf"));
fetch( documentServer
`${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`, .generateCards(generate_cards, locale)
{ .then((blob) => {
method: "POST", download(blob, `${$_("runnercards")}-${locale}-${createId()}.pdf`);
headers: { })
"Content-Type": "application/json", .catch((err) => {
}, console.error(err);
body: JSON.stringify(generate_cards), });
} }
)
.then((response) => {
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
})
.catch((err) => {
console.error(err);
});
}
async function generateRunnersCards(locale) { async function generateRunnersCards(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdf"));
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); const current_cards = await RunnerCardService.runnerCardControllerGetAll();
let cards = []; let cards = [];
for (let runner of generate_runners) { for (let runner of generate_runners) {
let card = current_cards.find((c) => c.runner?.id == runner.id); let card = current_cards.find((c) => c.runner?.id == runner.id);
if (!card) { if (!card) {
card = await RunnerCardService.runnerCardControllerPost({ card = await RunnerCardService.runnerCardControllerPost({
runner: runner.id, runner: runner.id,
}); });
} }
cards.push(card); cards.push(card);
} }
fetch( documentServer
`${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`, .generateCards(cards, locale)
{ .then((blob) => {
method: "POST", let fileName = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
headers: { if (generate_runners.length == 1) {
"Content-Type": "application/json", fileName = `${$_("runnercards")}_${generate_runners[0].firstname}_${
}, generate_runners[0].lastname
body: JSON.stringify(cards), }-${locale}-${createId()}.pdf`;
} }
) download(blob, fileName);
.then((response) => { })
if (response.status != "200") { .catch((err) => {});
toast.dismiss(); }
toast.error($_("pdf-generation-failed"));
} 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 = `${$_("runnercards")}_${generate_runners[0].firstname}_${
generate_runners[0].lastname
}-${locale}-${createId()}.pdf`;
} else {
a.download = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
}
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
})
.catch((err) => {});
}
async function generateTeamCards(locale) { async function generateTeamCards(locale) {
toast.loading($_("generating-pdfs")); toast.loading($_("generating-pdfs"));
let count = 0; let count = 0;
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); const current_cards = await RunnerCardService.runnerCardControllerGetAll();
for (const t of generate_teams) { for (const t of generate_teams) {
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id t.id
); );
let cards = []; let cards = [];
for (let runner of runners) { for (let runner of runners) {
let card = current_cards.find((c) => c.runner?.id == runner.id); let card = current_cards.find((c) => c.runner?.id == runner.id);
if (!card) { if (!card) {
card = await RunnerCardService.runnerCardControllerPost({ card = await RunnerCardService.runnerCardControllerPost({
runner: runner.id, runner: runner.id,
}); });
} }
cards.push(card); cards.push(card);
} }
fetch( documentServer
`${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`, .generateCards(cards, locale)
{ .then((blob) => {
method: "POST", download(
headers: { blob,
"Content-Type": "application/json", `${$_("runnercards")}_${t.name}-${locale}-${createId()}.pdf`
}, );
body: JSON.stringify(cards), })
} .catch((err) => {});
) }
.then((response) => { }
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} else {
return response.blob();
}
})
.then((blob) => {
count++;
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("runnercards")}_${
t.name
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (count === generate_teams.length) {
toast.dismiss();
toast.success($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
async function generateOrgCards(locale) { async function generateOrgCards(locale) {
toast.loading($_("generating-pdfs")); toast.loading($_("generating-pdfs"));
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); const current_cards = await RunnerCardService.runnerCardControllerGetAll();
let count = 0; let count = 0;
let count_orgs = 0; let count_orgs = 0;
for (const o of generate_orgs) { for (const o of generate_orgs) {
count_orgs++; count_orgs++;
let count = 0; let count = 0;
let runners = let runners =
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id, o.id,
true true
); );
let cards = []; let cards = [];
for (let runner of runners) { for (let runner of runners) {
let card = current_cards.find((c) => c.runner?.id == runner.id); let card = current_cards.find((c) => c.runner?.id == runner.id);
if (!card) { if (!card) {
card = await RunnerCardService.runnerCardControllerPost({ card = await RunnerCardService.runnerCardControllerPost({
runner: runner.id, runner: runner.id,
}); });
} }
cards.push(card); cards.push(card);
} }
await fetch( await documentServer
`${config.baseurl_documentserver}/cards?locale=${locale}&download=true&key=${config.documentserver_key}`, .generateCards(cards, locale)
{ .then((blob) => {
method: "POST", download(
headers: { blob,
"Content-Type": "application/json", `${$_("runnercards")}_${o.name}_direct-${locale}-${createId()}.pdf`
}, );
body: JSON.stringify(cards), })
} .catch((err) => {});
) for (const t of o.teams) {
.then((response) => { count++;
if (response.status != "200") { let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
toast.dismiss(); t.id
toast.error($_("pdf-generation-failed")); );
} else { let cards = [];
return response.blob(); for (let runner of runners) {
} let card = current_cards.find((c) => c.runner?.id == runner.id);
}) if (!card) {
.then((blob) => { card = await RunnerCardService.runnerCardControllerPost({
const url = window.URL.createObjectURL(blob); runner: runner.id,
let a = document.createElement("a"); });
a.href = url; }
a.download = `${$_("runnercards")}_${ cards.push(card);
o.name }
}_direct-${locale}-${createId()}.pdf`; await documentServer
document.body.appendChild(a); .generateCards(cards, locale)
a.click(); .then((blob) => {
a.remove(); download(
if (count === o.teams.length && count_orgs === generate_orgs.length) { blob,
toast.dismiss(); `${$_("runnercards")}_${o.name}_${
toast.success($_("pdfs-successfully-generated")); t.name
} }-${locale}-${createId()}.pdf`
}) );
.catch((err) => {}); })
for (const t of o.teams) { .catch((err) => {});
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.dismiss();
toast.error($_("pdf-generation-failed"));
} 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.dismiss();
toast($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
}
</script> </script>
{#if cards_show} {#if cards_show}
<div id="cards:dropdown" class="relative inline-block"> <div>
<div> <p class="text-base">{$_("generate-runnercards")}</p>
<button <div class="inline-flex rounded-lg shadow-2xs">
on:click={() => { <button
cards_dropdown_open = !cards_dropdown_open; on:click={() => {
}} generateRunnerCards("de");
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" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
id="options-menu" >
aria-haspopup="true" DE
aria-expanded="true" </button>
> <button
{$_("generate-runnercards")} on:click={() => {
<svg generateRunnerCards("en");
xmlns="http://www.w3.org/2000/svg" }}
width="24" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
height="24" >
viewBox="0 0 24 24" EN
class="-mr-1 ml-2 h-5 w-5" </button>
><path fill="none" d="M0 0h24v24H0z" /> </div>
<path </div>
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} {/if}

View File

@@ -1,312 +1,180 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { import {
DonationService, DonationService,
RunnerTeamService, RunnerTeamService,
RunnerOrganizationService, RunnerOrganizationService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import { init } from "@paralleldrive/cuid2"; import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast"; 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" });
const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
export let certificates_show = false; export let certificates_show = false;
export let generate_runners = []; export let generate_runners = [];
export let generate_orgs = []; export let generate_orgs = [];
export let generate_teams = []; export let generate_teams = [];
$: certificates_dropdown_open = false;
document.addEventListener("click", function (e) {
if (
e.target.parentNode?.parentNode?.id != "certificates:dropdown" &&
e.target.parentNode?.parentNode?.id != "certificates:dropdown:menu"
) {
certificates_dropdown_open = false;
}
});
function generateCertificates(locale) { function generateCertificates(locale) {
certificates_dropdown_open = false; if (generate_orgs.length > 0) {
generateOrgCertificates(locale);
} else if (generate_teams.length > 0) {
generateTeamCertificates(locale);
} else {
generateRunnerCertificates(locale);
}
}
function download(blob, fileName) {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
}
if (generate_orgs.length > 0) { async function generateRunnerCertificates(locale) {
generateOrgCertificates(locale); toast.loading($_("generating-pdf"));
} else if (generate_teams.length > 0) { const current_donations =
generateTeamCertificates(locale); (await DonationService.donationControllerGetAll()) || [];
} else { let certificateRunners = [];
generateRunnerCertificates(locale); for (let runner of generate_runners) {
} runner.distanceDonations =
} current_donations.filter((d) => d.runner?.id == runner.id) || [];
certificateRunners.push(runner);
}
documentServer
.generateCertificates(certificateRunners, locale)
.then((blob) => {
let fileName = `${$_("certificates")}-${locale}.pdf`;
if (generate_runners.length == 1) {
fileName = `${$_("certificates")}_${
generate_runners[0].firstname
}_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`;
}
download(blob, fileName);
})
.catch((err) => {});
}
async function generateRunnerCertificates(locale) { async function generateTeamCertificates(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdfs"));
const current_donations = let count = 0;
(await DonationService.donationControllerGetAll()) || []; const current_donations =
let certificateRunners = []; (await DonationService.donationControllerGetAll()) || [];
for (let runner of generate_runners) { for (const t of generate_teams) {
runner.distanceDonations = const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
current_donations.filter((d) => d.runner?.id == runner.id) || []; t.id
certificateRunners.push(runner); );
} let certificateRunners = [];
fetch( for (let runner of runners) {
`${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`, runner.distanceDonations =
{ current_donations.filter((d) => d.runner?.id == runner.id) || [];
method: "POST", certificateRunners.push(runner);
headers: { }
"Content-Type": "application/json", documentServer
}, .generateCertificates(certificateRunners, locale)
body: JSON.stringify(certificateRunners), .then((blob) => {
} count++;
) download(
.then((response) => { blob,
if (response.status != "200") { `${$_("certificates")}_${t.name}-${locale}-${createId()}.pdf`
toast.dismiss(); );
toast.error($_("pdf-generation-failed")); })
} else { .catch((err) => {});
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 = `${$_("certificates")}_${
generate_runners[0].firstname
}_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`;
} else {
a.download = `${$_("certificates")}-${locale}.pdf`;
}
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
})
.catch((err) => {});
}
async function generateTeamCertificates(locale) { async function generateOrgCertificates(locale) {
toast.loading($_("generating-pdfs")); toast.loading($_("generating-pdfs"));
let count = 0; const current_donations =
const current_donations = (await DonationService.donationControllerGetAll()) || [];
(await DonationService.donationControllerGetAll()) || []; let count = 0;
for (const t of generate_teams) { let count_orgs = 0;
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( for (const o of generate_orgs) {
t.id count_orgs++;
); let count = 0;
let certificateRunners = []; let runners =
for (let runner of runners) { await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
runner.distanceDonations = o.id,
current_donations.filter((d) => d.runner?.id == runner.id) || []; true
certificateRunners.push(runner); );
} let certificateRunners = [];
fetch( for (let runner of runners) {
`${config.baseurl_documentserver}/certificates?locale=${locale}&download=true&key=${config.documentserver_key}`, runner.distanceDonations =
{ current_donations.filter((d) => d.runner?.id == runner.id) || [];
method: "POST", certificateRunners.push(runner);
headers: { }
"Content-Type": "application/json", await documentServer
}, .generateCertificates(certificateRunners, locale)
body: JSON.stringify(certificateRunners), .then((blob) => {
} download(
) blob,
.then((response) => { `${$_("certificates")}_${o.name}-${locale}-${createId()}.pdf`
if (response.status != "200") { );
toast.dismiss(); })
toast.error($_("pdf-generation-failed")); .catch((err) => {});
} else { for (const t of o.teams) {
return response.blob(); count++;
} let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
}) t.id
.then((blob) => { );
count++; let certificateRunners = [];
const url = window.URL.createObjectURL(blob); for (let runner of runners) {
let a = document.createElement("a"); runner.distanceDonations =
a.href = url; current_donations.filter((d) => d.runner?.id == runner.id) || [];
a.download = `${$_("certificates")}_${ certificateRunners.push(runner);
t.name }
}-${locale}-${createId()}.pdf`; await documentServer
document.body.appendChild(a); .generateCertificates(certificateRunners, locale)
a.click(); .then((blob) => {
a.remove(); download(
if (count === generate_teams.length) { blob,
toast.dismiss(); `${$_("certificates")}_${o.name}_${
toast.success($_("pdfs-successfully-generated")); t.name
} }-${locale}-${createId()}.pdf`
}) );
.catch((err) => {}); if (
} count === o.teams.length &&
} count_orgs === generate_orgs.length
) {
async function generateOrgCertificates(locale) { toast.dismiss();
toast.loading($_("generating-pdfs")); toast($_("pdfs-successfully-generated"));
const current_donations = }
(await DonationService.donationControllerGetAll()) || []; })
let count = 0; .catch((err) => {});
let count_orgs = 0; }
for (const o of generate_orgs) { }
count_orgs++; }
let count = 0;
let runners =
await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id,
true
);
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.dismiss();
toast.error($_("pdf-generation-failed"));
} 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
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (count === o.teams.length && count_orgs === generate_orgs.length) {
toast.dismiss();
toast.success($_("pdfs-successfully-generated"));
}
})
.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.dismiss();
toast.error($_("pdf-generation-failed"));
} 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.dismiss();
toast($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
}
</script> </script>
{#if certificates_show} {#if certificates_show}
<div id="certificates:dropdown" class="relative inline-block"> <div>
<div> <p class="text-base">{$_("generate-runner-certificates")}</p>
<button <div class="inline-flex rounded-lg shadow-2xs">
on:click={() => { <button
certificates_dropdown_open = !certificates_dropdown_open; on:click={() => {
}} generateCertificates("de");
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" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
id="options-menu" >
aria-haspopup="true" DE
aria-expanded="true" </button>
> <button
{$_("generate-runner-certificates")} on:click={() => {
<svg generateCertificates("en");
xmlns="http://www.w3.org/2000/svg" }}
width="24" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
height="24" >
viewBox="0 0 24 24" EN
class="-mr-1 ml-2 h-5 w-5" </button>
><path fill="none" d="M0 0h24v24H0z" /> </div>
<path </div>
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} {/if}

View File

@@ -1,283 +1,143 @@
<script> <script>
import { getLocaleFromNavigator, _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { import {
RunnerOrganizationService, RunnerOrganizationService,
RunnerTeamService, RunnerTeamService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import DocumentServer from "./DocumentServer";
import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast";
const createId = init({ length: 10, fingerprint: "lfk-frontend" });
const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
import { init } from "@paralleldrive/cuid2"; export let sponsoring_contracts_show = false;
import toast from "svelte-french-toast"; export let generate_runners = [];
const createId = init({ length: 10, fingerprint: "lfk-frontend" }); export let generate_orgs = [];
export let generate_teams = [];
export let sponsoring_contracts_show = false; function generateSponsoringContract(locale) {
export let generate_runners = []; if (generate_orgs.length > 0) {
export let generate_orgs = []; generateOrgContracts(locale);
export let generate_teams = []; } else if (generate_teams.length > 0) {
$: sponsoring_contracts_download_open = false; generateTeamContracts(locale);
document.addEventListener("click", function (e) { } else {
if ( generateRunnerContracts(locale);
e.target.parentNode?.parentNode?.id != "sponsoring:dropdown" && }
e.target.parentNode?.parentNode?.id != "sponsoring:dropdown:menu" }
) { function download(blob, fileName) {
sponsoring_contracts_download_open = false; const url = window.URL.createObjectURL(blob);
} let a = document.createElement("a");
}); a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast($_("pdf-successfully-generated"));
}
function generateSponsoringContract(locale) { async function generateTeamContracts(locale) {
sponsoring_contracts_download_open = false; toast.loading($_("generating-pdfs"));
let count = 0;
for (const t of generate_teams) {
count++;
const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
documentServer
.generateContracts(runners, locale)
.then((blob) => {
download(
blob,
`${$_("sponsorings")}_${t.name}-${locale}-${createId()}.pdf`
);
})
.catch((err) => {});
}
}
if (generate_orgs.length > 0) { async function generateOrgContracts(locale) {
generateOrgContracts(locale); toast.loading($_("generating-pdf"));
} else if (generate_teams.length > 0) { let count_orgs = 0;
generateTeamContracts(locale); for (const o of generate_orgs) {
} else { count_orgs++;
generateRunnerContracts(locale); let count = 0;
} let runners =
} await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id,
true
);
await documentServer
.generateContracts(runners, locale)
.then((blob) => {
download(
blob,
`${$_("sponsorings")}_${o.name}_direct-${locale}-${createId()}.pdf`
);
})
.catch((err) => {});
for (const t of o.teams) {
count++;
let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
await documentServer
.generateContracts(runners, locale)
.then((blob) => {
download(
blob,
`${$_("sponsorings")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`
);
})
.catch((err) => {});
}
}
}
async function generateTeamContracts(locale) { function generateRunnerContracts(locale) {
toast.loading($_("generating-pdfs")); toast.loading($_("generating-pdf"));
let count = 0; documentServer
for (const t of generate_teams) { .generateContracts(generate_runners, locale)
count++; .then((blob) => {
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( let fileName = `${$_("sponsorings")}-${locale}-${createId()}.pdf`;
t.id if (generate_runners.length == 1) {
); fileName = `${$_("sponsorings")}_${generate_runners[0].firstname}_${
fetch( generate_runners[0].lastname
`${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`, }-${locale}-${createId()}.pdf`;
{ }
method: "POST", download(blob, fileName);
headers: { })
"Content-Type": "application/json", .catch((err) => {
}, console.error(err);
body: JSON.stringify(runners), });
} }
)
.then((response) => {
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} else {
return response.blob();
}
})
.then((blob) => {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = `${$_("sponsorings")}_${
t.name
}-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (count === generate_teams.length) {
toast.dismiss();
toast.success($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
async function generateOrgContracts(locale) {
toast.loading($_("generating-pdf"));
let count_orgs = 0;
for (const o of generate_orgs) {
count_orgs++;
let count = 0;
let runners =
await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id,
true
);
await fetch(
`${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(runners),
}
)
.then((response) => {
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} 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
}_direct-${locale}-${createId()}.pdf`;
document.body.appendChild(a);
a.click();
a.remove();
if (count === o.teams.length && count_orgs === generate_orgs.length) {
toast.dismiss();
toast.success($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
for (const t of o.teams) {
count++;
let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id
);
await fetch(
`${config.baseurl_documentserver}/contracts?locale=${locale}&download=true&key=${config.documentserver_key}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(runners),
}
)
.then((response) => {
if (response.status != "200") {
toast.dismiss();
toast.error($_("pdf-generation-failed"));
} 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.dismiss();
toast($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
}
function generateRunnerContracts(locale) {
toast.loading($_("generating-pdf"));
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.dismiss();
toast.error($_("pdf-generation-failed"));
} 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.dismiss();
toast($_("pdf-successfully-generated"));
})
.catch((err) => {
console.error(err);
});
}
</script> </script>
{#if sponsoring_contracts_show} {#if sponsoring_contracts_show}
<div id="sponsoring:dropdown" class="relative inline-block"> <div>
<div> <p class="text-base">{$_("generate-sponsoring-contracts")}</p>
<button <div class="inline-flex rounded-lg shadow-2xs">
on:click={() => { <button
sponsoring_contracts_download_open = on:click={() => {
!sponsoring_contracts_download_open; generateSponsoringContract("de");
}} }}
type="button" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
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" DE
aria-haspopup="true" </button>
aria-expanded="true" <button
> on:click={() => {
{$_("generate-sponsoring-contracts")} generateSponsoringContract("en");
<svg }}
xmlns="http://www.w3.org/2000/svg" class="py-3 px-4 inline-flex items-center gap-x-2 -ms-px first:rounded-s-lg first:ms-0 last:rounded-e-lg text-sm font-medium focus:z-10 border border-gray-200 bg-blue-600 text-white shadow-2xs hover:bg-blue-800 focus:outline-hidden focus:bg-blue-800 disabled:opacity-50 disabled:pointer-events-none"
width="24" >
height="24" EN
viewBox="0 0 24 24" </button>
class="-mr-1 ml-2 h-5 w-5" </div>
><path fill="none" d="M0 0h24v24H0z" /> </div>
<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} {/if}

View File

@@ -66,7 +66,7 @@
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
> >
Active Active
</span> </span>

View File

@@ -12,6 +12,7 @@
import Select from "svelte-select"; import Select from "svelte-select";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import toast from 'svelte-french-toast'
export let modal_open; export let modal_open;
$: selected_team = undefined; $: selected_team = undefined;
@@ -188,7 +189,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
@@ -211,7 +212,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -230,7 +231,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
@@ -247,7 +248,7 @@
>{$_("team")}</label >{$_("team")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id option.value.id
@@ -280,7 +281,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isPhoneValidOrEmpty} {#if !isPhoneValidOrEmpty}
<span <span
@@ -308,7 +309,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isEmailValidOrEmpty} {#if !isEmailValidOrEmpty}
<span <span

View File

@@ -268,7 +268,7 @@
<select <select
name="team" name="team"
bind:value={selected_org} bind:value={selected_org}
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="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"
> >
{#each passed_orgs as o} {#each passed_orgs as o}
<option value={o.id}>{o.name}</option> <option value={o.id}>{o.name}</option>
@@ -279,7 +279,7 @@
{#if opened_from === "RunnerOverview"} {#if opened_from === "RunnerOverview"}
<p>{$_("group")}</p> <p>{$_("group")}</p>
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.id.value option.id.value

View File

@@ -1,315 +1,298 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import store from "../../store"; import store from "../../store";
import { import {
RunnerService, RunnerService,
RunnerTeamService, RunnerTeamService,
RunnerOrganizationService, RunnerOrganizationService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import Select from "svelte-select"; import Select from "svelte-select";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
let data_loaded = false; let data_loaded = false;
export let params; export let params;
const runner_promise = RunnerService.runnerControllerGetOne(params.runnerid); const runner_promise = RunnerService.runnerControllerGetOne(params.runnerid);
$: delete_triggered = false; $: delete_triggered = false;
$: original_data_pdf = {}; $: original_data_pdf = {};
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: group = {}; $: group = {};
$: 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 &&
editable.group != null; editable.group != null;
$: sponsoring_contracts_show = true; $: sponsoring_contracts_show = true;
$: cards_show = true; $: cards_show = true;
$: certificates_show = true; $: certificates_show = true;
$: generate_runners = [original_data_pdf]; $: generate_runners = [original_data_pdf];
runner_promise.then((data) => { runner_promise.then((data) => {
data_loaded = true; data_loaded = true;
original_data_pdf = Object.assign(original_data_pdf, data); original_data_pdf = Object.assign(original_data_pdf, data);
data.group = data.group.id; data.group = data.group.id;
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);
RunnerOrganizationService.runnerOrganizationControllerGetAll().then( RunnerOrganizationService.runnerOrganizationControllerGetAll().then(
(val) => { (val) => {
const orgs = val.map((r) => { const orgs = val.map((r) => {
return { label: r.name, value: r }; return { label: r.name, value: r };
}); });
groups = groups.concat(orgs); groups = groups.concat(orgs);
RunnerTeamService.runnerTeamControllerGetAll().then((val) => { RunnerTeamService.runnerTeamControllerGetAll().then((val) => {
const teams = val.map((r) => { const teams = val.map((r) => {
return { label: `${r.parentGroup.name} > ${r.name}`, value: r }; return { label: `${r.parentGroup.name} > ${r.name}`, value: r };
}); });
groups = groups.concat(teams); groups = groups.concat(teams);
group = groups.find((g) => g.value.id == editable.group); group = groups.find((g) => g.value.id == editable.group);
}); });
} }
); );
}); });
let groups = []; let groups = [];
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast.loading($_("updating-runner")); toast.loading($_("updating-runner"));
let postdata = {}; let postdata = {};
postdata = Object.assign(postdata, editable); postdata = Object.assign(postdata, editable);
if (postdata.phone === "") { if (postdata.phone === "") {
postdata.phone = null; postdata.phone = null;
} }
RunnerService.runnerControllerPut(original_data.id, postdata) RunnerService.runnerControllerPut(original_data.id, postdata)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.dismiss(); toast.dismiss();
toast.success($_("runner-updated")); toast.success($_("runner-updated"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
} }
} }
function deleteRunner() { function deleteRunner() {
RunnerService.runnerControllerRemove(original_data.id, true) RunnerService.runnerControllerRemove(original_data.id, true)
.then((resp) => { .then((resp) => {
location.replace("./"); location.replace("./");
}) })
.catch((err) => {}); .catch((err) => {});
} }
</script> </script>
{#await runner_promise} {#await runner_promise}
{$_("loading-runners")} {$_("loading-runners")}
{: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">
<svg <a class="mr-2" href="./"
xmlns="http://www.w3.org/2000/svg" ><svg
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"
class="flex-shrink-0 w-5 h-5 mr-2" width="24"
fill="currentColor" height="24"
width="24" viewBox="0 0 24 24"
height="24" fill="none"
><path fill="none" d="M0 0h24v24H0z" /> stroke="currentColor"
<path stroke-width="2"
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" stroke-linecap="round"
/></svg stroke-linejoin="round"
> class="inline-block"
</li> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<li class="flex items-center"> >
<a class="mr-2" href="./">{$_("runners")}</a><svg {$_("runners")}</a
stroke="currentColor" >
fill="none" </li>
stroke-width="2" </ol>
viewBox="0 0 24 24" </nav>
stroke-linecap="round" </div>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" <div class="mb-4 text-3xl font-extrabold leading-tight">
height="1em" {original_data.firstname}
width="1em" {original_data.middlename || ""}
xmlns="http://www.w3.org/2000/svg" {original_data.lastname} [#{params.runnerid}]
><line x1="5" y1="12" x2="19" y2="12" /> <span
<polyline points="12 5 19 12 12 19" /></svg class="grid md:grid-cols-3 gap-1 md:gap-2"
> data-id="runner_actions_${editable.id}"
</li> >
<li class="flex items-center"> {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:DELETE")}
<span class="mr-2" <GenerateSponsoringContracts
>{original_data.firstname} bind:sponsoring_contracts_show
{original_data.middlename || ""} bind:generate_runners
{original_data.lastname}</span />
> <GenerateRunnerCards bind:cards_show bind:generate_runners />
</li> <GenerateRunnerCertificates
</ol> bind:certificates_show
</nav> bind:generate_runners
</div> />
</div> <div>
<div class="mb-8 text-3xl font-extrabold leading-tight"> {#if delete_triggered}
{original_data.firstname} <button
{original_data.middlename || ""} on:click={deleteRunner}
{original_data.lastname} 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"
<span data-id="runner_actions_${editable.id}"> >{$_("confirm-deletion")}</button
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:DELETE")} >
{#if delete_triggered} <button
<button on:click={() => {
on:click={deleteRunner} delete_triggered = !delete_triggered;
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 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
<button >
on:click={() => { {:else}
delete_triggered = !delete_triggered; <button
}} on:click={() => {
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" delete_triggered = true;
>{$_("cancel")}</button }}
> type="button"
{/if} 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"
<GenerateSponsoringContracts >{$_("delete-runner")}</button
bind:sponsoring_contracts_show >
bind:generate_runners <button
/> disabled={!save_enabled}
<GenerateRunnerCards bind:cards_show bind:generate_runners /> class:opacity-50={!save_enabled}
<GenerateRunnerCertificates type="button"
bind:certificates_show on:click={submit}
bind:generate_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"
/> >{$_("save-changes")}</button
{#if !delete_triggered} >
<button {/if}
on:click={() => { </div>
delete_triggered = true; {/if}
}} </span>
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-runner")}</button <div class="text-sm w-full mt-2">
> <label for="firstname" class="font-semibold text-gray-700"
{/if} >{$_("first-name")}</label
{/if} >
{#if !delete_triggered} <input
<button autocomplete="off"
disabled={!save_enabled} placeholder={$_("first-name")}
class:opacity-50={!save_enabled} type="text"
type="button" class:border-red-500={!isFirstnameValid}
on:click={submit} class:focus:border-red-500={!isFirstnameValid}
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" class:focus:ring-red-500={!isFirstnameValid}
>{$_("save-changes")}</button bind:value={editable.firstname}
> name="firstname"
{/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"
</span> />
</div> {#if !isFirstnameValid}
<!-- --> <span
<div class="text-sm w-full"> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<label for="firstname" class="font-medium text-gray-700" >
>{$_("first-name")}</label {$_("first-name-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("first-name")} <div class="text-sm w-full mt-2">
type="text" <label for="middlename" class="font-semibold text-gray-700"
class:border-red-500={!isFirstnameValid} >{$_("middle-name")}</label
class:focus:border-red-500={!isFirstnameValid} >
class:focus:ring-red-500={!isFirstnameValid} <input
bind:value={editable.firstname} autocomplete="off"
name="firstname" placeholder={$_("middle-name")}
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" type="text"
/> bind:value={editable.middlename}
{#if !isFirstnameValid} name="middlename"
<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-neutral-800 rounded-md p-2"
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" />
> </div>
{$_("first-name-is-required")} <div class="text-sm w-full mt-2">
</span> <label for="lastname" class="font-semibold text-gray-700"
{/if} >{$_("last-name")}</label
</div> >
<div class="text-sm w-full"> <input
<label for="middlename" class="font-medium text-gray-700" autocomplete="off"
>{$_("middle-name")}</label placeholder={$_("last-name")}
> type="text"
<input bind:value={editable.lastname}
autocomplete="off" class:border-red-500={!isLastnameValid}
placeholder={$_("middle-name")} class:focus:border-red-500={!isLastnameValid}
type="text" class:focus:ring-red-500={!isLastnameValid}
bind:value={editable.middlename} name="lastname"
name="middlename" 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}
</div> <span
<div class="text-sm w-full"> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<label for="lastname" class="font-medium text-gray-700" >
>{$_("last-name")}</label {$_("last-name-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("last-name")} <div class="text-sm w-full mt-2">
type="text" <label for="email" class="font-semibold text-gray-700"
bind:value={editable.lastname} >{$_("e-mail-adress")}</label
class:border-red-500={!isLastnameValid} >
class:focus:border-red-500={!isLastnameValid} <input
class:focus:ring-red-500={!isLastnameValid} autocomplete="off"
name="lastname" placeholder={$_("e-mail-adress")}
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" type="email"
/> bind:value={editable.email}
{#if !isLastnameValid} class:border-red-500={!isEmailValid}
<span class:focus:border-red-500={!isEmailValid}
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" class:focus:ring-red-500={!isEmailValid}
> name="email"
{$_("last-name-is-required")} 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> />
{/if} {#if !isEmailValid}
</div> <span
<div class="text-sm w-full"> class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
<label for="email" class="font-medium text-gray-700" >
>{$_("e-mail-adress")}</label {$_("valid-email-is-required")}
> </span>
<input {/if}
autocomplete="off" </div>
placeholder={$_("e-mail-adress")} <div class="text-sm w-full mt-2">
type="email" <label for="phone" class="font-semibold text-gray-700">{$_("phone")}</label>
bind:value={editable.email} <input
class:border-red-500={!isEmailValid} autocomplete="off"
class:focus:border-red-500={!isEmailValid} placeholder={$_("phone")}
class:focus:ring-red-500={!isEmailValid} type="tel"
name="email" bind:value={editable.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-gray-500 rounded-md p-2" 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"
{#if !isEmailValid} />
<span </div>
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1" <div class="text-sm w-full mt-2">
> <span class="font-semibold text-gray-700">{$_("group")}</span>
{$_("valid-email-is-required")} <Select
</span> 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-neutral-800 rounded-md p-2"
{/if} itemFilter={(label, filterText, option) =>
</div> label.toLowerCase().includes(filterText.toLowerCase()) ||
<div class="text-sm w-full"> option.id.value.toString().startsWith(filterText.toLowerCase())}
<label for="phone" class="font-medium text-gray-700">{$_("phone")}</label> items={groups}
<input showChevron={true}
autocomplete="off" placeholder={$_("search-for-an-organization-or-team-by-name-or-id")}
placeholder={$_("phone")} noOptionsMessage={$_("no-organization-or-team-found")}
type="tel" bind:selectedValue={group}
bind:value={editable.phone} on:select={(selectedValue) => {
name="phone" editable.group = selectedValue.detail.value.id;
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" }}
/> on:clear={() => (editable.group = null)}
</div> />
<div class="text-sm w-full"> </div>
<span class="font-medium text-gray-700">{$_("group")}</span> <div class="text-sm w-full mt-2">
<Select <span class="font-semibold text-gray-700">{$_("distance")}</span>
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" <br />
itemFilter={(label, filterText, option) => <span class="text-gray-700">{original_data.distance / 1000} km</span>
label.toLowerCase().includes(filterText.toLowerCase()) || </div>
option.id.value.toString().startsWith(filterText.toLowerCase())} </section>
items={groups}
showChevron={true}
placeholder={$_("search-for-an-organization-or-team-by-name-or-id")}
noOptionsMessage={$_("no-organization-or-team-found")}
bind:selectedValue={group}
on:select={(selectedValue) => {
editable.group = selectedValue.detail.value.id;
}}
on:clear={() => (editable.group = null)}
/>
</div>
<div class="text-sm w-full">
<span class="font-medium text-gray-700">{$_("distance")}</span>
<br />
<span class="text-gray-700">{original_data.distance / 1000} km</span>
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -11,29 +11,29 @@
</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">
{$_("runners")} {$_("runners")}
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER: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"
> 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 mt-1 sm:mt-0"
{$_("laeufer-hinzufuegen")} >
</button> {$_("laeufer-hinzufuegen")}
<button </button>
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"
> 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 mt-1 sm:mt-0"
{$_("import-runners")} >
</button> {$_("import-runners")}
{/if} </button>
</span> {/if}
<RunnersOverview bind:current_runners bind:addRunners /> <RunnersOverview bind:current_runners bind:addRunners />
</section> </section>

View File

@@ -131,7 +131,7 @@
>{$_("runner")}</label >{$_("runner")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
filterRunners(label, filterText, option)} filterRunners(label, filterText, option)}
items={runners} items={runners}
@@ -160,7 +160,7 @@
type="number" type="number"
step="1" step="1"
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 border-gray-300 border bg-gray-50 text-gray-500 p-2" 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"
placeholder="400" placeholder="400"
/> />
<span <span

View File

@@ -1,288 +1,273 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import { RunnerService, ScanService } from "@odit/lfk-client-js"; import { RunnerService, ScanService } from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import Select from "svelte-select"; import Select from "svelte-select";
let data_loaded = false; let data_loaded = false;
export let params; export let params;
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: current_runners = []; $: current_runners = [];
$: is_distance_valid = editable.distance > 0; $: is_distance_valid = editable.distance > 0;
$: is_everything_set = $: is_everything_set =
editable.runner != null && editable.runner != null &&
((original_data.responseType === "TRACKSCAN" && editable.track != null) || ((original_data.responseType === "TRACKSCAN" && editable.track != null) ||
original_data.responseType !== "TRACKSCAN"); original_data.responseType !== "TRACKSCAN");
$: runner = {}; $: runner = {};
$: changes_performed = !( $: changes_performed = !(
JSON.stringify(original_data) === JSON.stringify(editable) JSON.stringify(original_data) === JSON.stringify(editable)
); );
$: save_enabled = changes_performed && is_everything_set && is_distance_valid; $: save_enabled = changes_performed && is_everything_set && is_distance_valid;
const promise = ScanService.scanControllerGetOne(params.scanid).then( const promise = ScanService.scanControllerGetOne(params.scanid).then(
(data) => { (data) => {
data_loaded = true; data_loaded = true;
original_data = Object.assign(original_data, data); original_data = Object.assign(original_data, data);
original_data.runner = original_data.runner.id; original_data.runner = original_data.runner.id;
editable = Object.assign(editable, original_data); editable = Object.assign(editable, original_data);
RunnerService.runnerControllerGetAll().then((val) => { RunnerService.runnerControllerGetAll().then((val) => {
current_runners = val.map((r) => { current_runners = val.map((r) => {
return { label: getRunnerLabel(r), value: r }; return { label: getRunnerLabel(r), value: r };
}); });
runner = current_runners.find((r) => r.value.id == editable.runner); runner = current_runners.find((r) => r.value.id == editable.runner);
}); });
} }
); );
const getRunnerLabel = (option) => const getRunnerLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname; option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const filterRunners = (label, filterText, option) => const filterRunners = (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());
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast($_("scan-is-being-updated")); toast($_("scan-is-being-updated"));
let postdata = {}; let postdata = {};
if (original_data.responseType === "TRACKSCAN") { if (original_data.responseType === "TRACKSCAN") {
postdata = Object.assign(postdata, editable); postdata = Object.assign(postdata, editable);
postdata.track = postdata.track.id; postdata.track = postdata.track.id;
ScanService.scanControllerPutTrackScan(original_data.id, postdata) ScanService.scanControllerPutTrackScan(original_data.id, postdata)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.success($_("updated-scan")); toast.success($_("updated-scan"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
postdata = Object.assign(postdata, editable); postdata = Object.assign(postdata, editable);
ScanService.scanControllerPut(original_data.id, postdata) ScanService.scanControllerPut(original_data.id, postdata)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.success($_("updated-scan")); toast.success($_("updated-scan"));
}) })
.catch((err) => {}); .catch((err) => {});
} }
} else { } else {
} }
} }
function deleteScan() { function deleteScan() {
ScanService.scanControllerRemove(original_data.id, false) ScanService.scanControllerRemove(original_data.id, false)
.then((resp) => { .then((resp) => {
toast($_("deleted-scan")); toast($_("deleted-scan"));
location.replace("./"); location.replace("./");
}) })
.catch((err) => { .catch((err) => {
modal_open = true; modal_open = true;
delete_scan = original_data; delete_scan = original_data;
}); });
} }
function format_laptime(laptime) { function format_laptime(laptime) {
if (laptime == 0 || laptime == null) { if (laptime == 0 || laptime == null) {
return $_("first-scan-of-the-day"); return $_("first-scan-of-the-day");
} }
if (laptime < 60) { if (laptime < 60) {
return `${laptime}s`; return `${laptime}s`;
} }
if (laptime < 3600) { if (laptime < 3600) {
return `${Math.floor(laptime / 60)}min ${ return `${Math.floor(laptime / 60)}min ${
laptime - Math.floor(laptime / 60) * 60 laptime - Math.floor(laptime / 60) * 60
}s`; }s`;
} }
return `${Math.floor(laptime / 3600)}h ${ return `${Math.floor(laptime / 3600)}h ${
laptime - Math.floor(laptime / 3600) * 3600 laptime - Math.floor(laptime / 3600) * 3600
}min ${ }min ${
laptime - laptime -
Math.floor(laptime / 3600) * 3600 - Math.floor(laptime / 3600) * 3600 -
Math.floor(laptime / 60) * 60 Math.floor(laptime / 60) * 60
}`; }`;
} }
</script> </script>
{#await promise} {#await promise}
Loading scan details Loading scan 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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 24"
><path fill="none"
fill="currentColor" stroke="currentColor"
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">Scans</a><svg > Scans</a
stroke="currentColor" >
fill="none" </li>
stroke-width="2" </ol>
viewBox="0 0 24 24" </nav>
stroke-linecap="round" </div>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" <div class="mb-4 text-3xl font-extrabold leading-tight">
height="1em" {runner.value?.firstname}
width="1em" {runner.value?.middlename || ""}
xmlns="http://www.w3.org/2000/svg" {runner.value?.lastname}
><line x1="5" y1="12" x2="19" y2="12" /> - Scan #{original_data.id}
<polyline points="12 5 19 12 12 19" /></svg <div data-id="donation_actions_${original_data.id}">
> {#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:DELETE")}
</li> {#if delete_triggered}
<li class="flex items-center"> <button
<span class="mr-2">{original_data.id}</span> on:click={deleteScan}
</li> 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"
</ol> >{$_("confirm-deletion")}</button
</nav> >
</div> <button
</div> on:click={() => {
<div class="mb-8 text-3xl font-extrabold leading-tight"> delete_triggered = !delete_triggered;
{runner.value?.firstname} }}
{runner.value?.middlename || ""} 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"
{runner.value?.lastname} >{$_("cancel")}</button
#{original_data.id} >
<span data-id="donation_actions_${original_data.id}"> {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:DELETE")} {#if !delete_triggered}
{#if delete_triggered} <button
<button on:click={() => {
on:click={deleteScan} delete_triggered = true;
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"
> 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-scan")}</button
on:click={() => { >
delete_triggered = !delete_triggered; {/if}
}} {/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:" {#if !delete_triggered}
>{$_("cancel")}</button <button
> disabled={!save_enabled}
{/if} class:opacity-50={!save_enabled}
{#if !delete_triggered} type="button"
<button on:click={submit}
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"
delete_triggered = true; >{$_("save-changes")}</button
}} >
type="button" {/if}
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:" </div>
>{$_("delete-scan")}</button </div>
> <!-- -->
{/if} <div class="w-full inline-flex">
{/if} <label for="valid" class="block font-medium text-gray-700"
{#if !delete_triggered} >{$_("status")}:
<button </label>
disabled={!save_enabled} &nbsp;
class:opacity-50={!save_enabled} <input
type="button" id="valid"
on:click={submit} on:change={() => {
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:" editable.valid = !editable.valid;
>{$_("save-changes")}</button }}
> name="valid"
{/if} type="checkbox"
</span> checked={editable.valid}
</div> class="focus:ring-indigo-500 align-bottom h-7 w-5font-medium text-indigo-600 border-gray-300 rounded"
<!-- --> />
<div class="w-full inline-flex"> &nbsp;
<label for="valid" class="block font-medium text-gray-700" <p class="font-medium">
>{$_("status")}: {#if editable.valid}{$_("valid")}{:else}{$_("invalid")}{/if}
</label> </p>
&nbsp; </div>
<input {#if editable.responseType === "TRACKSCAN"}
id="valid" <div class="w-full inline-flex">
on:change={() => { <label for="valid" class="block font-semibold text-gray-700"
editable.valid = !editable.valid; >{$_("track")}:
}} </label>
name="valid" <a
type="checkbox" href="../tracks"
checked={editable.valid} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
class="focus:ring-indigo-500 align-bottom h-7 w-5font-medium text-indigo-600 border-gray-300 rounded" >{editable.track.name}
/> </a>
&nbsp; </div>
<p class="font-medium"> <div class="w-full inline-flex pb-3">
{#if editable.valid}{$_("valid")}{:else}{$_("invalid")}{/if} <label for="valid" class="block font-semibold text-gray-700"
</p> >{$_("laptime")}: {format_laptime(editable.laptime)}
</div> </label>
{#if editable.responseType === "TRACKSCAN"} </div>
<div class="w-full inline-flex"> {/if}
<label for="valid" class="block font-semibold text-gray-700" <div class=" w-full">
>{$_("track")}: <label for="runner" class="block font-medium text-gray-700"
</label> >{$_("runner")}</label
<a >
href="../tracks" <Select
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" 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-neutral-800 rounded-md p-2"
>{editable.track.name} itemFilter={(label, filterText, option) =>
</a> filterRunners(label, filterText, option)}
</div> items={current_runners}
<div class="w-full inline-flex pb-3"> showChevron={true}
<label for="valid" class="block font-semibold text-gray-700" isDisabled={editable.responseType === "TRACKSCAN"}
>{$_("laptime")}: {format_laptime(editable.laptime)} placeholder={$_("search-for-runner-by-name-or-id")}
</label> noOptionsMessage={$_("no-runners-found")}
</div> bind:selectedValue={runner}
{/if} on:select={(selectedValue) => {
<div class=" w-full"> editable.runner = selectedValue.detail.value.id;
<label for="runner" class="block font-medium text-gray-700" }}
>{$_("runner")}</label 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 rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2" <div class=" w-full">
itemFilter={(label, filterText, option) => <label
filterRunners(label, filterText, option)} for="scan_distance"
items={current_runners} class="block text-sm font-medium text-gray-700"
showChevron={true} >
isDisabled={editable.responseType === "TRACKSCAN"} {$_("distance")}</label
placeholder={$_("search-for-runner-by-name-or-id")} >
noOptionsMessage={$_("no-runners-found")} <div class="mt-1 flex rounded-md shadow-sm">
bind:selectedValue={runner} <input
on:select={(selectedValue) => { autocomplete="off"
editable.runner = selectedValue.detail.value.id; class:border-red-500={!is_distance_valid}
}} class:focus:border-red-500={!is_distance_valid}
on:clear={() => (editable.runner = null)} class:focus:ring-red-500={!is_distance_valid}
/> bind:value={editable.distance}
</div> disabled={editable.responseType === "TRACKSCAN"}
<div class=" w-full"> type="number"
<label step="1"
for="scan_distance" name="scan_distance"
class="block text-sm font-medium text-gray-700" 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"
> placeholder="400"
{$_("distance")}</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 text-sm"
<input >m</span
autocomplete="off" >
class:border-red-500={!is_distance_valid} </div>
class:focus:border-red-500={!is_distance_valid} {#if !is_distance_valid}
class:focus:ring-red-500={!is_distance_valid} <span
bind:value={editable.distance} class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
disabled={editable.responseType === "TRACKSCAN"} >
type="number" {$_("the-scans-distance-must-be-greater-than-0m")}
step="1" </span>
name="scan_distance" {/if}
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" </div>
placeholder="400" </section>
/>
<span
class="inline-flex items-center px-3 rounded-r-md border border-gray-300 bg-gray-50 text-gray-500 text-sm"
>m</span
>
</div>
{#if !is_distance_valid}
<span
class="flex items-center font-medium tracking-wide text-red-500 text-xs mt-1 ml-1"
>
{$_("the-scans-distance-must-be-greater-than-0m")}
</span>
{/if}
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -5,12 +5,12 @@
{#if valid} {#if valid}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("valid")}</span >{$_("valid")}</span
> >
{:else} {:else}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-red-100 text-red-800"
>{$_("invalid")}</span >{$_("invalid")}</span
> >
{/if} {/if}

View File

@@ -9,20 +9,20 @@
</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">
{$_("scans")} {$_("scans")}
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("SCAN: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"
> 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-scan")} >
</button> {$_("add-scan")}
{/if} </button>
</span> {/if}
<ScansOverview bind:current_scans bind:addScans /> <ScansOverview bind:current_scans bind:addScans />
</section> </section>

View File

@@ -5,7 +5,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="m-auto" style="height:15rem" src={scans_empty} alt="" /> <img class="m-auto mt-2" style="height:15rem" src={scans_empty} alt="" />
<span class="font-bold">{$_("there-are-no-scans-yet")}</span><br /> <span class="font-bold">{$_("there-are-no-scans-yet")}</span><br />
<span>{$_("add-your-fist-scan")}</span> <span>{$_("add-your-fist-scan")}</span>
</p> </p>

View File

@@ -179,7 +179,7 @@
onMount(async () => { onMount(async () => {
let page = 0; let page = 0;
let pagesize = 100; let pagesize = 500;
while (page >= 0) { while (page >= 0) {
const scans = await ScanService.scanControllerGetAll(page, pagesize); const scans = await ScanService.scanControllerGetAll(page, pagesize);
if (scans.length == 0) { if (scans.length == 0) {
@@ -194,9 +194,6 @@
dataLoaded = true; dataLoaded = true;
page++; page++;
if (pagesize < 1000) {
pagesize += 100;
}
} }
}); });
</script> </script>
@@ -223,7 +220,7 @@
{#if selected.length > 0} {#if selected.length > 0}
<button <button
type="button" 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 inline-flex" 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"
id="options-menu" id="options-menu"
on:click={async () => { on:click={async () => {
const prom = []; const prom = [];

View File

@@ -133,7 +133,7 @@
class="block text-sm font-medium text-gray-700">Track</label class="block text-sm font-medium text-gray-700">Track</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id option.value.id
@@ -161,11 +161,11 @@
bind:value={description} bind:value={description}
type="text" type="text"
name="description" name="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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label for="enabled" class="font-medium text-gray-700" <label for="enabled" class="font-semibold text-gray-700"
>{$_("enabled_large")}</label >{$_("enabled_large")}</label
> >
<br /> <br />

View File

@@ -1,200 +1,199 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { tick } from "svelte";
import bwipjs from "bwip-js";
import toast from "svelte-french-toast";
import { tick, createEventDispatcher } from "svelte"; export let copy_modal_open;
import bwipjs from "bwip-js"; export let new_station;
let valueCopy = null;
let areaDom;
let copied = false;
$: is_qrcode = false;
$: barcode = textToBase64Barcode(new_station.key, is_qrcode);
export let copy_modal_open; function close() {
export let new_station; copy_modal_open = false;
const dispatch = createEventDispatcher(); }
let valueCopy = null; async function copy() {
let areaDom; valueCopy = new_station.key;
let copied = false; await tick();
$: is_qrcode = false; areaDom.focus();
$: barcode = textToBase64Barcode(new_station.key, is_qrcode); areaDom.select();
try {
const successful = document.execCommand("copy");
if (!successful) {
throw new Error();
}
toast($_("copied-token-to-clipboard"));
copied = true;
} catch (err) {
toast.Error($_("error-whyile-copying-to-clipboard"));
}
// we can notify by event or storage about copy status
valueCopy = null;
}
function close() { function textToBase64Barcode(text, is_qrcode) {
copy_modal_open = false; const canvas = document.createElement("canvas");
} let bcid = "code128";
async function copy() { if (is_qrcode) {
valueCopy = new_station.key; bcid = "qrcode";
await tick(); }
areaDom.focus(); let codeconfig = {
areaDom.select(); bcid,
try { text: `${text}`,
const successful = document.execCommand("copy"); scale: 4,
if (!successful) { includetext: true,
throw new Error(); textxalign: "center",
} backgroundcolor: "ffffff",
toast($_("copied-token-to-clipboard")); };
copied = true; if (bcid == "code128") {
} catch (err) { codeconfig.height = 10;
toast.Error($_("error-whyile-copying-to-clipboard")); }
} bwipjs.toCanvas(canvas, codeconfig);
// we can notify by event or storage about copy status return canvas.toDataURL("image/png");
valueCopy = null; }
}
function textToBase64Barcode(text, is_qrcode) {
const canvas = document.createElement("canvas");
let bcid = "code128";
if (is_qrcode) {
bcid = "qrcode";
}
let codeconfig = {
bcid,
text: `${text}`,
scale: 4,
includetext: true,
textxalign: "center",
backgroundcolor: "ffffff",
};
if (bcid == "code128") {
codeconfig.height = 10;
}
bwipjs.toCanvas(canvas, codeconfig);
return canvas.toDataURL("image/png");
}
</script> </script>
{#if copy_modal_open} {#if copy_modal_open}
{#if valueCopy != null} {#if valueCopy != null}
<textarea bind:this={areaDom}>{valueCopy}</textarea> <textarea bind:this={areaDom}>{valueCopy}</textarea>
{/if} {/if}
<div class="fixed z-10 inset-0 overflow-y-auto"> <div class="fixed z-10 inset-0 overflow-y-auto">
<div <div
class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center 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> </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 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" 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">
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div <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" 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="h-6 w-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"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z"
/></svg /></svg
> >
</div> </div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 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">
{$_("token")} {$_("token")}
</h3> </h3>
<div class="mt-2 mb-6"> <div class="mt-2 mb-6">
<p class="text-sm text-gray-500"> <p class="text-sm text-gray-500">
{$_( {$_(
"the-scanstations-api-token-will-only-get-displayed-once-you-wont-be-able-to-change-or-view-it-again" "the-scanstations-api-token-will-only-get-displayed-once-you-wont-be-able-to-change-or-view-it-again"
)} )}
<br /> <br />
{$_("please-copy-the-token-and-store-it-somewhere-save")} {$_("please-copy-the-token-and-store-it-somewhere-save")}
</p> </p>
</div> </div>
<div class="mt-2 mb-6"> <div class="mt-2 mb-6">
<label <label
for="token" for="token"
class="block text-sm font-medium text-gray-700" class="block text-sm font-medium text-gray-700"
>{$_("token")}</label >{$_("token")}</label
> >
<button on:click={copy} class="inline-flex"> <button on:click={copy} class="inline-flex">
<p <p
name="token" name="token"
class:bg-green-200={copied} class:bg-green-200={copied}
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" 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 p-2"
> >
{new_station.key} {new_station.key}
</p> </p>
<div <div
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="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"
> >
<svg <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" 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="M7 4V2h10v2h3l1 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V5l1-1h3zm0 2H5v14h14V6h-2v2H7V6zm2-2v2h6V4H9z" d="M7 4V2h10v2h3l1 1v16a1 1 0 01-1 1H4a1 1 0 01-1-1V5l1-1h3zm0 2H5v14h14V6h-2v2H7V6zm2-2v2h6V4H9z"
/></svg /></svg
> >
</div> </div>
</button> </button>
<p class="text-gray-500 text-xs"> <p class="text-gray-500 text-xs">
{$_("click-to-copy-token-to-clipboard")} {$_("click-to-copy-token-to-clipboard")}
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div class="mx-auto text-center items-center"> <div class="mx-auto text-center items-center">
<h2 class="text-lg leading-6 font-medium text-gray-900"> <h2 class="text-lg leading-6 font-medium text-gray-900">
{$_("config-codes")} {$_("config-codes")}
</h2> </h2>
<span class="flex items-center text-center"> <span class="flex items-center text-center">
<p class="text-md text-gray-900 mr-3">Format:</p> <p class="text-md text-gray-900 mr-3">Format:</p>
<label for="codeswitch" class="text-md text-gray-900 mr-3" <label for="codeswitch" class="text-md text-gray-900 mr-3"
>Code128</label >Code128</label
> >
<input <input
id="codeswitch" id="codeswitch"
type="checkbox" type="checkbox"
bind:checked={is_qrcode} bind:checked={is_qrcode}
class="relative shrink-0 w-[3.25rem] h-7 bg-gray-100 checked:bg-none checked:bg-blue-600 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 ring-1 ring-transparent focus:border-blue-600 focus:ring-blue-600 ring-offset-white focus:outline-none appearance-none before:inline-block before:w-6 before:h-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:shadow before:rounded-full before:transform before:ring-0 before:transition before:ease-in-out before:duration-200" class="relative shrink-0 w-[3.25rem] h-7 bg-gray-100 checked:bg-none checked:bg-blue-600 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 ring-1 ring-transparent focus:border-blue-600 focus:ring-blue-600 ring-offset-white focus:outline-none appearance-none before:inline-block before:w-6 before:h-6 before:bg-white checked:before:bg-blue-200 before:translate-x-0 checked:before:translate-x-full before:shadow before:rounded-full before:transform before:ring-0 before:transition before:ease-in-out before:duration-200"
/> />
<label for="codeswitch" class="text-md text-gray-900 ml-3" <label for="codeswitch" class="text-md text-gray-900 ml-3"
>QR-Code</label >QR-Code</label
> >
</span> </span>
<h3 class="leading-6 font-medium text-gray-900"> <h3 class="leading-6 font-medium text-gray-900">
{$_("api-endpoint")} {$_("api-endpoint")}
</h3> </h3>
<img <img
class:w-[50%]={is_qrcode} class:w-[50%]={is_qrcode}
class:w-full={!is_qrcode} class:w-full={!is_qrcode}
class="md:w-auto mb-2 mx-auto" class="md:w-auto mb-2 mx-auto"
alt="Registrierungscode" alt="Registrierungscode"
src={textToBase64Barcode(window.config.baseurl, is_qrcode)} src={textToBase64Barcode(window.config.baseurl, is_qrcode)}
/> />
<h3 class="leading-6 font-medium text-gray-900">{$_("token")}</h3> <h3 class="leading-6 font-medium text-gray-900">{$_("token")}</h3>
<img <img
class:w-[50%]={is_qrcode} class:w-[50%]={is_qrcode}
class:w-full={!is_qrcode} class:w-full={!is_qrcode}
class="md:w-auto mb-2 mx-auto" class="md:w-auto mb-2 mx-auto"
alt="Registrierungscode" alt="Registrierungscode"
src={barcode} src={barcode}
/> />
</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 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button <button
on:click={close} on:click={close}
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-green-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-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-green-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
> >
{$_("yes-i-copied-the-token")} {$_("yes-i-copied-the-token")}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -1,204 +1,190 @@
<script> <script>
import { t, _ } from "svelte-i18n"; import { t, _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import { ScanStationService, TrackService } from "@odit/lfk-client-js"; import { ScanStationService, TrackService } from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import ConfirmScanStationDeletion from "./ConfirmScanStationDeletion.svelte"; import ConfirmScanStationDeletion from "./ConfirmScanStationDeletion.svelte";
import Select from "svelte-select"; import Select from "svelte-select";
let data_loaded = false; let data_loaded = false;
let modal_open; let modal_open;
let delete_station; let delete_station;
export let params; export let params;
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
$: editable = {}; $: editable = {};
$: tracks = []; $: tracks = [];
$: track = {}; $: track = {};
$: changes_performed = !( $: changes_performed = !(
JSON.stringify(original_data) === JSON.stringify(editable) JSON.stringify(original_data) === JSON.stringify(editable)
); );
$: save_enabled = changes_performed; $: save_enabled = changes_performed;
const promise = ScanStationService.scanStationControllerGetOne( const promise = ScanStationService.scanStationControllerGetOne(
params.stationid params.stationid
).then((data) => { ).then((data) => {
data_loaded = true; data_loaded = true;
data.track = data.track.id; data.track = data.track.id;
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);
TrackService.trackControllerGetAll().then((val) => { TrackService.trackControllerGetAll().then((val) => {
tracks = val.map((t) => { tracks = val.map((t) => {
return { label: t.name || `#{t.id}`, value: t }; return { label: t.name || `#{t.id}`, value: t };
}); });
track = tracks.find((t) => t.value.id == editable.track); track = tracks.find((t) => t.value.id == editable.track);
}); });
}); });
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast($_("station-is-being-updated")); toast($_("station-is-being-updated"));
ScanStationService.scanStationControllerPut(original_data.id, editable) ScanStationService.scanStationControllerPut(original_data.id, editable)
.then((resp) => { .then((resp) => {
Object.assign(original_data, editable); Object.assign(original_data, editable);
original_data = original_data; original_data = original_data;
toast.success($_("updated-station")); toast.success($_("updated-station"));
}) })
.catch((err) => {}); .catch((err) => {});
} else { } else {
} }
} }
function deleteStation() { function deleteStation() {
ScanStationService.scanStationControllerRemove(original_data.id, false) ScanStationService.scanStationControllerRemove(original_data.id, false)
.then((resp) => { .then((resp) => {
toast($_("station-deleted")); toast($_("station-deleted"));
location.replace("./"); location.replace("./");
}) })
.catch((err) => { .catch((err) => {
modal_open = true; modal_open = true;
delete_station = original_data; delete_station = original_data;
}); });
} }
</script> </script>
<ConfirmScanStationDeletion bind:modal_open bind:delete_station /> <ConfirmScanStationDeletion bind:modal_open bind:delete_station />
{#await promise} {#await promise}
{$_("loading-station-details")} {$_("loading-station-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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> fill="none"
<path stroke="currentColor"
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">{$_("scanstation")}</a><svg >
stroke="currentColor" {$_("scanstations")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {$_("scanstation")} #{original_data.id}<br>"{original_data.description}"
xmlns="http://www.w3.org/2000/svg" <div data-id="stations_actions_${editable.id}">
><line x1="5" y1="12" x2="19" y2="12" /> {#if store.state.jwtinfo.userdetails.permissions.includes("STATION:DELETE")}
<polyline points="12 5 19 12 12 19" /></svg {#if delete_triggered}
> <button
</li> on:click={deleteStation}
<li class="flex items-center"> 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"
<span class="mr-2">#{original_data.id}</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.id} >{$_("cancel")}</button
<span data-id="stations_actions_${editable.id}"> >
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:DELETE")} {/if}
{#if delete_triggered} {#if !delete_triggered}
<button <button
on:click={deleteStation} on:click={() => {
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_triggered = true;
>{$_("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-station")}</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" {/if}
>{$_("cancel")}</button {#if !delete_triggered}
> <button
{/if} disabled={!save_enabled}
{#if !delete_triggered} class:opacity-50={!save_enabled}
<button type="button"
on:click={() => { on:click={submit}
delete_triggered = 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"
}} >{$_("save-changes")}</button
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" {/if}
>{$_("delete-station")}</button </div>
> </div>
{/if} <!-- -->
{/if} <div class="mt-2 text-sm w-full">
{#if !delete_triggered} <label for="track" class="block text-sm font-semibold text-gray-700"
<button >Track</label
disabled={!save_enabled} >
class:opacity-50={!save_enabled} <Select
type="button" containerClasses="rounded-l-md 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"
on:click={submit} itemFilter={(label, filterText, option) =>
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" label.toLowerCase().includes(filterText.toLowerCase()) ||
>{$_("save-changes")}</button option.value.id.toString().startsWith(filterText.toLowerCase())}
> items={tracks}
{/if} showChevron={true}
</span> placeholder="Search for a track (by name or id)."
</div> noOptionsMessage="No track found"
<!-- --> bind:selectedValue={track}
<div class="text-sm w-full"> on:select={(selectedValue) =>
<label for="track" class="block text-sm font-medium text-gray-700" (editable.track = selectedValue.detail.value.id)}
>Track</label on:clear={() => (track = null)}
> />
<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="mt-2 text-sm w-full">
itemFilter={(label, filterText, option) => <label for="description" class="font-semibold text-gray-700"
label.toLowerCase().includes(filterText.toLowerCase()) || >{$_("description")}</label
option.value.id.toString().startsWith(filterText.toLowerCase())} >
items={tracks} <input
showChevron={true} autocomplete="off"
placeholder="Search for a track (by name or id)." placeholder={$_("description")}
noOptionsMessage="No track found" type="text"
bind:selectedValue={track} bind:value={editable.description}
on:select={(selectedValue) => name="description"
(editable.track = selectedValue.detail.value.id)} 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 rounded-md p-2"
on:clear={() => (track = null)} />
/> </div>
</div> <div class="mt-2 text-sm w-full">
<div class="text-sm w-full"> <label for="enabled" class="font-semibold text-gray-700"
<label for="description" class="font-medium text-gray-700" >{$_("enabled")}</label
>{$_("description")}</label >
> <br />
<input <p class="text-gray-500">
autocomplete="off" <input
placeholder={$_("description")} id="enabled"
type="text" on:change={() => {
bind:value={editable.description} editable.enabled = !editable.enabled;
name="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-gray-500 rounded-md p-2" name="enabled"
/> type="checkbox"
</div> checked={editable.enabled}
<div class="text-sm w-full"> class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
<label for="enabled" class="ml-1 font-medium text-gray-700" />
>{$_("enabled")}</label {$_("this-scanstation-is")}
> {#if editable.enabled}{$_("enabled")}{:else}{$_("disabled")}{/if}
<br /> </p>
<p class="text-gray-500"> </div>
<input </section>
id="enabled"
on:change={() => {
editable.enabled = !editable.enabled;
}}
name="enabled"
type="checkbox"
checked={editable.enabled}
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
{$_("this-scanstation-is")}
{#if editable.enabled}{$_("enabled")}{:else}{$_("disabled")}{/if}
</p>
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -1,44 +1,229 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import AddScanStationModal from "./AddScanStationModal.svelte"; import { ScanStationService } from "@odit/lfk-client-js";
import CopyScanStationTokenModal from "./CopyScanStationTokenModal.svelte"; import AddScanStationModal from "./AddScanStationModal.svelte";
import ScanStationsOverview from "./ScanStationsOverview.svelte"; import CopyScanStationTokenModal from "./CopyScanStationTokenModal.svelte";
export let modal_open = false; import ScanStationsEmptyState from "./ScanStationsEmptyState.svelte";
export let copy_modal_open = false; import ConfirmScanStationDeletion from "./ConfirmScanStationDeletion.svelte";
export let new_station = {}; import toast from "svelte-french-toast";
let current_stations = []; //
export let modal_open = false;
export let copy_modal_open = false;
export let new_station = {};
//
const promise = ScanStationService.scanStationControllerGetAll().then(
(result) => {
current_stations = result;
}
);
$: searchvalue = "";
$: active_deletes = [];
let delete_station = {};
let current_stations = [];
</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">
{$_("scanstations")} {$_("scanstations")}
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("STATION: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"
> 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-scanstation")} >
</button> {$_("create-a-new-scanstation")}
{/if} </button>
</span> {/if}
<ScanStationsOverview <ConfirmScanStationDeletion
bind:current_stations on:cancelDelete={(event) => {
bind:modal_open modal_open = false;
bind:new_station active_deletes[event.detail.id] = false;
bind:copy_modal_open }}
/> bind:modal_open
bind:delete_station
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION: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">{$_("scanstations-are-being-loaded")}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div>
{:then}
{#if current_stations.length === 0}
<ScanStationsEmptyState />
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")}
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"
>
<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"
>
{$_("track")}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{$_("description")}
</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>
</thead>
<tbody class="divide-y divide-gray-200">
{#each current_stations as s}
{#if Object.values(s)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr
class="odd:bg-white even:bg-gray-100"
data-rowid="station_{s.id}"
>
<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">
<a
href="../tracks"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
>
{s.track.name || s.track.distance + "m"}</a
>
</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">
{s.description}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if s.enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border border-current bg-green-100 text-green-800"
>{$_("active")}</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"
>{$_("inactive")}</span
>
{/if}
</div>
</td>
{#if active_deletes[s.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>
<button
on:click={() => {
active_deletes[s.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
>{$_("cancel-delete")}</button
>
<button
on:click={() => {
ScanStationService.scanStationControllerRemove(
s.id,
false
)
.then((resp) => {
current_stations = current_stations.filter(
(obj) => obj.id !== s.id
);
toast($_("station-deleted"));
})
.catch((err) => {
modal_open = true;
delete_station = s;
});
}}
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="/scanstations/{s.id}"
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:DELETE")}
<button
on:click={() => {
active_deletes[s.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}
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATION:CREATE")}
<AddScanStationModal <AddScanStationModal
bind:modal_open bind:modal_open
bind:current_stations bind:current_stations
bind:new_station bind:new_station
bind:copy_modal_open bind:copy_modal_open
/> />
<CopyScanStationTokenModal bind:copy_modal_open bind:new_station /> <CopyScanStationTokenModal bind:copy_modal_open bind:new_station />
{/if} {/if}

View File

@@ -1,195 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { ScanStationService } from "@odit/lfk-client-js";
const promise = ScanStationService.scanStationControllerGetAll().then(
(result) => {
current_stations = result;
}
);
import store from "../../store";
import ScanStationsEmptyState from "./ScanStationsEmptyState.svelte";
import ConfirmScanStationDeletion from "./ConfirmScanStationDeletion.svelte";
import toast from "svelte-french-toast";
$: searchvalue = "";
$: active_deletes = [];
let delete_station = {};
let modal_open = false;
export let current_stations = [];
</script>
<ConfirmScanStationDeletion
on:cancelDelete={(event) => {
modal_open = false;
active_deletes[event.detail.id] = false;
}}
bind:modal_open
bind:delete_station
/>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION: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">{$_("scanstations-are-being-loaded")}</p>
<p class="text-sm">{$_("this-might-take-a-moment")}</p>
</div>
{:then}
{#if current_stations.length === 0}
<ScanStationsEmptyState />
{:else}
<input
type="search"
bind:value={searchvalue}
placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")}
class="mb-4"
/>
<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"
>
{$_("track")}
</th>
<th
scope="col"
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
{$_("description")}
</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>
</thead>
<tbody class="divide-y divide-gray-200">
{#each current_stations as s}
{#if Object.values(s)
.toString()
.toLowerCase()
.includes(searchvalue)}
<tr
class="odd:bg-white even:bg-gray-100"
data-rowid="station_{s.id}"
>
<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">
<a
href="../tracks"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800"
>
{s.track.name || s.track.distance + "m"}</a
>
</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">
{s.description}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
{#if s.enabled}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800"
>{$_("active")}</span
>
{:else}
<span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800"
>{$_("inactive")}</span
>
{/if}
</div>
</td>
{#if active_deletes[s.id] === true}
<td
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>
<button
on:click={() => {
active_deletes[s.id] = false;
}}
tabindex="0"
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
>{$_("cancel-delete")}</button
>
<button
on:click={() => {
ScanStationService.scanStationControllerRemove(
s.id,
false
)
.then((resp) => {
current_stations = current_stations.filter(
(obj) => obj.id !== s.id
);
toast($_("station-deleted"));
})
.catch((err) => {
modal_open = true;
delete_station = s;
});
}}
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="/scanstations/{s.id}"
class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a
>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:DELETE")}
<button
on:click={() => {
active_deletes[s.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

@@ -2,6 +2,7 @@
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import { MeService } from "@odit/lfk-client-js"; import { MeService } from "@odit/lfk-client-js";
import toast from 'svelte-french-toast'
import ConfirmProfileDeletion from "./ConfirmProfileDeletion.svelte"; import ConfirmProfileDeletion from "./ConfirmProfileDeletion.svelte";
import PasswordStrength, { import PasswordStrength, {
@@ -61,21 +62,13 @@
</script> </script>
<ConfirmProfileDeletion bind:modal_open bind:delete_triggered /> <ConfirmProfileDeletion bind:modal_open bind:delete_triggered />
<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"
>
🔨<br />{$_("settings")}
</h1>
</div>
</div>
<div class="pt-0 pb-16 bg-gray-50 overflow-hidden lg:pt-12 lg:py-24"> <div class="pt-0 pb-16 bg-gray-50 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"> <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<span class="text-3xl font-bold">{$_("settings")}</span>
<div> <div>
<div class="md:grid md:grid-cols-3 md:gap-6"> <div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1"> <div class="md:col-span-1">
<div class="px-4 sm:px-0"> <div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900"> <h3 class="text-lg font-medium leading-6 text-gray-900">
{$_("profile")} {$_("profile")}
</h3> </h3>
@@ -90,8 +83,8 @@
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<div class="shadow sm:rounded-md sm:overflow-hidden"> <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="px-4 py-5 bg-white space-y-6 sm:p-6">
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="username" class="font-medium text-gray-700" <label for="username" class="font-semibold text-gray-700"
>{$_("username")}</label >{$_("username")}</label
> >
<input <input
@@ -100,11 +93,11 @@
type="text" type="text"
bind:value={editable.username} bind:value={editable.username}
name="username" name="username"
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="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>
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="email" class="font-medium text-gray-700" <label for="email" class="font-semibold text-gray-700"
>{$_("e-mail-adress")}</label >{$_("e-mail-adress")}</label
> >
<input <input
@@ -113,7 +106,7 @@
type="email" type="email"
bind:value={editable.email} bind:value={editable.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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
{#if !isEmail(editable.email)} {#if !isEmail(editable.email)}
@@ -122,8 +115,8 @@
>{$_("valid-email-is-required")}</span >{$_("valid-email-is-required")}</span
> >
{/if} {/if}
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="firstname" class="font-medium text-gray-700" <label for="firstname" class="font-semibold text-gray-700"
>{$_("first-name")}</label >{$_("first-name")}</label
> >
<input <input
@@ -132,11 +125,11 @@
type="text" type="text"
bind:value={editable.firstname} bind:value={editable.firstname}
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<!-- <div class="text-sm w-full"> <!-- <div class="text-sm w-full mt-2">
<label for="middlename" class="font-medium text-gray-700" <label for="middlename" class="font-semibold text-gray-700"
>{$_("middle-name")}</label >{$_("middle-name")}</label
> >
<input <input
@@ -145,11 +138,11 @@
type="text" type="text"
bind:value={editable.middlename} bind:value={editable.middlename}
name="middlename" name="middlename"
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="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> -->
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="lastname" class="font-medium text-gray-700" <label for="lastname" class="font-semibold text-gray-700"
>{$_("last-name")}</label >{$_("last-name")}</label
> >
<input <input
@@ -158,7 +151,7 @@
type="text" type="text"
bind:value={editable.lastname} bind:value={editable.lastname}
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
</div> </div>
@@ -168,7 +161,7 @@
disabled={!save_enabled} disabled={!save_enabled}
class:opacity-50={!save_enabled} class:opacity-50={!save_enabled}
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: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"
> >
{$_("save-changes")} {$_("save-changes")}
</button> </button>
@@ -183,7 +176,7 @@
<div> <div>
<div class="md:grid md:grid-cols-3 md:gap-6"> <div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1"> <div class="md:col-span-1">
<div class="px-4 sm:px-0"> <div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900"> <h3 class="text-lg font-medium leading-6 text-gray-900">
{$_("password")} {$_("password")}
</h3> </h3>
@@ -198,7 +191,7 @@
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<div class="shadow sm:rounded-md sm:overflow-hidden"> <div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-3 bg-gray-50 text-left sm:px-6"> <div class="px-4 py-3 bg-gray-50 text-left sm:px-6">
<label for="new_password" class="font-medium text-gray-700" <label for="new_password" class="font-semibold text-gray-700"
>{$_("new-password")}</label >{$_("new-password")}</label
> >
<div class="-mt-px relative"> <div class="-mt-px relative">
@@ -211,7 +204,7 @@
placeholder={$_("password")} placeholder={$_("password")}
/> />
</div> </div>
<label for="new_password" class="font-medium text-gray-700" <label for="new_password" class="font-semibold text-gray-700"
>{$_("confirm-the-new-password")}</label >{$_("confirm-the-new-password")}</label
> >
<div class="-mt-px relative"> <div class="-mt-px relative">
@@ -232,7 +225,7 @@
disabled={!update_password_enabled} disabled={!update_password_enabled}
class:opacity-50={!update_password_enabled} class:opacity-50={!update_password_enabled}
on:click={changePassword} on:click={changePassword}
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" 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"
> >
{$_("update-password")} {$_("update-password")}
</button> </button>
@@ -254,7 +247,7 @@
<div> <div>
<div class="md:grid md:grid-cols-3 md:gap-6"> <div class="md:grid md:grid-cols-3 md:gap-6">
<div class="md:col-span-1"> <div class="md:col-span-1">
<div class="px-4 sm:px-0"> <div class="sm:px-0">
<h3 class="text-lg font-medium leading-6 text-gray-900"> <h3 class="text-lg font-medium leading-6 text-gray-900">
{$_("danger-zone")} {$_("danger-zone")}
</h3> </h3>

View File

@@ -1,26 +1,26 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
export let detailsLink; export let detailsLink = null;
export let detailsAction; export let detailsAction = null;
export let deleteEnabled; export let deleteEnabled;
export let deleteAction; export let deleteAction;
</script> </script>
{#if detailsLink} {#if detailsLink}
<a href={detailsLink} class="text-indigo-600 hover:text-indigo-900" <a href={detailsLink} class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</a >{$_("details")}</a
> >
{:else if detailsAction} {:else if detailsAction}
<button on:click={detailsAction} class="text-indigo-600 hover:text-indigo-900" <button on:click={detailsAction} class="text-indigo-600 hover:text-indigo-900"
>{$_("details")}</button >{$_("details")}</button
> >
{/if} {/if}
{#if deleteEnabled} {#if deleteEnabled}
<button <button
tabindex="0" tabindex="0"
on:click={deleteAction} on:click={deleteAction}
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}

View File

@@ -126,7 +126,7 @@
bind:value={description} bind:value={description}
type="text" type="text"
name="description" name="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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
</div> </div>

View File

@@ -5,14 +5,14 @@
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
export let modal_open; export let modal_open;
export let delete_station; export let delete_client;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
function cancelDelete() { function cancelDelete() {
modal_open = false; modal_open = false;
dispatch("cancelDelete", { id: delete_station.id }); dispatch("cancelDelete", { id: delete_client.id });
} }
function deleteClient() { function deleteClient() {
StatsClientService.statsClientControllerRemove(delete_station.id, true) StatsClientService.statsClientControllerRemove(delete_client.id, true)
.then((resp) => { .then((resp) => {
toast($_("statsclient-deleted")); toast($_("statsclient-deleted"));
location.replace("./"); location.replace("./");

View File

@@ -4,6 +4,7 @@
import { tick, createEventDispatcher } from "svelte"; import { tick, createEventDispatcher } from "svelte";
export let copy_modal_open; export let copy_modal_open;
export let new_client; export let new_client;
import toast from 'svelte-french-toast'
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let valueCopy = null; let valueCopy = null;
let areaDom; let areaDom;
@@ -95,7 +96,7 @@
<p <p
name="token" name="token"
class:bg-green-200={copied} class:bg-green-200={copied}
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" 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 p-2"
> >
{new_client.key} {new_client.key}
</p> </p>

View File

@@ -1,124 +1,110 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import ConfirmStatsClientDeletion from "./ConfirmStatsClientDeletion.svelte"; import ConfirmStatsClientDeletion from "./ConfirmStatsClientDeletion.svelte";
import { StatsClientService } from "@odit/lfk-client-js"; import { StatsClientService } from "@odit/lfk-client-js";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
let data_loaded = false; let data_loaded = false;
let modal_open; let modal_open;
let delete_client; let delete_client;
export let params; export let params;
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
const promise = StatsClientService.statsClientControllerGetOne( const promise = StatsClientService.statsClientControllerGetOne(
params.clientid params.clientid
).then((data) => { ).then((data) => {
data_loaded = true; data_loaded = true;
original_data = Object.assign(original_data, data); original_data = Object.assign(original_data, data);
}); });
function deleteClient() { function deleteClient() {
StatsClientService.statsClientControllerRemove(original_data.id, false) StatsClientService.statsClientControllerRemove(original_data.id, false)
.then((resp) => { .then((resp) => {
toast($_("statsclient-deleted")); toast($_("statsclient-deleted"));
location.replace("./"); location.replace("./");
}) })
.catch((err) => { .catch((err) => {
modal_open = true; modal_open = true;
delete_client = original_data; delete_client = original_data;
}); });
} }
</script> </script>
<ConfirmStatsClientDeletion bind:modal_open bind:delete_client /> <ConfirmStatsClientDeletion bind:modal_open bind:delete_client />
{#await promise} {#await promise}
{$_("loading-statsclient-details")} {$_("loading-statsclient-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">
<svg <a class="mr-2" href="./"
fill="currentColor" ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" width="24"
width="24" height="24"
height="24" viewBox="0 0 24 24"
><path fill="none" d="M0 0h24v24H0z" /> fill="none"
<path stroke="currentColor"
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" stroke-width="2"
/></svg stroke-linecap="round"
> stroke-linejoin="round"
</li> class="inline-block"
<li class="flex items-center ml-2"> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<a class="mr-2" href="./">{$_("statsclient")}</a><svg >
stroke="currentColor" {$_("statsclients")}</a
fill="none" >
stroke-width="2" </li>
viewBox="0 0 24 24" </ol>
stroke-linecap="round" </nav>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="mb-4 text-3xl font-extrabold leading-tight">
width="1em" {$_("statsclient")} #{original_data.id}
xmlns="http://www.w3.org/2000/svg" <div data-id="stations_actions_${original_data.id}">
><line x1="5" y1="12" x2="19" y2="12" /> {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:DELETE")}
<polyline points="12 5 19 12 12 19" /></svg {#if delete_triggered}
> <button
</li> on:click={deleteClient}
<li class="flex items-center"> 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"
<span class="mr-2">#{original_data.id}</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.id} >{$_("cancel")}</button
<span data-id="stations_actions_${original_data.id}"> >
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:DELETE")} {/if}
{#if delete_triggered} {#if !delete_triggered}
<button <button
on:click={deleteClient} on:click={() => {
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_triggered = true;
>{$_("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-statsclient")}</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" {/if}
>{$_("cancel")}</button </div>
> </div>
{/if} <!-- -->
{#if !delete_triggered} <div class="text-sm w-full mt-2">
<button <label for="description" class="font-semibold text-gray-700"
on:click={() => { >{$_("description")}</label
delete_triggered = true; >
}} <p
type="button" name="description"
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" 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"
>{$_("delete-statsclient")}</button >
> {original_data.description}
{/if} </p>
{/if} </div>
</span> </section>
</div>
<!-- -->
<div class="text-sm w-full">
<label for="description" class="font-medium text-gray-700"
>{$_("description")}</label
>
<p
name="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-gray-500 rounded-md p-2"
>
{original_data.description}
</p>
</div>
</section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -1,44 +1,39 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import AddStatsClientModal from "./AddStatsClientModal.svelte"; import AddStatsClientModal from "./AddStatsClientModal.svelte";
import CopyStatsClientTokenModal from "./CopyStatsClientTokenModal.svelte"; import CopyStatsClientTokenModal from "./CopyStatsClientTokenModal.svelte";
import StatsClientsOverview from "./StatsClientsOverview.svelte"; import StatsClientsOverview from "./StatsClientsOverview.svelte";
export let modal_open = false; export let modal_open = false;
export let copy_modal_open = false; export let copy_modal_open = false;
export let new_client = {}; export let new_client = {};
let current_clients = []; let current_clients = [];
</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">
{$_("statsclients")} {$_("statsclients")}
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT: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"
> 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-statsclient")} >
</button> {$_("create-a-new-statsclient")}
{/if} </button>
</span> {/if}
<StatsClientsOverview <StatsClientsOverview bind:current_clients />
bind:current_clients
bind:modal_open
bind:new_client
bind:copy_modal_open
/>
</section> </section>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:CREATE")}
<AddStatsClientModal <AddStatsClientModal
bind:modal_open bind:modal_open
bind:current_clients bind:current_clients
bind:new_client bind:new_client
bind:copy_modal_open bind:copy_modal_open
/> />
<CopyStatsClientTokenModal bind:copy_modal_open bind:new_client /> <CopyStatsClientTokenModal bind:copy_modal_open bind:new_client />
{/if} {/if}

View File

@@ -43,7 +43,7 @@
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")} aria-label={$_("datatable.search")}
class="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<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"

View File

@@ -141,7 +141,7 @@
bind:this={teamname_input_dom} bind:this={teamname_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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isTeamNameValid} {#if !isTeamNameValid}
<span <span
@@ -158,7 +158,7 @@
>{$_("organization")}</label >{$_("organization")}</label
> >
<Select <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" 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) => itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id option.value.id

View File

@@ -1,300 +1,264 @@
<script> <script>
import { import {
GroupContactService, GroupContactService,
RunnerOrganizationService, RunnerOrganizationService,
RunnerTeamService, RunnerTeamService,
} from "@odit/lfk-client-js"; } from "@odit/lfk-client-js";
import { getLocaleFromNavigator, _ } from "svelte-i18n"; import { getLocaleFromNavigator, _ } from "svelte-i18n";
import toast from "svelte-french-toast";
import store from "../../store"; import store from "../../store";
import Select from "svelte-select"; import Select from "svelte-select";
import ImportRunnerModal from "../runners/ImportRunnerModal.svelte"; import ImportRunnerModal from "../runners/ImportRunnerModal.svelte";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import ConfirmTeamDeletion from "./ConfirmTeamDeletion.svelte"; import ConfirmTeamDeletion from "./ConfirmTeamDeletion.svelte";
import Teams from "./Teams.svelte"; import Teams from "./Teams.svelte";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
let [teamdata, original, delete_team, orgs, contacts, modal_open] = [ let [teamdata, original, delete_team, orgs, contacts, modal_open] = [
{}, {},
{}, {},
{}, {},
[], [],
[], [],
false, false,
]; ];
export let params; export let params;
export let import_modal_open = false; export let import_modal_open = false;
$: delete_triggered = false; $: delete_triggered = false;
$: save_enabled = data_changed && teamdata.parentGroup != null; $: save_enabled = data_changed && teamdata.parentGroup != null;
$: data_loaded = false; $: data_loaded = false;
$: data_changed = !(JSON.stringify(teamdata) === JSON.stringify(original)); $: data_changed = !(JSON.stringify(teamdata) === JSON.stringify(original));
$: sponsoring_contracts_show = true; $: sponsoring_contracts_show = true;
$: cards_show = true; $: cards_show = true;
$: certificates_show = true; $: certificates_show = true;
$: generate_teams = [original]; $: generate_teams = [original];
$: group = {}; $: group = {};
$: contact = {}; $: contact = {};
// //
const getContactLabel = (option) => const getContactLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname; option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const promise = RunnerTeamService.runnerTeamControllerGetOne( const promise = RunnerTeamService.runnerTeamControllerGetOne(
params.teamid params.teamid
).then((value) => { ).then((value) => {
data_loaded = true; data_loaded = true;
teamdata = Object.assign(teamdata, value); teamdata = Object.assign(teamdata, value);
original = Object.assign(original, value); original = Object.assign(original, value);
RunnerOrganizationService.runnerOrganizationControllerGetAll().then( RunnerOrganizationService.runnerOrganizationControllerGetAll().then(
(val) => { (val) => {
orgs = val.map((r) => { orgs = val.map((r) => {
delete r.contact; delete r.contact;
r.teams = []; r.teams = [];
return { label: r.name, value: r }; return { label: r.name, value: r };
}); });
group = orgs.find((g) => g.value.id == teamdata.parentGroup.id); group = orgs.find((g) => g.value.id == teamdata.parentGroup.id);
} }
); );
GroupContactService.groupContactControllerGetAll().then((val) => { GroupContactService.groupContactControllerGetAll().then((val) => {
contacts = val.map((r) => { contacts = val.map((r) => {
return { label: getContactLabel(r), value: r }; return { label: getContactLabel(r), value: r };
}); });
if (teamdata.contact) { if (teamdata.contact) {
contact = contacts.find((g) => g.value.id == teamdata.contact.id); contact = contacts.find((g) => g.value.id == teamdata.contact.id);
} else { } else {
contact = null; contact = null;
} }
}); });
}); });
function deleteTeam() { function deleteTeam() {
RunnerTeamService.runnerTeamControllerRemove(original.id, false) RunnerTeamService.runnerTeamControllerRemove(original.id, false)
.then((resp) => { .then((resp) => {
toast($_("team-deleted")); toast($_("team-deleted"));
location.replace("./"); location.replace("./");
}) })
.catch((err) => { .catch((err) => {
modal_open = true; modal_open = true;
delete_team = original; delete_team = original;
}); });
} }
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
toast($_("updating-team")); toast($_("updating-team"));
let postdata = teamdata; let postdata = teamdata;
postdata.parentGroup = teamdata.parentGroup.id; postdata.parentGroup = teamdata.parentGroup.id;
postdata.contact = teamdata.contact?.id; postdata.contact = teamdata.contact?.id;
RunnerTeamService.runnerTeamControllerPut(original.id, postdata) RunnerTeamService.runnerTeamControllerPut(original.id, postdata)
.then((resp) => { .then((resp) => {
Object.assign(original, teamdata); Object.assign(original, teamdata);
original = original; original = original;
toast.success($_("updated-team")); toast.success($_("updated-team"));
}) })
.catch((err) => {}); .catch((err) => {});
} }
} }
</script> </script>
<ImportRunnerModal <ImportRunnerModal
current_runners={[]} current_runners={[]}
on:cancelDelete={(event) => { on:cancelDelete={(event) => {
import_modal_open = false; import_modal_open = false;
}} }}
passed_team={teamdata} passed_team={teamdata}
passed_orgs={[]} passed_orgs={[]}
passed_org={{}} passed_org={{}}
opened_from="TeamDetail" opened_from="TeamDetail"
bind:import_modal_open bind:import_modal_open
/> />
<ConfirmTeamDeletion bind:modal_open bind:delete_team /> <ConfirmTeamDeletion bind:modal_open bind:delete_team />
{#if data_loaded} {#if data_loaded}
<section class="container p-5"> <section class="container p-5">
<div class="mb-8 text-3xl font-extrabold leading-tight"> <div class="flex flex-row mb-4">
{original.name} <div class="w-full">
<span data-id="org_actions_${teamdata.id}"> <nav class="w-full flex">
<GenerateSponsoringContracts <ol class="list-none flex flex-row items-center justify-start">
bind:sponsoring_contracts_show <li class="flex items-center">
bind:generate_teams <a class="mr-2" href="./"
/> ><svg
<GenerateRunnerCards bind:cards_show bind:generate_teams /> xmlns="http://www.w3.org/2000/svg"
<GenerateRunnerCertificates width="24"
bind:certificates_show height="24"
bind:generate_teams viewBox="0 0 24 24"
/> fill="none"
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:IMPORT")} stroke="currentColor"
<button stroke-width="2"
on:click={() => { stroke-linecap="round"
import_modal_open = true; stroke-linejoin="round"
}} class="inline-block"
type="button" ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
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" >
> {$_("teams")}</a
{$_("import-runners")} >
</button> </li>
{/if} </ol>
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")} </nav>
{#if delete_triggered} </div>
<button </div>
on:click={deleteTeam} <div class="mb-4 text-3xl font-extrabold leading-tight">
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" {#if group}
>{$_("confirm-delete")}</button {group.label}{" > "}
> {/if}
<button {original.name} [#{params.teamid}]
on:click={() => { <span data-id="org_actions_${teamdata.id}">
delete_triggered = !delete_triggered; <GenerateSponsoringContracts
}} bind:sponsoring_contracts_show
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" bind:generate_teams
>{$_("cancel")}</button />
> <GenerateRunnerCards bind:cards_show bind:generate_teams />
{/if} <GenerateRunnerCertificates
{#if !delete_triggered} bind:certificates_show
<button bind:generate_teams
on:click={() => { />
delete_triggered = true; {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:IMPORT")}
}} <button
type="button" on:click={() => {
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" import_modal_open = true;
>{$_("delete-team")}</button }}
> type="button"
{/if} 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"
{/if} >
{#if !delete_triggered} {$_("import-runners")}
<button </button>
on:click={submit} {/if}
disabled={!save_enabled} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")}
class:opacity-50={!save_enabled} {#if delete_triggered}
type="button" <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:ml-3 sm:w-auto sm:text-sm" on:click={deleteTeam}
>{$_("save-changes")}</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"
> >{$_("confirm-delete")}</button
{/if} >
</span> <button
</div> on:click={() => {
<div class="flex flex-row mb-4"> delete_triggered = !delete_triggered;
<div class="w-full"> }}
<nav class="w-full flex"> 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"
<ol class="list-none flex flex-row items-center justify-start"> >{$_("cancel")}</button
<li class="mr-2 flex items-center"> >
<svg {/if}
stroke="currentColor" {#if !delete_triggered}
fill="none" <button
stroke-width="2" on:click={() => {
viewBox="0 0 24 24" delete_triggered = true;
stroke-linecap="round" }}
stroke-linejoin="round" type="button"
class="h-3 w-3 stroke-current" 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"
height="1em" >{$_("delete-team")}</button
width="1em" >
xmlns="http://www.w3.org/2000/svg" {/if}
><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> {/if}
<polyline points="9 22 9 12 15 12 15 22" /></svg {#if !delete_triggered}
> <button
</li> on:click={submit}
<li class="flex items-center"> disabled={!save_enabled}
<a class="mr-2" href="/">Home</a><svg class:opacity-50={!save_enabled}
stroke="currentColor" type="button"
fill="none" 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"
stroke-width="2" >{$_("save-changes")}</button
viewBox="0 0 24 24" >
stroke-linecap="round" {/if}
stroke-linejoin="round" </span>
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="text-sm w-full mt-2">
width="1em" <label for="name" class="font-semibold text-gray-700">Name</label>
xmlns="http://www.w3.org/2000/svg" <input
><line x1="5" y1="12" x2="19" y2="12" /> autocomplete="off"
<polyline points="12 5 19 12 12 19" /></svg placeholder="Name"
> type="text"
</li> bind:value={teamdata.name}
<li class="mr-2 flex items-center"> name="name"
<svg 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="flex-shrink-0 w-5 h-5 mr-2" />
fill="currentColor" </div>
width="24" <div class="text-sm w-full mt-2">
height="24" <label for="contact" class="font-semibold text-gray-700"
xmlns="http://www.w3.org/2000/svg" >{$_("contact")}</label
viewBox="0 0 640 512" >
><path <Select
fill="currentColor" 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-neutral-800 rounded-md p-2"
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" itemFilter={(label, filterText, option) =>
/></svg label.toLowerCase().includes(filterText.toLowerCase()) ||
> option.value.id.toString().startsWith(filterText.toLowerCase())}
</li> items={contacts}
<li class="flex items-center"> showChevron={true}
<a class="mr-2" href="./">Teams</a><svg placeholder={$_("no-contact-selected")}
stroke="currentColor" noOptionsMessage={$_("no-contact-found")}
fill="none" bind:selectedValue={contact}
stroke-width="2" on:select={(selectedValue) =>
viewBox="0 0 24 24" (teamdata.contact = selectedValue.detail.value)}
stroke-linecap="round" on:clear={() => (teamdata.contact = null)}
stroke-linejoin="round" />
class="h-3 w-3 mr-2 stroke-current" </div>
height="1em" <div class="text-sm w-full mt-2">
width="1em" <label for="org" class="font-semibold text-gray-700"
xmlns="http://www.w3.org/2000/svg" >{$_("organization")}</label
><line x1="5" y1="12" x2="19" y2="12" /> >
<polyline points="12 5 19 12 12 19" /></svg <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-neutral-800 rounded-md p-2"
</li> itemFilter={(label, filterText, option) =>
<li class="flex items-center"> label.toLowerCase().includes(filterText.toLowerCase()) ||
<span class="mr-2">Team-Details #{params.teamid}</span> option.id.value.toString().startsWith(filterText.toLowerCase())}
</li> items={orgs}
</ol> showChevron={true}
</nav> placeholder={$_("search-for-an-organization-by-name-or-id")}
</div> noOptionsMessage={$_("no-organizations-found")}
</div> bind:selectedValue={group}
<div class="text-sm w-full"> on:select={(selectedValue) =>
<label for="name" class="font-medium text-gray-700">Name</label> (teamdata.parentGroup = selectedValue.detail.value)}
<input on:clear={() => (teamdata.parentGroup = null)}
autocomplete="off" />
placeholder="Name" </div>
type="text" <div class="text-sm w-full mt-2">
bind:value={teamdata.name} <span class="font-semibold text-gray-700">{$_("distance")}</span>
name="name" <br />
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 class="text-gray-700"
/> >{(original.total_distance / 1000).toFixed(2)} km</span
</div> >
<div class="text-sm w-full"> </div>
<label for="contact" class="font-medium text-gray-700" </section>
>{$_("contact")}</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"
itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id.toString().startsWith(filterText.toLowerCase())}
items={contacts}
showChevron={true}
placeholder={$_("no-contact-selected")}
noOptionsMessage={$_("no-contact-found")}
bind:selectedValue={contact}
on:select={(selectedValue) =>
(teamdata.contact = selectedValue.detail.value)}
on:clear={() => (teamdata.contact = null)}
/>
</div>
<div class="text-sm w-full">
<label for="org" class="font-medium text-gray-700"
>{$_("organization")}</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"
itemFilter={(label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) ||
option.id.value.toString().startsWith(filterText.toLowerCase())}
items={orgs}
showChevron={true}
placeholder={$_("search-for-an-organization-by-name-or-id")}
noOptionsMessage={$_("no-organizations-found")}
bind:selectedValue={group}
on:select={(selectedValue) =>
(teamdata.parentGroup = selectedValue.detail.value)}
on:clear={() => (teamdata.parentGroup = null)}
/>
</div>
</section>
{:else} {:else}
{#await promise} {#await promise}
{$_("team-detail-is-being-loaded")} {$_("team-detail-is-being-loaded")}
{:catch error} {:catch error}
<PromiseError /> <PromiseError />
{/await} {/await}
{/if} {/if}

View File

@@ -8,20 +8,20 @@
</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">
{$_("teams")} {$_("teams")}
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM: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"
> 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-team")} >
</button> {$_("create-team")}
{/if} </button>
</span> {/if}
<TeamsOverview bind:current_teams /> <TeamsOverview bind:current_teams />
</section> </section>

View File

@@ -1,247 +1,241 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { RunnerTeamService } from "@odit/lfk-client-js"; import { RunnerTeamService } from "@odit/lfk-client-js";
const teams_promise = RunnerTeamService.runnerTeamControllerGetAll(); const teams_promise = RunnerTeamService.runnerTeamControllerGetAll();
import store, { users as usersstore } from "../../store.js"; import store, { users as usersstore } from "../../store.js";
import TeamsEmptyState from "./TeamsEmptyState.svelte"; import TeamsEmptyState from "./TeamsEmptyState.svelte";
import ConfirmTeamDeletion from "./ConfirmTeamDeletion.svelte"; import ConfirmTeamDeletion from "./ConfirmTeamDeletion.svelte";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
$: searchvalue = ""; $: searchvalue = "";
$: active_deletes = []; $: active_deletes = [];
$: sponsoring_contracts_show = current_teams.some( $: sponsoring_contracts_show = current_teams.some(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
$: cards_show = current_teams.some((r) => r.is_selected === true); $: cards_show = current_teams.some((r) => r.is_selected === true);
$: certificates_show = current_teams.some((r) => r.is_selected === true); $: certificates_show = current_teams.some((r) => r.is_selected === true);
$: generate_teams = current_teams.filter((r) => r.is_selected === true); $: generate_teams = current_teams.filter((r) => r.is_selected === true);
export let current_teams = []; export let current_teams = [];
let modal_open = false; let modal_open = false;
let delete_team = {}; let delete_team = {};
usersstore.subscribe((val) => { usersstore.subscribe((val) => {
current_teams = val; current_teams = val;
}); });
teams_promise.then((data) => { teams_promise.then((data) => {
usersstore.set(data); usersstore.set(data);
}); });
</script> </script>
<ConfirmTeamDeletion <ConfirmTeamDeletion
on:cancelDelete={(event) => { on:cancelDelete={(event) => {
modal_open = false; modal_open = false;
active_deletes[event.detail.id] = false; active_deletes[event.detail.id] = false;
}} }}
bind:modal_open bind:modal_open
bind:delete_team bind:delete_team
/> />
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")}
{#await teams_promise} {#await teams_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">{$_("teams-are-being-loaded")}</p> <p class="font-bold">{$_("teams-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_teams.length === 0} {#if current_teams.length === 0}
<TeamsEmptyState /> <TeamsEmptyState />
{: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="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<div class="h-12"> <div class="h-12">
<GenerateSponsoringContracts <GenerateSponsoringContracts
bind:sponsoring_contracts_show bind:sponsoring_contracts_show
bind:generate_teams bind:generate_teams
/> />
<GenerateRunnerCards bind:cards_show bind:generate_teams /> <GenerateRunnerCards bind:cards_show bind:generate_teams />
<GenerateRunnerCertificates <GenerateRunnerCertificates
bind:certificates_show bind:certificates_show
bind:generate_teams bind:generate_teams
/> />
</div> </div>
<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 class="odd:bg-white even:bg-gray-100">
<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"
> >
<button <button
on:click={() => { on:click={() => {
const newstate = !current_teams.some( const newstate = !current_teams.some(
(r) => r.is_selected === true (r) => r.is_selected === true
); );
current_teams = current_teams.map((r) => { current_teams = current_teams.map((r) => {
r.is_selected = newstate; r.is_selected = newstate;
return r; return r;
}); });
}} }}
class="underline cursor-pointer select-none" class="underline cursor-pointer select-none"
>{#if current_teams.some((r) => r.is_selected === true)} >{#if current_teams.some((r) => r.is_selected === true)}
{$_("deselect-all")} {$_("deselect-all")}
{:else}{$_("select-all")}{/if} {:else}{$_("select-all")}{/if}
</button> </button>
</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"
> >
{$_("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"
> >
{$_("organization")} {$_("organization")}
</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"
> >
{$_("contact")} {$_("contact")}
</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>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200">
{#each current_teams as t} {#each current_teams as t}
{#if Object.values(t) {#if Object.values(t)
.toString() .toString()
.toLowerCase() .toLowerCase()
.includes(searchvalue)} .includes(searchvalue)}
<tr <tr
class="odd:bg-white even:bg-gray-100" class="odd:bg-white even:bg-gray-100"
data-rowid="team_{t.id}" data-rowid="team_{t.id}"
> >
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<input <input
bind:checked={t.is_selected} bind:checked={t.is_selected}
type="checkbox" type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/> />
</td> </td>
<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="text-sm font-medium text-gray-900">
<div class="text-sm font-medium text-gray-900"> {t.name}
{t.name} </div>
</div> </div>
</div> </td>
</div> <td class="px-6 py-4 whitespace-nowrap">
</td> <div class="flex items-center">
<td class="px-6 py-4 whitespace-nowrap"> <div class="text-sm font-medium text-gray-900">
<div class="flex items-center"> {#if t.parentGroup}
<div class="ml-4"> <a
<div class="text-sm font-medium text-gray-900"> href="../orgs/{t.parentGroup.id}"
{#if t.parentGroup} class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
<a >{t.parentGroup.name}</a
href="../orgs/{t.parentGroup.id}" >
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" {:else}{$_("no-organization-specified")}{/if}
>{t.parentGroup.name}</a </div>
> </div>
{:else}{$_("no-organization-specified")}{/if} </td>
</div> <td class="px-6 py-4 whitespace-nowrap">
</div> <div class="flex items-center">
</div> <div class="text-sm font-medium text-gray-900">
</td> {#if t.contact}
<td class="px-6 py-4 whitespace-nowrap"> <a
<div class="flex items-center"> href="../contacts/{t.contact.id}"
<div class="ml-4"> class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800 border border-current"
<div class="text-sm font-medium text-gray-900"> >{t.contact.firstname}
{#if t.contact} {t.contact.middlename || ""}
<a {t.contact.lastname}</a
href="../contacts/{t.contact.id}" >
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" {:else}{$_("no-contact-specified")}{/if}
>{t.contact.firstname} </div>
{t.contact.middlename || ""} </div>
{t.contact.lastname}</a </td>
> {#if active_deletes[t.id] === true}
{:else}{$_("no-contact-specified")}{/if} <td
</div> class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
</div> >
</div> <button
</td> on:click={() => {
{#if active_deletes[t.id] === true} active_deletes[t.id] = false;
<td }}
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" tabindex="0"
> class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer"
<button >{$_("cancel-delete")}</button
on:click={() => { >
active_deletes[t.id] = false; <button
}} on:click={() => {
tabindex="0" RunnerTeamService.runnerTeamControllerRemove(
class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer" t.id,
>{$_("cancel-delete")}</button false
> )
<button .then((resp) => {
on:click={() => { current_teams = current_teams.filter(
RunnerTeamService.runnerTeamControllerRemove( (obj) => obj.id !== t.id
t.id, );
false toast($_("team-deleted"));
) })
.then((resp) => { .catch((err) => {
current_teams = current_teams.filter( modal_open = true;
(obj) => obj.id !== t.id delete_team = t;
); });
toast($_("team-deleted")); }}
}) tabindex="0"
.catch((err) => { class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
modal_open = true; >{$_("confirm-delete")}</button
delete_team = t; >
}); </td>
}} {:else}
tabindex="0" <td
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"
>{$_("confirm-delete")}</button >
> <a
</td> href="./{t.id}"
{:else} class="text-indigo-600 hover:text-indigo-900"
<td >{$_("details")}</a
class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium" >
> {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")}
<a <button
href="./{t.id}" on:click={() => {
class="text-indigo-600 hover:text-indigo-900" active_deletes[t.id] = true;
>{$_("details")}</a }}
> tabindex="0"
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:DELETE")} class="ml-4 text-red-600 hover:text-red-900 cursor-pointer"
<button >{$_("delete")}</button
on:click={() => { >
active_deletes[t.id] = true; {/if}
}} </td>
tabindex="0" {/if}
class="ml-4 text-red-600 hover:text-red-900 cursor-pointer" </tr>
>{$_("delete")}</button {/if}
> {/each}
{/if} </tbody>
</td> </table>
{/if} </div>
</tr> {/if}
{/if} {:catch error}
{/each} <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
</tbody> <span class="inline-block align-middle mr-8">
</table> <b class="capitalize">{$_("general_promise_error")}</b>
</div> {error}
{/if} </span>
{:catch error} </div>
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500"> {/await}
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
{/if} {/if}

View File

@@ -139,7 +139,7 @@
bind:this={trackname_input} bind:this={trackname_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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if isTracknameValid} {#if isTracknameValid}
<span <span
@@ -164,7 +164,7 @@
bind:value={tracklength} bind:value={tracklength}
type="number" type="number"
name="track_length_m" name="track_length_m"
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" 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"
placeholder="1000" placeholder="1000"
/> />
<span <span
@@ -195,7 +195,7 @@
bind:value={track_min_duration} bind:value={track_min_duration}
type="number" type="number"
name="track_min_duration" name="track_min_duration"
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" 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"
placeholder={smart_track_min_duration_placeholder} placeholder={smart_track_min_duration_placeholder}
/> />
<span <span
@@ -223,7 +223,7 @@
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:ml-3 sm:w-auto sm:text-sm"
> >
Create {$_("create")}
</button> </button>
<button <button
on:click={() => { on:click={() => {

View File

@@ -19,18 +19,18 @@
</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">
Tracks {$_("tracks")}
<button </h4>
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"
> 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-track")} >
</button> {$_("create-track")}
</span> </button>
{#await tracks_promise} {#await tracks_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"
@@ -101,7 +101,7 @@
<div class="ml-4"> <div class="ml-4">
{#if editTracks.findIndex((tr) => tr.id === t.id) !== -1} {#if editTracks.findIndex((tr) => tr.id === t.id) !== -1}
<input <input
class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-0.5" class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-0.5"
type="text" type="text"
value={t.name} value={t.name}
on:input={(e) => { on:input={(e) => {
@@ -124,7 +124,7 @@
<div class="ml-4"> <div class="ml-4">
{#if editTracks.findIndex((tr) => tr.id === t.id) !== -1} {#if editTracks.findIndex((tr) => tr.id === t.id) !== -1}
<input <input
class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-0.5" class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-0.5"
type="number" type="number"
value={t.distance} value={t.distance}
on:input={(e) => { on:input={(e) => {
@@ -147,7 +147,7 @@
<div class="ml-4"> <div class="ml-4">
{#if editTracks.findIndex((tr) => tr.id === t.id) !== -1} {#if editTracks.findIndex((tr) => tr.id === t.id) !== -1}
<input <input
class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-0.5" class="shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-0.5"
type="number" type="number"
value={t.minimumLapTime} value={t.minimumLapTime}
on:input={(e) => { on:input={(e) => {

View File

@@ -156,7 +156,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isFirstnameValid} {#if !isFirstnameValid}
<span <span
@@ -179,7 +179,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> --> </div> -->
<div class="col-span-6"> <div class="col-span-6">
@@ -198,7 +198,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isLastnameValid} {#if !isLastnameValid}
<span <span
@@ -224,7 +224,7 @@
bind:this={password_input} bind:this={password_input}
type="password" type="password"
name="password" name="password"
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="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"
/> />
<PasswordStrength <PasswordStrength
bind:password_change={password_input_value} bind:password_change={password_input_value}
@@ -243,7 +243,7 @@
bind:this={username_input} bind:this={username_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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -259,7 +259,7 @@
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
{#if !isEmailValid} {#if !isEmailValid}
<span <span

View File

@@ -1,235 +1,217 @@
<script> <script>
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import store from "../../store"; import store from "../../store";
import isEmail from "validator/es/lib/isEmail"; import isEmail from "validator/es/lib/isEmail";
import { UserService, UserGroupService } from "@odit/lfk-client-js"; import { UserService, UserGroupService } from "@odit/lfk-client-js";
import PromiseError from "../base/PromiseError.svelte"; import PromiseError from "../base/PromiseError.svelte";
import toast from "svelte-french-toast"; import toast from "svelte-french-toast";
export let params; export let params;
const user_promise = UserService.userControllerGetOne(params.userid); const user_promise = UserService.userControllerGetOne(params.userid);
let data_loaded = false; let data_loaded = false;
let usergroups_array_original = []; let usergroups_array_original = [];
const colors = [ const colors = [
"#f3558e", "#f3558e",
"#17b978", "#17b978",
"#3498db", "#3498db",
"#3f3b3b", "#3f3b3b",
"#775ada", "#775ada",
"#7ed6df_#000000", "#7ed6df_#000000",
"#000000", "#000000",
"#21e6c1_#000000", "#21e6c1_#000000",
"#c0392b", "#c0392b",
"#d35400", "#d35400",
"#7f8c8d", "#7f8c8d",
"#6ab04c", "#6ab04c",
"#4834d4", "#4834d4",
"#ff1f5a", "#ff1f5a",
"#eac100", "#eac100",
]; ];
let matched_colors = []; let matched_colors = [];
$: delete_triggered = false; $: delete_triggered = false;
$: original_data = {}; $: original_data = {};
$: editable_userdata = {}; $: editable_userdata = {};
$: allgroups = []; $: allgroups = [];
$: allgroups_ids = []; $: allgroups_ids = [];
$: usergroups_array = []; $: usergroups_array = [];
$: search_permission = ""; $: search_permission = "";
user_promise.then((data) => { user_promise.then((data) => {
let current_target = ""; let current_target = "";
let colorindex = -1; let colorindex = -1;
// alphabetically sort permissions for color compatibility for target // alphabetically sort permissions for color compatibility for target
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_userdata = data; editable_userdata = data;
data.groups.forEach((g) => { data.groups.forEach((g) => {
usergroups_array = usergroups_array.concat([g.id]); usergroups_array = usergroups_array.concat([g.id]);
}); });
usergroups_array_original = usergroups_array; usergroups_array_original = usergroups_array;
allgroups.forEach((g) => { allgroups.forEach((g) => {
allgroups_ids.push(g.id); allgroups_ids.push(g.id);
}); });
}); });
UserGroupService.userGroupControllerGetAll().then((data) => { UserGroupService.userGroupControllerGetAll().then((data) => {
allgroups = data; allgroups = data;
}); });
$: changes_performed = !( $: changes_performed = !(
JSON.stringify(original_data) == JSON.stringify(editable_userdata) JSON.stringify(original_data) == JSON.stringify(editable_userdata)
); );
$: groups_changed = $: groups_changed =
JSON.stringify(usergroups_array) === JSON.stringify(usergroups_array) ===
JSON.stringify(usergroups_array_original); JSON.stringify(usergroups_array_original);
$: save_enabled = $: save_enabled =
(changes_performed || !groups_changed) && isEmail(editable_userdata.email); (changes_performed || !groups_changed) && isEmail(editable_userdata.email);
function submit() { function submit() {
if (data_loaded === true && save_enabled) { if (data_loaded === true && save_enabled) {
editable_userdata.groups = usergroups_array; editable_userdata.groups = usergroups_array;
toast.loading($_("updating-user")); toast.loading($_("updating-user"));
UserService.userControllerPut(original_data.id, editable_userdata) UserService.userControllerPut(original_data.id, editable_userdata)
.then((resp) => { .then((resp) => {
Object.assign(original_data, resp); Object.assign(original_data, resp);
Object.assign(editable_userdata, resp); Object.assign(editable_userdata, resp);
original_data.permissions = resp.permissions; original_data.permissions = resp.permissions;
usergroups_array = []; usergroups_array = [];
resp.groups.forEach((g) => { resp.groups.forEach((g) => {
usergroups_array = usergroups_array.concat([g.id]); usergroups_array = usergroups_array.concat([g.id]);
}); });
usergroups_array_original = usergroups_array; usergroups_array_original = usergroups_array;
// //
toast.dismiss(); toast.dismiss();
toast($_("user-updated")); toast($_("user-updated"));
}) })
.catch((err) => {}); .catch((err) => {});
} }
} }
function deleteUser() { function deleteUser() {
UserService.userControllerRemove(original_data.id, true) UserService.userControllerRemove(original_data.id, true)
.then((resp) => { .then((resp) => {
location.replace("./"); location.replace("./");
}) })
.catch((err) => {}); .catch((err) => {});
} }
</script> </script>
{#await user_promise} {#await user_promise}
<!-- --> <!-- -->
{:then user} {:then user}
<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">
<svg <a class="mr-2" href="./"
class="flex-shrink-0 w-5 h-5 mr-2" ><svg
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
viewBox="0 0 24 24" fill="none"
><path stroke="currentColor"
fill="currentColor" stroke-width="2"
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" stroke-linecap="round"
/></svg stroke-linejoin="round"
> class="inline-block"
</li> ><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></svg
<li class="flex items-center"> >
<a class="mr-2" href="./">{$_("users")}</a><svg {$_("users")}</a
stroke="currentColor" >
fill="none" </li>
stroke-width="2" </ol>
viewBox="0 0 24 24" </nav>
stroke-linecap="round" </div>
stroke-linejoin="round" </div>
class="h-3 w-3 mr-2 stroke-current" <div class="mb-4 text-3xl font-extrabold">
height="1em" {original_data.firstname}
width="1em" {original_data.lastname}
xmlns="http://www.w3.org/2000/svg" <span data-id="user_actions_${editable_userdata.id}">
><line x1="5" y1="12" x2="19" y2="12" /> {#if store.state.jwtinfo.userdetails.permissions.includes("USER:DELETE")}
<polyline points="12 5 19 12 12 19" /></svg {#if delete_triggered}
> <button
</li> on:click={deleteUser}
<li class="flex items-center"> 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"
<span class="mr-2" >{$_("confirm-delete")}</button
>{original_data.firstname} >
{original_data.lastname}</span <button
> on:click={() => {
</li> delete_triggered = !delete_triggered;
</ol> }}
</nav> 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"
</div> >{$_("cancel")}</button
</div> >
<div class="mb-8 text-3xl font-extrabold"> {/if}
{original_data.firstname} {#if !delete_triggered}
{original_data.lastname} <button
<span data-id="user_actions_${editable_userdata.id}"> on:click={() => {
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:DELETE")} delete_triggered = true;
{#if delete_triggered} }}
<button type="button"
on:click={deleteUser} 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-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-user")}</button
>{$_("confirm-delete")}</button >
> {/if}
<button {/if}
on:click={() => { {#if !delete_triggered}
delete_triggered = !delete_triggered; <button
}} disabled={!save_enabled}
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" class:opacity-50={!save_enabled}
>{$_("cancel")}</button type="button"
> 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"
{#if !delete_triggered} >{$_("save-changes")}</button
<button >
on:click={() => { {/if}
delete_triggered = true; </span>
}} </div>
type="button" <div class="mt-3 text-sm w-full">
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" <label for="enabled" class="font-semibold text-gray-700"
>{$_("delete-user")}</button >{$_("active")}?</label
> >
{/if} <br />
{/if} <p class="text-gray-500">
{#if !delete_triggered} <input
<button id="enabled"
disabled={!save_enabled} on:change={() => {
class:opacity-50={!save_enabled} editable_userdata.enabled = !editable_userdata.enabled;
type="button" }}
on:click={submit} name="enabled"
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" type="checkbox"
>{$_("save-changes")}</button checked={editable_userdata.enabled}
> class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
{/if} />
</span> {$_("set-the-user-active-inactive")}
</div> </p>
<div class="mt-3 text-sm w-full"> </div>
<label for="enabled" class="ml-1 font-medium text-gray-700" <div class="text-sm w-full mt-2">
>{$_("active")}?</label <label for="firstname" class="font-semibold text-gray-700"
> >{$_("first-name")}</label
<br /> >
<p class="text-gray-500"> <input
<input autocomplete="off"
id="enabled" placeholder={$_("first-name")}
on:change={() => { type="text"
editable_userdata.enabled = !editable_userdata.enabled; bind:value={editable_userdata.firstname}
}} name="firstname"
name="enabled" 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="checkbox" />
checked={editable_userdata.enabled} </div>
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" <!-- <div class="text-sm w-full mt-2">
/> <label for="middlename" class="font-semibold text-gray-700"
{$_("set-the-user-active-inactive")}
</p>
</div>
<div class="text-sm w-full">
<label for="firstname" class="font-medium text-gray-700"
>{$_("first-name")}</label
>
<input
autocomplete="off"
placeholder={$_("first-name")}
type="text"
bind:value={editable_userdata.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-gray-500 rounded-md p-2"
/>
</div>
<!-- <div class="text-sm w-full">
<label for="middlename" class="font-medium text-gray-700"
>{$_("middle-name")}</label >{$_("middle-name")}</label
> >
<input <input
@@ -238,102 +220,102 @@
type="text" type="text"
bind:value={editable_userdata.middlename} bind:value={editable_userdata.middlename}
name="middlename" name="middlename"
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="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> -->
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="lastname" class="font-medium text-gray-700" <label for="lastname" class="font-semibold text-gray-700"
>{$_("last-name")}</label >{$_("last-name")}</label
> >
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("last-name")} placeholder={$_("last-name")}
type="text" type="text"
bind:value={editable_userdata.lastname} bind:value={editable_userdata.lastname}
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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="email" class="font-medium text-gray-700" <label for="email" class="font-semibold 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")}
type="email" type="email"
bind:value={editable_userdata.email} bind:value={editable_userdata.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-gray-500 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-neutral-800 rounded-md p-2"
/> />
</div> </div>
{#if !isEmail(editable_userdata.email)} {#if !isEmail(editable_userdata.email)}
<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")}</span >{$_("valid-email-is-required")}</span
> >
{/if} {/if}
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<label for="username" class="font-medium text-gray-700" <label for="username" class="font-semibold text-gray-700"
>{$_("username")}</label >{$_("username")}</label
> >
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("username")} placeholder={$_("username")}
type="text" type="text"
bind:value={editable_userdata.username} bind:value={editable_userdata.username}
name="username" name="username"
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="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>
<div class="text-sm w-full"> <div class="text-sm w-full mt-2">
<span class="font-medium">{$_("groups")}</span> <span class="font-semibold">{$_("groups")}</span>
<!-- svelte-ignore a11y-no-onchange --> <!-- svelte-ignore a11y-no-onchange -->
<select <select
bind:value={usergroups_array} bind:value={usergroups_array}
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="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"
multiple multiple
> >
{#each allgroups as g} {#each allgroups as g}
{#if usergroups_array.includes(g.id)} {#if usergroups_array.includes(g.id)}
<option selected value={g.id}>{g.name}</option> <option selected value={g.id}>{g.name}</option>
{:else} {:else}
<option value={g.id}>{g.name}</option> <option value={g.id}>{g.name}</option>
{/if} {/if}
{/each} {/each}
</select> </select>
</div> </div>
<div class="text-sm w-full mt-8"> <div class="text-sm w-full mt-8">
<p class="font-medium mb-4"> <p class="font-medium mb-4">
{$_("permissions")} {$_("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="/users/{params.userid}/permissions/">{$_("edit-permissions")}</a href="/users/{params.userid}/permissions/">{$_("edit-permissions")}</a
> >
</p> </p>
<div class="w-full sm:my-px sm:px-px sm:w-1/2"> <div class="w-full sm:my-px sm:px-px sm:w-1/2">
<input <input
autocomplete="off" autocomplete="off"
placeholder={$_("search-for-permission")} placeholder={$_("search-for-permission")}
type="text" type="text"
bind:value={search_permission} bind:value={search_permission}
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 rounded-md p-2" 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"
/> />
</div> </div>
{#each original_data.permissions as p} {#each original_data.permissions as p}
{#if p.toLowerCase().includes(search_permission.toLowerCase())} {#if p.toLowerCase().includes(search_permission.toLowerCase())}
<span <span
style="background:{matched_colors[ style="background:{matched_colors[
p.split(':')[0] p.split(':')[0]
][0]};color:{matched_colors[p.split(':')[0]][1]};" ][0]};color:{matched_colors[p.split(':')[0]][1]};"
class="mt-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-indigo-100 rounded" 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 >{p}</span
> >
<!-- --> <!-- -->
{/if} {/if}
{/each} {/each}
</div> </div>
</section> </section>
{:catch error} {:catch error}
<PromiseError {error} /> <PromiseError {error} />
{/await} {/await}

View File

@@ -138,7 +138,7 @@
</nav> </nav>
</div> </div>
</div> </div>
<div class="mb-8 text-3xl font-extrabold"> <div class="mb-4 text-3xl font-extrabold">
{$_("permissions")}: {$_("permissions")}:
{original_data.firstname} {original_data.firstname}
{original_data.middlename || ""} {original_data.middlename || ""}
@@ -150,13 +150,13 @@
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: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"
>{$_("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: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-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"
>{$_("applying-changes")}</button >{$_("applying-changes")}</button
> >
{/if} {/if}
@@ -203,7 +203,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: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-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"
>+</button >+</button
> >
</p> </p>
@@ -243,7 +243,7 @@
} }
}} }}
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: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-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"
>-</button >-</button
> >
</p> </p>

View File

@@ -8,20 +8,20 @@
</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">
{$_("users")} {$_("users")}
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:CREATE")} </h4>
<button {#if store.state.jwtinfo.userdetails.permissions.includes("USER: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"
> 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-user")} >
</button> {$_("create-user")}
{/if} </button>
</span> {/if}
<UsersOverview bind:current_users /> <UsersOverview bind:current_users />
</section> </section>

View File

@@ -37,7 +37,7 @@
bind:value={searchvalue} bind:value={searchvalue}
placeholder={$_("datatable.search")} placeholder={$_("datatable.search")}
aria-label={$_("datatable.search")} aria-label={$_("datatable.search")}
class="mb-4" class="mb-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border"
/> />
<!-- {/if} --> <!-- {/if} -->
<!-- <button <!-- <button
@@ -45,7 +45,7 @@
advanced_search = !advanced_search; advanced_search = !advanced_search;
}} }}
type="button" type="button"
class="w-full inline-flex 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"> class="w-full inline-flex 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:w-auto sm:text-sm">
{#if advanced_search} {#if advanced_search}
toggle simple search toggle simple search
{:else}toggle advanced search{/if} {:else}toggle advanced search{/if}
@@ -105,21 +105,21 @@
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
{#if u.enabled} {#if u.enabled}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border bg-green-100 text-green-800"
>{$_("active")}</span >{$_("active")}</span
> >
{:else} {:else}
<span <span
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border bg-red-100 text-red-800"
>{$_("inactive")}</span >{$_("inactive")}</span
> >
{/if} {/if}
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 gap-0.5 flex flex-wrap">
{#each u.groups as g} {#each u.groups as g}
<a <a
href="../groups/{g.id}" href="../groups/{g.id}"
class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full border bg-gray-100 text-gray-800 border-current"
>{g.name}</a >{g.name}</a
> >
{/each} {/each}

View File

@@ -37,6 +37,7 @@
"all-associated-scans-will-get-deleted-as-well": "Alle Scans dieser Station werden ebenfalls gelöscht", "all-associated-scans-will-get-deleted-as-well": "Alle Scans dieser Station werden ebenfalls gelöscht",
"all-associated-teams-and-runners-will-be-deleted-too": "Alle assoziierten Teams und Läufer werden auch gelöscht!", "all-associated-teams-and-runners-will-be-deleted-too": "Alle assoziierten Teams und Läufer werden auch gelöscht!",
"all-cards-loaded": "Alle Karten geladen", "all-cards-loaded": "Alle Karten geladen",
"all-donors-loaded": "Alle Sponsoren geladen",
"already-paid": "Bereits bezahlt", "already-paid": "Bereits bezahlt",
"amount": "Anzahl", "amount": "Anzahl",
"amount-per-kilometer": "Betrag pro Kilometer", "amount-per-kilometer": "Betrag pro Kilometer",
@@ -111,7 +112,7 @@
"create-a-new-scanstation": "Neue Station erstellen", "create-a-new-scanstation": "Neue Station erstellen",
"create-a-new-statsclient": "Neuen Statsclient erstellen", "create-a-new-statsclient": "Neuen Statsclient erstellen",
"create-a-new-team": "Erstelle ein neues Team", "create-a-new-team": "Erstelle ein neues Team",
"create-a-new-track": "Neuen Track erstellen", "create-a-new-track": "Neue Laufstrecke erstellen",
"create-a-new-user": "Neuen Benutzer anlegen", "create-a-new-user": "Neuen Benutzer anlegen",
"create-a-new-user-group": "Erstelle eine neue Gruppe", "create-a-new-user-group": "Erstelle eine neue Gruppe",
"create-and-generate-pdf": "Erstellen und PDF herunterladen", "create-and-generate-pdf": "Erstellen und PDF herunterladen",
@@ -119,7 +120,7 @@
"create-bulk-cards": "Blankokarten erstellen", "create-bulk-cards": "Blankokarten erstellen",
"create-organization": "Organisation erstellen", "create-organization": "Organisation erstellen",
"create-team": "Team erstellen", "create-team": "Team erstellen",
"create-track": "Track erstellen", "create-track": "Laufstrecke erstellen",
"create-user": "Benutzer anlegen", "create-user": "Benutzer anlegen",
"create-without-pdf": "Ohne PDF erstellen", "create-without-pdf": "Ohne PDF erstellen",
"created-blanco-cards": "Blankokarten wurden erstellt", "created-blanco-cards": "Blankokarten wurden erstellt",
@@ -270,6 +271,7 @@
"loading-contact-details": "Kontaktdaten werden geladen ...", "loading-contact-details": "Kontaktdaten werden geladen ...",
"loading-donation-details": "Lade Sponsoringdetails", "loading-donation-details": "Lade Sponsoringdetails",
"loading-donor-details": "Lade Details", "loading-donor-details": "Lade Details",
"loading-donors": "Sponsoren werden geladen",
"loading-group-detail": "Lade Gruppendetails...", "loading-group-detail": "Lade Gruppendetails...",
"loading-profile-data": "Lade Profildaten", "loading-profile-data": "Lade Profildaten",
"loading-runners": "Läufer werden geladen...", "loading-runners": "Läufer werden geladen...",
@@ -452,20 +454,20 @@
"total-paid-amount": "Gezahlt", "total-paid-amount": "Gezahlt",
"total-scans": "Scans", "total-scans": "Scans",
"total_donation_amount_in_eur": "Gesamtbetrag in €", "total_donation_amount_in_eur": "Gesamtbetrag in €",
"track": "Track", "track": "Laufstrecke",
"track-added": "Track hinzugefügt", "track-added": "Laufstrecke hinzugefügt",
"track-data-is-being-loaded": "Trackdaten werden geladen", "track-data-is-being-loaded": "Laufstrecke wird geladen",
"track-deleted": "Track gelöscht", "track-deleted": "Laufstrecke gelöscht",
"track-is-being-added": "Track wird hinzugefügt...", "track-is-being-added": "Laufstrecke wird hinzugefügt...",
"track-is-being-deleted": "Track wird gelöscht", "track-is-being-deleted": "Laufstrecke wird gelöscht",
"track-is-being-updated": "Track wird aktualisiert...", "track-is-being-updated": "Laufstrecke wird aktualisiert...",
"track-length-in-m": "Tracklänge (in Metern)", "track-length-in-m": "Laufstrecke in Metern",
"track-length-must-be-greater-than-0": "Die Länge muss größer als 0 (Meter) sein", "track-length-must-be-greater-than-0": "Die Länge muss größer als 0 (Meter) sein",
"track-name": "Trackname", "track-name": "Name der Laufstrecke",
"track-name-must-not-be-empty": "Der Name muss angegeben werden", "track-name-must-not-be-empty": "Der Name muss angegeben werden",
"track-updated": "Track aktualisiert", "track-updated": "Laufstrecke aktualisiert",
"track-was-updated": "Track wurde aktualisiert", "track-was-updated": "Laufstrecke wurde aktualisiert",
"tracks": "Tracks", "tracks": "Laufstrecken",
"unpaid": "Offen", "unpaid": "Offen",
"update-card": "Karte aktualisieren", "update-card": "Karte aktualisieren",
"update-password": "Passwort ändern", "update-password": "Passwort ändern",

View File

@@ -37,6 +37,7 @@
"all-associated-scans-will-get-deleted-as-well": "All associated scans will get deleted as well", "all-associated-scans-will-get-deleted-as-well": "All associated scans will get deleted as well",
"all-associated-teams-and-runners-will-be-deleted-too": "All associated teams and runners will be deleted too!", "all-associated-teams-and-runners-will-be-deleted-too": "All associated teams and runners will be deleted too!",
"all-cards-loaded": "All cards loaded", "all-cards-loaded": "All cards loaded",
"all-donors-loaded": "All donors loaded",
"already-paid": "Already paid", "already-paid": "Already paid",
"amount": "Amount", "amount": "Amount",
"amount-per-kilometer": "Amount per kilometer", "amount-per-kilometer": "Amount per kilometer",
@@ -270,6 +271,7 @@
"loading-contact-details": "Loading contact details...", "loading-contact-details": "Loading contact details...",
"loading-donation-details": "Loading donation details", "loading-donation-details": "Loading donation details",
"loading-donor-details": "Loading donor details", "loading-donor-details": "Loading donor details",
"loading-donors": "Loading donors",
"loading-group-detail": "Loading group detail...", "loading-group-detail": "Loading group detail...",
"loading-profile-data": "Loading profile data", "loading-profile-data": "Loading profile data",
"loading-runners": "loading runners...", "loading-runners": "loading runners...",

View File

@@ -1,5 +1,11 @@
import "./style.css"; import "./style.css";
import App from "./App.svelte"; import App from "./App.svelte";
import "@fontsource/athiti/200.css";
import "@fontsource/athiti/300.css";
import "@fontsource/athiti/400.css";
import "@fontsource/athiti/500.css";
import "@fontsource/athiti/600.css";
import "@fontsource/athiti/700.css";
const app = new App({ const app = new App({
target: document.body, target: document.body,

View File

@@ -1,3 +1,10 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
.activenav {
@apply bg-gray-300;
@apply text-black;
}
* {
font-family: Athiti;
}
@tailwind utilities; @tailwind utilities;

View File

@@ -1,10 +1,10 @@
import fs from "fs"; import fs from "fs";
const packagejson = JSON.parse( const packagejson = JSON.parse(
fs.readFileSync(`./package.json`, { encoding: "utf-8" }) fs.readFileSync(`./package.json`, { encoding: "utf-8" }),
); );
const original = fs.readFileSync(`./index.html`, { encoding: "utf-8" }); const original = fs.readFileSync(`./index.html`, { encoding: "utf-8" });
let out = original.replace( let out = original.replace(
/RELEASE_INFO-(\S)+-RELEASE_INFO/gi, /RELEASE_INFO-(\S)+-RELEASE_INFO/gi,
"RELEASE_INFO-" + packagejson.version + "-RELEASE_INFO" "RELEASE_INFO-" + packagejson.version + "-RELEASE_INFO",
); );
fs.writeFileSync(`./index.html`, out); fs.writeFileSync(`./index.html`, out);

4675
yarn.lock Normal file

File diff suppressed because it is too large Load Diff