Compare commits

..

1 Commits

Author SHA1 Message Date
bbad338ced Merge pull request 'Merge Minor into main' (#113) from dev into main
Reviewed-on: #113
Reviewed-by: Philipp Dormann <philipp@philippdormann.de>
2021-03-26 16:34:53 +00:00
182 changed files with 12010 additions and 22383 deletions

View File

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

84
.drone.yml Normal file
View File

@@ -0,0 +1,84 @@
---
kind: secret
name: docker_username
get:
path: odit-registry-builder
name: username
---
kind: secret
name: docker_password
get:
path: odit-registry-builder
name: password
---
kind: secret
name: git_ssh
get:
path: odit-git-bot
name: sshkey
---
kind: pipeline
type: kubernetes
name: build:dev
steps:
- name: run full license export
depends_on: ["clone"]
image: node:alpine
commands:
- yarn
- yarn licenses:export
- name: push new licenses file to repo
depends_on: ["run full license export"]
image: appleboy/drone-git-push
settings:
branch: dev
commit: true
commit_message: new license file version [CI SKIP]
author_email: bot@odit.services
remote: git@git.odit.services:lfk/frontend.git
ssh_key:
from_secret: git_ssh
- name: build dev
image: plugins/docker
depends_on: [clone]
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: registry.odit.services/lfk/frontend
tags:
- dev
registry: registry.odit.services
mtu: 1000
trigger:
branch:
- dev
event:
- push
---
kind: pipeline
type: kubernetes
name: build:tags
steps:
- name: build $DRONE_TAG
image: plugins/docker
depends_on: [clone]
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: registry.odit.services/lfk/frontend
tags:
- '${DRONE_TAG}'
registry: registry.odit.services
mtu: 1000
trigger:
event:
- tag

View File

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

11
.gitignore vendored
View File

@@ -1,8 +1,11 @@
node_modules node_modules
build
package-lock.json
yarn.lock
*.map *.map
public/env.js public/env.js
public/sw.js
public/index.html
public/workbox-*.js
svelte.config.js
public/index.html public/index.html
/dist
.pnpm-store
.yarn
.pnp.*

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,18 @@
FROM registry.odit.services/hub/library/node:23.10.0-alpine3.21 AS build FROM node:15.5.1-alpine3.12
ARG NPM_REGISTRY_URL=https://registry.npmjs.org
WORKDIR /app WORKDIR /app
RUN npm i -g pnpm
COPY package.json pnpm-lock.yaml vite.config.js index.html ./ COPY package.json ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.8 RUN pnpm i
RUN mkdir /pnpm && pnpm config set store-dir /pnpm && pnpm i COPY package.json *.config.js workbox-config.js template-copy.js index.template.html s-config.template.js ./
COPY src ./src COPY src ./src
COPY public ./public COPY public ./public
RUN pnpm build RUN pnpm run build
# final image # final image
FROM registry.odit.services/library/nginx-brotli:3.15 AS final FROM alpine
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=0 /app/build /app
RUN rm -rf /app/build/_dist_/components
RUN rm -rf /app/build/_dist_/locales
RUN rm -rf /app/build-manifest.json
FROM fholzer/nginx-brotli:v1.19.1
COPY --from=1 /app /usr/share/nginx/html
COPY ./nginx.conf /etc/nginx/nginx.conf COPY ./nginx.conf /etc/nginx/nginx.conf

View File

@@ -1,27 +0,0 @@
# @odit/lfk-frontend
## ✒️ Overview
This is an API client for [https://git.odit.services/lfk/backend](@lfk/backend)
This application is intended for use by admin users/ members only.
## 🚀 Getting Started
```
pnpm i
```
## Development
```
pnpm dev
/
pnpm dev --open
```
## Build
```
pnpm build
```

View File

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

View File

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

23
index.template.html Normal file
View File

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

View File

@@ -6,20 +6,6 @@ http {
server { server {
error_page 404 /index.html; error_page 404 /index.html;
root /usr/share/nginx/html; root /usr/share/nginx/html;
location /assets {
expires 1y;
log_not_found off;
access_log off;
}
location = /index.html {
add_header Cache-Control 'no-store';
}
location = / {
add_header Cache-Control 'no-store';
}
location = /env.js {
add_header Cache-Control 'no-store';
}
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }

View File

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

View File

@@ -1,67 +1,61 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.14.2", "version": "0.9.0",
"type": "module", "scripts": {
"scripts": { "i18n-order": "node order.js",
"i18n-order": "node order.js", "dev:all": "yarn prebuild && snowpack dev",
"dev": "vite", "dev": "cross-env NODE_ENV_ODIT=development_fast node template-copy.js && yarn build:sw && snowpack dev",
"format": "prettier --write --plugin-search-dir=. .", "build": "yarn prebuild && snowpack build",
"build": "vite build", "prebuild": "cross-env NODE_ENV_ODIT=production node template-copy.js && yarn build:sw",
"release": "release-it --only-version", "build:sw": "workbox generateSW workbox-config.js",
"licenses:export": "license-exporter --json -o public" "release": "release-it",
}, "licenses:export": "license-exporter --json -o public"
"license": "CC-BY-NC-SA-4.0", },
"devDependencies": { "license": "CC-BY-NC-SA-4.0",
"@odit/license-exporter": "0.2.0", "dependencies": {
"@sveltejs/vite-plugin-svelte": "2.1.1", "@odit/lfk-client-js": "0.7.0",
"@types/papaparse": "^5.3.15", "csvtojson": "^2.0.10",
"@types/underscore": "^1.13.0", "gridjs": "3.3.0",
"auto-changelog": "2.5.0", "localforage": "1.9.0",
"prettier": "3.5.3", "marked": "^2.0.1",
"prettier-plugin-svelte": "3.3.3", "svelte-focus-trap": "1.0.1",
"release-it": "17.10.0", "svelte-i18n": "3.3.7",
"svelte-select": "3.17.0", "svelte-select": "^3.17.0",
"vite": "6.3.2", "tailwindcss": "2.0.3",
"vite-plugin-mkcert": "^1.17.8" "tinro": "0.6.1",
}, "toastify-js": "1.9.3",
"release-it": { "validator": "13.5.2",
"git": { "xlsx": "^0.16.9"
"commit": true, },
"requireCleanWorkingDir": false, "devDependencies": {
"commitMessage": "chore(release): ${version}", "@odit/license-exporter": "^0.0.11",
"push": true, "@snowpack/plugin-svelte": "3.5.2",
"tag": true, "auto-changelog": "^2.2.1",
"tagName": "${version}", "autoprefixer": "10.2.5",
"tagAnnotation": "${version}" "cross-env": "^7.0.3",
}, "postcss": "8.2.8",
"npm": { "postcss-load-config": "3.0.1",
"publish": false "release-it": "^14.4.1",
}, "snowpack": "3.0.13",
"hooks": { "svelte": "3.35.0",
"after:bump": "npx auto-changelog --commit-limit false -p -u --hide-credit && git add CHANGELOG.md && node versionbuilder.js && git add index.html && node order.js && git add src/locales" "svelte-preprocess": "4.6.9",
} "workbox-cli": "6.1.2"
}, },
"dependencies": { "release-it": {
"@bwip-js/browser": "^4.6.0", "git": {
"@fontsource/athiti": "^5.2.5", "commit": true,
"@odit/lfk-client-js": "1.2.7", "requireCleanWorkingDir": false,
"@paralleldrive/cuid2": "2.2.2", "commitMessage": "🚀RELEASE v${version}",
"@tailwindcss/vite": "^4.1.4", "push": false,
"@tanstack/svelte-table": "8.9.1", "tag": true,
"check-password-strength": "2.0.10", "tagName": null,
"html5-qrcode": "^2.3.8", "tagAnnotation": "v${version}"
"localforage": "1.10.0", },
"papaparse": "^5.5.2", "npm": {
"svelte": "3.58.0", "publish": false
"svelte-french-toast": "1.2.0", },
"svelte-i18n": "4.0.1", "hooks": {
"tailwindcss": "^4.1.4", "after:bump": "npx auto-changelog --commit-limit false -p -u --hide-credit && git add CHANGELOG.md && node versionbuilder.js && git add index.template.html && node order.js && git add src/locales"
"tinro": "0.6.12", }
"underscore": "^1.13.7", }
"validator": "13.15.0", }
"xlsx": "0.18.5"
},
"volta": {
"node": "20.0.0"
}
}

4154
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

4
public/logo.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

1
public/privacy_en.md Normal file
View File

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

6
s-config.template.js Normal file
View File

@@ -0,0 +1,6 @@
const sveltePreprocess = require('svelte-preprocess');
const preprocess = sveltePreprocess(__insert__);
module.exports = {
preprocess
};

26
snowpack.config.js Normal file
View File

@@ -0,0 +1,26 @@
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
public: '/',
src: '/_dist_'
},
plugins: [ '@snowpack/plugin-svelte' ],
routes: [
/* Enable an SPA Fallback in development: */
{ match: 'routes', src: '.*', dest: '/index.html' }
],
packageOptions: {
/* ... */
sourceMap: false
},
devOptions: {
/* ... */
},
buildOptions: {
/* ... */
},
alias: {
/* ... */
},
optimize: { bundle: true, minify: true }
};

