Compare commits

...

20 Commits
1.7.0 ... 1.8.0

Author SHA1 Message Date
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
77 changed files with 7863 additions and 6518 deletions

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

View File

@ -1,26 +0,0 @@
steps:
- name: run full license export
image: registry.odit.services/hub/library/node:19.7.0-alpine3.16
commands:
- npm i -g pnpm@8
- pnpm i
- pnpm licenses:export
- name: build dev
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/frontend
tags:
- dev
- latest
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/frontend:dev
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
event:
- push
branch:
- dev

View File

@ -1,17 +0,0 @@
steps:
- name: build tag
image: woodpeckerci/plugin-docker-buildx
settings:
repo: registry.odit.services/lfk/frontend
tags:
- "${CI_COMMIT_TAG}"
registry: registry.odit.services
platforms: linux/amd64,linux/arm64
cache_from: registry.odit.services/lfk/frontend:latest
username:
from_secret: odit-registry-builder-username
password:
from_secret: odit-registry-builder-password
when:
event:
- tag

View File

@ -2,12 +2,37 @@
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.0](https://git.odit.services/lfk/frontend/compare/1.7.0...1.8.0)
- 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)
- 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) #### [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 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): 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(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) - 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) - 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) #### [1.6.0](https://git.odit.services/lfk/frontend/compare/1.5.3...1.6.0)

View File

