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
180 changed files with 11736 additions and 24313 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.7 && 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 tailwind.config.cjs postcss.config.cjs index.html ./ COPY package.json ./
RUN npm config set registry $NPM_REGISTRY_URL && npm i -g pnpm@10.7 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" />
<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.10.3-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,65 +1,61 @@
{ {
"name": "@odit/lfk-frontend", "name": "@odit/lfk-frontend",
"version": "1.10.3", "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", "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",
"auto-changelog": "2.5.0", "csvtojson": "^2.0.10",
"autoprefixer": "10.4.21", "gridjs": "3.3.0",
"postcss": "8.5.3", "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",
"tailwindcss": "3.4.15", "tailwindcss": "2.0.3",
"vite": "4.3.3" "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": {
"@fontsource/athiti": "^5.2.5", "git": {
"@odit/lfk-client-js": "1.2.4", "commit": true,
"@paralleldrive/cuid2": "2.2.2", "requireCleanWorkingDir": false,
"@tanstack/svelte-table": "8.9.1", "commitMessage": "🚀RELEASE v${version}",
"bwip-js": "3.4.0", "push": false,
"check-password-strength": "2.0.10", "tag": true,
"csvtojson": "2.0.10", "tagName": null,
"html5-qrcode": "^2.3.8", "tagAnnotation": "v${version}"
"localforage": "1.10.0", },
"marked": "4.3.0", "npm": {
"svelte": "3.58.0", "publish": false
"svelte-french-toast": "1.2.0", },
"svelte-i18n": "3.6.0", "hooks": {
"tinro": "0.6.12", "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"
"validator": "13.15.0", }
"xlsx": "0.18.5" }
}, }
"volta": {
"node": "20.0.0"
}
}

3761
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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;

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"
}
]
} }

View File