View File

@@ -1,236 +1,224 @@
<script> <script>
import { Route, router } from "tinro"; import "./TailwindStyles.svelte";
router.subscribe((routeInfo) => { import "toastify-js/src/toastify.css";
window.scrollTo(0, 0); import "gridjs/dist/theme/mermaid.css";
}); import { Route, router } from "tinro";
if (config.prefersHashRouting) { router.subscribe((routeInfo) => {
if (config.prefersHashRouting === true) { window.scrollTo(0, 0);
router.useHashNavigation(); });
} if (config.prefersHashRouting) {
} if (config.prefersHashRouting === true) {
import localForage from "localforage"; router.useHashNavigation();
import { addMessages, init, getLocaleFromNavigator } from "svelte-i18n"; }
import en from "./locales/en.json"; }
import de from "./locales/de.json"; import localForage from "localforage";
addMessages("en", en); import { addMessages, init, getLocaleFromNavigator } from "svelte-i18n";
addMessages("de", de); import en from "./locales/en.json";
init({ import de from "./locales/de.json";
fallbackLocale: "en", addMessages("en", en);
initialLocale: getLocaleFromNavigator(), addMessages("de", de);
}); init({
localForage.config({ fallbackLocale: "en",
name: "lfk_admin", initialLocale: getLocaleFromNavigator(),
version: 1.0, });
storeName: "lfk_admin", localForage.config({
description: "LfK! admin dashboard", name: "lfk_admin",
}); version: 1.0,
window.onunhandledrejection = (event) => { storeName: "lfk_admin",
if (event.reason.toString() == "Error: Unauthorized") { description: "LfK! admin dashbaord",
localForage.clear(); });
location.replace("/"); window.onunhandledrejection = (event) => {
} if (event.reason.toString() == "Error: Unauthorized") {
}; console.log("Found 1");
// localForage.clear();
import Login from "./components/auth/Login.svelte"; location.replace("/");
import Dashboard from "./components/dashboard/Dashboard.svelte"; }
import store from "./store.js"; };
import ForgotPassword from "./components/auth/ForgotPassword.svelte"; //
import MainDashContent from "./components/dashboard/MainDashContent.svelte"; import Login from "./components/auth/Login.svelte";
import Users from "./components/users/Users.svelte"; import Dashboard from "./components/dashboard/Dashboard.svelte";
import About from "./components/general/About.svelte"; import store from "./store.js";
import Settings from "./components/settings/Settings.svelte"; import ForgotPassword from "./components/auth/ForgotPassword.svelte";
import Transition from "./components/base/Transition.svelte"; import MainDashContent from "./components/dashboard/MainDashContent.svelte";
import Orgs from "./components/orgs/Orgs.svelte"; import Users from "./components/users/Users.svelte";
import CardAssignment from "./components/tools/CardAssignment.svelte"; import About from "./components/general/About.svelte";
import Runners from "./components/runners/Runners.svelte"; import Settings from "./components/settings/Settings.svelte";
import Footer from "./components/general/Footer.svelte"; import Transition from "./components/base/Transition.svelte";
import TracksOverview from "./components/tracks/TracksOverview.svelte"; import Orgs from "./components/orgs/Orgs.svelte";
import OrgDetail from "./components/orgs/OrgDetail.svelte"; import Runners from "./components/runners/Runners.svelte";
import Teams from "./components/teams/Teams.svelte"; import Footer from "./components/general/Footer.svelte";
import { OpenAPI } from "@odit/lfk-client-js"; import TracksOverview from "./components/tracks/TracksOverview.svelte";
import UserDetail from "./components/users/UserDetail.svelte"; import OrgDetail from "./components/orgs/OrgDetail.svelte";
OpenAPI.BASE = config.baseurl; import Teams from "./components/teams/Teams.svelte";
import TeamDetail from "./components/teams/TeamDetail.svelte"; import { OpenAPI } from "@odit/lfk-client-js";
import UserPermissions from "./components/users/UserPermissions.svelte"; import UserDetail from "./components/users/UserDetail.svelte";
import GroupPermissions from "./components/groups/GroupPermissions.svelte"; OpenAPI.BASE = config.baseurl;
import RunnerDetail from "./components/runners/RunnerDetail.svelte"; import { register as registerSW } from "./swmodule";
import ResetPassword from "./components/auth/ResetPassword.svelte"; import TeamDetail from "./components/teams/TeamDetail.svelte";
import Contacts from "./components/contacts/Contacts.svelte"; import UserPermissions from "./components/users/UserPermissions.svelte";
import ContactDetail from "./components/contacts/ContactDetail.svelte"; import GroupPermissions from "./components/groups/GroupPermissions.svelte";
import Donors from "./components/donors/Donors.svelte"; import RunnerDetail from "./components/runners/RunnerDetail.svelte";
import Groups from "./components/groups/Groups.svelte"; import Imprint from "./components/general/Imprint.svelte";
import DonorDetail from "./components/donors/DonorDetail.svelte"; import Privacy from "./components/general/Privacy.svelte";
import Donations from "./components/donations/Donations.svelte"; import ResetPassword from "./components/auth/ResetPassword.svelte";
import DonationDetail from "./components/donations/DonationDetail.svelte"; import Contacts from "./components/contacts/Contacts.svelte";
import GroupDetail from "./components/groups/GroupDetail.svelte"; import ContactDetail from "./components/contacts/ContactDetail.svelte";
import ScanStations from "./components/scanstations/ScanStations.svelte"; import Donors from "./components/donors/Donors.svelte";
import ScanStationDetail from "./components/scanstations/ScanStationDetail.svelte"; import Groups from "./components/groups/Groups.svelte";
import Scans from "./components/scans/Scans.svelte"; import DonorDetail from "./components/donors/DonorDetail.svelte";
import ScanDetail from "./components/scans/ScanDetail.svelte"; import Donations from "./components/donations/Donations.svelte";
import Cards from "./components/cards/Cards.svelte"; import DonationDetail from "./components/donations/DonationDetail.svelte";
import StatsClients from "./components/statsclients/StatsClients.svelte"; import GroupDetail from "./components/groups/GroupDetail.svelte";
import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte"; import ScanStations from "./components/scanstations/ScanStations.svelte";
import CardReplacement from "./components/tools/CardReplacement.svelte"; import ScanStationDetail from "./components/scanstations/ScanStationDetail.svelte";
import ScanClient from "./components/tools/ScanClient.svelte"; import Scans from "./components/scans/Scans.svelte";
import DonationCreate from "./components/tools/DonationCreate.svelte"; import ScanDetail from "./components/scans/ScanDetail.svelte";
store.init(); import Cards from "./components/cards/Cards.svelte";
</script> store.init();
registerSW();
<Route> </script>
{#if $router.path === "/forgot_password"}
<Route path="/forgot_password"> <Route>
<ForgotPassword /> {#if $router.path === '/forgot_password'}
</Route> <Route path="/forgot_password">
{:else if $router.path.includes("/reset")} <ForgotPassword />
<Route path="/reset/:resetkey" let:params> </Route>
<ResetPassword {params} /> {:else if $router.path.includes('/reset')}
</Route> <Route path="/reset/:resetkey" let:params>
{:else if $router.path === "/about"} <ResetPassword {params} />
<Route path="/about"> </Route>
<About /> {:else if $router.path === '/about'}
</Route> <Route path="/about">
{:else if $store.isLoggedIn} <About />
<Dashboard> </Route>
<Transition> {:else if $router.path === '/imprint'}
<Route path="/"> <Route path="/imprint">
<MainDashContent /> <Imprint />
</Route> </Route>
<Route path="/users/*"> {:else if $router.path === '/privacy'}
<Route path="/"> <Route path="/privacy">
<Users /> <Privacy />
</Route> </Route>
<Route path="/:userid/*" let:params> {:else if $store.isLoggedIn}
<Route path="/"> <Dashboard>
<UserDetail {params} /> <Transition>
</Route> <Route path="/">
<Route path="/permissions/"> <MainDashContent />
<UserPermissions {params} /> </Route>
</Route> <Route path="/users/*">
</Route> <Route path="/">
</Route> <Users />
<Route path="/groups/*"> </Route>
<Route path="/"> <Route path="/:userid/*" let:params>
<Groups /> <Route path="/">
</Route> <UserDetail {params} />
<Route path="/:groupid/*" let:params> </Route>
<Route path="/"> <Route path="/permissions/">
<GroupDetail {params} /> <UserPermissions {params} />
</Route> </Route>
<Route path="/permissions/"> </Route>
<GroupPermissions {params} /> </Route>
</Route> <Route path="/groups/*">
</Route> <Route path="/">
</Route> <Groups />
<Route path="/tracks/*"> </Route>
<Route path="/"> <Route path="/:groupid/*" let:params>
<TracksOverview /> <Route path="/">
</Route> <GroupDetail {params} />
<Route path="/:trackid" let:params /> </Route>
</Route> <Route path="/permissions/">
<Route path="/runners/*"> <GroupPermissions {params} />
<Route path="/" let:meta> </Route>
<Runners created_via={meta.query.created_via} /> </Route>
</Route> </Route>
<Route path="/:runnerid" let:params> <Route path="/tracks/*">
<RunnerDetail {params} /> <Route path="/">
</Route> <TracksOverview />
</Route> </Route>
<Route path="/tools/*"> <Route path="/:trackid" let:params />
<Route path="/cardassignment/"> </Route>
<CardAssignment /> <Route path="/runners/*">
</Route> <Route path="/">
<Route path="/cardreplacement/"> <Runners />
<CardReplacement /> </Route>
</Route> <Route path="/:runnerid" let:params>
<Route path="/scanclient/"> <RunnerDetail {params} />
<ScanClient /> </Route>
</Route> </Route>
<Route path="/donationcreate/"> <Route path="/teams/*">
<DonationCreate /> <Route path="/">
</Route> <Teams />
</Route> </Route>
<Route path="/teams/*"> <Route path="/:teamid" let:params>
<Route path="/"> <TeamDetail {params} />
<Teams /> </Route>
</Route> </Route>
<Route path="/:teamid" let:params> <Route path="/contacts/*">
<TeamDetail {params} /> <Route path="/">
</Route> <Contacts />
</Route> </Route>
<Route path="/contacts/*"> <Route path="/:contact" let:params>
<Route path="/"> <ContactDetail {params} />
<Contacts /> </Route>
</Route> </Route>
<Route path="/:contact" let:params> <Route path="/orgs/*">
<ContactDetail {params} /> <Route path="/">
</Route> <Orgs />
</Route> </Route>
<Route path="/orgs/*"> <Route path="/:orgid" let:params>
<Route path="/"> <OrgDetail {params} />
<Orgs /> </Route>
</Route> </Route>
<Route path="/:orgid" let:params> <Route path="/donors/*">
<OrgDetail {params} /> <Route path="/">
</Route> <Donors />
</Route> </Route>
<Route path="/donors/*"> <Route path="/:donorid" let:params>
<Route path="/"> <DonorDetail {params} />
<Donors /> </Route>
</Route> </Route>
<Route path="/:donorid" let:params> <Route path="/donations/*">
<DonorDetail {params} /> <Route path="/">
</Route> <Donations />
</Route> </Route>
<Route path="/donations/*"> <Route path="/:donationid" let:params>
<Route path="/"> <DonationDetail {params} />
<Donations /> </Route>
</Route> </Route>
<Route path="/:donationid" let:params> <Route path="/cards/*">
<DonationDetail {params} /> <Route path="/">
</Route> <Cards />
</Route> </Route>
<Route path="/cards/*"> <!-- <Route path="/:scanid" let:params>
<Route path="/"> <ScanDetail {params} />
<Cards /> </Route> -->
</Route> </Route>
<!-- <Route path="/:scanid" let:params> <Route path="/scans/*">
<ScanDetail {params} /> <Route path="/">
</Route> --> <Scans />
</Route> </Route>
<Route path="/scans/*"> <Route path="/:scanid" let:params>
<Route path="/"> <ScanDetail {params} />
<Scans /> </Route>
</Route> </Route>
<Route path="/:scanid" let:params> <Route path="/scanstations/*">
<ScanDetail {params} /> <Route path="/">
</Route> <ScanStations />
</Route> </Route>
<Route path="/scanstations/*"> <Route path="/:stationid" let:params>
<Route path="/"> <ScanStationDetail {params} />
<ScanStations /> </Route>
</Route> </Route>
<Route path="/:stationid" let:params> <Route path="/about">
<ScanStationDetail {params} /> <About />
</Route> </Route>
</Route> <Route path="/settings">
<Route path="/statsclients/*"> <Settings />
<Route path="/"> </Route>
<StatsClients /> </Transition>
</Route> <Footer />
<Route path="/:clientid" let:params> </Dashboard>
<StatsClientDetail {params} /> {:else}
</Route> <Login />
</Route> {/if}
<Route path="/about"> </Route>
<About />
</Route>
<Route path="/settings">
<Settings />
</Route>
</Transition>
<Footer />
</Dashboard>
{:else}
<Login />
{/if}
</Route>

View File

@@ -0,0 +1,6 @@
<style global>
/*! @import */
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
<!--
Temporary tailwind import fixes for classes that wouldn't be directly used otherwise.
Or as others may call it: Real big bullshit time.
Issue: https://git.odit.services/lfk/frontend/issues/136
-->
<div class="opacity-50" />

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,165 @@
<script>
import { StatsService } from "@odit/lfk-client-js";
import { _ } from "svelte-i18n";
const stats_promise = StatsService.statsControllerGet();
</script>
<!-- -->
<h1>{$_('general-stats')}</h1>
{#await stats_promise}
<div
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2"
role="alert">
<p class="font-bold">{$_('stats-are-being-loaded')}</p>
<p class="text-sm">{$_('this-might-take-a-moment')}</p>
</div>
{:then stats}
<div
class="flex flex-col lg:flex-row w-full lg:space-x-2 space-y-2 lg:space-y-0 mb-2 lg:mb-4">
<a href="/runners/" class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('runners')}
</div>
<div class="text-xl font-bold">{stats.total_runners}</div>
</div>
<svg
height="24"
width="24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none" />
<path
d="M13.49 5.48c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm-3.6 13.9l1-4.4 2.1 2v6h2v-7.5l-2.1-2 .6-3c1.3 1.5 3.3 2.5 5.5 2.5v-2c-1.9 0-3.5-1-4.3-2.4l-1-1.6c-.4-.6-1-1-1.7-1-.3 0-.5.1-.8.1l-5.2 2.2v4.7h2v-3.4l1.8-.7-1.6 8.1-4.9-1-.4 2 7 1.4z" /></svg>
</div>
</div>
</a>
<div class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('total-scans')}
</div>
<div class="text-xl font-bold">{stats.total_scans}</div>
</div><svg
stroke="currentColor"
fill="currentColor"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
size="24"
class="stroke-current text-grey-500"
height="24"
width="24"
xmlns="http://www.w3.org/2000/svg"><polyline
points="22 12 18 12 15 21 9 3 6 12 2 12" /></svg>
</div>
</div>
</div>
<div class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('total-donations')}
</div>
<div class="text-xl font-bold">{stats.total_donation}</div>
</div><svg
xmlns="http://www.w3.org/2000/svg"
height="24"
fill="currentColor"
width="24"><path d="M0 0h24v24H0z" fill="none" />
<path
d="M15 18.5A6.48 6.48 0 019.24 15H15v-2H8.58c-.05-.33-.08-.66-.08-1s.03-.67.08-1H15V9H9.24A6.491 6.491 0 0115 5.5c1.61 0 3.09.59 4.23 1.57L21 5.3A8.955 8.955 0 0015 3c-3.92 0-7.24 2.51-8.48 6H3v2h3.06a8.262 8.262 0 000 2H3v2h3.52c1.24 3.49 4.56 6 8.48 6 2.31 0 4.41-.87 6-2.3l-1.78-1.77c-1.13.98-2.6 1.57-4.22 1.57z" /></svg>
</div>
</div>
</div>
<div class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('total-distance')}
</div>
<div class="text-xl font-bold">
{stats.total_distance / 1000}
km
</div>
</div>
<svg
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
height="24"
width="24"><path d="M0 0h24v24H0z" fill="none" />
<path
d="M21 6H3c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 10H3V8h2v4h2V8h2v4h2V8h2v4h2V8h2v4h2V8h2v8z" /></svg>
</div>
</div>
</div>
<a href="/teams/" class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('count_teams')}
</div>
<div class="text-xl font-bold">{stats.total_teams}</div>
</div>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
size="24"
class="stroke-current text-grey-500"
height="24"
width="24"
xmlns="http://www.w3.org/2000/svg"><path
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" /></svg>
</div>
</div>
</a>
<a href="/orgs/" class="w-full lg:w-1/4">
<div
class="widget w-full p-4 rounded-lg bg-white border border-grey-100">
<div class="flex flex-row items-center justify-between">
<div class="flex flex-col">
<div class="text-xs uppercase font-light text-grey-500">
{$_('count_organizations')}
</div>
<div class="text-xl font-bold">{stats.total_orgs}</div>
</div>
<svg
height="24"
fill="currentColor"
width="24"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
<path
d="M17 11V3H7v4H3v14h8v-4h2v4h8V11h-4zM7 19H5v-2h2v2zm0-4H5v-2h2v2zm0-4H5V9h2v2zm4 4H9v-2h2v2zm0-4H9V9h2v2zm0-4H9V5h2v2zm4 8h-2v-2h2v2zm0-4h-2V9h2v2zm0-4h-2V5h2v2zm4 12h-2v-2h2v2zm0-4h-2v-2h2v2z" /></svg>
</div>
</div>
</a>
</div>
{:catch error}
<div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
<span class="inline-block align-middle mr-8">
<b class="capitalize">{$_('general_promise_error')}</b>
{error}
</span>
</div>
{/await}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,220 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import {
DonationService,
RunnerTeamService,
RunnerOrganizationService,
RunnerService
} from "@odit/lfk-client-js";
import { init } from "@paralleldrive/cuid2";
import toast from "svelte-french-toast";
import DocumentServer from "./DocumentServer";
const createId = init({ length: 10, fingerprint: "lfk-frontend" });
const documentServer = new DocumentServer(
config.baseurl_documentserver,
config.documentserver_key
);
export let certificates_show = false;
export let generate_runners = [];
export let generate_orgs = [];
export let generate_teams = [];
function generateCertificates(locale, include0runners = false) {
if (generate_orgs.length > 0) {
generateOrgCertificates(locale, include0runners = false);
} else if (generate_teams.length > 0) {
generateTeamCertificates(locale, include0runners = false);
} else {
generateRunnerCertificates(locale, include0runners = false);
}
}
function download(blob, fileName) {
const url = window.URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
a.remove();
toast.dismiss();
toast.success($_("pdf-successfully-generated"));
}
async function generateRunnerCertificates(locale, include0runners = false) {
toast.loading($_("generating-pdf"));
const current_donations =
(await DonationService.donationControllerGetAll()) || [];
let certificateRunners = [];
for (let runner of generate_runners) {
const linkRunner = await RunnerService.runnerControllerGetOne(runner.id)
linkRunner.distanceDonations =
current_donations.filter((d) => d.runner?.id == runner.id) || [];
// check if linkRunner.distance is 0, if so, and include0runners is false, skip this runner
if (
!include0runners &&
(linkRunner.distance === 0 || linkRunner.distance === null)
) {
continue;
} else {
certificateRunners.push(linkRunner);
}
}
documentServer
.generateCertificates(certificateRunners, locale)
.then((blob) => {
let fileName = `${$_("certificates")}-${locale}.pdf`;
if (generate_runners.length == 1) {
fileName = `${$_("certificates")}_${
generate_runners[0].firstname
}_${generate_runners[0].lastname}-${locale}-${createId()}.pdf`;
}
download(blob, fileName);
})
.catch((err) => {});
}
async function generateTeamCertificates(locale, include0runners = false) {
toast.loading($_("generating-pdfs"));
let count = 0;
const current_donations =
(await DonationService.donationControllerGetAll()) || [];
for (const t of generate_teams) {
const runners = await RunnerTeamService.runnerTeamControllerGetRunners(
t.id,
true
);
let certificateRunners = [];
for (let runner of runners) {
runner.distanceDonations =
current_donations.filter((d) => d.runner?.id == runner.id) || [];
// check if runner.distance is 0, if so, and include0runners is false, skip this runner
if (
!include0runners &&
(runner.distance === 0 || runner.distance === null)
) {
continue;
} else {
certificateRunners.push(runner);
}
}
documentServer
.generateCertificates(certificateRunners, locale)
.then((blob) => {
count++;
download(
blob,
`${$_("certificates")}_${t.name}-${locale}-${createId()}.pdf`
);
})
.catch((err) => {});
}
}
async function generateOrgCertificates(locale, include0runners = false) {
toast.loading($_("generating-pdfs"));
const current_donations =
(await DonationService.donationControllerGetAll()) || [];
let count = 0;
let count_orgs = 0;
for (const o of generate_orgs) {
count_orgs++;
let count = 0;
let runners =
await RunnerOrganizationService.runnerOrganizationControllerGetRunners(
o.id,
true,
true
);
let certificateRunners = [];
for (let runner of runners) {
runner.distanceDonations =
current_donations.filter((d) => d.runner?.id == runner.id) || [];
// check if runner.distance is 0, if so, and include0runners is false, skip this runner
if (
!include0runners &&
(runner.distance === 0 || runner.distance === null)
) {
continue;
} else {
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,
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}_${
t.name
}-${locale}-${createId()}.pdf`
);
if (
count === o.teams.length &&
count_orgs === generate_orgs.length
) {
toast.dismiss();
toast.success($_("pdfs-successfully-generated"));
}
})
.catch((err) => {});
}
}
}
</script>
{#if certificates_show}
<button
on:click={() => {
generateCertificates("de", true);
}}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
>
{$_("generate-runner-certificates")}: DE
</button>
<button
on:click={() => {
generateCertificates("de", false);
}}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
>
{$_("generate-runner-certificates")}: DE [{$_('exclude_0m_runners_certificate')}]
</button>
<button
on:click={() => {
generateCertificates("en", true);
}}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
>
{$_("generate-runner-certificates")}: EN
</button>
<button
on:click={() => {
generateCertificates("en", false);
}}
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm mb-1 lg:mb-0"
>
{$_("generate-runner-certificates")}: EN [{$_('exclude_0m_runners_certificate')}]
</button>
{/if}

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