@ -1,4 +1,4 @@
FROM registry.odit.services/hub/library/node:23.2.0-alpine3.20 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

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.7.0-RELEASE_INFO</span >RELEASE_INFO-1.8.0-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.7.0", "version": "1.8.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"i18n-order": "node order.js", "i18n-order": "node order.js",
@ -15,10 +15,10 @@
"@odit/license-exporter": "0.2.0", "@odit/license-exporter": "0.2.0",
"@sveltejs/vite-plugin-svelte": "2.1.1", "@sveltejs/vite-plugin-svelte": "2.1.1",
"auto-changelog": "2.5.0", "auto-changelog": "2.5.0",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.21",
"postcss": "8.4.49", "postcss": "8.5.3",
"prettier": "3.3.3", "prettier": "3.5.3",
"prettier-plugin-svelte": "3.2.8", "prettier-plugin-svelte": "3.3.3",
"release-it": "17.10.0", "release-it": "17.10.0",
"svelte-select": "3.17.0", "svelte-select": "3.17.0",
"tailwindcss": "3.4.15", "tailwindcss": "3.4.15",
@ -42,6 +42,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@fontsource/athiti": "^5.2.5",
"@odit/lfk-client-js": "1.1.3", "@odit/lfk-client-js": "1.1.3",
"@paralleldrive/cuid2": "2.2.2", "@paralleldrive/cuid2": "2.2.2",
"@tanstack/svelte-table": "8.9.1", "@tanstack/svelte-table": "8.9.1",
@ -54,7 +55,7 @@
"svelte-french-toast": "1.2.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.12.0", "validator": "13.15.0",
"xlsx": "0.18.5" "xlsx": "0.18.5"
}, },
"volta": { "volta": {

3510
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because one or more lines are too long

View File

@ -163,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,7 +11,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("cards")} {$_("cards")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("CARD:CREATE")}

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: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: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: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,7 +8,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("contacts")} {$_("contacts")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:CREATE")}

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-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" 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,428 +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:activenav={$router.path === "/"} class:activenav={$router.path === "/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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 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("RUNNER:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET")}
<a <a
class:activenav={$router.path === "/runners/"} class:activenav={$router.path === "/runners/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/runners/"
> >
<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"
class="flex-shrink-0 w-5 h-5 mr-2 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"
><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.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>{$_("runners")}</span> <span>{$_("runners")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")}
<a <a
class:activenav={$router.path === "/teams/"} class:activenav={$router.path === "/teams/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/teams/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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 640 512" viewBox="0 0 640 512"
><path ><path
fill="currentColor" fill="currentColor"
d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" 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>{$_("teams")}</span> <span>{$_("teams")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")}
<a <a
class:activenav={$router.path.includes("/orgs/")} class:activenav={$router.path.includes("/orgs/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/orgs/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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" 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="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="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>{$_("orgs")}</span> <span>{$_("orgs")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")}
<a <a
class:activenav={$router.path.includes("/donors/")} class:activenav={$router.path.includes("/donors/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/donors/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"
/></svg /></svg
> >
<span>{$_("donors")}</span> <span>{$_("donors")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")}
<a <a
class:activenav={$router.path.includes("/donations/")} class:activenav={$router.path.includes("/donations/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/donations/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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" 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 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" d="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>{$_("donations")}</span> <span>{$_("donations")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")}
<a <a
class:activenav={$router.path === "/tracks/"} class:activenav={$router.path === "/tracks/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/tracks/" href="/tracks/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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 640 512" viewBox="0 0 640 512"
><path ><path
fill="currentColor" fill="currentColor"
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" 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>{$_("tracks")}</span> <span>{$_("tracks")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")}
<a <a
class:activenav={$router.path === "/cards/"} class:activenav={$router.path === "/cards/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/cards/" href="/cards/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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
fill="currentColor" fill="currentColor"
d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z" d="M22 10v10a1 1 0 01-1 1H3a1 1 0 01-1-1V10h20zm0-2H2V4a1 1 0 011-1h18a1 1 0 011 1v4zm-7 8v2h4v-2h-4z"
/></svg /></svg
> >
<span>{$_("cards")}</span> <span>{$_("cards")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")}
<a <a
class:activenav={$router.path === "/scans/"} 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" 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="/scans/" href="/scans/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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
fill="currentColor" fill="currentColor"
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z"
/></svg /></svg
> >
<span>Scans</span> <span>Scans</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")}
<a <a
class:activenav={$router.path === "/contacts/"} class:activenav={$router.path.includes("/contacts/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/contacts/" href="/contacts/"
> >
<svg <svg
fill="currentColor" fill="currentColor"
class="flex-shrink-0 w-5 h-5 mr-2 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 24 24" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <path
d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z"
/></svg /></svg
> >
<span>{$_("contacts")}</span> <span>{$_("contacts")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")}
<a <a
class:activenav={$router.path === "/scanstations/"} class:activenav={$router.path.includes("/scanstations/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/scanstations/" href="/scanstations/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <path
fill="currentColor" fill="currentColor"
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
> >
<span>{$_("scanstations")}</span> <span>{$_("scanstations")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")}
<a <a
class:activenav={$router.path === "/statsclients/"} class:activenav={$router.path.includes("/statsclients/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 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="/statsclients/" href="/statsclients/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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"
viewBox="0 0 24 24" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><path fill="none" d="M0 0h24v24H0z" /> ><path fill="none" d="M0 0h24v24H0z" />
<path <path
fill="currentColor" fill="currentColor"
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
> >
<span>{$_("statsclients")}</span> <span>{$_("statsclients")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")}
<a <a
class:activenav={$router.path === "/users/"} 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" 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="/users/"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
class="flex-shrink-0 w-5 h-5 mr-2 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"
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 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="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 /></svg
> >
<span>{$_("users")}</span> <span>{$_("users")}</span>
</a> </a>
{/if} {/if}
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")} {#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")}
<a <a
class:activenav={$router.path === "/groups/"} class:activenav={$router.path.includes("/groups/")}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900" 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="/groups/"
> >
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 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 640 512" viewBox="0 0 640 512"
><path ><path
fill="currentColor" fill="currentColor"
d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" d="M610.5 341.3c2.6-14.1 2.6-28.5 0-42.6l25.8-14.9c3-1.7 4.3-5.2 3.3-8.5-6.7-21.6-18.2-41.2-33.2-57.4-2.3-2.5-6-3.1-9-1.4l-25.8 14.9c-10.9-9.3-23.4-16.5-36.9-21.3v-29.8c0-3.4-2.4-6.4-5.7-7.1-22.3-5-45-4.8-66.2 0-3.3.7-5.7 3.7-5.7 7.1v29.8c-13.5 4.8-26 12-36.9 21.3l-25.8-14.9c-2.9-1.7-6.7-1.1-9 1.4-15 16.2-26.5 35.8-33.2 57.4-1 3.3.4 6.8 3.3 8.5l25.8 14.9c-2.6 14.1-2.6 28.5 0 42.6l-25.8 14.9c-3 1.7-4.3 5.2-3.3 8.5 6.7 21.6 18.2 41.1 33.2 57.4 2.3 2.5 6 3.1 9 1.4l25.8-14.9c10.9 9.3 23.4 16.5 36.9 21.3v29.8c0 3.4 2.4 6.4 5.7 7.1 22.3 5 45 4.8 66.2 0 3.3-.7 5.7-3.7 5.7-7.1v-29.8c13.5-4.8 26-12 36.9-21.3l25.8 14.9c2.9 1.7 6.7 1.1 9-1.4 15-16.2 26.5-35.8 33.2-57.4 1-3.3-.4-6.8-3.3-8.5l-25.8-14.9zM496 368.5c-26.8 0-48.5-21.8-48.5-48.5s21.8-48.5 48.5-48.5 48.5 21.8 48.5 48.5-21.7 48.5-48.5 48.5zM96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm224 32c1.9 0 3.7-.5 5.6-.6 8.3-21.7 20.5-42.1 36.3-59.2 7.4-8 17.9-12.6 28.9-12.6 6.9 0 13.7 1.8 19.6 5.3l7.9 4.6c.8-.5 1.6-.9 2.4-1.4 7-14.6 11.2-30.8 11.2-48 0-61.9-50.1-112-112-112S208 82.1 208 144c0 61.9 50.1 112 112 112zm105.2 194.5c-2.3-1.2-4.6-2.6-6.8-3.9-8.2 4.8-15.3 9.8-27.5 9.8-10.9 0-21.4-4.6-28.9-12.6-18.3-19.8-32.3-43.9-40.2-69.6-10.7-34.5 24.9-49.7 25.8-50.3-.1-2.6-.1-5.2 0-7.8l-7.9-4.6c-3.8-2.2-7-5-9.8-8.1-3.3.2-6.5.6-9.8.6-24.6 0-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h255.4c-3.7-6-6.2-12.8-6.2-20.3v-9.2zM173.1 274.6C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z"
/></svg /></svg
> >
<span>{$_("user-groups")}</span> <span>{$_("user-groups")}</span>
</a> </a>
{/if} {/if}
<a <a
class:activenav={$router.path === "/settings/"} class:activenav={$router.path === "/settings/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900" 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 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:activenav={$router.path === "/about/"} class:activenav={$router.path === "/about/"}
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900" 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 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-200 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 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 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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> <span class="sr-only">Menu</span><svg
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /> xmlns="http://www.w3.org/2000/svg"
</svg> fill="none"
</button viewBox="0 0 24 24"
> stroke-width="1.5"
<span class="inline-block"> stroke="currentColor"
<img src="/lfk-logo.png" alt="Logo" class="h-8 inline-block" /> class="size-6"
<span class="text-lg font-bold">LfK!Admin</span> >
</span> <path
</header> stroke-linecap="round"
<Toaster position="top-right" /> stroke-linejoin="round"
<slot> d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
<NoComponentLoaded /> />
</slot> </svg>
</div> </button>
{#if navOpen === true} <span class="inline-block">
<button <img src="/lfk-logo.png" alt="Logo" class="h-8 inline-block" />
on:click={() => { <span class="text-lg font-bold">LfK!Admin</span>
navOpen = false; </span>
}} </header>
class:hidden={!navOpen} <Toaster position="top-right" />
class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden" <slot>
/> <NoComponentLoaded />
{/if} </slot>
</div>
{#if navOpen === true}
<button
on:click={() => {
navOpen = false;
}}
class:hidden={!navOpen}
class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden"
/>
{/if}
</section> </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

@ -7,7 +7,7 @@
</script> </script>
<div class="p-2 md:p-5 overflow-x-hidden"> <div class="p-2 md:p-5 overflow-x-hidden">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("dashboard-greeting")} <span class="text-blue-500" {$_("dashboard-greeting")} <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

View File

@ -192,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}
@ -212,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}
@ -244,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

@ -1,365 +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 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,7 +9,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("donations")} {$_("donations")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:CREATE")}

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,434 +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)
$: changes_performed = !( );
JSON.stringify(original_data) === JSON.stringify(editable) $: isEmailValid =
); (editable.email || "") === "" ||
$: isEmailValid = (editable.email && isEmail(editable.email || ""));
(editable.email || "") === "" || $: isFirstnameValid = editable.firstname !== "";
(editable.email && isEmail(editable.email || "")); $: isLastnameValid = editable.lastname !== "";
$: isFirstnameValid = editable.firstname !== ""; $: save_enabled =
$: isLastnameValid = editable.lastname !== ""; changes_performed &&
$: save_enabled = isFirstnameValid &&
changes_performed && isLastnameValid &&
isFirstnameValid && isEmailValid &&
isLastnameValid && isPhoneValidOrEmpty &&
isEmailValid && ((isAddress1Valid && iszipcodevalid && iscityvalid) ||
isPhoneValidOrEmpty && editable.address_checked === false);
((isAddress1Valid && iszipcodevalid && iscityvalid) || const promise = DonorService.donorControllerGetOne(params.donorid).then(
editable.address_checked === false); (data) => {
const promise = DonorService.donorControllerGetOne(params.donorid).then( data_loaded = true;
(data) => { original_data = Object.assign(original_data, data);
data_loaded = true; editable = Object.assign(editable, original_data);
original_data = Object.assign(original_data, data); editable.address_checked = editable.address.address1 !== null;
editable = Object.assign(editable, original_data); original_data.address_checked = editable.address.address1 !== null;
editable.address_checked = editable.address.address1 !== null; if (editable.address_checked === false) {
original_data.address_checked = editable.address.address1 !== null; editable.address = {
if (editable.address_checked === false) { address1: "",
editable.address = { address2: "",
address1: "", city: "",
address2: "", postalcode: "",
city: "", country: "",
postalcode: "", };
country: "", }
}; }
} );
} $: isPhoneValidOrEmpty =
); editable.phone?.includes("+") ||
$: isPhoneValidOrEmpty = editable.phone === "" ||
editable.phone?.includes("+") || editable.phone === null;
editable.phone === "" || $: isAddress1Valid = editable.address?.address1?.trim().length !== 0;
editable.phone === null; $: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0;
$: isAddress1Valid = editable.address?.address1?.trim().length !== 0; $: iscityvalid = editable.address?.city?.trim().length !== 0;
$: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0; function submit() {
$: iscityvalid = editable.address?.city?.trim().length !== 0; if (data_loaded === true && save_enabled) {
let modal_open = false; toast($_("donor-is-being-updated"));
let delete_donor = {}; editable.address.country = "DE";
function submit() { if (editable.address_checked === false) {
if (data_loaded === true && save_enabled) { editable.address = null;
toast($_("donor-is-being-updated")); }
editable.address.country = "DE"; if (editable.email) editable.email = editable.email;
if (editable.address_checked === false) { else editable.email = null;
editable.address = null; if (editable.phone) editable.phone = editable.phone;
} else editable.phone = null;
if (editable.email) editable.email = editable.email; if (editable.middlename) editable.middlename = editable.middlename;
else editable.email = null; editable.receiptNeeded = editable.address_checked;
if (editable.phone) editable.phone = editable.phone; DonorService.donorControllerPut(original_data.id, editable)
else editable.phone = null; .then((resp) => {
if (editable.middlename) editable.middlename = editable.middlename; Object.assign(original_data, editable);
editable.receiptNeeded = editable.address_checked; original_data = original_data;
DonorService.donorControllerPut(original_data.id, editable) toast.success($_("updated-donor"));
.then((resp) => { })
Object.assign(original_data, editable); .catch((err) => {});
original_data = original_data; } else {
toast.success($_("updated-donor")); }
}) }
.catch((err) => {}); function deleteDonor() {
} else { DonorService.donorControllerRemove(original_data.id, true)
} .then((resp) => {
} toast($_("donor-deleted"));
function deleteDonor() { location.replace("./");
DonorService.donorControllerRemove(original_data.id, false) })
.then((resp) => { .catch((err) => {
toast($_("donor-deleted")); console.log(err);
location.replace("./"); });
}) }
.catch((err) => {
modal_open = true;
delete_donor = original_data;
});
}
</script> </script>
<ConfirmDonorDeletion bind:modal_open bind:delete_donor />
{#await promise} {#await 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 original_data.donations.length > 0} .toLocaleString("de-DE", { valute: "EUR" })}€</a
{#each original_data.donations 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,7 +9,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("donors")} {$_("donors")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:CREATE")}

View File

@ -97,7 +97,7 @@
{/if} {/if}
<!-- /// --> <!-- /// -->
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("about")} {$_("about")}
</h4> </h4>
<p class="mt-2 mb-2"> <p class="mt-2 mb-2">
@ -109,7 +109,7 @@
<br /> <br />
<span>{$_("lfk-is-os")}</span> <span>{$_("lfk-is-os")}</span>
</p> </p>
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("credits")} {$_("credits")}
</h4> </h4>
<p class="text-left">{$_("oss_credit_description")}</p> <p class="text-left">{$_("oss_credit_description")}</p>

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,238 +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 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: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: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: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

@ -133,10 +133,8 @@
</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}
@ -153,7 +151,7 @@
>{$_("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">

View File

@ -8,7 +8,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("user-groups")} {$_("user-groups")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:CREATE")}

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,488 +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 toast from 'svelte-french-toast' import { _ } from "svelte-i18n";
import { tick } from "svelte";
import store from "../../store"; import Select from "svelte-select";
import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte"; import store from "../../store";
import ImportRunnerModal from "../runners/ImportRunnerModal.svelte"; import PromiseError from "../base/PromiseError.svelte";
import PromiseError from "../base/PromiseError.svelte"; import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte";
import Select from "svelte-select"; import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte";
import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte"; import GenerateSponsoringContracts from "../pdf_generation/GenerateSponsoringContracts.svelte";
import GenerateRunnerCards from "../pdf_generation/GenerateRunnerCards.svelte"; import ImportRunnerModal from "../runners/ImportRunnerModal.svelte";
import GenerateRunnerCertificates from "../pdf_generation/GenerateRunnerCertificates.svelte"; import ConfirmOrgDeletion from "./ConfirmOrgDeletion.svelte";
import { tick } from "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; export let params;
let copied = false; $: editable = {};
export let params; $: contact = {};
$: editable = {}; $: data_loaded = false;
$: contact = {}; $: data_changed = !(JSON.stringify(editable) === original);
$: data_loaded = false; $: isAddress1Valid = editable.address?.address1?.trim().length !== 0;
$: data_changed = !(JSON.stringify(editable) === original); $: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0;
$: isAddress1Valid = editable.address?.address1?.trim().length !== 0; $: iscityvalid = editable.address?.city?.trim().length !== 0;
$: iszipcodevalid = editable.address?.postalcode?.trim().length !== 0; $: sponsoring_contracts_show = true;
$: iscityvalid = editable.address?.city?.trim().length !== 0; $: cards_show = true;
$: sponsoring_contracts_show = true; $: certificates_show = true;
$: cards_show = true; $: generate_orgs = [original_object];
$: certificates_show = true; $: registrationLink = `${config.baseurl_selfservice}/register/${editable.registrationKey}`;
$: generate_orgs = [original_object]; const getContactLabel = (option) =>
$: registrationLink = `${config.baseurl_selfservice}/register/${editable.registrationKey}`; option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const getContactLabel = (option) => const promise = RunnerOrganizationService.runnerOrganizationControllerGetOne(
option.firstname + " " + (option.middlename || "") + " " + option.lastname; params.orgid
const promise = RunnerOrganizationService.runnerOrganizationControllerGetOne( ).then((value) => {
params.orgid data_loaded = true;
).then((value) => { value.address_checked = value.address.address1 !== null;
data_loaded = true; if (value.address_checked === false) {
value.address_checked = value.address.address1 !== null; value.address = {
if (value.address_checked === false) { address1: "",
value.address = { address2: "",
address1: "", city: "",
address2: "", postalcode: "",
city: "", country: "",
postalcode: "", };
country: "", }
}; editable = Object.assign(editable, value);
} editable = editable;
editable = Object.assign(editable, value); original_object = Object.assign(editable, value);
editable = editable; original = JSON.stringify(value);
original_object = Object.assign(editable, value); GroupContactService.groupContactControllerGetAll().then((val) => {
original = JSON.stringify(value); contacts = val.map((r) => {
GroupContactService.groupContactControllerGetAll().then((val) => { return { label: getContactLabel(r), value: r };
contacts = val.map((r) => { });
return { label: getContactLabel(r), value: r }; if (editable.contact) {
}); contact = contacts.find((g) => g.value.id == editable.contact.id);
if (editable.contact) { } else {
contact = contacts.find((g) => g.value.id == editable.contact.id); contact = null;
} else { }
contact = null; });
} });
}); let modal_open = false;
}); let delete_org = {};
let modal_open = false; function deleteOrganization() {
let delete_org = {}; RunnerOrganizationService.runnerOrganizationControllerRemove(
function deleteOrganization() { original_object.id,
RunnerOrganizationService.runnerOrganizationControllerRemove( false
original_object.id, )
false .then((resp) => {
) toast($_("organization-deleted"));
.then((resp) => { location.replace("./");
toast($_("organization-deleted")); })
location.replace("./"); .catch((err) => {
}) modal_open = true;
.catch((err) => { delete_org = original_object;
modal_open = true; });
delete_org = original_object; }
}); function submit() {
} if (data_loaded === true && save_enabled) {
function submit() { toast($_("updating-organization"));
if (data_loaded === true && save_enabled) { let postdata = Object.assign({}, editable);
toast($_("updating-organization")); if (postdata.address_checked === false) {
let postdata = Object.assign({}, editable); postdata.address = null;
if (postdata.address_checked === false) { }
postdata.address = null; postdata.contact = postdata.contact?.id;
} RunnerOrganizationService.runnerOrganizationControllerPut(
postdata.contact = postdata.contact?.id; original_object.id,
RunnerOrganizationService.runnerOrganizationControllerPut( postdata
original_object.id, )
postdata .then((resp) => {
) editable.registrationKey = resp.registrationKey;
.then((resp) => { original_object = Object.assign({}, editable);
editable.registrationKey = resp.registrationKey; original = JSON.stringify(original_object);
original_object = Object.assign({}, editable); toast.success($_("updated-organization"));
original = JSON.stringify(original_object); })
toast.success($_("updated-organization")); .catch((err) => {});
}) } else {
.catch((err) => {}); }
} else { }
} async function copy() {
} if (!editable.registrationKey) {
async function copy() { toast.error($_("you-have-to-save-your-changes-to-generate-a-link"));
if (!editable.registrationKey) { return;
toast.error($_("you-have-to-save-your-changes-to-generate-a-link")); }
return; valueCopy = registrationLink;
} await tick();
valueCopy = registrationLink; areaDom.focus();
await tick(); areaDom.select();
areaDom.focus(); try {
areaDom.select(); const successful = document.execCommand("copy");
try { if (!successful) {
const successful = document.execCommand("copy"); throw new Error();
if (!successful) { }
throw new Error(); toast($_("copied-link-to-clipboard"));
} } catch (err) {
toast($_("copied-link-to-clipboard")); toast.error($_("error-whyile-copying-to-clipboard"));
copied = true; }
} catch (err) { // we can notifi by event or storage about copy status
toast.error($_("error-whyile-copying-to-clipboard")); valueCopy = null;
} }
// we can notifi by event or storage about copy status export let import_modal_open = false;
valueCopy = null;
}
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: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: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: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: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 class="text-sm w-full">
<span class="font-medium text-gray-700">{$_("distance")}</span>
<br />
<span class="text-gray-700">{(original_object.total_distance / 1000).toFixed(2)} km</span>
</div>
</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-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" 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,7 +10,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("organizations")} {$_("organizations")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:CREATE")}

View File

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

View File

@ -1,244 +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 toast from "svelte-french-toast";
import DocumentServer from "./DocumentServer.ts" 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); 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 download (blob, fileName){ function download(blob, fileName) {
const url = window.URL.createObjectURL(blob); const url = window.URL.createObjectURL(blob);
let a = document.createElement("a"); let a = document.createElement("a");
a.href = url; a.href = url;
a.download = fileName; a.download = fileName;
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
a.remove(); a.remove();
toast.dismiss(); toast.dismiss();
toast($_("pdf-successfully-generated")); toast($_("pdf-successfully-generated"));
} }
function generateRunnerCards(locale) { function generateRunnerCards(locale) {
cards_dropdown_open = false; if (generate_orgs.length > 0) {
generateOrgCards(locale);
} else if (generate_teams.length > 0) {
generateTeamCards(locale);
} else if (generate_runners.length > 0) {
generateRunnersCards(locale);
} else {
generateCards(locale);
}
}
if (generate_orgs.length > 0) { function generateCards(locale) {
generateOrgCards(locale); toast.loading($_("generating-pdf"));
} else if (generate_teams.length > 0) { documentServer
generateTeamCards(locale); .generateCards(generate_cards, locale)
} else if (generate_runners.length > 0) { .then((blob) => {
generateRunnersCards(locale); download(blob, `${$_("runnercards")}-${locale}-${createId()}.pdf`);
} else { })
generateCards(locale); .catch((err) => {
} console.error(err);
} });
}
function generateCards(locale) { async function generateRunnersCards(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdf"));
documentServer.generateCards(generate_cards, locale) const current_cards = await RunnerCardService.runnerCardControllerGetAll();
.then((blob) => { let cards = [];
download(blob, `${$_("runnercards")}-${locale}-${createId()}.pdf`); for (let runner of generate_runners) {
}) let card = current_cards.find((c) => c.runner?.id == runner.id);
.catch((err) => { if (!card) {
console.error(err); card = await RunnerCardService.runnerCardControllerPost({
}); runner: runner.id,
} });
}
cards.push(card);
}
documentServer
.generateCards(cards, locale)
.then((blob) => {
let fileName = `${$_("runnercards")}-${locale}-${createId()}.pdf`;
if (generate_runners.length == 1) {
fileName = `${$_("runnercards")}_${generate_runners[0].firstname}_${
generate_runners[0].lastname
}-${locale}-${createId()}.pdf`;
}
download(blob, fileName);
})
.catch((err) => {});
}
async function generateRunnersCards(locale) { async function generateTeamCards(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdfs"));
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); let count = 0;
let cards = []; const current_cards = await RunnerCardService.runnerCardControllerGetAll();
for (let runner of generate_runners) { for (const t of generate_teams) {
let card = current_cards.find((c) => c.runner?.id == runner.id); const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
if (!card) { t.id
card = await RunnerCardService.runnerCardControllerPost({ );
runner: runner.id, let cards = [];
}); for (let runner of runners) {
} let card = current_cards.find((c) => c.runner?.id == runner.id);
cards.push(card); if (!card) {
} card = await RunnerCardService.runnerCardControllerPost({
documentServer.generateCards(cards, locale) runner: runner.id,
.then((blob) => { });
let fileName = `${$_("runnercards")}-${locale}-${createId()}.pdf`; }
if (generate_runners.length == 1) { cards.push(card);
fileName = `${$_("runnercards")}_${generate_runners[0].firstname}_${ }
generate_runners[0].lastname documentServer
}-${locale}-${createId()}.pdf`; .generateCards(cards, locale)
} .then((blob) => {
download(blob, fileName); download(
}) blob,
.catch((err) => {}); `${$_("runnercards")}_${t.name}-${locale}-${createId()}.pdf`
} );
})
.catch((err) => {});
}
}
async function generateTeamCards(locale) { async function generateOrgCards(locale) {
toast.loading($_("generating-pdfs")); toast.loading($_("generating-pdfs"));
let count = 0; const current_cards = await RunnerCardService.runnerCardControllerGetAll();
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); 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 cards = []; let runners =
for (let runner of runners) { await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
let card = current_cards.find((c) => c.runner?.id == runner.id); o.id,
if (!card) { false
card = await RunnerCardService.runnerCardControllerPost({ );
runner: runner.id, let cards = [];
}); for (let runner of runners) {
} let card = current_cards.find((c) => c.runner?.id == runner.id);
cards.push(card); if (!card) {
} card = await RunnerCardService.runnerCardControllerPost({
documentServer.generateCards(cards, locale) runner: runner.id,
.then((blob) => { });
download(blob, `${$_("runnercards")}_${ }
t.name cards.push(card);
}-${locale}-${createId()}.pdf`) }
}) await documentServer
.catch((err) => {}); .generateCards(cards, locale)
} .then((blob) => {
} download(
blob,
async function generateOrgCards(locale) { `${$_("runnercards")}_${o.name}_direct-${locale}-${createId()}.pdf`
toast.loading($_("generating-pdfs")); );
const current_cards = await RunnerCardService.runnerCardControllerGetAll(); })
let count = 0; .catch((err) => {});
let count_orgs = 0; for (const t of o.teams) {
for (const o of generate_orgs) { count++;
count_orgs++; let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
let count = 0; t.id
let runners = );
await RunnerOrganizationService.runnerOrganizationControllerGetRunners( let cards = [];
o.id, for (let runner of runners) {
true let card = current_cards.find((c) => c.runner?.id == runner.id);
); if (!card) {
let cards = []; card = await RunnerCardService.runnerCardControllerPost({
for (let runner of runners) { runner: runner.id,
let card = current_cards.find((c) => c.runner?.id == runner.id); });
if (!card) { }
card = await RunnerCardService.runnerCardControllerPost({ cards.push(card);
runner: runner.id, }
}); await documentServer
} .generateCards(cards, locale)
cards.push(card); .then((blob) => {
} download(
await documentServer.generateCards(cards, locale) blob,
.then((blob) => { `${$_("runnercards")}_${o.name}_${
download(blob, `${$_("runnercards")}_${ t.name
o.name }-${locale}-${createId()}.pdf`
}_direct-${locale}-${createId()}.pdf`) );
}) })
.catch((err) => {}); .catch((err) => {});
for (const t of o.teams) { }
count++; }
let runners = await RunnerTeamService.runnerTeamControllerGetRunners( }
t.id
);
let cards = [];
for (let runner of runners) {
let card = current_cards.find((c) => c.runner?.id == runner.id);
if (!card) {
card = await RunnerCardService.runnerCardControllerPost({
runner: runner.id,
});
}
cards.push(card);
}
await documentServer.generateCards(cards, locale)
.then((blob) => {
download(blob, `${$_("runnercards")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`)
})
.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,224 +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";
import DocumentServer from "./DocumentServer"; import DocumentServer from "./DocumentServer";
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); const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
export let certificates_show = false;
export let generate_runners = [];
export let generate_orgs = [];
export let generate_teams = [];
export let certificates_show = false; function generateCertificates(locale) {
export let generate_runners = []; if (generate_orgs.length > 0) {
export let generate_orgs = []; generateOrgCertificates(locale);
export let generate_teams = []; } else if (generate_teams.length > 0) {
$: certificates_dropdown_open = false; generateTeamCertificates(locale);
document.addEventListener("click", function (e) { } else {
if ( generateRunnerCertificates(locale);
e.target.parentNode?.parentNode?.id != "certificates:dropdown" && }
e.target.parentNode?.parentNode?.id != "certificates:dropdown:menu" }
) { function download(blob, fileName) {
certificates_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"));
}
function generateCertificates(locale) { async function generateRunnerCertificates(locale) {
certificates_dropdown_open = false; toast.loading($_("generating-pdf"));
const current_donations =
(await DonationService.donationControllerGetAll()) || [];
let certificateRunners = [];
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) => {});
}
if (generate_orgs.length > 0) { async function generateTeamCertificates(locale) {
generateOrgCertificates(locale); toast.loading($_("generating-pdfs"));
} else if (generate_teams.length > 0) { let count = 0;
generateTeamCertificates(locale); const current_donations =
} else { (await DonationService.donationControllerGetAll()) || [];
generateRunnerCertificates(locale); for (const t of generate_teams) {
} const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
} t.id
function download (blob, fileName){ );
const url = window.URL.createObjectURL(blob); let certificateRunners = [];
let a = document.createElement("a"); for (let runner of runners) {
a.href = url; runner.distanceDonations =
a.download = fileName; current_donations.filter((d) => d.runner?.id == runner.id) || [];
document.body.appendChild(a); certificateRunners.push(runner);
a.click(); }
a.remove(); documentServer
toast.dismiss(); .generateCertificates(certificateRunners, locale)
toast($_("pdf-successfully-generated")); .then((blob) => {
} count++;
download(
blob,
`${$_("certificates")}_${t.name}-${locale}-${createId()}.pdf`
);
})
.catch((err) => {});
}
}
async function generateRunnerCertificates(locale) { async function generateOrgCertificates(locale) {
toast.loading($_("generating-pdf")); toast.loading($_("generating-pdfs"));
const current_donations = const current_donations =
(await DonationService.donationControllerGetAll()) || []; (await DonationService.donationControllerGetAll()) || [];
let certificateRunners = []; let count = 0;
for (let runner of generate_runners) { let count_orgs = 0;
runner.distanceDonations = for (const o of generate_orgs) {
current_donations.filter((d) => d.runner?.id == runner.id) || []; count_orgs++;
certificateRunners.push(runner); let count = 0;
} let runners =
documentServer.generateCertificates(certificateRunners, locale) await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
.then((blob) => { o.id,
let fileName = `${$_("certificates")}-${locale}.pdf` false
if (generate_runners.length == 1) { );
fileName = `${$_("certificates")}_${ let certificateRunners = [];
generate_runners[0].firstname for (let runner of runners) {
}_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`; runner.distanceDonations =
} current_donations.filter((d) => d.runner?.id == runner.id) || [];
download(blob, fileName); certificateRunners.push(runner);
}) }
.catch((err) => {}); await documentServer
} .generateCertificates(certificateRunners, locale)
.then((blob) => {
async function generateTeamCertificates(locale) { download(
toast.loading($_("generating-pdfs")); blob,
let count = 0; `${$_("certificates")}_${o.name}-${locale}-${createId()}.pdf`
const current_donations = );
(await DonationService.donationControllerGetAll()) || []; })
for (const t of generate_teams) { .catch((err) => {});
const runners = await RunnerTeamService.runnerTeamControllerGetRunners( for (const t of o.teams) {
t.id count++;
); let runners = await RunnerTeamService.runnerTeamControllerGetRunners(
let certificateRunners = []; t.id
for (let runner of runners) { );
runner.distanceDonations = let certificateRunners = [];
current_donations.filter((d) => d.runner?.id == runner.id) || []; for (let runner of runners) {
certificateRunners.push(runner); runner.distanceDonations =
} current_donations.filter((d) => d.runner?.id == runner.id) || [];
documentServer.generateCertificates(certificateRunners, locale) certificateRunners.push(runner);
.then((blob) => { }
count++; await documentServer
download(blob, `${$_("certificates")}_${ .generateCertificates(certificateRunners, locale)
t.name .then((blob) => {
}-${locale}-${createId()}.pdf`) download(
}) blob,
.catch((err) => {}); `${$_("certificates")}_${o.name}_${
} t.name
} }-${locale}-${createId()}.pdf`
);
async function generateOrgCertificates(locale) { if (
toast.loading($_("generating-pdfs")); count === o.teams.length &&
const current_donations = count_orgs === generate_orgs.length
(await DonationService.donationControllerGetAll()) || []; ) {
let count = 0; toast.dismiss();
let count_orgs = 0; toast($_("pdfs-successfully-generated"));
for (const o of generate_orgs) { }
count_orgs++; })
let count = 0; .catch((err) => {});
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 documentServer.generateCertificates(certificateRunners, locale)
.then((blob) => {
download(blob, `${$_("certificates")}_${
o.name
}-${locale}-${createId()}.pdf`)
})
.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 documentServer.generateCertificates(certificateRunners, locale)
.then((blob) => {
download(blob, `${$_("certificates")}_${o.name}_${
t.name
}-${locale}-${createId()}.pdf`)
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,188 +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 DocumentServer from "./DocumentServer";
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" }); const createId = init({ length: 10, fingerprint: "lfk-frontend" });
const documentServer = new DocumentServer(config.baseurl_documentserver,config.documentserver_key); const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
export let sponsoring_contracts_show = false;
export let generate_runners = [];
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(
function download (blob, fileName){ o.id,
const url = window.URL.createObjectURL(blob); false
let a = document.createElement("a"); );
a.href = url; await documentServer
a.download = fileName; .generateContracts(runners, locale)
document.body.appendChild(a); .then((blob) => {
a.click(); download(
a.remove(); blob,
toast.dismiss(); `${$_("sponsorings")}_${o.name}_direct-${locale}-${createId()}.pdf`
toast($_("pdf-successfully-generated")); );
} })
.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}_${
documentServer.generateContracts(runners, locale) generate_runners[0].lastname
.then((blob) => { }-${locale}-${createId()}.pdf`;
download(blob, `${$_("sponsorings")}_${ }
t.name download(blob, fileName);
}-${locale}-${createId()}.pdf`) })
}) .catch((err) => {
.catch((err) => {}); console.error(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 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) => {});
}
}
}
function generateRunnerContracts(locale) {
toast.loading($_("generating-pdf"));
documentServer.generateContracts(generate_runners, locale)
.then((blob) => {
let fileName = `${$_("sponsorings")}-${locale}-${createId()}.pdf`
if (generate_runners.length == 1) {
fileName= `${$_("sponsorings")}_${generate_runners[0].firstname}_${
generate_runners[0].lastname
}-${locale}-${createId()}.pdf`;
}
download(blob, fileName);
})
.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

@ -189,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
@ -212,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">
@ -231,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
@ -248,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
@ -281,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
@ -309,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: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: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: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,7 +11,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("runners")} {$_("runners")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:CREATE")}

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,7 +9,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("scans")} {$_("scans")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:CREATE")}

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

@ -119,7 +119,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_station.key} {new_station.key}
</p> </p>

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: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: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: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

@ -24,7 +24,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("scanstations")} {$_("scanstations")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATION:CREATE")}
@ -111,7 +111,7 @@
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900">
<a <a
href="../tracks" href="../tracks"
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"
> >
{s.track.name || s.track.distance + "m"}</a {s.track.name || s.track.distance + "m"}</a
> >
@ -132,12 +132,12 @@
<div class="flex items-center"> <div class="flex items-center">
{#if s.enabled} {#if s.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"
>{$_("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 border-current bg-red-100 text-red-800"
>{$_("inactive")}</span >{$_("inactive")}</span
> >
{/if} {/if}

View File

@ -62,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>
@ -91,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
@ -101,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
@ -114,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)}
@ -123,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
@ -133,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
@ -146,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
@ -159,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>
@ -184,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>
@ -199,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">
@ -212,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">
@ -255,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

@ -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

@ -96,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: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: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

@ -11,7 +11,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("statsclients")} {$_("statsclients")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:CREATE")}

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,308 +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 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: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: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: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: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>
<div class="text-sm w-full">
<span class="font-medium text-gray-700">{$_("distance")}</span>
<br />
<span class="text-gray-700"
>{(original.total_distance / 1000).toFixed(2)} km</span
>
</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,7 +8,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("teams")} {$_("teams")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:CREATE")}

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-2 w-full sm:w-auto mt-1 sm:mt-0 p-2 rounded-md border" 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

View File

@ -19,7 +19,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("tracks")} {$_("tracks")}
</h4> </h4>
<button <button
@ -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: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: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: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 || ""}

View File

@ -8,7 +8,7 @@
</script> </script>
<section class="container p-5"> <section class="container p-5">
<h4 class="mb-1 text-3xl font-extrabold leading-tight font-mono"> <h4 class="mb-1 text-3xl font-extrabold leading-tight">
{$_("users")} {$_("users")}
</h4> </h4>
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:CREATE")} {#if store.state.jwtinfo.userdetails.permissions.includes("USER:CREATE")}

View File

@ -115,11 +115,11 @@
> >
{/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 border 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

@ -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,7 +1,10 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
.activenav { .activenav {
@apply bg-gray-300; @apply bg-gray-300;
@apply text-black; @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);