@@ -1 +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. 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,234 +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/general/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 Imprint from "./components/general/Imprint.svelte"; import TeamDetail from "./components/teams/TeamDetail.svelte";
import Privacy from "./components/general/Privacy.svelte"; import UserPermissions from "./components/users/UserPermissions.svelte";
import ResetPassword from "./components/auth/ResetPassword.svelte"; import GroupPermissions from "./components/groups/GroupPermissions.svelte";
import Contacts from "./components/contacts/Contacts.svelte"; import RunnerDetail from "./components/runners/RunnerDetail.svelte";
import ContactDetail from "./components/contacts/ContactDetail.svelte"; import Imprint from "./components/general/Imprint.svelte";
import Donors from "./components/donors/Donors.svelte"; import Privacy from "./components/general/Privacy.svelte";
import Groups from "./components/groups/Groups.svelte"; import ResetPassword from "./components/auth/ResetPassword.svelte";
import DonorDetail from "./components/donors/DonorDetail.svelte"; import Contacts from "./components/contacts/Contacts.svelte";
import Donations from "./components/donations/Donations.svelte"; import ContactDetail from "./components/contacts/ContactDetail.svelte";
import DonationDetail from "./components/donations/DonationDetail.svelte"; import Donors from "./components/donors/Donors.svelte";
import GroupDetail from "./components/groups/GroupDetail.svelte"; import Groups from "./components/groups/Groups.svelte";
import ScanStations from "./components/scanstations/ScanStations.svelte"; import DonorDetail from "./components/donors/DonorDetail.svelte";
import ScanStationDetail from "./components/scanstations/ScanStationDetail.svelte"; import Donations from "./components/donations/Donations.svelte";
import Scans from "./components/scans/Scans.svelte"; import DonationDetail from "./components/donations/DonationDetail.svelte";
import ScanDetail from "./components/scans/ScanDetail.svelte"; import GroupDetail from "./components/groups/GroupDetail.svelte";
import Cards from "./components/cards/Cards.svelte"; import ScanStations from "./components/scanstations/ScanStations.svelte";
import StatsClients from "./components/statsclients/StatsClients.svelte"; import ScanStationDetail from "./components/scanstations/ScanStationDetail.svelte";
import StatsClientDetail from "./components/statsclients/StatsClientDetail.svelte"; import Scans from "./components/scans/Scans.svelte";
store.init(); import ScanDetail from "./components/scans/ScanDetail.svelte";
</script> import Cards from "./components/cards/Cards.svelte";
store.init();
<Route> registerSW();
{#if $router.path === "/forgot_password"} </script>
<Route path="/forgot_password">
<ForgotPassword /> <Route>
</Route> {#if $router.path === '/forgot_password'}
{:else if $router.path.includes("/reset")} <Route path="/forgot_password">
<Route path="/reset/:resetkey" let:params> <ForgotPassword />
<ResetPassword {params} /> </Route>
</Route> {:else if $router.path.includes('/reset')}
{:else if $router.path === "/about"} <Route path="/reset/:resetkey" let:params>
<Route path="/about"> <ResetPassword {params} />
<About /> </Route>
</Route> {:else if $router.path === '/about'}
{:else if $router.path === "/imprint"} <Route path="/about">
<Route path="/imprint"> <About />
<Imprint /> </Route>
</Route> {:else if $router.path === '/imprint'}
{:else if $router.path === "/privacy"} <Route path="/imprint">
<Route path="/privacy"> <Imprint />
<Privacy /> </Route>
</Route> {:else if $router.path === '/privacy'}
{:else if $store.isLoggedIn} <Route path="/privacy">
<Dashboard> <Privacy />
<Transition> </Route>
<Route path="/"> {:else if $store.isLoggedIn}
<MainDashContent /> <Dashboard>
</Route> <Transition>
<Route path="/users/*"> <Route path="/">
<Route path="/"> <MainDashContent />
<Users /> </Route>
</Route> <Route path="/users/*">
<Route path="/:userid/*" let:params> <Route path="/">
<Route path="/"> <Users />
<UserDetail {params} /> </Route>
</Route> <Route path="/:userid/*" let:params>
<Route path="/permissions/"> <Route path="/">
<UserPermissions {params} /> <UserDetail {params} />
</Route> </Route>
</Route> <Route path="/permissions/">
</Route> <UserPermissions {params} />
<Route path="/groups/*"> </Route>
<Route path="/"> </Route>
<Groups /> </Route>
</Route> <Route path="/groups/*">
<Route path="/:groupid/*" let:params> <Route path="/">
<Route path="/"> <Groups />
<GroupDetail {params} /> </Route>
</Route> <Route path="/:groupid/*" let:params>
<Route path="/permissions/"> <Route path="/">
<GroupPermissions {params} /> <GroupDetail {params} />
</Route> </Route>
</Route> <Route path="/permissions/">
</Route> <GroupPermissions {params} />
<Route path="/tracks/*"> </Route>
<Route path="/"> </Route>
<TracksOverview /> </Route>
</Route> <Route path="/tracks/*">
<Route path="/:trackid" let:params /> <Route path="/">
</Route> <TracksOverview />
<Route path="/runners/*"> </Route>
<Route path="/"> <Route path="/:trackid" let:params />
<Runners created_via="all" /> </Route>
</Route> <Route path="/runners/*">
<Route path="/:runnerid" let:params> <Route path="/">
<RunnerDetail {params} /> <Runners />
</Route> </Route>
</Route> <Route path="/:runnerid" let:params>
<Route path="/cardassignment/*"> <RunnerDetail {params} />
<Route path="/"> </Route>
<CardAssignment /> </Route>
</Route> <Route path="/teams/*">
</Route> <Route path="/">
<Route path="/teams/*"> <Teams />
<Route path="/"> </Route>
<Teams /> <Route path="/:teamid" let:params>
</Route> <TeamDetail {params} />
<Route path="/:teamid" let:params> </Route>
<TeamDetail {params} /> </Route>
</Route> <Route path="/contacts/*">
</Route> <Route path="/">
<Route path="/contacts/*"> <Contacts />
<Route path="/"> </Route>
<Contacts /> <Route path="/:contact" let:params>
</Route> <ContactDetail {params} />
<Route path="/:contact" let:params> </Route>
<ContactDetail {params} /> </Route>
</Route> <Route path="/orgs/*">
</Route> <Route path="/">
<Route path="/orgs/*"> <Orgs />
<Route path="/"> </Route>
<Orgs /> <Route path="/:orgid" let:params>
</Route> <OrgDetail {params} />
<Route path="/:orgid" let:params> </Route>
<OrgDetail {params} /> </Route>
</Route> <Route path="/donors/*">
</Route> <Route path="/">
<Route path="/donors/*"> <Donors />
<Route path="/"> </Route>
<Donors /> <Route path="/:donorid" let:params>
</Route> <DonorDetail {params} />
<Route path="/:donorid" let:params> </Route>
<DonorDetail {params} /> </Route>
</Route> <Route path="/donations/*">
</Route> <Route path="/">
<Route path="/donations/*"> <Donations />
<Route path="/"> </Route>
<Donations /> <Route path="/:donationid" let:params>
</Route> <DonationDetail {params} />
<Route path="/:donationid" let:params> </Route>
<DonationDetail {params} /> </Route>
</Route> <Route path="/cards/*">
</Route> <Route path="/">
<Route path="/cards/*"> <Cards />
<Route path="/"> </Route>
<Cards /> <!-- <Route path="/:scanid" let:params>
</Route> <ScanDetail {params} />
<!-- <Route path="/:scanid" let:params> </Route> -->
<ScanDetail {params} /> </Route>
</Route> --> <Route path="/scans/*">
</Route> <Route path="/">
<Route path="/scans/*"> <Scans />
<Route path="/"> </Route>
<Scans /> <Route path="/:scanid" let:params>
</Route> <ScanDetail {params} />
<Route path="/:scanid" let:params> </Route>
<ScanDetail {params} /> </Route>
</Route> <Route path="/scanstations/*">
</Route> <Route path="/">
<Route path="/scanstations/*"> <ScanStations />
<Route path="/"> </Route>
<ScanStations /> <Route path="/:stationid" let:params>
</Route> <ScanStationDetail {params} />
<Route path="/:stationid" let:params> </Route>
<ScanStationDetail {params} /> </Route>
</Route> <Route path="/about">
</Route> <About />
<Route path="/statsclients/*"> </Route>
<Route path="/"> <Route path="/settings">
<StatsClients /> <Settings />
</Route> </Route>
<Route path="/:clientid" let:params> </Transition>
<StatsClientDetail {params} /> <Footer />
</Route> </Dashboard>
</Route> {:else}
<Route path="/about"> <Login />
<About /> {/if}
</Route> </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 class="capitalize">{$_("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" 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="h-6 w-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]" <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="h-6 w-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 rounded-l-md 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="w-full 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 hidden lg:block"
>
{$_("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]" 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="h-6 w-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 rounded-l-md 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 h-4 w-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="w-full 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 hidden lg:block"
>
{$_("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 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,316 +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={(event) => { tabindex="0"
current_cards[ class="ml-4 text-indigo-600 hover:text-indigo-900 cursor-pointer">{$_('cancel-delete')}</button>
current_cards.findIndex((c) => c.id === event.detail.card.id) <button
] = event.detail.card; on:click={() => {
current_cards = current_cards; RunnerCardService.runnerCardControllerRemove(card.id, false).then(
options.update((options) => ({ (resp) => {
...options, current_cards = current_cards.filter(
data: current_cards, (obj) => obj.id !== card.id
})); );
}} Toastify({
/> text: $_('card-deleted'),
{/if} duration: 500,
backgroundColor:
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} 'linear-gradient(to right, #00b09b, #96c93d)',
<DeleteCardModal }).showToast();
delete_card={active_delete} }
modal_open={active_delete != undefined} );
on:delete={(event) => { }}
deleteCard(event.detail.id); tabindex="0"
}} class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('confirm-delete')}</button>
/> </td>
{#if !dataLoaded} {:else}
<div <td
class="bg-teal-lightest border-t-4 border-teal rounded-b text-teal-darkest px-4 py-3 shadow-md my-2" class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
role="alert" <button
> on:click={() => {
<p class="font-bold">{$_("loading-cards")}</p> open_edit_modal(card);
<p class="text-sm">{$_("this-might-take-a-moment")}</p> }}
</div> class="text-indigo-600 hover:text-indigo-900">{$_('details')}</button>
{:else if current_cards.length === 0} {#if store.state.jwtinfo.userdetails.permissions.includes('CARD:DELETE')}
<CardsEmptyState /> <button
{:else} on:click={() => {
<div class="h-12 mt-1"> active_deletes[card.id] = true;
{#if selected.length > 0} }}
<button tabindex="0"
type="button" class="ml-4 text-red-600 hover:text-red-900 cursor-pointer">{$_('delete')}</button>
class="w-full justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:w-auto sm:text-sm inline-flex" {/if}
id="options-menu" </td>
on:click={async () => { {/if}
const prom = []; </tr>
for (const card of selectedCards) { {/if}
prom.push( {/each}
await RunnerCardService.runnerCardControllerRemove( </tbody>
card.id, </table>
true </div>
) {/if}
); {:catch error}
} <div class="text-white px-6 py-4 border-0 rounded relative mb-4 bg-red-500">
await Promise.all(prom); <span class="inline-block align-middle mr-8">
for (const card of selectedCards) { <b class="capitalize">{$_('general_promise_error')}</b>
current_cards = current_cards.filter((r) => r.id !== card.id); {error}
} </span>
options.update((options) => ({ </div>
...options, {/await}
data: current_cards, {/if}
}));
$table.resetRowSelection();
}}
>
{$_("delete-cards")}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
{/if}
<GenerateRunnerCards
cards_show={selected.length > 0}
bind:generate_cards={selectedCards}
/>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead class="border-b border-gray-400">
{#each $table.getHeaderGroups() as headerGroup}
<tr class="select-none">
<th class="inset-y-0 left-0 px-4 py-2 text-left w-px">
<InputElement
type="checkbox"
checked={$table.getIsAllRowsSelected()}
indeterminate={$table.getIsSomeRowsSelected()}
on:change={() => $table.toggleAllRowsSelected()}
/>
</th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each $table.getRowModel().rows as row}
<tr class="odd:bg-white even:bg-gray-100">
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px">
<InputElement
type="checkbox"
checked={row.getIsSelected()}
on:change={() => row.toggleSelected()}
/>
</td>
{#each row.getVisibleCells() as cell}
<td>
<svelte:component
this={flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
/>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<TableBottom {table} {selected} />
{/if}
{/if}
<style>
table tbody tr td:nth-child(2) {
font-family: monospace;
}
</style>

View File

@@ -1,123 +0,0 @@
<script>
import { _ } from "svelte-i18n";
import { clickOutside } from "../base/outsideclick";
import { createEventDispatcher, onMount } from "svelte";
export let modal_open;
export let delete_card = {
id: 0,
code: "",
runner: {
firstname: "",
lastname: "",
},
};
const dispatch = createEventDispatcher();
onMount(() => {
document.onkeydown = (e) => {
e = e || window.event;
if (e.key === "Escape") {
modal_open = false;
}
if (e.keyCode === 13) {
if (createbtnenabled === true) {
createbtnenabled = false;
submit();
}
}
};
});
async function submit() {
dispatch("delete", { id: delete_card.id });
modal_open = false;
}
</script>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-hidden"
use:clickOutside
on:click_outside={() => {
modal_open = false;
}}
>
<div
class="flex items-end justify-center h-screen text-center sm:block p-0 lg:p-4"
>
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div
class="absolute inset-0 bg-gray-500 opacity-75"
data-id="modal_backdrop"
/>
</div>
<span
class="hidden sm:inline-block sm:align-middle sm:h-screen"
aria-hidden="true">&#8203;</span
>
<div
class="inline-block align-bottom text-left shadow-xl transform transition-all sm:align-middle w-full lg:w-auto min-w-auto lg:min-w-[35vw]"
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="h-6 w-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="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"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="w-full 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 hidden lg:block"
>
{$_("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]" 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="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
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 h-4 w-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</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="w-full 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 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>

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 h-4 w-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 class="capitalize">{$_("general_promise_error")}</b>
{error}
</span>
</div>
{/await}
{/if} {/if}

View File

@@ -1,459 +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/">
{#if store.state.jwtinfo.userdetails.permissions.includes("RUNNER:GET") && store.state.jwtinfo.userdetails.permissions.includes("CARD: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("/cardassignment/")} 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" xmlns="http://www.w3.org/2000/svg"
href="/cardassignment/" viewBox="0 0 24 24"
> width="24"
<svg height="24"><path fill="none" d="M0 0h24v24H0z" />
xmlns="http://www.w3.org/2000/svg" <path
viewBox="0 0 24 24" 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>
fill="currentColor" <span>{$_('orgs')}</span>
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" </a>
> {/if}
<path {#if store.state.jwtinfo.userdetails.permissions.includes('USER:GET')}
fill-rule="evenodd" <a
d="M14.615 1.595a.75.75 0 0 1 .359.852L12.982 9.75h7.268a.75.75 0 0 1 .548 1.262l-10.5 11.25a.75.75 0 0 1-1.272-.71l1.992-7.302H3.75a.75.75 0 0 1-.548-1.262l10.5-11.25a.75.75 0 0 1 .913-.143Z" class:bg-gray-100={$router.path === '/users/'}
clip-rule="evenodd" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
/> href="/users/">
</svg> <svg
xmlns="http://www.w3.org/2000/svg"
<span>Card Assignment</span> width="24"
</a> height="24"
<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("/runners/")} 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" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
href="/runners/" <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>
<svg <span>{$_('users')}</span>
xmlns="http://www.w3.org/2000/svg" </a>
viewBox="0 0 24 24" {/if}
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" {#if store.state.jwtinfo.userdetails.permissions.includes('USERGROUP:GET')}
fill="currentColor" <a
width="24" class:bg-gray-100={$router.path === '/groups/'}
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="/groups/">
<path <svg
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" 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>{$_("runners")}</span> height="24"
</a> xmlns="http://www.w3.org/2000/svg"
{/if} viewBox="0 0 640 512"><path
{#if store.state.jwtinfo.userdetails.permissions.includes("TEAM:GET")} fill="currentColor"
<a 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>
class:activenav={$router.path.includes("/teams/")} <span>{$_('user-groups')}</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="/teams/" {/if}
> {#if store.state.jwtinfo.userdetails.permissions.includes('RUNNER:GET')}
<svg <a
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" class:bg-gray-100={$router.path === '/runners/'}
fill="currentColor" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
width="24" href="/runners/">
height="24" <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512" viewBox="0 0 24 24"
><path class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
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" width="24"
/></svg height="24"><path fill="none" d="M0 0h24v24H0z" />
> <path
<span>{$_("teams")}</span> 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>
</a> <span>{$_('runners')}</span>
{/if} </a>
{#if store.state.jwtinfo.userdetails.permissions.includes("ORGANIZATION:GET")} {/if}
<a {#if store.state.jwtinfo.userdetails.permissions.includes('TEAM:GET')}
class:activenav={$router.path.includes("/orgs/")} <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 === '/teams/'}
href="/orgs/" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
> href="/teams/">
<svg <svg
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" width="24"
viewBox="0 0 24 24" height="24"
width="24" xmlns="http://www.w3.org/2000/svg"
height="24" viewBox="0 0 640 512"><path
><path fill="none" d="M0 0h24v24H0z" /> fill="currentColor"
<path d="M96 224c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm448 0c35.3 0 64-28.7 64-64s-28.7-64-64-64-64 28.7-64 64 28.7 64 64 64zm32 32h-64c-17.6 0-33.5 7.1-45.1 18.6 40.3 22.1 68.9 62 75.1 109.4h66c17.7 0 32-14.3 32-32v-32c0-35.3-28.7-64-64-64zm-256 0c61.9 0 112-50.1 112-112S381.9 32 320 32 208 82.1 208 144s50.1 112 112 112zm76.8 32h-8.3c-20.8 10-43.9 16-68.5 16s-47.6-6-68.5-16h-8.3C179.6 288 128 339.6 128 403.2V432c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48v-28.8c0-63.6-51.6-115.2-115.2-115.2zm-223.7-13.4C161.5 263.1 145.6 256 128 256H64c-35.3 0-64 28.7-64 64v32c0 17.7 14.3 32 32 32h65.9c6.3-47.4 34.9-87.3 75.2-109.4z" /></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" <span>{$_('teams')}</span>
/></svg </a>
> {/if}
<span>{$_("orgs")}</span> {#if store.state.jwtinfo.userdetails.permissions.includes('DONOR:GET')}
</a> <a
{/if} class:bg-gray-100={$router.path.includes('/donors/')}
{#if store.state.jwtinfo.userdetails.permissions.includes("DONOR:GET")} class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<a href="/donors/">
class:activenav={$router.path.includes("/donors/")} <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="/donors/" fill="currentColor"
> xmlns="http://www.w3.org/2000/svg"
<svg viewBox="0 0 24 24"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" width="24"
fill="currentColor" height="24"><path fill="none" d="M0 0h24v24H0z" />
xmlns="http://www.w3.org/2000/svg" <path
viewBox="0 0 24 24" d="M9.33 11.5h2.17A4.5 4.5 0 0 1 16 16H8.999L9 17h8v-1a5.578 5.578 0 0 0-.886-3H19a5 5 0 0 1 4.516 2.851C21.151 18.972 17.322 21 13 21c-2.761 0-5.1-.59-7-1.625L6 10.071A6.967 6.967 0 0 1 9.33 11.5zM5 19a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v9zM18 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm-7-3a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" /></svg>
width="24" <span>{$_('donors')}</span>
height="24" </a>
><path fill="none" d="M0 0h24v24H0z" /> {/if}
<path {#if store.state.jwtinfo.userdetails.permissions.includes('DONATION:GET')}
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" <a
/></svg class:bg-gray-100={$router.path.includes('/donations/')}
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<span>{$_("donors")}</span> href="/donations/">
</a> <svg
{/if} class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
{#if store.state.jwtinfo.userdetails.permissions.includes("DONATION:GET")} fill="currentColor"
<a xmlns="http://www.w3.org/2000/svg"
class:activenav={$router.path.includes("/donations/")} viewBox="0 0 24 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" width="24"
href="/donations/" height="24"><path fill="none" d="M0 0h24v24H0z" />
> <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" /></svg>
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" <span>{$_('donations')}</span>
fill="currentColor" </a>
xmlns="http://www.w3.org/2000/svg" {/if}
viewBox="0 0 24 24" {#if store.state.jwtinfo.userdetails.permissions.includes('TRACK:GET')}
width="24" <a
height="24" class:bg-gray-100={$router.path === '/tracks/'}
><path fill="none" d="M0 0h24v24H0z" /> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<path href="/tracks/">
d="M14 2a8 8 0 013.3 15.3A8 8 0 116.7 6.7 8 8 0 0114 2zm-3 7H9v1a2.5 2.5 0 00-.16 5h2.25a.5.5 0 010 1H7v2h2v1h2v-1a2.5 2.5 0 00.16-5H8.91a.5.5 0 010-1H13v-2h-2V9zm3-5a5.99 5.99 0 00-4.48 2.01 8 8 0 018.47 8.47A6 6 0 0014 4z" <svg
/></svg class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
> fill="currentColor"
<span>{$_("donations")}</span> width="24"
</a> height="24"
{/if} xmlns="http://www.w3.org/2000/svg"
{#if store.state.jwtinfo.userdetails.permissions.includes("TRACK:GET")} viewBox="0 0 640 512"><path
<a fill="currentColor"
class:activenav={$router.path === "/tracks/"} 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>
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>{$_('tracks')}</span>
href="/tracks/" </a>
> {/if}
<svg {#if store.state.jwtinfo.userdetails.permissions.includes('CARD:GET')}
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" <a
fill="currentColor" class:bg-gray-100={$router.path === '/cards/'}
width="24" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
height="24" href="/cards/">
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 640 512" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
><path fill="currentColor"
fill="currentColor" width="24"
d="M635.7 167.2L556.1 31.7c-8.8-15-28.3-20.1-43.5-11.5l-69 39.1L503.3 161c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L416 75l-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L333.2 122 278 153.3 337.8 255c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-59.7-101.7-55.2 31.3 27.9 47.4c2.2 3.8.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9l-27.9-47.5-55.2 31.3 59.7 101.7c2.2 3.7.9 8.5-2.9 10.7l-13.8 7.8c-3.8 2.2-8.7.9-10.9-2.9L84.9 262.9l-69 39.1C.7 310.7-4.6 329.8 4.2 344.8l79.6 135.6c8.8 15 28.3 20.1 43.5 11.5L624.1 210c15.2-8.6 20.4-27.8 11.6-42.8z" height="24"
/></svg xmlns="http://www.w3.org/2000/svg"
> viewBox="0 0 24 24">
<span>{$_("tracks")}</span> <path fill="none" d="M0 0h24v24H0z" />
</a> <path
{/if} fill="currentColor"
{#if store.state.jwtinfo.userdetails.permissions.includes("CARD:GET")} 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>
<a <span>{$_('cards')}</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" {/if}
href="/cards/" {#if store.state.jwtinfo.userdetails.permissions.includes('SCAN:GET')}
> <a
<svg class:bg-gray-100={$router.path === '/scans/'}
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
fill="currentColor" href="/scans/">
width="24" <svg
height="24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 24 24" width="24"
> height="24"
<path fill="none" d="M0 0h24v24H0z" /> xmlns="http://www.w3.org/2000/svg"
<path viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
fill="currentColor" <path
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" fill="currentColor"
/></svg d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" /></svg>
> <span>Scans</span>
<span>{$_("cards")}</span> </a>
</a> {/if}
{/if} {#if store.state.jwtinfo.userdetails.permissions.includes('CONTACT:GET')}
{#if store.state.jwtinfo.userdetails.permissions.includes("SCAN:GET")} <a
<a class:bg-gray-100={$router.path === '/contacts/'}
class:activenav={$router.path.includes("/scans/")} class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" href="/contacts/">
href="/scans/" <svg
> fill="currentColor"
<svg class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 24 24"
width="24" width="24"
height="24" height="24"><path fill="none" d="M0 0h24v24H0z" />
xmlns="http://www.w3.org/2000/svg" <path
viewBox="0 0 24 24" d="M2 22a8 8 0 1 1 16 0H2zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6 6 2.685 6 6-2.685 6-6 6zm10 4h4v2h-4v-2zm-3-5h7v2h-7v-2zm2-5h5v2h-5V7z" /></svg>
><path fill="none" d="M0 0h24v24H0z" /> <span>{$_('contacts')}</span>
<path </a>
fill="currentColor" {/if}
d="M2 4h2v16H2V4zm4 0h1v16H6V4zm2 0h2v16H8V4zm3 0h2v16h-2V4zm3 0h2v16h-2V4zm3 0h1v16h-1V4zm2 0h3v16h-3V4z" {#if store.state.jwtinfo.userdetails.permissions.includes('STATION:GET')}
/></svg <a
> class:bg-gray-100={$router.path === '/scanstations/'}
<span>Scans</span> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
</a> href="/scanstations/">
{/if} <svg
{#if store.state.jwtinfo.userdetails.permissions.includes("CONTACT:GET")} class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
<a fill="currentColor"
class:activenav={$router.path.includes("/contacts/")} width="24"
class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-200 hover:text-gray-900 w-full font-semibold" height="24"
href="/contacts/" viewBox="0 0 24 24"
> xmlns="http://www.w3.org/2000/svg"><path
<svg fill="none"
fill="currentColor" d="M0 0h24v24H0z" />
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" <path
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
viewBox="0 0 24 24" d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" /></svg>
width="24" <span>{$_('scanstations')}</span>
height="24" </a>
><path fill="none" d="M0 0h24v24H0z" /> {/if}
<path <a
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" class:bg-gray-100={$router.path === '/settings/'}
/></svg class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
> href="/settings/">
<span>{$_("contacts")}</span> <svg
</a> class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
{/if} xmlns="http://www.w3.org/2000/svg"
{#if store.state.jwtinfo.userdetails.permissions.includes("STATION:GET")} viewBox="0 0 20 20"
<a fill="currentColor">
class:activenav={$router.path.includes("/scanstations/")} <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-rule="evenodd"
href="/scanstations/" 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 </svg>
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" <span>{$_('settings')}</span>
fill="currentColor" </a>
width="24" <a
height="24" class:bg-gray-100={$router.path === '/about/'}
viewBox="0 0 24 24" class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
xmlns="http://www.w3.org/2000/svg" href="/about/">
><path fill="none" d="M0 0h24v24H0z" /> <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="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" fill="none"
/></svg stroke="currentColor"
> stroke-width="2"
<span>{$_("scanstations")}</span> stroke-linecap="round"
</a> stroke-linejoin="round"
{/if} viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" />
{#if store.state.jwtinfo.userdetails.permissions.includes("STATSCLIENT:GET")} <path d="M12 16v-4M12 8h.01" /></svg>
<a <span>{$_('about')}</span>
class:activenav={$router.path.includes("/statsclients/")} </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" <span
href="/statsclients/" tabindex="0"
> class="flex items-center px-4 py-3 transition cursor-pointer group hover:bg-gray-100 hover:text-gray-900"
<svg on:click={() => {
class="flex-shrink-0 w-5 h-5 mr-2 transition group-hover:text-gray-600" AuthService.authControllerLogout();
fill="currentColor" logout();
width="24" }}>
height="24" <svg
viewBox="0 0 24 24" class="flex-shrink-0 w-5 h-5 mr-2 text-gray-400 transition group-hover:text-gray-600"
xmlns="http://www.w3.org/2000/svg" fill="currentColor"
><path fill="none" d="M0 0h24v24H0z" /> width="24"
<path height="24"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
d="M4 5v11h16V5H4zM2 4a1 1 0 011-1h18a1 1 0 011 1v14H2V4zM1 19h22v2H1v-2z" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z" />
/></svg <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>{$_("statsclients")}</span> <span>{$_('logout')}</span>
</a> </span>
{/if} </nav>
{#if store.state.jwtinfo.userdetails.permissions.includes("USER:GET")} </nav>
<a <div class="ml-0 transition md:ml-60">
class:activenav={$router.path.includes("/users/")} <header
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={() => {
href="/users/" navOpen = true;
> }}
<svg class="flex items-center justify-between w-full px-4 bg-white border-b h-14 md:hidden">
xmlns="http://www.w3.org/2000/svg" <button class="block btn btn-light md:hidden">
width="24" <span class="sr-only">Menu</span><svg
height="24" class="w-4 h-4"
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 20 20"
viewBox="0 0 24 24" fill="currentcolor"><path
><path fill="none" d="M0 0h24v24H0z" /> fill-rule="evenodd"
<path 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"
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" clip-rule="evenodd" /></svg></button>
/></svg </header>
> <slot>
<span>{$_("users")}</span> <NoComponentLoaded />
</a> </slot>
{/if} </div>
{#if store.state.jwtinfo.userdetails.permissions.includes("USERGROUP:GET")} <div
<a on:click={() => {
class:activenav={$router.path.includes("/groups/")} navOpen = false;
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/" class:hidden={!navOpen}
> class="fixed inset-0 z-10 w-screen h-screen bg-black bg-opacity-25 md:hidden" />
<svg </section>
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}
<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/"
>
<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/"
>
<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 class="capitalize">{$_("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,28 +1,39 @@
<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;
const dispatch = createEventDispatcher(); export let current_donations;
const getDonorLabel = (option) => const getDonorLabel = (option) =>
option.firstname + " " + (option.middlename || "") + " " + option.lastname; option.firstname + " " + (option.middlename || "") + " " + option.lastname;
const filterDonors = (label, filterText, option) => const filterDonors = (label, filterText, option) =>
label.toLowerCase().includes(filterText.toLowerCase()) || label.toLowerCase().includes(filterText.toLowerCase()) ||
option.value.id.toString().startsWith(filterText.toLowerCase()); option.value.id.toString().startsWith(filterText.toLowerCase());
function focus(el) {
el.focus();
}
$: donor = 0; $: donor = 0;
$: runner = 0; $: runner = 0;
$: donors = []; $: donors = [];
$: runners = []; $: runners = [];
$: is_fixed = false; $: is_fixed = false;
$: is_paid = false; DonorService.donorControllerGetAll().then((val) => {
donors = val.map((r) => {
return { label: getDonorLabel(r), value: r };
});
});
RunnerService.runnerControllerGetAll().then((val) => {
runners = val.map((r) => {
return { label: getDonorLabel(r), value: r };
});
});
$: amount_input = 0; $: amount_input = 0;
$: processed_last_submit = true; $: processed_last_submit = true;
$: is_amount_valid = amount_input > 0; $: is_amount_valid = amount_input > 0;
@@ -45,16 +56,15 @@
if (processed_last_submit === true) { if (processed_last_submit === true) {
let amount_cent = Math.floor(amount_input * 100); let amount_cent = Math.floor(amount_input * 100);
processed_last_submit = false; processed_last_submit = false;
toast.loading($_("adding-donation")); const toast = Toastify({
text: "adding donation",
duration: -1,
}).showToast();
if (is_fixed) { if (is_fixed) {
let postdata = { let postdata = {
donor, donor,
amount: amount_cent, amount: amount_cent,
paidAmount: 0,
}; };
if (is_paid) {
postdata.paidAmount = amount_cent;
}
DonationService.donationControllerPostFixed(postdata) DonationService.donationControllerPostFixed(postdata)
.then((result) => { .then((result) => {
donor = donors[0].id || 0; donor = donors[0].id || 0;
@@ -62,15 +72,21 @@
amount_input = 0; amount_input = 0;
modal_open = false; modal_open = false;
// //
toast.dismiss(); Toastify({
toast.success($_("donation_added")); text: "donation_added",
dispatch("created", { donations: [result] }); duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_donations.push(result);
current_donations = current_donations;
}) })
.catch((err) => { .catch((err) => {
// //
}) })
.finally(() => { .finally(() => {
processed_last_submit = true; processed_last_submit = true;
//
toast.hideToast();
}); });
} else { } else {
let postdata = { let postdata = {
@@ -85,237 +101,29 @@
amount_input = 0; amount_input = 0;
modal_open = false; modal_open = false;
// //
toast.dismiss(); Toastify({
toast.success($_("donation_added")); text: "donation_added",
dispatch("created", { donations: [result] }); duration: 500,
backgroundColor: "linear-gradient(to right, #00b09b, #96c93d)",
}).showToast();
current_donations.push(result);
current_donations = current_donations;
}) })
.catch((err) => { .catch((err) => {
// //
}) })
.finally(() => { .finally(() => {
processed_last_submit = true; 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}
<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]"
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="h-6 w-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-lg leading-6 font-medium text-gray-900">
{#if is_fixed}
{$_("create-a-new-fixed-donation")}
{:else}{$_("create-a-new-distance-donation")}{/if}
</h3>
<label class="content-center align-middle object-center">
<span class="text-base" class:text-gray-300={is_fixed}
>{$_("distance-donation")}</span
>
<input
class="toggle relative w-10 h-5 transition-all duration-200 ease-in-out bg-gray-400 rounded-full shadow-inner outline-none appearance-none align-middle"
type="checkbox"
bind:checked={is_fixed}
/>
<span class="ml-2 text-base" class:text-gray-300={!is_fixed}
>{$_("fixed-donation")}</span
>
</label>
<div class="mb-6">
<p class="text-sm text-gray-500">
{$_(
"please-provide-the-nessecary-information-to-create-a-new-donation"
)}
</p>
</div>
<div class="grid grid-cols-6 gap-2 lg:gap-6 text-left">
<div class="col-span-6">
<label
for="donor"
class="block text-sm 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-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) =>
filterDonors(label, filterText, option)}
items={donors}
showChevron={true}
placeholder={$_("search-for-donor-name-or-id")}
noOptionsMessage={$_("no-donors-found")}
on:select={(selectedValue) =>
(donor = selectedValue.detail.value.id)}
on:clear={() => (donors = null)}
/>
</div>
{#if !is_fixed}
<div class="col-span-6">
<label
for="donor"
class="block text-sm font-medium text-gray-700"
>{$_("runner")}</label
>
<Select
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-neutral-800 rounded-md p-2"
itemFilter={(label, filterText, option) =>
filterDonors(label, filterText, option)}
items={runners}
showChevron={true}
placeholder={$_("search-for-runner-by-name-or-id")}
noOptionsMessage={$_("no-runners-found")}
on:select={(selectedValue) =>
(runner = selectedValue.detail.value.id)}
on:clear={() => (runner = null)}
/>
</div>
{/if}
<div class="col-span-6">
<label
for="donation_amount_eur"
class="block text-sm font-medium text-gray-700"
>
{#if !is_fixed}
{$_("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:text-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 text-sm"
></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>
{#if is_fixed}
<div class="col-span-6">
<label
for="paid"
class="block text-sm font-medium text-gray-700"
>{$_("already-paid")}</label
>
<p class="text-gray-500">
<input
id="paid"
bind:checked={is_paid}
name="paid"
type="checkbox"
class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
<span class="align-text-bottom">
{#if is_paid}
{$_("paid")}
{:else}
{$_("open")}
{/if}
</span>
</p>
</div>
{/if}
</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"
>
{$_("create")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="w-full 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 hidden lg:block"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}
<style> <style>
.toggle:before { input:before {
content: ""; content: "";
position: absolute; position: absolute;
width: 1.25rem; width: 1.25rem;
@@ -329,12 +137,159 @@
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
} }
.toggle:checked { input:checked {
/* @apply: bg-indigo-400; */ /* @apply: bg-indigo-400; */
background-color: #7f9cf5; background-color: #7f9cf5;
} }
.toggle:checked:before { input:checked:before {
left: 1.25rem; left: 1.25rem;
} }
</style> </style>
{#if modal_open}
<div
class="fixed z-10 inset-0 overflow-y-auto"
use:focusTrap
use:clickOutside
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
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 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"
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 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg leading-6 font-medium text-gray-900">
{#if is_fixed}
{$_('create-a-new-fixed-donation')}
{:else}{$_('create-a-new-distance-donation')}{/if}
</h3>
<label class="content-center align-middle object-center">
<span
class="ml-2 text-base"
class:text-gray-300={is_fixed}>{$_('distance-donation')}</span>
<input
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"
type="checkbox"
bind:checked={is_fixed} />
<span
class="ml-2 text-base "
class:text-gray-300={!is_fixed}>{$_('fixed-donation')}</span>
</label>
<div class="mt-2 mb-6">
<p class="text-sm text-gray-500">
{$_('please-provide-the-nessecary-information-to-create-a-new-donation')}
</p>
</div>
<div class="grid grid-cols-6 gap-6">
<div class="col-span-6">
<label
for="donor"
class="block text-sm 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"
itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
items={donors}
showChevron={true}
placeholder={$_('search-for-donor-name-or-id')}
noOptionsMessage={$_('no-donors-found')}
on:select={(selectedValue) => (donor = selectedValue.detail.value.id)}
on:clear={() => (donors = null)} />
</div>
{#if !is_fixed}
<div class="col-span-6">
<label
for="donor"
class="block text-sm font-medium text-gray-700">{$_('runner')}</label>
<Select
containerClasses="rounded-l-md mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm rounded-l-md sm:text-sm border-gray-300 border bg-gray-50 text-gray-500 rounded-md p-2"
itemFilter={(label, filterText, option) => filterDonors(label, filterText, option)}
items={runners}
showChevron={true}
placeholder={$_('search-for-runner-by-name-or-id')}
noOptionsMessage={$_('no-runners-found')}
on:select={(selectedValue) => (runner = selectedValue.detail.value.id)}
on:clear={() => (runner = null)} />
</div>
{/if}
<div class="col-span-6">
<label
for="donation_amount_eur"
class="block text-sm font-medium text-gray-700">
{#if !is_fixed}
{$_('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:text-sm border-gray-300 border bg-gray-50 text-gray-500 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 text-sm"></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>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button
disabled={!createbtnenabled}
class:opacity-50={!createbtnenabled}
on:click={submit}
type="button"
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:ml-3 sm:w-auto sm:text-sm">
{$_('create')}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
{$_('cancel')}
</button>
</div>
</div>
</div>
</div>
{/if}

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]"
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="h-6 w-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="w-full 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 hidden lg:block"
>
{$_("cancel")}
</button>
</div>
</div>
</div>
</div>
{/if}

View File

@@ -1,117 +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: "",
},
};
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]"
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="h-6 w-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] overflow-y-auto">
<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"
><b>{$_("donor")}</b>: {delete_donation.donor.firstname}
{delete_donation.donor.lastname}</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="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"
>
{$_("delete")}
</button>
<button
on:click={() => {
modal_open = false;
}}
type="button"
class="w-full 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 hidden lg:block"
>
{$_("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 rounded-l-md 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 rounded-l-md 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,18 +0,0 @@
<script>
import { _ } from "svelte-i18n";
export let donor;
</script>
{#if !donor || donor.firstname == 0}
{$_("donor-has-no-associated-donations")}
{: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,21 +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>
<button
on:click={paymentAction}
class="text-[#025a21] hover:text-green-900 mr-4">{$_("enter-payment")}</button
>
<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,291 +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) => {
return renderComponent(DonationTableAction, {
detailsLink: `./${info.row.original.id}`,
deleteAction: () => {
active_deletes = current_donations.filter(
(r) => r.id == info.row.original.id
);
},
paymentAction: () => {
active_edits = current_donations.filter(
(r) => r.id == info.row.original.id
);
},
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"
<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 {$_('donor')}
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">
/> {$_('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> </th>
{#each headerGroup.headers as header}
<TableHeader {header} />
{/each}
</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()
<td class="inset-y-0 left-0 px-4 py-2 text-center w-px"> .includes(
<InputElement searchvalue.toLowerCase()
type="checkbox" ) || donation.donor.lastname
checked={row.getIsSelected()} .toLowerCase()
on:change={() => row.toggleSelected()} .includes(
/> searchvalue.toLowerCase()
</td> ) || donation.runner?.firstname
{#each row.getVisibleCells() as cell} .toLowerCase()
<td> .includes(
<svelte:component searchvalue.toLowerCase()
this={flexRender( ) || donation.runner?.lastname
cell.column.columnDef.cell, .toLowerCase()
cell.getContext() .includes(
)} searchvalue.toLowerCase()
/> ) || should_display_based_on_id(donation.id)}
</td> <tr data-rowid="donation_{donation.id}">
{/each} <td class="px-6 py-4 whitespace-nowrap">
</tr> <div class="flex items-center">
{/each} <a
</tbody> href="../donors/{donation.donor.id}"
</table> 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]" 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="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
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 h-4 w-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</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="w-full 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 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>

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]" 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="h-6 w-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="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" </div>
> </div>
{$_("confirm-delete-donor-with-all-donations")}
</button>
<button
on:click={cancelDelete}
type="button"
class="w-full 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 hidden lg:block"
>
{$_("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 h-4 w-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]" 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="h-6 w-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 class="capitalize">{$_("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,123 +0,0 @@
<script>
import { RunnerCardService, RunnerService } from "@odit/lfk-client-js";
import QrCodeScanner from "./QrCodeScanner.svelte";
let state = "scan_runner";
let runnerinfo = { id: 0, firstname: "", lastname: "" };
let cardCode = "";
$: scannerActive = state.includes("scan");
</script>
<div class="p-4">
<h3 class="text-3xl font-bold">Card Assignment for Mobile</h3>
<!-- <p>state</p>
<p>{state}</p>
<p>scannerActive</p>
<p>{scannerActive}</p> -->
{#if state.includes("scan_")}
<!-- -->
{#if state === "scan_runner"}
<h3 class="text-xl font-bold">Scan Runner (Selfservice QR)</h3>
{/if}
{#if state === "scan_card"}
<h3 class="text-xl font-bold">Runner Scanned</h3>
<p>{runnerinfo.firstname} {runnerinfo.lastname} [#{runnerinfo.id}]</p>
<h3 class="text-xl font-bold">Scan Card (Code 128 Barcode)</h3>
{/if}
<QrCodeScanner
paused={!scannerActive}
on:detect={(e) => {
console.log({ type: "DETECT", code: e.detail.decodedText });
if (state === "scan_runner") {
if (
e.detail.decodedText.includes(
"https://portal.lauf-fuer-kaya.de/profile/"
)
) {
const runnerID = JSON.parse(
atob(
e.detail.decodedText
.replace("https://portal.lauf-fuer-kaya.de/profile/", "")
.split(".")[1]
)
).id;
new Audio("/beep.mp3").play();
RunnerService.runnerControllerGetOne(runnerID).then((runner) => {
console.log(runner);
runnerinfo = runner;
});
state = "scan_card";
}
}
if (state === "scan_card") {
if (
!e.detail.decodedText.includes(
"https://portal.lauf-fuer-kaya.de/profile/"
)
) {
cardCode = e.detail.decodedText;
new Audio("/beep.mp3").play();
state = "assigning";
RunnerCardService.runnerCardControllerGetAll().then((cards) => {
console.log(cards);
const card = cards.find((c) => c.code === cardCode);
if (card) {
console.log("card found", card);
RunnerCardService.runnerCardControllerPut(card.id, {
enabled: true,
id: card.id,
runner: runnerinfo.id,
}).then(() => {
state = "done";
});
} else {
state = "error_card_404";
}
});
}
}
}}
width={320}
height={320}
class="w-full max-w-sm bg-neutral-300 rounded-lg overflow-hidden"
/>
{#if state === "scan_card"}
<button
on:click={() => {
state = "scan_runner";
runnerinfo = { id: 0, firstname: "", lastname: "" };
cardCode = "";
}}
type="button"
class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-red-100 text-red-800 hover:bg-red-200 focus:outline-hidden focus:bg-red-200 disabled:opacity-50 disabled:pointer-events-none dark:text-red-500 dark:bg-red-800/30 dark:hover:bg-red-800/20 dark:focus:bg-red-800/20 w-full mt-2"
>
Cancel
</button>
{/if}
<!-- -->
{:else}
<!-- -->
{#if state === "assigning"}
<p>Assigning Card {cardCode}</p>
<p>Please wait a moment while we assign the card...</p>
{/if}
{#if state === "done"}
<p>
Assigned Card {cardCode} to {runnerinfo.firstname}
{runnerinfo.lastname} [#{runnerinfo.id}] ✅
</p>
<button
on:click={() => {
// state = "scan_runner";
// runnerinfo = { id: 0, firstname: "", lastname: "" };
// cardCode = "";
location.reload();
}}
type="button"
class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-100 text-blue-800 hover:bg-blue-200 focus:outline-hidden focus:bg-blue-200 disabled:opacity-50 disabled:pointer-events-none dark:text-blue-500 dark:bg-blue-800/30 dark:hover:bg-blue-800/20 dark:focus:bg-blue-800/20 w-full mt-2"
>
Done
</button>
{/if}
<!-- -->
{/if}
</div>

View File

@@ -20,32 +20,23 @@
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 href="https://git.odit.services/lfk/frontend/">LfK!Frontend</a>@<a
>@<a
class="underline" class="underline"
target="_blank" target="_blank"
rel="noopener, noreferrer" rel="noopener, noreferrer"
href="https://git.odit.services/lfk/frontend/src/tag/{releaseinfo}" href="https://git.odit.services/lfk/frontend/src/tag/{releaseinfo}">{releaseinfo}</a>
>{releaseinfo}</a -
> <a class="underline" href="https://docs.lauf-fuer-kaya.de" target="_blank">{$_('documentation')}</a>
- -
<a <a class="underline" href="/privacy">{$_('privacy')}</a>
rel="noopener, noreferrer"
class="underline"
href="https://docs.lauf-fuer-kaya.de"
target="_blank">{$_("documentation")}</a
>
- -
<a class="underline" href="/privacy">{$_("privacy")}</a> <a class="underline" href="/imprint">{$_('imprint')}</a>
-
<a class="underline" href="/imprint">{$_("imprint")}</a>
</p> </p>
</footer> </footer>

View File

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

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

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

View File

@@ -1,84 +0,0 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import {
Html5QrcodeScanner,
Html5QrcodeScanType,
Html5QrcodeSupportedFormats,
Html5QrcodeScannerState,
} from "html5-qrcode";
export let width;
export let height;
export let paused = false;
const dispatch = createEventDispatcher();
function onScanSuccess(decodedText, decodedResult) {
if (!paused) {
dispatch("detect", { decodedText });
}
}
// usually better to ignore and keep scanning
function onScanFailure(message) {
if (!paused) {
dispatch("error", { message });
}
}
let scanner;
onMount(() => {
scanner = new Html5QrcodeScanner(
"qr-scanner",
{
fps: 10,
rememberLastUsedCamera: true,
qrbox: { width, height },
aspectRatio: 1,
supportedScanTypes: [Html5QrcodeScanType.SCAN_TYPE_CAMERA],
formatsToSupport: [
Html5QrcodeSupportedFormats.CODE_39,
Html5QrcodeSupportedFormats.EAN_8,
Html5QrcodeSupportedFormats.EAN_13,
Html5QrcodeSupportedFormats.QR_CODE,
Html5QrcodeSupportedFormats.CODE_128,
],
},
false // non-verbose
);
scanner.render(onScanSuccess, onScanFailure);
});
// pause/resume scanner to avoid unintended scans
$: togglePause(paused);
function togglePause(paused) {
if (paused && scanner?.getState() === Html5QrcodeScannerState.SCANNING) {
scanner?.pause();
} else if (scanner?.getState() === Html5QrcodeScannerState.PAUSED) {
scanner?.resume();
}
}
</script>
<div id="qr-scanner" class={$$props.class} />
<style>
/* Hide unwanted icons */
#qr-scanner :global(img[alt="Info icon"]),
#qr-scanner :global(img[alt="Camera based scan"]) {
display: none;
}
/* Change camera permission button text */
#qr-scanner :global(#html5-qrcode-button-camera-permission) {
visibility: hidden;
}
#qr-scanner :global(#html5-qrcode-button-camera-permission::after) {
position: absolute;
inset: auto 0 0;
display: block;
content: "Allow camera access";
visibility: visible;
padding: 10px 0;
}
</style>

View File

@@ -4,33 +4,26 @@
<div class="md:flex flex-col md:flex-row h-screen w-full"> <div class="md:flex flex-col md:flex-row h-screen w-full">
<div <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" 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 <div
class="flex-shrink-0 px-8 py-4 flex flex-row items-center justify-between" class="flex-shrink-0 px-8 py-4 flex flex-row items-center justify-between">
>
<a <a
href="/#/test" 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" 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>
>Sidebar</a
>
<button <button
class="rounded-lg md:hidden focus:outline-none focus:shadow-outline" class="rounded-lg md:hidden focus:outline-none focus:shadow-outline">
>
<svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
{#if open} {#if open}
<path <path
fill-rule="evenodd" 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" 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" clip-rule="evenodd" />
/>
{/if} {/if}
{#if !open} {#if !open}
<path <path
fill-rule="evenodd" 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" 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" clip-rule="evenodd" />
/>
{/if} {/if}
</svg> </svg>
</button> </button>
@@ -38,63 +31,49 @@
<nav <nav
:class:block={open} :class:block={open}
:class:hidden={!open} :class:hidden={!open}
class="flex-grow md:block px-4 pb-4 md:pb-0 md:overflow-y-auto" class="flex-grow md:block px-4 pb-4 md:pb-0 md:overflow-y-auto">
>
<a <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" 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 href="#">Blog</a>
>
<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" 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 href="#">Portfolio</a>
>
<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" 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 href="#">About</a>
>
<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" 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 href="#">Contact</a>
>
<div class="relative"> <div class="relative">
<button <button
on:click={() => { on:click={() => {
open = !open; 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" 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> <span>Dropdown</span>
<svg <svg
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20"
class="inline w-4 h-4 mt-1 ml-1 transition-transform duration-200 transform md:-mt-1" class="inline w-4 h-4 mt-1 ml-1 transition-transform duration-200 transform md:-mt-1"><path
><path
fill-rule="evenodd" 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" 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" clip-rule="evenodd" /></svg>
/></svg
>
</button> </button>
<div <div
class:block={open} class:block={open}
class:hidden={!open} class:hidden={!open}
class="absolute right-0 w-full mt-2 origin-top-right rounded-md shadow-lg" class="absolute right-0 w-full mt-2 origin-top-right rounded-md shadow-lg">
>
<div <div
class="px-2 py-2 bg-white rounded-md shadow dark-mode:bg-gray-800" class="px-2 py-2 bg-white rounded-md shadow dark-mode:bg-gray-800">
>
<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" 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 href="#">Link #1</a>
>
<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" 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 href="#">Link #2</a>
>
<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" 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 href="#">Link #3</a>
>
</div> </div>
</div> </div>
</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]" 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="h-6 w-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="w-full 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 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>

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 class="capitalize">{$_("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]" 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="h-6 w-6 text-blue-600" class="h-6 w-6 text-blue-600"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
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 h-4 w-4 text-indigo-600 border-gray-300 rounded" class="focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded" />
/>
</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="w-full 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 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>

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]"
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="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 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="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"
>
{$_("confirm-delete-organization-and-associated-teams-runners")}
</button>
<button
on:click={cancelDelete}
type="button"
class="w-full 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 hidden lg:block"
>
{$_("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 rounded-l-md 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 h-4 w-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 h-4 w-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 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="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 class="capitalize">{$_("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,151 +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: cards[i].id,
enabled: cards[i].enabled,
code: cards[i].code,
runner: {
id: cards[i]?.runner?.id,
first_name: cards[i]?.runner?.firstname,
middle_name: cards[i]?.runner?.middlename,
last_name: cards[i]?.runner?.lastname,
group: {
id: cards[i]?.runner?.group.id,
name: cards[i]?.runner?.group.name,
parent_group: {
id: cards[i]?.runner?.group?.parentGroup?.id,
name: cards[i]?.runner?.group?.parentGroup?.name,
},
},
},
};
generateCards.push(card);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/cards?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
cards: generateCards,
}),
},
);
const blob = await response.blob();
return blob;
}
async generateContracts(runners: any[], locale: string) {
const generateRunners = new Array<any>();
for (let i = 0; i < runners.length; i++) {
console.log(runners[i]);
const card = {
id: runners[i].id,
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
group: {
id: runners[i].group.id,
name: runners[i].group.name,
parent_group: {
id: runners[i]?.group?.parentGroup?.id,
name: runners[i]?.group?.parentGroup?.name,
},
},
};
generateRunners.push(card);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/contracts?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
runners: generateRunners,
}),
},
);
const blob = await response.blob();
return blob;
}
async generateCertificates(runners: any[], locale: string) {
const generateRunners = new Array<any>();
for (let i = 0; i < runners.length; i++) {
const certificate = {
id: runners[i].id,
first_name: runners[i].firstname,
middle_name: runners[i].middlename,
last_name: runners[i].lastname,
self_service_link: runners[i].selfserviceLink,
group: {
id: runners[i].group.id,
name: runners[i].group.name,
parent_group: {
id: runners[i]?.group?.parentGroup?.id,
name: runners[i]?.group?.parentGroup?.name,
},
},
distance: runners[i].distance,
distance_donations: runners[i].distanceDonations.map(
(distanceDonation: any) => {
return {
id: distanceDonation.id,
amount: distanceDonation.amount,
amount_per_distance: distanceDonation.amountPerDistance,
donor: {
id: distanceDonation.donor.id,
first_name: distanceDonation.donor.firstname,
middle_name: distanceDonation.donor.middlename,
last_name: distanceDonation.donor.lastname,
},
};
},
),
};
generateRunners.push(certificate);
}
const response = await fetch(
`${this.baseUrl}/v1/pdfs/certificates?key=${this.apiKey}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
locale,
runners: generateRunners,
}),
},
);
const blob = await response.blob();
return blob;
}
}
export default DocumentServer;

View File

@@ -1,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]"
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="h-6 w-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}

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