Compare commits
87 commits
Author | SHA1 | Date | |
---|---|---|---|
f3913a906c | |||
cfe621debd | |||
5b7a9a3ace | |||
12b1d08ea4 | |||
04c01b6780 | |||
4ee16c624a | |||
35fd8a8e82 | |||
832f5053a0 | |||
362fc80891 | |||
2d35e2416b | |||
738765bcb4 | |||
a39044dffc | |||
369a67abda | |||
9bac6e2f97 | |||
f64397862c | |||
18d52e4bab | |||
b4fdd5fc60 | |||
c17355fcd1 | |||
fa5fb54876 | |||
ccc6d47d9e | |||
0be649c3ba | |||
424f4772f0 | |||
f01c895d30 | |||
f04fabefb0 | |||
625a2df308 | |||
f65b3108ee | |||
196a92325a | |||
b39198c935 | |||
ee52363bde | |||
63d97d0b83 | |||
9cf2cf2d80 | |||
fa8f051252 | |||
d5193842d2 | |||
bbf5b65aab | |||
eb622658d9 | |||
751370fed4 | |||
939c982c40 | |||
91ede95530 | |||
0771b43f56 | |||
06380e48c5 | |||
6f155ada66 | |||
b7dd5a95cd | |||
9bd663f266 | |||
beaf6a5926 | |||
e607f8c599 | |||
8880af2880 | |||
5d9007f517 | |||
20a2a3ccd0 | |||
a20c0d3ed3 | |||
916e61897a | |||
b19e8df561 | |||
fb78360946 | |||
caf1919930 | |||
d25fa07512 | |||
48502efc1d | |||
9a9742597a | |||
ea38b1835c | |||
802b7d25f0 | |||
d1bde66e1e | |||
dea2a1c40f | |||
f94cc8b365 | |||
238a35da9f | |||
d39ebc5029 | |||
9a7785917c | |||
fc1185d1c8 | |||
68b0aeffa8 | |||
8087108b90 | |||
5ce7aa8a17 | |||
d018f97274 | |||
6d45325543 | |||
1296331796 | |||
303ce7a58d | |||
796909a92e | |||
815d5c16fa | |||
6494752058 | |||
e4c2f47eb0 | |||
387736721f | |||
2c61ca0c8c | |||
552b6f6438 | |||
7aa0db3684 | |||
fdbf9e7f0a | |||
517527258a | |||
e68544a362 | |||
f8192a187b | |||
d04dde688f | |||
8ec3b04824 | |||
b1daa7e64f |
200 changed files with 8695 additions and 4863 deletions
.env.example.env.productionDockerfileREADME.mddemo-totp-qrcode.pngentrypoint.shindex.htmlpackage-lock.jsonpackage.jsonpostcss.config.js
public
src
App.vue
components
AppIcon.vueAppLogo.vueCheckProgressBar.vueCustomCalendar.vueFormBottomBar.vueHeader.vuePagination.vueUserMenu.vue
config.tsaccount
admin
MemberSearchSelect.vuePermission.vue
club
calendar
member
CreateMemberModal.vueMemberAwardCreateModal.vueMemberAwardEditModal.vueMemberCommunicationCreateModal.vueMemberExecutivePositionCreateModal.vueMemberExecutivePositionEditModal.vueMemberPrintModal.vueMemberQualificationCreateModal.vueMemberQualificationEditModal.vueMembershipCreateModal.vueMembershipEditModal.vue
newsletter
CreateNewsletterModal.vueNewsletterMailProgressModal.vueNewsletterMailRecipientsModal.vueNewsletterPreviewModal.vueNewsletterPrintingProgressModal.vueNewsletterPrintingRecipientsModal.vue
protocol
configuration
calendarType
communicationType
newsletterConfig
template
templateUsage
management
queryBuilder
BuilderHost.vueColumnSelect.vueCondition.vueJoin.vueJoinTable.vueNestedCondition.vueOrder.vueOrderStructure.vueStructureModal.vueTable.vueWhere.vue
setup
enums
helpers
main.cssmain.tsrouter
serverCom.tsstores
|
@ -1,5 +1 @@
|
||||||
VITE_SERVER_ADDRESS = backend_url #ohne pfad
|
VITE_SERVER_ADDRESS = backend_url #ohne pfad
|
||||||
VITE_APP_NAME_OVERWRITE = Mitgliederverwaltung # overwrites FF Admin
|
|
||||||
VITE_IMPRINT_LINK = https://mywebsite-imprint-url
|
|
||||||
VITE_PRIVACY_LINK = https://mywebsite-privacy-url
|
|
||||||
VITE_CUSTOM_LOGIN_MESSAGE = betrieben von xy
|
|
|
@ -1,5 +1 @@
|
||||||
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
||||||
VITE_APP_NAME_OVERWRITE = __APPNAMEOVERWRITE__
|
|
||||||
VITE_IMPRINT_LINK = __IMPRINTLINK__
|
|
||||||
VITE_PRIVACY_LINK = __PRIVACYLINK__
|
|
||||||
VITE_CUSTOM_LOGIN_MESSAGE = __CUSTOMLOGINMESSAGE__
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:18-alpine AS build
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
17
README.md
17
README.md
|
@ -8,12 +8,9 @@ Dieses Repository dient hauptsächlich zur Verwaltung der Mitgliederdaten, aber
|
||||||
|
|
||||||
Eine Demo dieser Seite finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
|
Eine Demo dieser Seite finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
|
||||||
|
|
||||||
Für die Verwendung muss ein TOTP-Code eingegeben werden.
|
Die Zugangsdaten (Lesebeschränkt) sind unterhalb dem Login angegeben.
|
||||||
|
|
||||||
Die Zugangsdaten (Lesebeschränkt) sind:\
|
Das Handbuch zur Anwendung finden sie unter [https://ff-admin.de/ff-admin-handbook](https://ff-admin.de/ff-admin-handbook).
|
||||||
EMAIL: demo-besucher\
|
|
||||||
TOTP: \
|
|
||||||
TOTP-Code: FBMDAJKFOYQXM2DNH47GWWBGJ5KWOUCW
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -29,17 +26,9 @@ services:
|
||||||
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/app:latest
|
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/app:latest
|
||||||
container_name: ff_admin
|
container_name: ff_admin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
#environment:
|
#environment:
|
||||||
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
||||||
# - APPNAMEOVERWRITE=<appname> # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
|
|
||||||
# - IMPRINTLINK=<imprint link>
|
|
||||||
# - PRIVACYLINK=<privacy link>
|
|
||||||
# - CUSTOMLOGINMESSAGE=betrieben von xy
|
|
||||||
#volumes:
|
|
||||||
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
|
||||||
# - <volume|local path>/favicon.png:/usr/share/nginx/html/favicon.png # 512x512 px Auflösung - wird als pwa Icon genutzt
|
|
||||||
# - <volume|local path>/Logo.png:/usr/share/nginx/html/Logo.png
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Wenn keine Server-Adresse angegeben wird, wird versucht das Backend unter der URL des Frontends zu erreichen. Dazu muss das Backend auf der gleichen URL wie das Frontend laufen. Zur Unterscheidung von Frontend und Backend bei gleicher URL müssen alle Anfragen mit dem PathPrefix `/api` an das Backend weitergeleitet werden.
|
Wenn keine Server-Adresse angegeben wird, wird versucht das Backend unter der URL des Frontends zu erreichen. Dazu muss das Backend auf der gleichen URL wie das Frontend laufen. Zur Unterscheidung von Frontend und Backend bei gleicher URL müssen alle Anfragen mit dem PathPrefix `/api` an das Backend weitergeleitet werden.
|
||||||
|
|
Binary file not shown.
Before ![]() (image error) Size: 1.9 KiB |
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
keys="SERVERADDRESS APPNAMEOVERWRITE IMPRINTLINK PRIVACYLINK CUSTOMLOGINMESSAGE"
|
keys="SERVERADDRESS"
|
||||||
files="/usr/share/nginx/html/assets/config-*.js /usr/share/nginx/html/manifest.webmanifest"
|
files="/usr/share/nginx/html/assets/config-*.js"
|
||||||
|
|
||||||
# Replace env vars in files served by NGINX
|
# Replace env vars in files served by NGINX
|
||||||
for file in $files
|
for file in $files
|
||||||
|
@ -12,11 +12,6 @@ do
|
||||||
# Get environment variable
|
# Get environment variable
|
||||||
value=$(eval echo "\$$key")
|
value=$(eval echo "\$$key")
|
||||||
|
|
||||||
# Set default value for APPNAMEOVERWRITE if empty
|
|
||||||
if [ "$key" = "APPNAMEOVERWRITE" ] && [ -z "$value" ]; then
|
|
||||||
value="FF Admin"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "replace $key by $value"
|
echo "replace $key by $value"
|
||||||
|
|
||||||
# replace __[variable_name]__ value with environment variable
|
# replace __[variable_name]__ value with environment variable
|
||||||
|
|
|
@ -2,11 +2,14 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<!-- icon and manifest are provided by App.vue -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
<script>
|
||||||
|
// screen.orientation.lock("portrait-primary").catch(() => {});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
8490
package-lock.json
generated
8490
package-lock.json
generated
File diff suppressed because it is too large
Load diff
88
package.json
88
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ff-admin",
|
"name": "ff-admin",
|
||||||
"version": "1.3.4",
|
"version": "1.5.3",
|
||||||
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -24,17 +24,19 @@
|
||||||
"author": "JK Effects",
|
"author": "JK Effects",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.17",
|
||||||
"@fullcalendar/daygrid": "^6.1.15",
|
"@fullcalendar/daygrid": "^6.1.17",
|
||||||
"@fullcalendar/interaction": "^6.1.15",
|
"@fullcalendar/interaction": "^6.1.17",
|
||||||
"@fullcalendar/timegrid": "^6.1.15",
|
"@fullcalendar/list": "^6.1.17",
|
||||||
"@fullcalendar/vue3": "^6.1.15",
|
"@fullcalendar/timegrid": "^6.1.17",
|
||||||
"@headlessui/vue": "^1.7.13",
|
"@fullcalendar/vue3": "^6.1.17",
|
||||||
"@heroicons/vue": "^2.1.5",
|
"@headlessui/vue": "^1.7.23",
|
||||||
|
"@heroicons/vue": "^2.2.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.5",
|
||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.9.0",
|
||||||
"event-source-polyfill": "^1.0.31",
|
"event-source-polyfill": "^1.0.31",
|
||||||
"grapesjs": "^0.22.4",
|
"grapesjs": "^0.22.7",
|
||||||
"grapesjs-preset-newsletter": "^1.0.2",
|
"grapesjs-preset-newsletter": "^1.0.2",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
|
@ -44,49 +46,49 @@
|
||||||
"lodash.isequal": "^4.5.0",
|
"lodash.isequal": "^4.5.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-anchor": "^9.2.0",
|
"markdown-it-anchor": "^9.2.0",
|
||||||
"markdown-it-prism": "^2.3.0",
|
"markdown-it-prism": "^3.0.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"pdf-dist": "^1.0.0",
|
"pdf-dist": "^1.0.0",
|
||||||
"pinia": "^2.3.0",
|
"pinia": "^3.0.2",
|
||||||
"qrcode": "^1.5.3",
|
"pwacompat": "^2.0.17",
|
||||||
"qs": "^6.11.2",
|
"qrcode": "^1.5.4",
|
||||||
"socket.io-client": "^4.5.0",
|
"qs": "^6.14.0",
|
||||||
"unplugin-vue-markdown": "^0.28.0",
|
"socket.io-client": "^4.8.1",
|
||||||
"uuid": "^9.0.0",
|
"unplugin-vue-markdown": "^28.3.1",
|
||||||
"vue": "^3.4.29",
|
"uuid": "^11.1.0",
|
||||||
"vue-router": "^4.3.3"
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.8.0",
|
"@rushstack/eslint-patch": "^1.11.0",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tailwindcss/postcss": "^4.1.5",
|
||||||
"@types/eslint": "~9.6.0",
|
"@tsconfig/node20": "^20.1.5",
|
||||||
|
"@types/eslint": "~9.6.1",
|
||||||
"@types/event-source-polyfill": "^1.0.5",
|
"@types/event-source-polyfill": "^1.0.5",
|
||||||
"@types/lodash.clonedeep": "^4.5.9",
|
"@types/lodash.clonedeep": "^4.5.9",
|
||||||
"@types/lodash.difference": "^4.5.9",
|
"@types/lodash.difference": "^4.5.9",
|
||||||
"@types/lodash.differencewith": "^4.5.9",
|
"@types/lodash.differencewith": "^4.5.9",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/markdown-it": "^14.1.2",
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^20.14.5",
|
"@types/node": "^22.15.12",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/qs": "^6.9.11",
|
"@types/qs": "^6.9.18",
|
||||||
"@types/uuid": "^9.0.3",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vite-pwa/assets-generator": "^0.2.2",
|
"@vite-pwa/assets-generator": "^1.0.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
"@vue/tsconfig": "^0.5.1",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"eslint": "^9.26.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint-plugin-vue": "^10.1.0",
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
"npm-run-all2": "^8.0.1",
|
||||||
"npm-run-all2": "^6.2.0",
|
"prettier": "^3.5.3",
|
||||||
"postcss": "^8.4.41",
|
"tailwindcss": "^4.1.5",
|
||||||
"prettier": "^3.2.5",
|
"typescript": "^5.8.3",
|
||||||
"tailwindcss": "^3.4.10",
|
"vite": "^6.3.5",
|
||||||
"typescript": "~5.4.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite": "^5.3.1",
|
"vite-plugin-vue-devtools": "^7.7.6",
|
||||||
"vite-plugin-pwa": "^0.17.4",
|
"vue-tsc": "^2.2.10"
|
||||||
"vite-plugin-vue-devtools": "^7.6.8",
|
|
||||||
"vue-tsc": "^2.0.21"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
BIN
public/Logo.png
BIN
public/Logo.png
Binary file not shown.
Before ![]() (image error) Size: 34 KiB |
Binary file not shown.
Before Width: 48px | Height: 48px | Size: 9.4 KiB |
Binary file not shown.
Before ![]() (image error) Size: 29 KiB |
18
src/App.vue
18
src/App.vue
|
@ -8,6 +8,12 @@
|
||||||
</div>
|
</div>
|
||||||
<Footer @contextmenu.prevent />
|
<Footer @contextmenu.prevent />
|
||||||
<Notification />
|
<Notification />
|
||||||
|
|
||||||
|
<Teleport to="head">
|
||||||
|
<title>{{ clubName }}</title>
|
||||||
|
<link rel="icon" type="image/ico" :href="config.server_address + '/api/public/favicon.ico'" />
|
||||||
|
<link rel="manifest" :href="config.server_address + '/api/public/manifest.webmanifest'" />
|
||||||
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
@ -15,20 +21,27 @@ import { defineComponent } from "vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import Header from "./components/Header.vue";
|
import Header from "./components/Header.vue";
|
||||||
import Footer from "./components/Footer.vue";
|
import Footer from "./components/Footer.vue";
|
||||||
import { mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import { useAuthStore } from "./stores/auth";
|
import { useAuthStore } from "./stores/auth";
|
||||||
import { isAuthenticatedPromise } from "./router/authGuard";
|
import { isAuthenticatedPromise } from "./router/authGuard";
|
||||||
import ContextMenu from "./components/ContextMenu.vue";
|
import ContextMenu from "./components/ContextMenu.vue";
|
||||||
import Modal from "./components/Modal.vue";
|
import Modal from "./components/Modal.vue";
|
||||||
import Notification from "./components/Notification.vue";
|
import Notification from "./components/Notification.vue";
|
||||||
|
import { config } from "./config";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
|
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["authCheck"]),
|
...mapState(useAuthStore, ["authCheck"]),
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
resetAllPiniaStores();
|
||||||
|
this.configure();
|
||||||
|
|
||||||
if (!this.authCheck && localStorage.getItem("access_token")) {
|
if (!this.authCheck && localStorage.getItem("access_token")) {
|
||||||
isAuthenticatedPromise().catch(() => {
|
isAuthenticatedPromise().catch(() => {
|
||||||
localStorage.removeItem("access_token");
|
localStorage.removeItem("access_token");
|
||||||
|
@ -36,5 +49,8 @@ export default defineComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useConfigurationStore, ["configure"]),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
26
src/components/AppIcon.vue
Normal file
26
src/components/AppIcon.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<img ref="icon" :src="url + '/api/public/icon.png'" alt="LOGO" class="h-full w-auto" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { url } from "@/serverCom";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
icon() {
|
||||||
|
(this.$refs.icon as HTMLImageElement).src = url + "/api/public/icon.png?" + new Date().getTime();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readSetting"]),
|
||||||
|
icon() {
|
||||||
|
return this.readSetting("club.icon");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
26
src/components/AppLogo.vue
Normal file
26
src/components/AppLogo.vue
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<img ref="logo" :src="url + '/api/public/applogo.png'" alt="LOGO" class="h-full w-auto" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { url } from "@/serverCom";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
logo() {
|
||||||
|
(this.$refs.logo as HTMLImageElement).src = url + "/api/public/applogo.png?t=" + new Date().getTime();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readSetting"]),
|
||||||
|
logo() {
|
||||||
|
return this.readSetting("club.logo");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
53
src/components/CheckProgressBar.vue
Normal file
53
src/components/CheckProgressBar.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full flex flex-row items-center">
|
||||||
|
<div class="contents" v-for="(i, index) in total" :key="index">
|
||||||
|
<div
|
||||||
|
v-if="index <= successfull && index != step"
|
||||||
|
class="relative flex items-center justify-center h-8 w-8 border-4 border-success rounded-full"
|
||||||
|
>
|
||||||
|
<SuccessCheckmark class="h-8! asolute top-0 m-0!" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="index <= step"
|
||||||
|
class="flex items-center justify-center h-8 w-8 border-4 border-success rounded-full"
|
||||||
|
>
|
||||||
|
<div class="h-2 w-2 border-4 border-success bg-success rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="h-8 w-8 border-4 border-gray-400 rounded-full"></div>
|
||||||
|
<div v-if="i != total" class="grow border-2" :class="index < step ? ' border-success' : 'border-gray-400'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import SuccessCheckmark from "./SuccessCheckmark.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
successfull: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
total: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
validator(value: number) {
|
||||||
|
return value >= 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
181
src/components/CustomCalendar.vue
Normal file
181
src/components/CustomCalendar.vue
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-full h-full gap-2 justify-between overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="flex flex-row gap-2 justify-between max-sm:justify-center"
|
||||||
|
:class="smallStyling ? 'max-lg:flex-wrap' : 'max-xl:flex-wrap'"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row" :class="smallStyling ? 'max-lg:order-2' : 'max-xl:order-2'">
|
||||||
|
<button
|
||||||
|
:primary="view == 'dayGridMonth'"
|
||||||
|
:primary-outline="view != 'dayGridMonth'"
|
||||||
|
class="rounded-r-none!"
|
||||||
|
@click="setView('dayGridMonth')"
|
||||||
|
>
|
||||||
|
Monat
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:primary="view == 'timeGridWeek'"
|
||||||
|
:primary-outline="view != 'timeGridWeek'"
|
||||||
|
class="rounded-none! border-x-0!"
|
||||||
|
@click="setView('timeGridWeek')"
|
||||||
|
>
|
||||||
|
Woche
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:primary="view == 'listMonth'"
|
||||||
|
:primary-outline="view != 'listMonth'"
|
||||||
|
class="rounded-l-none!"
|
||||||
|
@click="setView('listMonth')"
|
||||||
|
>
|
||||||
|
Liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-3xl w-full text-center" :class="smallStyling ? 'max-lg:order-1' : 'max-xl:order-1'">
|
||||||
|
{{ currentTitle }}
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-row" :class="smallStyling ? 'max-lg:order-3' : 'max-xl:order-3'">
|
||||||
|
<button primary-outline class="rounded-r-none!" @click="navigateView('prev')">
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:primary="containsToday"
|
||||||
|
:primary-outline="!containsToday"
|
||||||
|
class="rounded-none! border-x-0!"
|
||||||
|
@click="
|
||||||
|
calendarApi?.today();
|
||||||
|
containsToday = true;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
heute
|
||||||
|
</button>
|
||||||
|
<button primary-outline class="rounded-l-none!" @click="navigateView('next')">
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col w-full grow overflow-hidden">
|
||||||
|
<FullCalendar ref="fullCalendar" :options="calendarOptions" class="max-h-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import FullCalendar from "@fullcalendar/vue3";
|
||||||
|
import deLocale from "@fullcalendar/core/locales/de";
|
||||||
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||||
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
|
import listPlugin from "@fullcalendar/list";
|
||||||
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import type { CalendarOptions } from "@fullcalendar/core/index.js";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<
|
||||||
|
{
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
start: string;
|
||||||
|
end: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
}[]
|
||||||
|
>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
allowInteraction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
smallStyling: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
dateSelect: ({ start, end, allDay }: { start: string; end: string; allDay: boolean }) => {
|
||||||
|
return typeof start == "string" && typeof end == "string" && typeof allDay === "boolean";
|
||||||
|
},
|
||||||
|
eventSelect: (id: string) => {
|
||||||
|
return typeof id == "string";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
view: "dayGridMonth" as "dayGridMonth" | "timeGridWeek" | "listMonth",
|
||||||
|
calendarApi: null as null | typeof FullCalendar,
|
||||||
|
currentTitle: "" as string,
|
||||||
|
containsToday: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
calendarOptions() {
|
||||||
|
return {
|
||||||
|
timeZone: "local",
|
||||||
|
locale: deLocale,
|
||||||
|
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
|
||||||
|
initialView: "dayGridMonth",
|
||||||
|
eventDisplay: "block",
|
||||||
|
headerToolbar: false,
|
||||||
|
weekends: true,
|
||||||
|
editable: this.allowInteraction,
|
||||||
|
selectable: this.allowInteraction,
|
||||||
|
selectMirror: false,
|
||||||
|
dayMaxEvents: true,
|
||||||
|
weekNumbers: true,
|
||||||
|
displayEventTime: true,
|
||||||
|
nowIndicator: true,
|
||||||
|
weekText: "KW",
|
||||||
|
allDaySlot: false,
|
||||||
|
events: this.items,
|
||||||
|
select: this.select,
|
||||||
|
eventClick: this.eventClick,
|
||||||
|
} as CalendarOptions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.calendarApi = (this.$refs.fullCalendar as typeof FullCalendar).getApi();
|
||||||
|
this.setTitle();
|
||||||
|
this.setContainsToday();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTitle() {
|
||||||
|
this.currentTitle = this.calendarApi?.view.title ?? "";
|
||||||
|
},
|
||||||
|
setView(view: "dayGridMonth" | "timeGridWeek" | "listMonth") {
|
||||||
|
this.calendarApi?.changeView(view);
|
||||||
|
this.view = view;
|
||||||
|
this.setTitle();
|
||||||
|
this.setContainsToday();
|
||||||
|
},
|
||||||
|
navigateView(change: "prev" | "next") {
|
||||||
|
if (change == "prev") {
|
||||||
|
this.calendarApi?.prev();
|
||||||
|
} else {
|
||||||
|
this.calendarApi?.next();
|
||||||
|
}
|
||||||
|
this.setTitle();
|
||||||
|
this.setContainsToday();
|
||||||
|
},
|
||||||
|
setContainsToday() {
|
||||||
|
const start = this.calendarApi?.view.currentStart;
|
||||||
|
const end = this.calendarApi?.view.currentEnd;
|
||||||
|
const today = new Date();
|
||||||
|
this.containsToday = today >= start && today < end;
|
||||||
|
},
|
||||||
|
select(e: any) {
|
||||||
|
this.$emit("dateSelect", {
|
||||||
|
start: e?.startStr ?? new Date().toISOString(),
|
||||||
|
end: e?.endStr ?? new Date().toISOString(),
|
||||||
|
allDay: e?.allDay ?? false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
eventClick(e: any) {
|
||||||
|
this.$emit("eventSelect", e.event.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col text-gray-400 text-sm mt-4 items-center">
|
<div class="flex flex-col text-gray-400 text-sm mt-4 items-center">
|
||||||
<div class="flex flex-row gap-2 justify-center">
|
<p v-if="appCustom_login_message">{{ appCustom_login_message }}</p>
|
||||||
<a v-if="config.imprint_link" :href="config.imprint_link" target="_blank">Datenschutz</a>
|
<div class="flex flex-row gap-2 justify-center mb-3">
|
||||||
<a v-if="config.privacy_link" :href="config.privacy_link" target="_blank">Impressum</a>
|
<a v-if="clubWebsite" :href="clubWebsite" target="_blank">Webseite</a>
|
||||||
|
<a v-if="clubImprint" :href="clubImprint" target="_blank">Datenschutz</a>
|
||||||
|
<a v-if="clubPrivacy" :href="clubPrivacy" target="_blank">Impressum</a>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="config.custom_login_message">{{ config.custom_login_message }}</p>
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://ff-admin.de/admin" target="_blank">FF Admin</a>
|
<a href="https://ff-admin.de/admin" target="_blank">FF Admin</a>
|
||||||
entwickelt von
|
entwickelt von
|
||||||
|
@ -14,5 +15,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { config } from "@/config";
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useConfigurationStore, [
|
||||||
|
"appCustom_login_message",
|
||||||
|
"appShow_link_to_calendar",
|
||||||
|
"clubImprint",
|
||||||
|
"clubPrivacy",
|
||||||
|
"clubWebsite",
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-sm">
|
<header class="flex flex-row h-16 min-h-16 justify-between p-3 md:px-5 bg-white shadow-xs">
|
||||||
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
<RouterLink to="/" class="flex flex-row gap-2 align-bottom w-fit h-full">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-full w-auto" />
|
<AppLogo />
|
||||||
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">
|
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">
|
||||||
{{ config.app_name_overwrite || "FF Admin" }}
|
{{ clubName }}
|
||||||
</h1>
|
</h1>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
@ -37,15 +37,17 @@ import { useAuthStore } from "@/stores/auth";
|
||||||
import { useNavigationStore } from "@/stores/admin/navigation";
|
import { useNavigationStore } from "@/stores/admin/navigation";
|
||||||
import TopLevelLink from "./admin/TopLevelLink.vue";
|
import TopLevelLink from "./admin/TopLevelLink.vue";
|
||||||
import UserMenu from "./UserMenu.vue";
|
import UserMenu from "./UserMenu.vue";
|
||||||
import { config } from "@/config";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
import AppLogo from "./AppLogo.vue";
|
||||||
|
import { useConfigurationStore } from "@/stores/configuration";
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAuthStore, ["authCheck"]),
|
...mapState(useAuthStore, ["authCheck"]),
|
||||||
...mapState(useNavigationStore, ["topLevel"]),
|
...mapState(useNavigationStore, ["topLevel"]),
|
||||||
|
...mapState(useConfigurationStore, ["clubName"]),
|
||||||
routeName() {
|
routeName() {
|
||||||
return typeof this.$route.name == "string" ? this.$route.name : "";
|
return typeof this.$route.name == "string" ? this.$route.name : "";
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<Spinner v-if="deferingSearch" />
|
<Spinner v-if="deferingSearch" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="!max-w-64 !w-64 rounded-md shadow-sm relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
class="max-w-64! w-64! rounded-md shadow-xs relative block px-3 py-2 pr-5 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
||||||
placeholder="Suche"
|
placeholder="Suche"
|
||||||
v-model="searchString"
|
v-model="searchString"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute right-0 mt-2 w-56 z-20 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="absolute right-0 mt-2 w-56 z-20 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg drop-shadow-lg border border-gray-300 focus:outline-hidden"
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1 pt-2">
|
<div class="px-3 py-1 pt-2">
|
||||||
<p class="text-xs">Angemeldet als</p>
|
<p class="text-xs">Angemeldet als</p>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<button button primary @click="close">Mein Account</button>
|
<button button primary @click="close">Mein Account</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ close }">
|
<MenuItem v-if="false" v-slot="{ close }">
|
||||||
<RouterLink to="/docs" target="_blank">
|
<RouterLink to="/docs" target="_blank">
|
||||||
<button button primary @click="close">Dokumentation</button>
|
<button button primary @click="close">Dokumentation</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
106
src/components/account/ChangeToPassword.vue
Normal file
106
src/components/account/ChangeToPassword.vue
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="change">
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="new"
|
||||||
|
name="new"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="neues Passwort"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
:class="notMatching ? 'border-red-600!' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="new_rep"
|
||||||
|
name="new_rep"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="neues Passwort wiederholen"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-t-none!"
|
||||||
|
:class="notMatching ? 'border-red-600!' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="notMatching">Passwörter stimmen nicht überein</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="changeStatus == 'loading' || changeStatus == 'success'">
|
||||||
|
zu Passwort wechseln
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="changeStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="changeStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="changeStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="changeError" class="text-center">{{ changeError }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
import { hashString } from "@/helpers/crypto";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
currentRoutine: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["updateCurrent"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
verification: "loading" as "success" | "loading" | "failed",
|
||||||
|
changeStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
changeError: "" as string,
|
||||||
|
notMatching: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
async change(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
|
||||||
|
let new_pw = await hashString(formData.new.value);
|
||||||
|
let new_rep = await hashString(formData.new_rep.value);
|
||||||
|
if (new_pw != new_rep) {
|
||||||
|
this.notMatching = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.notMatching = false;
|
||||||
|
|
||||||
|
this.changeStatus = "loading";
|
||||||
|
this.changeError = "";
|
||||||
|
this.$http
|
||||||
|
.patch(`/user/changeToPW`, {
|
||||||
|
newpassword: await hashString(formData.new.value),
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.changeStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.changeStatus = "failed";
|
||||||
|
this.changeError = err.response.data;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.changeStatus = undefined;
|
||||||
|
this.$emit("updateCurrent");
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
92
src/components/account/ChangeToTOTP.vue
Normal file
92
src/components/account/ChangeToTOTP.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||||
|
|
||||||
|
<TextCopy :copyText="otp" />
|
||||||
|
</div>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="verify">
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="totp" name="totp" type="text" required placeholder="TOTP" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="verifyStatus == 'loading' || verifyStatus == 'success'">
|
||||||
|
zu TOTP wechseln
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="verifyStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="verifyStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="verifyStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="verifyError" class="text-center">{{ verifyError }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
currentRoutine: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["updateCurrent"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
verification: "loading" as "success" | "loading" | "failed",
|
||||||
|
image: undefined as undefined | string,
|
||||||
|
otp: undefined as undefined | string,
|
||||||
|
verifyStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
verifyError: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$http
|
||||||
|
.get(`/user/changeToTOTP`)
|
||||||
|
.then((result) => {
|
||||||
|
this.verification = "success";
|
||||||
|
this.image = result.data.dataUrl;
|
||||||
|
this.otp = result.data.otp;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.verification = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
verify(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.verifyStatus = "loading";
|
||||||
|
this.verifyError = "";
|
||||||
|
this.$http
|
||||||
|
.patch(`/user/changeToTOTP`, {
|
||||||
|
otp: this.otp,
|
||||||
|
totp: formData.totp.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.verifyStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.verifyStatus = "failed";
|
||||||
|
this.verifyError = err.response.data;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.verifyStatus = undefined;
|
||||||
|
this.$emit("updateCurrent");
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
109
src/components/account/PasswordChange.vue
Normal file
109
src/components/account/PasswordChange.vue
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="change">
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="current"
|
||||||
|
name="current"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="aktuelles Passwort"
|
||||||
|
autocomplete="current-password"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="new"
|
||||||
|
name="new"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="neues Passwort"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-none!"
|
||||||
|
:class="notMatching ? 'border-red-600!' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="new_rep"
|
||||||
|
name="new_rep"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
placeholder="neues Passwort wiederholen"
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-t-none!"
|
||||||
|
:class="notMatching ? 'border-red-600!' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="notMatching">Passwörter stimmen nicht überein</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="changeStatus == 'loading' || changeStatus == 'success'">
|
||||||
|
Passwort ändern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="changeStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="changeStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="changeStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="changeError" class="text-center">{{ changeError }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { hashString } from "@/helpers/crypto";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
verification: "loading" as "success" | "loading" | "failed",
|
||||||
|
changeStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
changeError: "" as string,
|
||||||
|
notMatching: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
methods: {
|
||||||
|
async change(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
|
||||||
|
let new_pw = await hashString(formData.new.value);
|
||||||
|
let new_rep = await hashString(formData.new_rep.value);
|
||||||
|
if (new_pw != new_rep) {
|
||||||
|
this.notMatching = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.notMatching = false;
|
||||||
|
|
||||||
|
this.changeStatus = "loading";
|
||||||
|
this.changeError = "";
|
||||||
|
this.$http
|
||||||
|
.patch(`/user/changepw`, {
|
||||||
|
current: await hashString(formData.current.value),
|
||||||
|
newpassword: await hashString(formData.new.value),
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.changeStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.changeStatus = "failed";
|
||||||
|
this.changeError = err.response.data;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.changeStatus = undefined;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
83
src/components/account/TotpCheckAndScan.vue
Normal file
83
src/components/account/TotpCheckAndScan.vue
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||||
|
|
||||||
|
<TextCopy :copyText="otp" />
|
||||||
|
</div>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="verify">
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="totp" name="totp" type="text" required placeholder="TOTP prüfen" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="verifyStatus == 'loading' || verifyStatus == 'success'">
|
||||||
|
TOTP prüfen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="verifyStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="verifyStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="verifyStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="verifyError" class="text-center">{{ verifyError }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
verification: "loading" as "success" | "loading" | "failed",
|
||||||
|
image: undefined as undefined | string,
|
||||||
|
otp: undefined as undefined | string,
|
||||||
|
verifyStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
verifyError: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$http
|
||||||
|
.get(`/user/totp`)
|
||||||
|
.then((result) => {
|
||||||
|
this.verification = "success";
|
||||||
|
this.image = result.data.dataUrl;
|
||||||
|
this.otp = result.data.otp;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.verification = "failed";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
verify(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.verifyStatus = "loading";
|
||||||
|
this.verifyError = "";
|
||||||
|
this.$http
|
||||||
|
.post(`/user/verify`, {
|
||||||
|
totp: formData.totp.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.verifyStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.verifyStatus = "failed";
|
||||||
|
this.verifyError = err.response.data;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.verifyStatus = undefined;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Combobox v-model="selected" :disabled="disabled" multiple>
|
<Combobox v-model="selected" :disabled="disabled" multiple>
|
||||||
<ComboboxLabel>{{ title }}</ComboboxLabel>
|
<ComboboxLabel v-if="!showTitleAsPlaceholder">{{ title }}</ComboboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative" :class="{ 'mt-1': !showTitleAsPlaceholder }">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
:placeholder="showTitleAsPlaceholder ? title : ''"
|
||||||
@input="query = $event.target.value"
|
@input="query = $event.target.value"
|
||||||
/>
|
/>
|
||||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
@after-leave="query = ''"
|
@after-leave="query = ''"
|
||||||
>
|
>
|
||||||
<ComboboxOptions
|
<ComboboxOptions
|
||||||
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-hidden sm:text-sm"
|
||||||
>
|
>
|
||||||
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
|
<ComboboxOption v-if="loading || deferingSearch" as="template" disabled>
|
||||||
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
<li class="flex flex-row gap-2 text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
@ -86,7 +87,7 @@ import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
import difference from "lodash.difference";
|
import difference from "lodash.difference";
|
||||||
import Spinner from "../Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -101,6 +102,10 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showTitleAsPlaceholder: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -18,22 +18,22 @@
|
||||||
<div class="flex flex-row border border-white rounded-md overflow-hidden">
|
<div class="flex flex-row border border-white rounded-md overflow-hidden">
|
||||||
<EyeIcon
|
<EyeIcon
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
:class="_can(permissionUpdate, 'read', section) ? 'bg-success' : ''"
|
:class="_canSection(permissionUpdate, 'read', section) ? 'bg-success' : ''"
|
||||||
@click="togglePermission('read', section)"
|
@click="togglePermission('read', section)"
|
||||||
/>
|
/>
|
||||||
<PlusIcon
|
<PlusIcon
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
:class="_can(permissionUpdate, 'create', section) ? 'bg-success' : ''"
|
:class="_canSection(permissionUpdate, 'create', section) ? 'bg-success' : ''"
|
||||||
@click="togglePermission('create', section)"
|
@click="togglePermission('create', section)"
|
||||||
/>
|
/>
|
||||||
<PencilIcon
|
<PencilIcon
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
:class="_can(permissionUpdate, 'update', section) ? 'bg-success' : ''"
|
:class="_canSection(permissionUpdate, 'update', section) ? 'bg-success' : ''"
|
||||||
@click="togglePermission('update', section)"
|
@click="togglePermission('update', section)"
|
||||||
/>
|
/>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
:class="_can(permissionUpdate, 'delete', section) ? 'bg-success' : ''"
|
:class="_canSection(permissionUpdate, 'delete', section) ? 'bg-success' : ''"
|
||||||
@click="togglePermission('delete', section)"
|
@click="togglePermission('delete', section)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -69,8 +69,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!disableEdit" class="flex flex-row gap-2 self-end pt-4">
|
<div v-if="!disableEdit" class="flex flex-row gap-2 self-end pt-4">
|
||||||
<button primary-outline class="!w-fit" @click="reset" :disabled="canSaveOrReset">verwerfen</button>
|
<button primary-outline class="w-fit!" @click="reset" :disabled="canSaveOrReset">verwerfen</button>
|
||||||
<button primary class="!w-fit" @click="submit" :disabled="status == 'loading' || canSaveOrReset">
|
<button primary class="w-fit!" @click="submit" :disabled="status == 'loading' || canSaveOrReset">
|
||||||
speichern
|
speichern
|
||||||
</button>
|
</button>
|
||||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
@ -132,7 +132,7 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAbilityStore, ["_can"]),
|
...mapState(useAbilityStore, ["_can", "_canSection"]),
|
||||||
canSaveOrReset(): boolean {
|
canSaveOrReset(): boolean {
|
||||||
return isEqual(this.permissions, this.permissionUpdate);
|
return isEqual(this.permissions, this.permissionUpdate);
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Typen zur Anzeige auswählen</ListboxLabel>
|
<ListboxLabel>Typen zur Anzeige auswählen</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Termintyp</ListboxLabel>
|
<ListboxLabel>Termintyp</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<ListboxLabel>Termintyp</ListboxLabel>
|
<ListboxLabel>Termintyp</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -35,7 +35,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
<ListboxOption v-if="calendarTypes.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
@ -152,10 +152,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<button primary-outline type="reset" class="!w-fit" :disabled="canSaveOrReset" @click="resetForm">
|
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
|
||||||
verwerfen
|
verwerfen
|
||||||
</button>
|
</button>
|
||||||
<button primary type="submit" class="!w-fit" :disabled="status == 'loading' || canSaveOrReset">
|
<button primary type="submit" class="w-fit!" :disabled="status == 'loading' || canSaveOrReset">
|
||||||
speichern
|
speichern
|
||||||
</button>
|
</button>
|
||||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Anrede</ListboxLabel>
|
<ListboxLabel>Anrede</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start"> {{ selectedSalutation?.salutation }}</span>
|
<span class="block truncate w-full text-start"> {{ selectedSalutation?.salutation }}</span>
|
||||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
v-slot="{ active, selected }"
|
v-slot="{ active, selected }"
|
||||||
|
@ -69,7 +69,15 @@
|
||||||
<input type="date" id="birthdate" required />
|
<input type="date" id="birthdate" required />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="internalId">Interne ID (optional)</label>
|
<div class="flex flex-row">
|
||||||
|
<label for="internalId" class="grow">
|
||||||
|
Interne ID (optional{{ lastId ? ` - zuletzte verwendet: ${lastId}` : "" }})
|
||||||
|
</label>
|
||||||
|
<div title="Es empfiehlt sich, die Interne Id mit Platzhaltern wie '0' vorne aufzufüllen.">
|
||||||
|
<InformationCircleIcon class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="text" id="internalId" />
|
<input type="text" id="internalId" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
|
@ -101,8 +109,9 @@ import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } f
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
import type { CreateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
import type { CreateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
import { useSalutationStore } from "../../../../stores/admin/configuration/salutation";
|
import { useSalutationStore } from "@/stores/admin/configuration/salutation";
|
||||||
import type { SalutationViewModel } from "../../../../viewmodels/admin/configuration/salutation.models";
|
import type { SalutationViewModel } from "@/viewmodels/admin/configuration/salutation.models";
|
||||||
|
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -112,6 +121,7 @@ export default defineComponent({
|
||||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
timeout: undefined as any,
|
timeout: undefined as any,
|
||||||
selectedSalutation: null as null | SalutationViewModel,
|
selectedSalutation: null as null | SalutationViewModel,
|
||||||
|
lastId: "" as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -119,6 +129,11 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchSalutations();
|
this.fetchSalutations();
|
||||||
|
this.fetchLastInternalId()
|
||||||
|
.then((res) => {
|
||||||
|
this.lastId = res.data;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
try {
|
try {
|
||||||
|
@ -127,7 +142,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMemberStore, ["createMember"]),
|
...mapActions(useMemberStore, ["createMember", "fetchLastInternalId"]),
|
||||||
...mapActions(useSalutationStore, ["fetchSalutations"]),
|
...mapActions(useSalutationStore, ["fetchSalutations"]),
|
||||||
triggerCreate(e: any) {
|
triggerCreate(e: any) {
|
||||||
if (!this.selectedSalutation) return;
|
if (!this.selectedSalutation) return;
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Auszeichnung</ListboxLabel>
|
<ListboxLabel>Auszeichnung</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{ awards.length != 0 ? (selectedAward?.award ?? "bitte auswählen") : "keine Auswahl vorhanden" }}</span
|
{{ awards.length != 0 ? (selectedAward?.award ?? "bitte auswählen") : "keine Auswahl vorhanden" }}</span
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="awards.length == 0" disabled as="template">
|
<ListboxOption v-if="awards.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ListboxLabel>Auszeichnung</ListboxLabel>
|
<ListboxLabel>Auszeichnung</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{ awards.length != 0 ? (selectedAward ?? "bitte auswählen") : "keine Auswahl vorhanden" }}</span
|
{{ awards.length != 0 ? (selectedAward ?? "bitte auswählen") : "keine Auswahl vorhanden" }}</span
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="awards.length == 0" disabled as="template">
|
<ListboxOption v-if="awards.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Kommunikationsart</ListboxLabel>
|
<ListboxLabel>Kommunikationsart</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="communicationTypes.length == 0" disabled as="template">
|
<ListboxOption v-if="communicationTypes.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Qualifikation</ListboxLabel>
|
<ListboxLabel>Qualifikation</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="executivePositions.length == 0" disabled as="template">
|
<ListboxOption v-if="executivePositions.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ListboxLabel>Auszeichnung</ListboxLabel>
|
<ListboxLabel>Auszeichnung</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="executivePositions.length == 0" disabled as="template">
|
<ListboxOption v-if="executivePositions.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row gap-2 justify-end">
|
<div class="flex flex-row gap-2 justify-end">
|
||||||
<a ref="download" button primary class="!w-fit">download</a>
|
<a ref="download" button primary class="w-fit!">download</a>
|
||||||
<button primary-outline class="!w-fit" @click="closeModal">schließen</button>
|
<button primary-outline class="w-fit!" @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Qualifikation</ListboxLabel>
|
<ListboxLabel>Qualifikation</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="qualifications.length == 0" disabled as="template">
|
<ListboxOption v-if="qualifications.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ListboxLabel>Qualifikation</ListboxLabel>
|
<ListboxLabel>Qualifikation</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="qualifications.length == 0" disabled as="template">
|
<ListboxOption v-if="qualifications.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ListboxLabel>Status</ListboxLabel>
|
<ListboxLabel>Status</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="membershipStatus.length == 0" disabled as="template">
|
<ListboxOption v-if="membershipStatus.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<ListboxLabel>Status</ListboxLabel>
|
<ListboxLabel>Status</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start">
|
<span class="block truncate w-full text-start">
|
||||||
{{
|
{{
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption v-if="membershipStatus.length == 0" disabled as="template">
|
<ListboxOption v-if="membershipStatus.length == 0" disabled as="template">
|
||||||
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
|
||||||
|
|
|
@ -36,8 +36,8 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
|
||||||
import { useNewsletterStore } from "../../../../stores/admin/club/newsletter/newsletter";
|
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||||
import type { CreateNewsletterViewModel } from "../../../../viewmodels/admin/club/newsletter/newsletter.models";
|
import type { CreateNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<div class="flex flex-row justify-end">
|
<div class="flex flex-row justify-end">
|
||||||
<div class="flex flex-row gap-4 py-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
<button primary-outline @click="closeModal">abbrechen</button>
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter Mail Empfänger</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p v-if="receivers.length == 0">keine Empfänger gefunden</p>
|
||||||
|
<p v-else>{{ receivers.length }} Empfänger gefunden</p>
|
||||||
|
<div class="flex flex-col gap-2 h-96 overflow-y-scroll">
|
||||||
|
<p
|
||||||
|
v-for="rec in receivers"
|
||||||
|
:key="rec.id"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center rounded-md"
|
||||||
|
>
|
||||||
|
{{ rec.lastname }}, {{ rec.firstname }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4 py-2 w-full">
|
||||||
|
<button primary @click="start">Versand starten</button>
|
||||||
|
<button primary-outline @click="closeModal">Versand abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
receivers: [] as Array<MemberViewModel>,
|
||||||
|
error: null as null | string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletterMailReceivers()
|
||||||
|
.then((res) => {
|
||||||
|
this.receivers = res.data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.error = "Fehler beim Laden der Empfänger";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterPrintoutStore, ["fetchNewsletterMailReceivers", "createNewsletterSend"]),
|
||||||
|
...mapActions(useModalStore, ["openModal", "closeModal"]),
|
||||||
|
start() {
|
||||||
|
this.createNewsletterSend();
|
||||||
|
this.openMailLogs();
|
||||||
|
},
|
||||||
|
openMailLogs() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(
|
||||||
|
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterMailProgressModal.vue"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -5,7 +5,7 @@
|
||||||
<iframe ref="viewer" class="w-full h-full" />
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
<button primary-outline class="w-fit! self-end" @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<div class="flex flex-row justify-end">
|
<div class="flex flex-row justify-end">
|
||||||
<div class="flex flex-row gap-4 py-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
<button primary-outline @click="closeModal">abbrechen</button>
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter Druck Empfänger</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p v-if="receivers.length == 0">keine Empfänger gefunden</p>
|
||||||
|
<p v-else>{{ receivers.length }} Empfänger gefunden</p>
|
||||||
|
<div class="flex flex-col gap-2 h-96 overflow-y-scroll">
|
||||||
|
<p
|
||||||
|
v-for="rec in receivers"
|
||||||
|
:key="rec.id"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center rounded-md"
|
||||||
|
>
|
||||||
|
{{ rec.lastname }}, {{ rec.firstname }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4 py-2 w-full">
|
||||||
|
<button primary @click="start">Druck starten</button>
|
||||||
|
<button primary-outline @click="closeModal">Druck abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
receivers: [] as Array<MemberViewModel>,
|
||||||
|
error: null as null | string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletterPrintReceivers()
|
||||||
|
.then((res) => {
|
||||||
|
this.receivers = res.data;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.error = "Fehler beim Laden der Empfänger";
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterPrintoutStore, ["fetchNewsletterPrintReceivers", "createNewsletterPrintout"]),
|
||||||
|
...mapActions(useModalStore, ["openModal", "closeModal"]),
|
||||||
|
start() {
|
||||||
|
this.createNewsletterPrintout();
|
||||||
|
this.openPdfLogs();
|
||||||
|
},
|
||||||
|
openPdfLogs() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(
|
||||||
|
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterPrintingProgressModal.vue"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -4,7 +4,7 @@
|
||||||
<iframe ref="viewer" class="w-full h-full" />
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
<button primary-outline class="w-fit! self-end" @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<input type="text" id="type" required />
|
<input type="text" id="type" required />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
<input type="color" id="color" required class="!px-1 !py-0 !w-10" />
|
<input type="color" id="color" required class="px-1! py-0! w-10!" />
|
||||||
<label for="color">Farbe</label>
|
<label for="color">Farbe</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2">
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<ListboxLabel>Felder</ListboxLabel>
|
<ListboxLabel>Felder</ListboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
class="rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
>
|
>
|
||||||
<span class="block truncate w-full text-start"> {{ selectedFields.join(", ") }}</span>
|
<span class="block truncate w-full text-start"> {{ selectedFields.join(", ") }}</span>
|
||||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
leave-to-class="opacity-0"
|
leave-to-class="opacity-0"
|
||||||
>
|
>
|
||||||
<ListboxOptions
|
<ListboxOptions
|
||||||
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm h-32 overflow-y-auto"
|
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
|
||||||
>
|
>
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
v-slot="{ active, selected }"
|
v-slot="{ active, selected }"
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
<p>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
<p>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
||||||
<div v-if="can('create', 'configuration', 'newsletter_config')" class="flex flex-row justify-end w-16">
|
<div v-if="can('create', 'configuration', 'newsletter_config')" class="flex flex-row justify-end w-16">
|
||||||
<button v-if="status == null" type="submit" class="!p-0 !h-fit !w-fit" title="speichern">
|
<button v-if="status == null" type="submit" class="p-0! h-fit! w-fit!" title="Änderung speichern">
|
||||||
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
<button type="button" class="!p-0 !h-fit !w-fit" title="zurücksetzen" @click="resetForm">
|
<button type="button" class="p-0! h-fit! w-fit!" title="Änderung zurücksetzen" @click="resetForm">
|
||||||
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
import { NewsletterConfigEnum } from "@/enums/newsletterConfigEnum";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/configuration/communicationType.models";
|
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/configuration/communicationType.models";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
@ -62,7 +62,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.configs = Object.values(NewsletterConfigType);
|
this.configs = Object.values(NewsletterConfigEnum);
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
>
|
>
|
||||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<button v-if="status == null" class="!p-0 !h-fit !w-fit" title="duplizieren" @click="cloneElement">
|
<button v-if="status == null" class="p-0! h-fit! w-fit!" title="duplizieren" @click="cloneElement">
|
||||||
<DocumentDuplicateIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<DocumentDuplicateIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<iframe ref="viewer" class="w-full h-full" />
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
<button primary-outline class="w-fit! self-end" @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
<p>Templates zu "{{ templateUsage.scope }}" zuweisen</p>
|
<p>Templates zu "{{ templateUsage.scope }}" zuweisen</p>
|
||||||
<div class="flex flex-row justify-end w-16">
|
<div class="flex flex-row justify-end w-16">
|
||||||
<button type="button" class="!p-0 !h-fit !w-fit" title="Vorschau erzeugen" @click="previewUsage">
|
<button type="button" class="p-0! h-fit! w-fit!" title="Vorschau erzeugen" @click="previewUsage">
|
||||||
<EyeIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<EyeIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="status == null && can('create', 'configuration', 'newsletter_config')"
|
v-if="status == null && can('create', 'configuration', 'newsletter_config')"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="!p-0 !h-fit !w-fit"
|
class="p-0! h-fit! w-fit!"
|
||||||
title="speichern"
|
title="speichern"
|
||||||
>
|
>
|
||||||
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
v-if="can('create', 'configuration', 'newsletter_config')"
|
v-if="can('create', 'configuration', 'newsletter_config')"
|
||||||
class="!p-0 !h-fit !w-fit"
|
class="p-0! h-fit! w-fit!"
|
||||||
title="zurücksetzen"
|
title="zurücksetzen"
|
||||||
@click="resetForm"
|
@click="resetForm"
|
||||||
>
|
>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
:min="15"
|
:min="15"
|
||||||
:value="templateUsage.headerHeight"
|
:value="templateUsage.headerHeight"
|
||||||
class="!w-24"
|
class="w-24!"
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
type="number"
|
type="number"
|
||||||
:min="15"
|
:min="15"
|
||||||
:value="templateUsage.footerHeight"
|
:value="templateUsage.footerHeight"
|
||||||
class="!w-24"
|
class="w-24!"
|
||||||
placeholder="15"
|
placeholder="15"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { mapState, mapActions } from "pinia";
|
||||||
import { ArchiveBoxArrowDownIcon, ArrowDownTrayIcon, BarsArrowUpIcon } from "@heroicons/vue/24/outline";
|
import { ArchiveBoxArrowDownIcon, ArrowDownTrayIcon, BarsArrowUpIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import { useBackupStore } from "../../../../stores/admin/management/backup";
|
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -57,9 +57,9 @@ import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useBackupStore } from "@/stores/admin/management/backup";
|
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||||
import type { BackupRestoreViewModel } from "../../../../viewmodels/admin/management/backup.models";
|
import type { BackupRestoreViewModel } from "@/viewmodels/admin/management/backup.models";
|
||||||
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
import { backupSections, type BackupSection } from "../../../../types/backupTypes";
|
import { backupSections, type BackupSection } from "@/types/backupTypes";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<p class="hidden md:block text-center">oder</p>
|
<p class="hidden md:block text-center">oder</p>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
class="!hidden"
|
class="hidden!"
|
||||||
type="file"
|
type="file"
|
||||||
ref="fileSelect"
|
ref="fileSelect"
|
||||||
accept="application/JSON"
|
accept="application/JSON"
|
||||||
|
|
67
src/components/admin/management/setting/AppSetting.vue
Normal file
67
src/components/admin/management/setting/AppSetting.vue
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="Anwendungs Einstellungen" :submit-function="submit" v-slot="{ enableEdit }">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="custom_login_message">Nachricht unter Login (optional)</label>
|
||||||
|
<input
|
||||||
|
id="custom_login_message"
|
||||||
|
type="text"
|
||||||
|
:readonly="!enableEdit"
|
||||||
|
:value="appSettings['app.custom_login_message']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-row items-center gap-2">
|
||||||
|
<div
|
||||||
|
v-if="!enableEdit"
|
||||||
|
class="border-2 border-gray-500 rounded-sm"
|
||||||
|
:class="appSettings['app.show_link_to_calendar'] ? 'bg-gray-500' : 'h-3.5 w-3.5'"
|
||||||
|
>
|
||||||
|
<CheckIcon v-if="appSettings['app.show_link_to_calendar']" class="h-2.5 w-2.5 stroke-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<input v-else id="show_link_to_calendar" type="checkbox" :checked="appSettings['app.show_link_to_calendar']" />
|
||||||
|
<label for="show_link_to_calendar">Kalender-Link anzeigen</label>
|
||||||
|
</div>
|
||||||
|
</BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { CheckIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEdit: false as boolean,
|
||||||
|
status: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
appSettings() {
|
||||||
|
return this.readByTopic("app");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings"]),
|
||||||
|
submit(e: any) {
|
||||||
|
const formData = e.target.elements;
|
||||||
|
return this.updateSettings([
|
||||||
|
{
|
||||||
|
key: "app.custom_login_message",
|
||||||
|
value: formData.custom_login_message.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "app.show_link_to_calendar",
|
||||||
|
value: formData.show_link_to_calendar.checked || null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
53
src/components/admin/management/setting/BackupSetting.vue
Normal file
53
src/components/admin/management/setting/BackupSetting.vue
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="Backup Einstellungen" :submit-function="submit" v-slot="{ enableEdit }">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="copies">Anzahl paralleler Backups (optional)</label>
|
||||||
|
<input id="copies" type="text" :readonly="!enableEdit" :value="backupSettings['backup.copies']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="interval">Intervall zur Backup-Erstellung (optional)</label>
|
||||||
|
<input id="interval" type="text" :readonly="!enableEdit" :value="backupSettings['backup.interval']" /></div
|
||||||
|
></BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEdit: false as boolean,
|
||||||
|
status: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
backupSettings() {
|
||||||
|
return this.readByTopic("backup");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings"]),
|
||||||
|
submit(e: any) {
|
||||||
|
const formData = e.target.elements;
|
||||||
|
return this.updateSettings([
|
||||||
|
{
|
||||||
|
key: "backup.copies",
|
||||||
|
value: formData.copies.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "backup.interval",
|
||||||
|
value: formData.interval.value || null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
87
src/components/admin/management/setting/BaseSetting.vue
Normal file
87
src/components/admin/management/setting/BaseSetting.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<form ref="form" class="flex flex-col w-full" @submit.prevent="submit">
|
||||||
|
<div class="flex flex-row gap-2 items-center border-l-3 border-l-primary p-2 rounded-t-lg bg-red-200">
|
||||||
|
<p class="text-lg font-semibold grow">{{ title }}</p>
|
||||||
|
<Spinner v-if="status == 'loading'" />
|
||||||
|
<SuccessCheckmark v-else-if="status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status == 'failed'" />
|
||||||
|
<div v-else-if="enableEdit" class="flex flex-row gap-2">
|
||||||
|
<button type="submit" class="w-fit! h-fit! p-0!">
|
||||||
|
<CheckIcon class="h-5 w-5 cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="w-fit! h-fit! p-0!"
|
||||||
|
@click="
|
||||||
|
enableEdit = false;
|
||||||
|
$emit('reset');
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<XMarkIcon class="h-5 w-5 cursor-pointer" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<PencilSquareIcon
|
||||||
|
v-else-if="can('create', 'management', 'setting')"
|
||||||
|
class="h-5 w-5 cursor-pointer"
|
||||||
|
@click="enableEdit = true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="border-l-3 border-l-primary p-2 rounded-b-lg">
|
||||||
|
<slot :enableEdit="enableEdit"></slot>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { CheckIcon, PencilSquareIcon, XMarkIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import type { PropType } from "vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
submitFunction: {
|
||||||
|
type: Function as PropType<(e: any) => Promise<any>>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["reset"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEdit: false as boolean,
|
||||||
|
status: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit(e: any) {
|
||||||
|
this.status = "loading";
|
||||||
|
this.submitFunction(e)
|
||||||
|
.then(() => {
|
||||||
|
this.status = "success";
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = "failed";
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.status == "success") this.enableEdit = false;
|
||||||
|
this.status = undefined;
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
152
src/components/admin/management/setting/ClubImageSetting.vue
Normal file
152
src/components/admin/management/setting/ClubImageSetting.vue
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="Vereins-Auftritt Einstellungen" :submit-function="submit" v-slot="{ enableEdit }" @reset="reset">
|
||||||
|
<div class="w-full">
|
||||||
|
<p>Vereins-Icon</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<AppIcon v-if="icon != '' && !overwriteIcon" class="h-10! max-w-full mx-auto" />
|
||||||
|
<div
|
||||||
|
v-else-if="!overwriteIcon"
|
||||||
|
class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm"
|
||||||
|
:class="{ 'cursor-pointer': enableEdit }"
|
||||||
|
@click="enableEdit ? ($refs.icon as HTMLInputElement).click() : null"
|
||||||
|
>
|
||||||
|
Kein eigenes Icon ausgewählt
|
||||||
|
</div>
|
||||||
|
<img ref="icon_img" class="hidden w-full h-10 object-contain" />
|
||||||
|
<XMarkIcon
|
||||||
|
v-if="enableEdit && (icon != '' || overwriteIcon)"
|
||||||
|
class="h-5 w-5 cursor-pointer"
|
||||||
|
@click="resetImage('icon')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input class="hidden!" type="file" ref="icon" accept="image/png" @change="previewImage('icon')" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<p>Vereins-Logo</p>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<AppLogo v-if="logo != '' && !overwriteLogo" class="h-10! max-w-full mx-auto" />
|
||||||
|
<div
|
||||||
|
v-else-if="!overwriteLogo"
|
||||||
|
class="flex h-10 w-full border-2 border-gray-300 rounded-md items-center justify-center text-sm"
|
||||||
|
:class="{ 'cursor-pointer': enableEdit }"
|
||||||
|
@click="enableEdit ? ($refs.logo as HTMLInputElement).click() : null"
|
||||||
|
>
|
||||||
|
Kein eigenes Logo ausgewählt
|
||||||
|
</div>
|
||||||
|
<img ref="logo_img" class="hidden w-full h-10 object-contain" />
|
||||||
|
<XMarkIcon
|
||||||
|
v-if="enableEdit && (logo != '' || overwriteLogo)"
|
||||||
|
class="h-5 w-5 cursor-pointer"
|
||||||
|
@click="resetImage('logo')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input class="hidden!" type="file" ref="logo" accept="image/png" @change="previewImage('logo')" />
|
||||||
|
</div>
|
||||||
|
</BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import AppIcon from "@/components/AppIcon.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import type { SettingString } from "@/types/settingTypes";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
import { XMarkIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
watch: {
|
||||||
|
clubSettings() {
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
logo: "",
|
||||||
|
icon: "",
|
||||||
|
overwriteIcon: false as boolean,
|
||||||
|
overwriteLogo: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
clubSettings() {
|
||||||
|
return this.readByTopic("club");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings", "uploadImage"]),
|
||||||
|
reset() {
|
||||||
|
this.icon = this.clubSettings["club.icon"];
|
||||||
|
this.overwriteIcon = false;
|
||||||
|
(this.$refs.icon_img as HTMLImageElement).style.display = "none";
|
||||||
|
(this.$refs.icon as HTMLInputElement).value = "";
|
||||||
|
|
||||||
|
this.logo = this.clubSettings["club.logo"];
|
||||||
|
this.overwriteLogo = false;
|
||||||
|
(this.$refs.logo_img as HTMLImageElement).style.display = "none";
|
||||||
|
(this.$refs.logo as HTMLInputElement).value = "";
|
||||||
|
},
|
||||||
|
resetImage(inputname: "icon" | "logo") {
|
||||||
|
if (inputname == "icon") {
|
||||||
|
this.icon = "";
|
||||||
|
this.overwriteIcon = false;
|
||||||
|
(this.$refs.icon_img as HTMLImageElement).style.display = "none";
|
||||||
|
(this.$refs.icon as HTMLInputElement).value = "";
|
||||||
|
} else {
|
||||||
|
this.logo = "";
|
||||||
|
this.overwriteLogo = false;
|
||||||
|
(this.$refs.logo_img as HTMLImageElement).style.display = "none";
|
||||||
|
(this.$refs.logo as HTMLInputElement).value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
previewImage(inputname: "icon" | "logo") {
|
||||||
|
let input = this.$refs[inputname] as HTMLInputElement;
|
||||||
|
let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
previewElement.src = e.target?.result as string;
|
||||||
|
previewElement.style.display = "block";
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
|
||||||
|
if (inputname == "icon") {
|
||||||
|
this.overwriteIcon = true;
|
||||||
|
} else {
|
||||||
|
this.overwriteLogo = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
previewElement.src = "";
|
||||||
|
previewElement.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit(e: any) {
|
||||||
|
return this.uploadImage([
|
||||||
|
{
|
||||||
|
key: "club.icon",
|
||||||
|
value:
|
||||||
|
(this.$refs.icon as HTMLInputElement).files?.[0] ??
|
||||||
|
(this.icon != "" && !this.overwriteIcon ? "keep" : undefined),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "club.logo",
|
||||||
|
value:
|
||||||
|
(this.$refs.logo as HTMLInputElement).files?.[0] ??
|
||||||
|
(this.logo != "" && !this.overwriteLogo ? "keep" : undefined),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
89
src/components/admin/management/setting/ClubSetting.vue
Normal file
89
src/components/admin/management/setting/ClubSetting.vue
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="Vereins Einstellungen" :submit-function="submit" v-slot="{ enableEdit }">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="clubname">Vereins-Name (optional)</label>
|
||||||
|
<input id="clubname" type="text" :readonly="!enableEdit" :value="clubSettings['club.name']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="imprint">Vereins-Impressum Link (optional)</label>
|
||||||
|
<input id="imprint" type="url" :readonly="!enableEdit" :value="clubSettings['club.imprint']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="privacy">Vereins-Datenschutz Link (optional)</label>
|
||||||
|
<input id="privacy" type="url" :readonly="!enableEdit" :value="clubSettings['club.privacy']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="website">Vereins-Webseite Link (optional)</label>
|
||||||
|
<input id="website" type="url" :readonly="!enableEdit" :value="clubSettings['club.website']" /></div
|
||||||
|
></BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import AppIcon from "@/components/AppIcon.vue";
|
||||||
|
import AppLogo from "@/components/AppLogo.vue";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import type { SettingString } from "@/types/settingTypes";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
overwriteIcon: false as boolean,
|
||||||
|
overwriteLogo: false as boolean,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
clubSettings() {
|
||||||
|
return this.readByTopic("club");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings", "uploadImage"]),
|
||||||
|
previewImage(inputname: "icon" | "logo") {
|
||||||
|
let input = this.$refs[inputname] as HTMLInputElement;
|
||||||
|
let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
previewElement.src = e.target?.result as string;
|
||||||
|
previewElement.style.display = "block";
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
} else {
|
||||||
|
previewElement.src = "";
|
||||||
|
previewElement.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submit(e: any) {
|
||||||
|
const formData = e.target.elements;
|
||||||
|
return this.updateSettings([
|
||||||
|
{
|
||||||
|
key: "club.name",
|
||||||
|
value: formData.clubname.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "club.imprint",
|
||||||
|
value: formData.imprint.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "club.privacy",
|
||||||
|
value: formData.privacy.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "club.website",
|
||||||
|
value: formData.website.value || null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
100
src/components/admin/management/setting/MailSetting.vue
Normal file
100
src/components/admin/management/setting/MailSetting.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="E-Mail Einstellungen" :submit-function="submit" v-slot="{ enableEdit }">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="email">Mailadresse</label>
|
||||||
|
<input id="email" type="email" autocomplete="email" :readonly="!enableEdit" :value="mailSettings['mail.email']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="username">Benutzername</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
type="text"
|
||||||
|
:readonly="!enableEdit"
|
||||||
|
autocomplete="username"
|
||||||
|
:value="mailSettings['mail.username']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="host">Server-Host</label>
|
||||||
|
<input id="host" type="text" :readonly="!enableEdit" :value="mailSettings['mail.host']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="port">Server-Port (25, 465, 587)</label>
|
||||||
|
<input id="port" type="number" :readonly="!enableEdit" :value="mailSettings['mail.port']" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-row items-center gap-2">
|
||||||
|
<div
|
||||||
|
v-if="!enableEdit"
|
||||||
|
class="border-2 border-gray-500 rounded-sm"
|
||||||
|
:class="mailSettings['mail.secure'] ? 'bg-gray-500' : 'h-3.5 w-3.5'"
|
||||||
|
>
|
||||||
|
<CheckIcon v-if="mailSettings['mail.secure']" class="h-2.5 w-2.5 stroke-4 text-white" />
|
||||||
|
</div>
|
||||||
|
<input v-else id="secure" type="checkbox" :checked="mailSettings['mail.secure']" />
|
||||||
|
<label for="secure">Secure-Verbindung (setzen bei Port 465)</label>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="password">Passwort (optional - leeres Feld setzt Passwort nicht zurück)</label>
|
||||||
|
<input id="password" type="password" :readonly="!enableEdit" autocomplete="new-password" />
|
||||||
|
</div>
|
||||||
|
</BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { CheckIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEdit: false as boolean,
|
||||||
|
status: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
mailSettings() {
|
||||||
|
return this.readByTopic("mail");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings"]),
|
||||||
|
submit(e: any) {
|
||||||
|
const formData = e.target.elements;
|
||||||
|
return this.updateSettings([
|
||||||
|
{
|
||||||
|
key: "mail.email",
|
||||||
|
value: formData.email.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mail.username",
|
||||||
|
value: formData.username.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mail.host",
|
||||||
|
value: formData.host.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mail.port",
|
||||||
|
value: formData.port.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mail.secure",
|
||||||
|
value: formData.secure.checked,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "mail.password",
|
||||||
|
value: formData.password.value || null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
76
src/components/admin/management/setting/SessionSetting.vue
Normal file
76
src/components/admin/management/setting/SessionSetting.vue
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<BaseSetting title="Login-Session Einstellungen" :submit-function="submit" v-slot="{ enableEdit }">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="jwt_expiration">JWT-Gültigkeitsdauer (optional)</label>
|
||||||
|
<input
|
||||||
|
id="jwt_expiration"
|
||||||
|
type="text"
|
||||||
|
:readonly="!enableEdit"
|
||||||
|
:value="sessionSettings['session.jwt_expiration']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="refresh_expiration">Session-Gültigkeitsdauer (optional)</label>
|
||||||
|
<input
|
||||||
|
id="refresh_expiration"
|
||||||
|
type="text"
|
||||||
|
:readonly="!enableEdit"
|
||||||
|
:value="sessionSettings['session.refresh_expiration']"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="pwa_refresh_expiration">Sesion-Gültigkeitsdauer PWA (optional)</label>
|
||||||
|
<input
|
||||||
|
id="pwa_refresh_expiration"
|
||||||
|
type="text"
|
||||||
|
:readonly="!enableEdit"
|
||||||
|
:value="sessionSettings['session.pwa_refresh_expiration']"
|
||||||
|
/></div
|
||||||
|
></BaseSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useSettingStore } from "@/stores/admin/management/setting";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import BaseSetting from "./BaseSetting.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enableEdit: false as boolean,
|
||||||
|
status: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingStore, ["readByTopic"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
sessionSettings() {
|
||||||
|
return this.readByTopic("session");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSettingStore, ["updateSettings"]),
|
||||||
|
submit(e: any) {
|
||||||
|
const formData = e.target.elements;
|
||||||
|
return this.updateSettings([
|
||||||
|
{
|
||||||
|
key: "session.jwt_expiration",
|
||||||
|
value: formData.jwt_expiration.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "session.refresh_expiration",
|
||||||
|
value: formData.refresh_expiration.value || null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "session.pwa_refresh_expiration",
|
||||||
|
value: formData.pwa_refresh_expiration.value || null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -8,16 +8,16 @@
|
||||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="invite">
|
<form class="flex flex-col gap-4 py-2" @submit.prevent="invite">
|
||||||
<div class="-space-y-px">
|
<div class="-space-y-px">
|
||||||
<div>
|
<div>
|
||||||
<input id="username" name="username" type="text" required placeholder="Benutzer" class="!rounded-b-none" />
|
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="!rounded-none" />
|
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="rounded-none!" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="!rounded-none" />
|
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="rounded-none!" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="!rounded-t-none" />
|
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="rounded-t-none!" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
|
|
|
@ -39,7 +39,7 @@ import Spinner from "@/components/Spinner.vue";
|
||||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useWebapiStore } from "@/stores/admin/management/webapi";
|
import { useWebapiStore } from "@/stores/admin/management/webapi";
|
||||||
import type { CreateWebapiViewModel } from "../../../../viewmodels/admin/management/webapi.models";
|
import type { CreateWebapiViewModel } from "@/viewmodels/admin/management/webapi.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import TextCopy from "@/components/TextCopy.vue";
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/vue/24/outline";
|
import { CalendarDaysIcon, InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
import { host } from "@/serverCom";
|
import { host } from "@/serverCom";
|
||||||
import { useWebapiStore } from "../../../../stores/admin/management/webapi";
|
import { useWebapiStore } from "@/stores/admin/management/webapi";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -30,9 +30,12 @@
|
||||||
v-if="allowPredefinedSelect && can('read', 'configuration', 'query_store')"
|
v-if="allowPredefinedSelect && can('read', 'configuration', 'query_store')"
|
||||||
class="flex flex-row gap-2 max-lg:w-full max-lg:order-10"
|
class="flex flex-row gap-2 max-lg:w-full max-lg:order-10"
|
||||||
>
|
>
|
||||||
<select v-model="activeQueryId" class="max-h-[34px] !py-0">
|
<div v-if="!isAsStored" class="p-1 border border-gray-400 bg-gray-100 rounded-md" title="Änderung erkannt">
|
||||||
|
<DocumentCurrencyRupeeIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
<select v-model="activeQueryId" class="max-h-[34px] py-0!">
|
||||||
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option>
|
<option :value="undefined" disabled>gepeicherte Anfrage auswählen</option>
|
||||||
<option v-for="query in queries" :key="query.id" :value="query.id" @click="value = query.query">
|
<option v-for="query in queries" :key="query.id" :value="query.id">
|
||||||
{{ query.title }}
|
{{ query.title }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
@ -54,7 +57,7 @@
|
||||||
class="p-1"
|
class="p-1"
|
||||||
:class="typeof value == 'object' ? 'bg-gray-200' : ''"
|
:class="typeof value == 'object' ? 'bg-gray-200' : ''"
|
||||||
title="Visual Builder"
|
title="Visual Builder"
|
||||||
@click="queryMode = 'builder'"
|
@click="changeMode('builder')"
|
||||||
>
|
>
|
||||||
<RectangleGroupIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
<RectangleGroupIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,7 +65,7 @@
|
||||||
class="p-1"
|
class="p-1"
|
||||||
:class="typeof value == 'string' ? 'bg-gray-200' : ''"
|
:class="typeof value == 'string' ? 'bg-gray-200' : ''"
|
||||||
title="SQL Editor"
|
title="SQL Editor"
|
||||||
@click="queryMode = 'editor'"
|
@click="changeMode('editor')"
|
||||||
>
|
>
|
||||||
<CommandLineIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
<CommandLineIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2 h-44 md:h-60 w-full overflow-y-auto">
|
<div class="p-2 h-44 md:h-60 w-full overflow-y-auto">
|
||||||
<textarea v-if="typeof value == 'string'" v-model="value" placeholder="SQL Query" class="h-full w-full" />
|
<textarea v-if="typeof value == 'string'" v-model="value" placeholder="SQL Query" class="h-full w-full" />
|
||||||
<Table v-else v-model="value" />
|
<Table v-else v-model="value" enableOrder />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -78,7 +81,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
import { type DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||||
import {
|
import {
|
||||||
ArchiveBoxArrowDownIcon,
|
ArchiveBoxArrowDownIcon,
|
||||||
CommandLineIcon,
|
CommandLineIcon,
|
||||||
|
@ -88,12 +91,15 @@ import {
|
||||||
RectangleGroupIcon,
|
RectangleGroupIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
|
DocumentCurrencyRupeeIcon,
|
||||||
} from "@heroicons/vue/24/outline";
|
} from "@heroicons/vue/24/outline";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import Table from "./Table.vue";
|
import Table from "./Table.vue";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
|
import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import cloneDeep from "lodash.clonedeep";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -102,6 +108,7 @@ export default defineComponent({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: [Object, String] as PropType<DynamicQueryStructure | string>,
|
type: [Object, String] as PropType<DynamicQueryStructure | string>,
|
||||||
default: {
|
default: {
|
||||||
|
id: uuid(),
|
||||||
select: "*",
|
select: "*",
|
||||||
table: "",
|
table: "",
|
||||||
where: [],
|
where: [],
|
||||||
|
@ -116,21 +123,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "query:run", "query:save", "results:export", "results:clear"],
|
emits: ["update:model-value", "query:run", "query:save", "results:export", "results:clear"],
|
||||||
watch: {
|
watch: {
|
||||||
queryMode() {
|
|
||||||
if (this.queryMode == "builder") {
|
|
||||||
this.value = {
|
|
||||||
select: "*",
|
|
||||||
table: "",
|
|
||||||
where: [],
|
|
||||||
join: [],
|
|
||||||
orderBy: [],
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.value = "";
|
|
||||||
}
|
|
||||||
this.activeQueryId = undefined;
|
|
||||||
},
|
|
||||||
activeQueryId() {
|
activeQueryId() {
|
||||||
|
if (this.activeQueryId == undefined) return;
|
||||||
|
|
||||||
let query = this.queries.find((t) => t.id == this.activeQueryId)?.query;
|
let query = this.queries.find((t) => t.id == this.activeQueryId)?.query;
|
||||||
if (query != undefined) {
|
if (query != undefined) {
|
||||||
if (typeof query == "string") {
|
if (typeof query == "string") {
|
||||||
|
@ -138,7 +133,12 @@ export default defineComponent({
|
||||||
} else {
|
} else {
|
||||||
this.queryMode = "builder";
|
this.queryMode = "builder";
|
||||||
}
|
}
|
||||||
this.value = query;
|
this.value = cloneDeep(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
if (typeof this.value != "string" && !this.value.id) {
|
||||||
|
this.value.id = uuid();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -161,6 +161,11 @@ export default defineComponent({
|
||||||
this.$emit("update:model-value", val);
|
this.$emit("update:model-value", val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
isAsStored() {
|
||||||
|
let stored = this.queries.find((q) => q.id == this.activeQueryId);
|
||||||
|
if (!stored) return true;
|
||||||
|
return JSON.stringify(this.value) == JSON.stringify(stored.query);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchTableMetas();
|
this.fetchTableMetas();
|
||||||
|
@ -175,6 +180,7 @@ export default defineComponent({
|
||||||
this.activeQueryId = undefined;
|
this.activeQueryId = undefined;
|
||||||
if (typeof this.value != "string") {
|
if (typeof this.value != "string") {
|
||||||
this.value = {
|
this.value = {
|
||||||
|
id: uuid(),
|
||||||
select: "*",
|
select: "*",
|
||||||
table: "",
|
table: "",
|
||||||
where: [],
|
where: [],
|
||||||
|
@ -188,6 +194,23 @@ export default defineComponent({
|
||||||
showStructure() {
|
showStructure() {
|
||||||
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/queryBuilder/StructureModal.vue"))));
|
this.openModal(markRaw(defineAsyncComponent(() => import("@/components/queryBuilder/StructureModal.vue"))));
|
||||||
},
|
},
|
||||||
|
changeMode(mode: "editor" | "builder") {
|
||||||
|
this.queryMode = mode;
|
||||||
|
|
||||||
|
this.activeQueryId = undefined;
|
||||||
|
if (this.queryMode == "builder") {
|
||||||
|
this.value = {
|
||||||
|
id: uuid(),
|
||||||
|
select: "*",
|
||||||
|
table: "",
|
||||||
|
where: [],
|
||||||
|
join: [],
|
||||||
|
orderBy: [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.value = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<p class="w-14 min-w-14 pt-2">SELECT</p>
|
<p class="w-14 min-w-14 pt-2">SELECT</p>
|
||||||
<div class="flex flex-row flex-wrap gap-2 items-center">
|
<div class="flex flex-row flex-wrap gap-2 items-center">
|
||||||
<p
|
<p
|
||||||
class="rounded-md shadow-sm relative block w-fit px-3 py-2 border border-gray-300 text-gray-900 rounded-b-md sm:text-sm"
|
class="rounded-md shadow-xs relative block w-fit px-3 py-2 border border-gray-300 text-gray-900 rounded-b-md sm:text-sm"
|
||||||
:class="value == '*' ? 'border-gray-600 bg-gray-200' : ''"
|
:class="value == '*' ? 'border-gray-600 bg-gray-200' : ''"
|
||||||
@click="value = '*'"
|
@click="value = '*'"
|
||||||
>
|
>
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<p
|
<p
|
||||||
v-for="col in columns"
|
v-for="col in columns"
|
||||||
:key="col.column"
|
:key="col.column"
|
||||||
class="rounded-md shadow-sm relative block w-fit px-3 py-2 border border-gray-300 text-gray-900 rounded-b-md sm:text-sm"
|
class="rounded-md shadow-xs relative block w-fit px-3 py-2 border border-gray-300 text-gray-900 rounded-b-md sm:text-sm"
|
||||||
:class="value.includes(col.column) ? 'border-gray-600 bg-gray-200' : ''"
|
:class="value.includes(col.column) ? 'border-gray-600 bg-gray-200' : ''"
|
||||||
@click="value = [col.column]"
|
@click="value = [col.column]"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2 items-center w-full">
|
<div class="flex flex-row gap-2 items-center w-full">
|
||||||
<select v-if="concat != '_'" v-model="concat" class="!w-20 !h-fit">
|
<select v-if="!isFirst" v-model="concat" class="w-20! h-fit!">
|
||||||
<option value="" disabled>Verknüpfung auswählen</option>
|
<option value="" disabled>Verknüpfung auswählen</option>
|
||||||
<option v-for="operation in ['AND', 'OR']" :value="operation">
|
<option v-for="operation in ['AND', 'OR']" :value="operation">
|
||||||
{{ operation }}
|
{{ operation }}
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
{{ foreignColumns?.includes(col.column) ? "FK:" : "" }} {{ col.column }}:{{ col.type }}
|
{{ foreignColumns?.includes(col.column) ? "FK:" : "" }} {{ col.column }}:{{ col.type }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-model="operation" class="!w-fit !h-fit">
|
<select v-model="operation" class="w-fit! h-fit!">
|
||||||
<option value="" disabled>Vergleich auswählen</option>
|
<option value="" disabled>Vergleich auswählen</option>
|
||||||
<option v-for="op in whereOperationArray" :value="op">
|
<option v-for="op in whereOperationArray" :value="op">
|
||||||
{{ op }}
|
{{ op }}
|
||||||
|
@ -68,6 +68,10 @@ import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
table: {
|
table: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
@ -78,9 +82,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "remove"],
|
emits: ["update:model-value", "remove"],
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
activeTable() {
|
activeTable() {
|
||||||
|
@ -144,5 +145,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.concat == "_") {
|
||||||
|
this.concat = "AND";
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<p class="w-14 min-w-14 pt-2">JOIN</p>
|
<p class="w-14 min-w-14 pt-2">I_JOIN</p>
|
||||||
<div class="flex flex-row flex-wrap gap-2 items-center w-full">
|
<div class="flex flex-row flex-wrap gap-2 items-center w-full">
|
||||||
<div class="flex flex-row flex-wrap gap-2 items-center justify-end w-full">
|
<div class="flex flex-row flex-wrap gap-2 items-center justify-end w-full">
|
||||||
<JoinTable
|
<JoinTable
|
||||||
v-for="(join, index) in value"
|
v-for="(join, index) in value"
|
||||||
:model-value="join"
|
:model-value="join"
|
||||||
:table="table"
|
:table="table"
|
||||||
|
:alreadyJoined="alreadyJoined"
|
||||||
@update:model-value="($event) => (value[index] = $event)"
|
@update:model-value="($event) => (value[index] = $event)"
|
||||||
@remove="removeAtIndex(index)"
|
@remove="removeAtIndex(index)"
|
||||||
/>
|
/>
|
||||||
|
@ -21,10 +22,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
import { type DynamicQueryStructure, type JoinStructure } from "@/types/dynamicQueries";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||||
import { PlusIcon } from "@heroicons/vue/24/outline";
|
import { PlusIcon } from "@heroicons/vue/24/outline";
|
||||||
import JoinTable from "./JoinTable.vue";
|
import JoinTable from "./JoinTable.vue";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -35,24 +37,22 @@ export default defineComponent({
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<Array<DynamicQueryStructure & { foreignColumn: string }>>,
|
type: Array as PropType<Array<DynamicQueryStructure & JoinStructure>>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
alreadyJoined: {
|
||||||
|
type: Array as PropType<Array<string>>,
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value"],
|
emits: ["update:model-value"],
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
activeTable() {
|
|
||||||
return this.tableMetas.find((tm) => tm.tableName == this.table);
|
|
||||||
},
|
|
||||||
value: {
|
value: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
},
|
},
|
||||||
set(val: Array<DynamicQueryStructure & { foreignColumn: string }>) {
|
set(val: Array<DynamicQueryStructure & JoinStructure>) {
|
||||||
this.$emit("update:model-value", val);
|
this.$emit("update:model-value", val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -60,11 +60,13 @@ export default defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
addToValue() {
|
addToValue() {
|
||||||
this.value.push({
|
this.value.push({
|
||||||
|
id: uuid(),
|
||||||
select: "*",
|
select: "*",
|
||||||
table: "",
|
table: "",
|
||||||
where: [],
|
where: [],
|
||||||
join: [],
|
join: [],
|
||||||
orderBy: [],
|
orderBy: [],
|
||||||
|
type: "defined",
|
||||||
foreignColumn: "",
|
foreignColumn: "",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,13 +2,52 @@
|
||||||
<div class="flex flex-row gap-2 w-full">
|
<div class="flex flex-row gap-2 w-full">
|
||||||
<div class="flex flex-row gap-2 w-full">
|
<div class="flex flex-row gap-2 w-full">
|
||||||
<div class="flex flex-col gap-2 w-full">
|
<div class="flex flex-col gap-2 w-full">
|
||||||
<select v-model="foreignColumn" class="w-full">
|
<div class="flex flex-row gap-2 w-full">
|
||||||
<option value="" disabled>Relation auswählen</option>
|
<div
|
||||||
<option v-for="relation in activeTable?.relations" :value="relation.column">
|
v-if="false"
|
||||||
{{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }}
|
class="h-fit p-1 border border-gray-400 hover:bg-gray-200 rounded-md"
|
||||||
</option>
|
title="Join Modus wechseln"
|
||||||
</select>
|
@click="swapJoinType(value.type)"
|
||||||
<Table v-model="value" disable-table-select />
|
>
|
||||||
|
<ArrowsUpDownIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select v-if="type == 'defined'" v-model="context" class="w-full">
|
||||||
|
<option value="" disabled>Relation auswählen</option>
|
||||||
|
<option
|
||||||
|
v-for="relation in activeTable?.relations"
|
||||||
|
:value="relation.column"
|
||||||
|
:disabled="
|
||||||
|
alreadyJoined.includes(joinTableName(relation.referencedTableName)) &&
|
||||||
|
joinTableName(relation.referencedTableName) != value.table
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ relation.column }} -> {{ joinTableName(relation.referencedTableName) }}
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
alreadyJoined.includes(joinTableName(relation.referencedTableName)) &&
|
||||||
|
joinTableName(relation.referencedTableName) != value.table
|
||||||
|
"
|
||||||
|
>
|
||||||
|
(Join auf dieser Ebene besteht schon)
|
||||||
|
</span>
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div v-else class="flex flex-col w-full">
|
||||||
|
<select v-model="joinTable">
|
||||||
|
<option value="" disabled>Tabelle auswählen</option>
|
||||||
|
<option
|
||||||
|
v-for="table in tableMetas"
|
||||||
|
:value="table.tableName"
|
||||||
|
:disabled="alreadyJoined.includes(table.tableName) && table.tableName != value.table"
|
||||||
|
>
|
||||||
|
{{ table.tableName }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input v-model="context" type="text" placeholder="Join Condition tabA.col = tabB.col" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table v-model="value" disable-table-select :show-table-select="false" />
|
||||||
</div>
|
</div>
|
||||||
<div class="h-fit p-1 border border-gray-400 hover:bg-gray-200 rounded-md" @click="$emit('remove')">
|
<div class="h-fit p-1 border border-gray-400 hover:bg-gray-200 rounded-md" @click="$emit('remove')">
|
||||||
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
<TrashIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
|
@ -20,11 +59,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
import { type DynamicQueryStructure, type JoinStructure } from "@/types/dynamicQueries";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||||
import Table from "./Table.vue";
|
import Table from "./Table.vue";
|
||||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
import { ArrowsUpDownIcon, TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { joinTableName } from "@/helpers/queryFormatter";
|
import { joinTableName } from "@/helpers/queryFormatter";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -35,25 +75,15 @@ export default defineComponent({
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object as PropType<
|
type: Object as PropType<DynamicQueryStructure & JoinStructure>,
|
||||||
DynamicQueryStructure & {
|
required: true,
|
||||||
foreignColumn: string;
|
},
|
||||||
}
|
alreadyJoined: {
|
||||||
>,
|
type: Array as PropType<Array<string>>,
|
||||||
default: {
|
default: [],
|
||||||
select: "*",
|
|
||||||
table: "",
|
|
||||||
where: [],
|
|
||||||
join: [],
|
|
||||||
orderBy: [],
|
|
||||||
foreignColumn: "",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "remove"],
|
emits: ["update:model-value", "remove"],
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
activeTable() {
|
activeTable() {
|
||||||
|
@ -63,27 +93,73 @@ export default defineComponent({
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
},
|
},
|
||||||
set(
|
set(val: DynamicQueryStructure & JoinStructure) {
|
||||||
val: DynamicQueryStructure & {
|
|
||||||
foreignColumn: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
this.$emit("update:model-value", val);
|
this.$emit("update:model-value", val);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
foreignColumn: {
|
context: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue.foreignColumn;
|
if (this.modelValue.type == "defined") {
|
||||||
|
return this.modelValue.foreignColumn ?? "";
|
||||||
|
} else {
|
||||||
|
return this.modelValue.condition ?? "";
|
||||||
|
}
|
||||||
},
|
},
|
||||||
set(val: string) {
|
set(val: string) {
|
||||||
let relTable = this.activeTable?.relations.find((r) => r.column == val);
|
if (this.modelValue.type == "defined") {
|
||||||
|
let relTable = this.activeTable?.relations.find((r) => r.column == val);
|
||||||
|
this.$emit("update:model-value", {
|
||||||
|
...this.modelValue,
|
||||||
|
foreignColumn: val,
|
||||||
|
table: joinTableName(relTable?.referencedTableName ?? ""),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$emit("update:model-value", {
|
||||||
|
...this.modelValue,
|
||||||
|
condition: val,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
get(): string {
|
||||||
|
return this.modelValue.type ?? "defined";
|
||||||
|
},
|
||||||
|
set(val: "custom" | "defined") {
|
||||||
this.$emit("update:model-value", {
|
this.$emit("update:model-value", {
|
||||||
...this.modelValue,
|
...this.modelValue,
|
||||||
foreignColumn: val,
|
type: val,
|
||||||
table: joinTableName(relTable?.referencedTableName ?? ""),
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
joinTable: {
|
||||||
|
get(): string {
|
||||||
|
return this.modelValue.table;
|
||||||
|
},
|
||||||
|
set(val: string) {
|
||||||
|
this.$emit("update:model-value", {
|
||||||
|
...this.modelValue,
|
||||||
|
table: val,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.value.id) {
|
||||||
|
this.value.id = uuid();
|
||||||
|
}
|
||||||
|
if (!this.value.type) {
|
||||||
|
this.type = "defined";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
swapJoinType(type: string) {
|
||||||
|
if (type == "defined") {
|
||||||
|
this.type = "custom";
|
||||||
|
} else {
|
||||||
|
this.type = "defined";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2 w-full border border-gray-300 rounded-md p-1">
|
<div class="flex flex-row gap-2 w-full border border-gray-300 rounded-md p-1">
|
||||||
<select v-if="concat != '_'" v-model="concat" class="!w-20 !h-fit">
|
<select v-if="isFirst" v-model="concat" class="w-20! h-fit!">
|
||||||
<option value="" disabled>Verknüpfung auswählen</option>
|
<option value="" disabled>Verknüpfung auswählen</option>
|
||||||
<option v-for="operation in ['AND', 'OR']" :value="operation">
|
<option v-for="operation in ['AND', 'OR']" :value="operation">
|
||||||
{{ operation }}
|
{{ operation }}
|
||||||
|
@ -28,6 +28,10 @@ import NestedWhere from "./NestedWhere.vue";
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
isFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
table: {
|
table: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
@ -38,9 +42,6 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "remove"],
|
emits: ["update:model-value", "remove"],
|
||||||
data() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
concat: {
|
concat: {
|
||||||
|
@ -60,5 +61,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.concat == "_") {
|
||||||
|
this.concat = "AND";
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<p class="w-14 min-w-14 pt-2">ORDER</p>
|
<p class="w-14 min-w-14 pt-2">SORT</p>
|
||||||
<div class="flex flex-row flex-wrap gap-2 items-center w-full">
|
<div class="flex flex-row flex-wrap gap-2 items-center w-full">
|
||||||
<OrderStructure
|
<OrderStructure
|
||||||
v-for="(order, index) in value"
|
v-for="(order, index) in value"
|
||||||
:model-value="order"
|
:model-value="order"
|
||||||
:table="table"
|
:table="table"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
:alreadySorted="alreadySorted"
|
||||||
|
:notFirst="index != 0"
|
||||||
|
:notLast="index != value.length - 1"
|
||||||
|
@up="changeSort('up', index)"
|
||||||
|
@down="changeSort('down', index)"
|
||||||
@update:model-value="($event) => (value[index] = $event)"
|
@update:model-value="($event) => (value[index] = $event)"
|
||||||
@remove="removeAtIndex(index)"
|
@remove="removeAtIndex(index)"
|
||||||
/>
|
/>
|
||||||
|
@ -35,9 +40,15 @@ export default defineComponent({
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
// columns: {
|
||||||
|
// type: [Array, String] as PropType<"*" | Array<string>>,
|
||||||
|
// default: "*",
|
||||||
|
// },
|
||||||
columns: {
|
columns: {
|
||||||
type: [Array, String] as PropType<"*" | Array<string>>,
|
type: Array as PropType<
|
||||||
default: "*",
|
Array<{ table: string; id: string; depth: number; path: string[]; columns: "*" | string[] }>
|
||||||
|
>,
|
||||||
|
default: [],
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<Array<OrderByStructure>>,
|
type: Array as PropType<Array<OrderByStructure>>,
|
||||||
|
@ -50,6 +61,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
|
alreadySorted() {
|
||||||
|
return this.modelValue.map((m) => ({ id: m.id, col: m.column }));
|
||||||
|
},
|
||||||
value: {
|
value: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue;
|
return this.modelValue;
|
||||||
|
@ -62,6 +76,9 @@ export default defineComponent({
|
||||||
methods: {
|
methods: {
|
||||||
addToValue() {
|
addToValue() {
|
||||||
this.value.push({
|
this.value.push({
|
||||||
|
id: "",
|
||||||
|
depth: 0,
|
||||||
|
table: "",
|
||||||
column: "",
|
column: "",
|
||||||
order: "ASC",
|
order: "ASC",
|
||||||
});
|
});
|
||||||
|
@ -69,6 +86,12 @@ export default defineComponent({
|
||||||
removeAtIndex(index: number) {
|
removeAtIndex(index: number) {
|
||||||
this.value.splice(index, 1);
|
this.value.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
changeSort(dir: "up" | "down", index: number) {
|
||||||
|
const swapIndex = dir === "up" ? index - 1 : index + 1;
|
||||||
|
if (swapIndex >= 0 && swapIndex < this.value.length) {
|
||||||
|
[this.value[index], this.value[swapIndex]] = [this.value[swapIndex], this.value[index]];
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row gap-2 items-center w-full">
|
<div class="flex flex-row gap-2 items-center w-full">
|
||||||
|
<div class="flex flex-col min-w-fit">
|
||||||
|
<ChevronUpIcon v-if="notFirst" class="w-4 h-4 stroke-2 cursor-pointer" @click.prevent="$emit('up')" />
|
||||||
|
<ChevronDownIcon v-if="notLast" class="w-4 h-4 stroke-2 cursor-pointer" @click.prevent="$emit('down')" />
|
||||||
|
</div>
|
||||||
<select v-model="column" class="w-full">
|
<select v-model="column" class="w-full">
|
||||||
<option value="" disabled>Spalte auswählen</option>
|
<option value="" disabled>Spalte auswählen</option>
|
||||||
<option v-for="column in selectableColumns" :value="column">
|
<option
|
||||||
{{ column }}
|
v-for="selectable in selectableColumns"
|
||||||
|
:value="`${selectable.id}_${selectable.column}`"
|
||||||
|
:disabled="
|
||||||
|
alreadySorted.some((as) => as.id == selectable.id && as.col == selectable.column) &&
|
||||||
|
`${selectable.id}_${selectable.column}` != column
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ [...selectable.path, selectable.table].join("-") }} -> {{ selectable.column }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select v-model="order">
|
<select v-model="order">
|
||||||
<option value="" disabled>Sortierung auswählen</option>
|
<option value="" disabled>Sortierung auswählen</option>
|
||||||
<option v-for="order in ['ASC', 'DESC']" :value="order">
|
<option v-for="order in orderable" :value="order.key">
|
||||||
{{ order }}
|
{{ order.val }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="p-1 border border-gray-400 hover:bg-gray-200 rounded-md" @click="$emit('remove')">
|
<div class="p-1 border border-gray-400 hover:bg-gray-200 rounded-md" @click="$emit('remove')">
|
||||||
|
@ -23,47 +34,102 @@ import { defineComponent, type PropType } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import type { OrderByStructure, OrderByType } from "@/types/dynamicQueries";
|
import type { OrderByStructure, OrderByType } from "@/types/dynamicQueries";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
import { TrashIcon, ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
notFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
defailt: false,
|
||||||
|
},
|
||||||
|
notLast: {
|
||||||
|
type: Boolean,
|
||||||
|
defailt: false,
|
||||||
|
},
|
||||||
table: {
|
table: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
// columns: {
|
||||||
|
// type: [Array, String] as PropType<"*" | Array<string>>,
|
||||||
|
// default: "*",
|
||||||
|
// },
|
||||||
columns: {
|
columns: {
|
||||||
type: [Array, String] as PropType<"*" | Array<string>>,
|
type: Array as PropType<
|
||||||
default: "*",
|
Array<{ table: string; id: string; depth: number; path: string[]; columns: "*" | string[] }>
|
||||||
|
>,
|
||||||
|
default: [],
|
||||||
},
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object as PropType<OrderByStructure>,
|
type: Object as PropType<OrderByStructure>,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
|
alreadySorted: {
|
||||||
|
type: Array as PropType<Array<{ id: string; col: string }>>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:model-value", "remove", "up", "down"],
|
||||||
|
watch: {
|
||||||
|
columns() {
|
||||||
|
if (!this.columns.some((c) => c.id == this.modelValue.id)) {
|
||||||
|
this.$emit("remove");
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value", "remove"],
|
|
||||||
data() {
|
data() {
|
||||||
return {};
|
return {
|
||||||
|
orderable: [
|
||||||
|
{ key: "ASC", val: "Aufsteigend (ABC)" },
|
||||||
|
{ key: "DESC", val: "Absteigend (CBA)" },
|
||||||
|
],
|
||||||
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
...mapState(useQueryBuilderStore, ["tableMetas"]),
|
||||||
|
// selectableColumns() {
|
||||||
|
// if (this.columns == "*") {
|
||||||
|
// let meta = this.tableMetas.find((tm) => tm.tableName == this.table);
|
||||||
|
// if (!meta) return [];
|
||||||
|
// let relCols = meta.relations.map((r) => r.column);
|
||||||
|
// return meta.columns.map((c) => c.column).filter((c) => !relCols.includes(c));
|
||||||
|
// } else {
|
||||||
|
// return this.columns;
|
||||||
|
// }
|
||||||
|
// },
|
||||||
selectableColumns() {
|
selectableColumns() {
|
||||||
if (this.columns == "*") {
|
return this.columns.reduce(
|
||||||
let meta = this.tableMetas.find((tm) => tm.tableName == this.table);
|
(acc, curr) => {
|
||||||
if (!meta) return [];
|
if (curr.columns == "*") {
|
||||||
let relCols = meta.relations.map((r) => r.column);
|
let meta = this.tableMetas.find((tm) => tm.tableName == curr.table);
|
||||||
return meta.columns.map((c) => c.column).filter((c) => !relCols.includes(c));
|
if (meta) {
|
||||||
} else {
|
let relCols = meta.relations.map((r) => r.column);
|
||||||
return this.columns;
|
meta.columns
|
||||||
}
|
.map((c) => c.column)
|
||||||
|
.filter((c) => !relCols.includes(c))
|
||||||
|
.forEach((c) =>
|
||||||
|
acc.push({ id: curr.id, depth: curr.depth, table: curr.table, column: c, path: curr.path })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
curr.columns.forEach((c) =>
|
||||||
|
acc.push({ id: curr.id, depth: curr.depth, table: curr.table, column: c, path: curr.path })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[] as Array<{ id: string; depth: number; table: string; column: string; path: string[] }>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
column: {
|
column: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue.column;
|
return `${this.modelValue.id}_${this.modelValue.column}`;
|
||||||
},
|
},
|
||||||
set(val: string) {
|
set(val: `${string}_${string}`) {
|
||||||
this.$emit("update:model-value", { ...this.modelValue, column: val });
|
let col = this.selectableColumns.find((sc) => sc.id == val.split("_")[0] && sc.column == val.split("_")[1]);
|
||||||
|
this.$emit("update:model-value", { ...this.modelValue, ...col });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
order: {
|
order: {
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
<div class="flex flex-row justify-end">
|
<div class="flex flex-row justify-end">
|
||||||
<div class="flex flex-row gap-4 py-2">
|
<div class="flex flex-row gap-4 py-2">
|
||||||
<a href="/administration-db.png" button primary-outline download="Datenbank-Schema" class="!whitespace-nowrap"
|
<a href="/administration-db.png" button primary-outline download="Datenbank-Schema" class="whitespace-nowrap!"
|
||||||
>Bild herunterladen</a
|
>Bild herunterladen</a
|
||||||
>
|
>
|
||||||
<button primary-outline @click="closeModal">schließen</button>
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 w-full">
|
<div class="flex flex-col gap-2 w-full">
|
||||||
<TableSelect v-model="table" :disableTableSelect="disableTableSelect" />
|
<TableSelect v-if="showTableSelect" v-model="table" :disableTableSelect="disableTableSelect" />
|
||||||
<ColumnSelect v-if="table != ''" v-model="columnSelect" :table="table" />
|
<ColumnSelect v-if="table != ''" v-model="columnSelect" :table="table" />
|
||||||
<Where v-if="table != ''" v-model="where" :table="table" />
|
<Where v-if="table != ''" v-model="where" :table="table" />
|
||||||
<Order v-if="table != ''" v-model="order" :table="table" :columns="columnSelect" />
|
<Join v-if="table != ''" v-model="modelValue.join" :table="table" :alreadyJoined="alreadyJoined" />
|
||||||
<Join v-if="table != ''" v-model="modelValue.join" :table="table" />
|
<Order v-if="table != '' && enableOrder" v-model="order" :table="table" :columns="nestedTablesByDepth" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, type PropType } from "vue";
|
import { defineComponent, type PropType } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { type ConditionStructure, type DynamicQueryStructure, type OrderByStructure } from "@/types/dynamicQueries";
|
||||||
import type { ConditionStructure, DynamicQueryStructure, OrderByStructure } from "@/types/dynamicQueries";
|
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
|
||||||
import ColumnSelect from "./ColumnSelect.vue";
|
import ColumnSelect from "./ColumnSelect.vue";
|
||||||
import Where from "./Where.vue";
|
import Where from "./Where.vue";
|
||||||
import Order from "./Order.vue";
|
import Order from "./Order.vue";
|
||||||
import Join from "./Join.vue";
|
import Join from "./Join.vue";
|
||||||
import TableSelect from "./TableSelect.vue";
|
import TableSelect from "./TableSelect.vue";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -25,21 +24,50 @@ export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object as PropType<DynamicQueryStructure>,
|
type: Object as PropType<DynamicQueryStructure>,
|
||||||
default: {
|
required: true,
|
||||||
select: "*",
|
|
||||||
table: "",
|
|
||||||
where: [],
|
|
||||||
join: [],
|
|
||||||
orderBy: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
disableTableSelect: {
|
disableTableSelect: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
enableOrder: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showTableSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ["update:model-value"],
|
emits: ["update:model-value"],
|
||||||
computed: {
|
computed: {
|
||||||
|
alreadyJoined() {
|
||||||
|
return this.modelValue.join?.map((j) => j.table);
|
||||||
|
},
|
||||||
|
nestedTablesByDepth() {
|
||||||
|
const tables: Array<{ table: string; id: string; depth: number; path: string[]; columns: "*" | string[] }> = [];
|
||||||
|
|
||||||
|
function recurse(item: DynamicQueryStructure, path: string[]) {
|
||||||
|
tables.push({ table: item.table, id: item.id, depth: path.length, path, columns: item.select });
|
||||||
|
if (item.join) {
|
||||||
|
item.join.forEach((child) => {
|
||||||
|
recurse(child, [...path, item.table]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recurse(this.modelValue, []);
|
||||||
|
|
||||||
|
return tables;
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(val: DynamicQueryStructure) {
|
||||||
|
this.$emit("update:model-value", val);
|
||||||
|
},
|
||||||
|
},
|
||||||
table: {
|
table: {
|
||||||
get() {
|
get() {
|
||||||
return this.modelValue.table || "";
|
return this.modelValue.table || "";
|
||||||
|
@ -81,5 +109,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.value.id) {
|
||||||
|
this.value.id = uuid();
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<div v-for="(condition, index) in value" class="contents">
|
<div v-for="(condition, index) in value" class="contents">
|
||||||
<NestedCondition
|
<NestedCondition
|
||||||
v-if="condition.structureType == 'nested'"
|
v-if="condition.structureType == 'nested'"
|
||||||
|
:isFirst="index == 0"
|
||||||
:model-value="condition"
|
:model-value="condition"
|
||||||
:table="table"
|
:table="table"
|
||||||
@update:model-value="($event) => (value[index] = $event)"
|
@update:model-value="($event) => (value[index] = $event)"
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
/>
|
/>
|
||||||
<Condition
|
<Condition
|
||||||
v-else
|
v-else
|
||||||
|
:isFirst="index == 0"
|
||||||
:model-value="condition"
|
:model-value="condition"
|
||||||
:table="table"
|
:table="table"
|
||||||
@update:model-value="($event) => (value[index] = $event)"
|
@update:model-value="($event) => (value[index] = $event)"
|
||||||
|
@ -74,14 +76,14 @@ export default defineComponent({
|
||||||
addNestedToValue() {
|
addNestedToValue() {
|
||||||
this.value.push({
|
this.value.push({
|
||||||
structureType: "nested",
|
structureType: "nested",
|
||||||
concat: this.value.length == 0 ? "_" : "AND",
|
concat: "AND",
|
||||||
conditions: [],
|
conditions: [],
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addConditionToValue() {
|
addConditionToValue() {
|
||||||
this.value.push({
|
this.value.push({
|
||||||
structureType: "condition",
|
structureType: "condition",
|
||||||
concat: this.value.length == 0 ? "_" : "AND",
|
concat: "AND",
|
||||||
operation: "eq",
|
operation: "eq",
|
||||||
column: "",
|
column: "",
|
||||||
value: "",
|
value: "",
|
||||||
|
|
70
src/components/setup/Account.vue
Normal file
70
src/components/setup/Account.vue
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Admin Account</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="username" name="username" type="text" required placeholder="Benutzer" class="rounded-b-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="mail" name="mail" type="email" required placeholder="Mailadresse" class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="firstname" name="firstname" type="text" required placeholder="Vorname" class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="lastname" name="lastname" type="text" required placeholder="Nachname" class="rounded-t-none!" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Admin-Account anlegen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["createAdmin"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.createAdmin({
|
||||||
|
username: formData.username.value,
|
||||||
|
mail: formData.mail.value,
|
||||||
|
firstname: formData.firstname.value,
|
||||||
|
lastname: formData.lastname.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
66
src/components/setup/App.vue
Normal file
66
src/components/setup/App.vue
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">App Konfiguration</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input id="login_message" name="login_message" type="text" placeholder="Nachricht unter Login (optional)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center gap-2 pt-1">
|
||||||
|
<input type="checkbox" id="show_cal_link" checked />
|
||||||
|
<label for="show_cal_link">Link zum Kalender anzeigen (optional)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('app')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Anwendungsdaten speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setApp", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setApp({
|
||||||
|
login_message: formData.login_message.value,
|
||||||
|
show_cal_link: formData.show_cal_link.checked,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
99
src/components/setup/Club.vue
Normal file
99
src/components/setup/Club.vue
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Feuerwehr-/Vereinsdaten</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
placeholder="Feuerwehr-/Vereinsname (optional)"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="imprint"
|
||||||
|
name="imprint"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zum Impressum (optional)"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="privacy"
|
||||||
|
name="privacy"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zur Datenschutzerklärung (optional)"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="website"
|
||||||
|
name="website"
|
||||||
|
type="url"
|
||||||
|
placeholder="Link zur Webseite (optional)"
|
||||||
|
class="rounded-t-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('club')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Vereinsdaten speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setClub", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setClub({
|
||||||
|
name: formData.name.value,
|
||||||
|
imprint: formData.imprint.value,
|
||||||
|
privacy: formData.privacy.value,
|
||||||
|
website: formData.website.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
3
src/components/setup/Finished.vue
Normal file
3
src/components/setup/Finished.vue
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<p class="text-center">Sie haben einen Verifizierungslink per Mail erhalten.</p>
|
||||||
|
</template>
|
87
src/components/setup/Images.vue
Normal file
87
src/components/setup/Images.vue
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Feuerwehr-/Vereins-Auftritt</p>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>quadratisches Icon für App (optional)</p>
|
||||||
|
<img ref="icon_img" class="hidden w-full h-20 object-contain" />
|
||||||
|
<input class="hidden!" type="file" ref="icon" accept="image/*" @change="previewImage('icon')" />
|
||||||
|
<button type="button" primary-outline @click="($refs.icon as HTMLInputElement).click()">Icon auswählen</button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p>Logo (optional)</p>
|
||||||
|
<img ref="logo_img" class="hidden w-full h-20 object-contain" />
|
||||||
|
<input class="hidden!" type="file" ref="logo" accept="image/*" @change="previewImage('logo')" />
|
||||||
|
<button type="button" primary-outline @click="($refs.logo as HTMLInputElement).click()">Logo auswählen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p class="text-primary cursor-pointer ml-auto" @click="skip('appImages')">überspringen</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Bilder speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setClubImages", "skip"]),
|
||||||
|
previewImage(inputname: "icon" | "logo") {
|
||||||
|
let input = this.$refs[inputname] as HTMLInputElement;
|
||||||
|
let previewElement = this.$refs[inputname + "_img"] as HTMLImageElement;
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = function (e) {
|
||||||
|
previewElement.src = e.target?.result as string;
|
||||||
|
previewElement.style.display = "block";
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
} else {
|
||||||
|
previewElement.src = "";
|
||||||
|
previewElement.style.display = "none";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(e: any) {
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setClubImages({
|
||||||
|
icon: (this.$refs.icon as HTMLInputElement).files?.[0],
|
||||||
|
logo: (this.$refs.logo as HTMLInputElement).files?.[0],
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
95
src/components/setup/Mail.vue
Normal file
95
src/components/setup/Mail.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<form class="flex flex-col gap-2" @submit.prevent="setup">
|
||||||
|
<p class="text-center">Mailversand</p>
|
||||||
|
<div class="-space-y-px">
|
||||||
|
<div class="mb-2">
|
||||||
|
<input id="mail" name="mail" type="email" placeholder="Mailadresse" required autocomplete="email" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="user"
|
||||||
|
name="user"
|
||||||
|
type="text"
|
||||||
|
placeholder="Benutzername (kann auch Mail sein)"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
class="rounded-b-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder="Passwort"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
class="rounded-none!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="host" name="host" type="text" placeholder="Server-Host" required class="rounded-none!" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="port" name="port" type="number" placeholder="Port (25, 465, 587)" required class="rounded-t-none!" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center gap-2 pt-1">
|
||||||
|
<input type="checkbox" id="secure" />
|
||||||
|
<label for="secure">SSL-Verbindung (setzen bei Port 465)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button type="submit" primary :disabled="setupStatus == 'loading' || setupStatus == 'success'">
|
||||||
|
Mailversand speichern
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="setupStatus == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="setupStatus == 'success'" />
|
||||||
|
<FailureXMark v-else-if="setupStatus == 'failed'" />
|
||||||
|
</div>
|
||||||
|
<p v-if="setupMessage" class="text-center">{{ setupMessage }}</p>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { mapActions } from "pinia";
|
||||||
|
import { useSetupStore } from "@/stores/setup";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
setupStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
|
setupMessage: "" as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useSetupStore, ["setMailConfig", "skip"]),
|
||||||
|
setup(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
this.setupStatus = "loading";
|
||||||
|
this.setupMessage = "";
|
||||||
|
this.setMailConfig({
|
||||||
|
host: formData.host.value,
|
||||||
|
port: formData.port.value,
|
||||||
|
secure: formData.secure.checked,
|
||||||
|
mail: formData.mail.value,
|
||||||
|
username: formData.user.value,
|
||||||
|
password: formData.password.value,
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
// this.setupStatus = "success";
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.setupStatus = "failed";
|
||||||
|
this.setupMessage = err.response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -1,15 +1,7 @@
|
||||||
export interface Config {
|
export interface Config {
|
||||||
server_address: string;
|
server_address: string;
|
||||||
app_name_overwrite: string;
|
|
||||||
imprint_link: string;
|
|
||||||
privacy_link: string;
|
|
||||||
custom_login_message: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config: Config = {
|
export const config: Config = {
|
||||||
server_address: import.meta.env.VITE_SERVER_ADDRESS,
|
server_address: import.meta.env.VITE_SERVER_ADDRESS,
|
||||||
app_name_overwrite: import.meta.env.VITE_APP_NAME_OVERWRITE,
|
|
||||||
imprint_link: import.meta.env.VITE_IMPRINT_LINK,
|
|
||||||
privacy_link: import.meta.env.VITE_PRIVACY_LINK,
|
|
||||||
custom_login_message: import.meta.env.VITE_CUSTOM_LOGIN_MESSAGE,
|
|
||||||
};
|
};
|
||||||
|
|
5
src/enums/newsletterConfigEnum.ts
Normal file
5
src/enums/newsletterConfigEnum.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export enum NewsletterConfigEnum {
|
||||||
|
pdf = "pdf",
|
||||||
|
mail = "mail",
|
||||||
|
none = "none",
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
export enum NewsletterConfigType {
|
|
||||||
pdf = "pdf",
|
|
||||||
mail = "mail",
|
|
||||||
}
|
|
7
src/helpers/crypto.ts
Normal file
7
src/helpers/crypto.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export async function hashString(message = ""): Promise<string> {
|
||||||
|
const msgUint8 = new TextEncoder().encode(message);
|
||||||
|
const hashBuffer = await window.crypto.subtle.digest("SHA-256", msgUint8);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||||
|
return hashHex;
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { joinTableFormatter, type FieldType, type QueryResult } from "../types/dynamicQueries";
|
import { joinTableFormatter, type FieldType, type QueryResult } from "@/types/dynamicQueries";
|
||||||
|
|
||||||
export function joinTableName(name: string): string {
|
export function joinTableName(name: string): string {
|
||||||
let normalized = joinTableFormatter[name];
|
let normalized = joinTableFormatter[name];
|
||||||
|
|
52
src/main.css
52
src/main.css
|
@ -1,6 +1,14 @@
|
||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
@theme {
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-error: var(--error);
|
||||||
|
--color-warning: var(--warning);
|
||||||
|
--color-info: var(--info);
|
||||||
|
--color-success: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
|
@ -10,7 +18,7 @@
|
||||||
--error: #9a0d55;
|
--error: #9a0d55;
|
||||||
--warning: #bb6210;
|
--warning: #bb6210;
|
||||||
--info: #388994;
|
--info: #388994;
|
||||||
--success: #73ad0f;
|
--success: #7ac142;
|
||||||
}
|
}
|
||||||
.dark {
|
.dark {
|
||||||
--primary: #ff0d00;
|
--primary: #ff0d00;
|
||||||
|
@ -19,10 +27,12 @@
|
||||||
--error: #9a0d55;
|
--error: #9a0d55;
|
||||||
--warning: #bb6210;
|
--warning: #bb6210;
|
||||||
--info: #4ccbda;
|
--info: #4ccbda;
|
||||||
--success: #73ad0f;
|
--success: #7ac142;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@custom-variant hover (&:hover);
|
||||||
|
|
||||||
/* ===== Scrollbar CSS ===== */
|
/* ===== Scrollbar CSS ===== */
|
||||||
/* Firefox */
|
/* Firefox */
|
||||||
* {
|
* {
|
||||||
|
@ -59,12 +69,12 @@ body {
|
||||||
/*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/
|
/*:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"])*/
|
||||||
button:not([class*="ql"] *):not([class*="fc"]):not([id*="headlessui-combobox"]),
|
button:not([class*="ql"] *):not([class*="fc"]):not([id*="headlessui-combobox"]),
|
||||||
a[button] {
|
a[button] {
|
||||||
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
|
@apply cursor-pointer relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-hidden focus:ring-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[primary]:not([primary="false"]),
|
button[primary]:not([primary="false"]),
|
||||||
a[button][primary]:not([primary="false"]) {
|
a[button][primary]:not([primary="false"]) {
|
||||||
@apply border border-transparent text-white bg-primary hover:bg-primary;
|
@apply border-2 border-transparent text-white bg-primary hover:bg-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[primary-outline]:not([primary-outline="false"]),
|
button[primary-outline]:not([primary-outline="false"]),
|
||||||
|
@ -81,7 +91,7 @@ a[button].disabled {
|
||||||
input:not([type="checkbox"]),
|
input:not([type="checkbox"]),
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
@apply rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none;
|
@apply bg-white rounded-md shadow-xs relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[readonly],
|
input[readonly],
|
||||||
|
@ -123,29 +133,3 @@ summary > svg {
|
||||||
summary::-webkit-details-marker {
|
summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fc-button-primary {
|
|
||||||
@apply !bg-primary !border-primary !outline-none !ring-0 hover:!bg-red-700 hover:!border-red-700 h-10 text-center;
|
|
||||||
}
|
|
||||||
.fc-button-active {
|
|
||||||
@apply !bg-red-500 !border-red-500;
|
|
||||||
}
|
|
||||||
.fc-toolbar {
|
|
||||||
@apply flex-wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For screens between 850px and 768px */
|
|
||||||
@media (max-width: 850px) and (min-width: 768px) {
|
|
||||||
.fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) {
|
|
||||||
@apply !order-1;
|
|
||||||
}
|
|
||||||
/* Your styles for this range */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For screens between 525px and 0px */
|
|
||||||
@media (max-width: 525px) and (min-width: 0px) {
|
|
||||||
/* Your styles for this range */
|
|
||||||
.fc-header-toolbar.fc-toolbar.fc-toolbar-ltr > .fc-toolbar-chunk:nth-child(2) {
|
|
||||||
@apply !order-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ import "../node_modules/nprogress/nprogress.css";
|
||||||
import { http } from "./serverCom";
|
import { http } from "./serverCom";
|
||||||
import "./main.css";
|
import "./main.css";
|
||||||
|
|
||||||
|
// auto generates splash screen for iOS
|
||||||
|
import "pwacompat";
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false });
|
NProgress.configure({ showSpinner: false });
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
|
@ -12,7 +12,14 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
|
||||||
let section = to.meta.section;
|
let section = to.meta.section;
|
||||||
let module = to.meta.module;
|
let module = to.meta.module;
|
||||||
|
|
||||||
if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
|
if (to.name == "admin-default") {
|
||||||
|
navigation.activeNavigation = "club";
|
||||||
|
navigation.activeLink = null;
|
||||||
|
navigation.updateTopLevel();
|
||||||
|
navigation.updateNavigation();
|
||||||
|
NProgress.done();
|
||||||
|
next();
|
||||||
|
} else if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
navigation.activeNavigation = to.name.split("-")[1];
|
navigation.activeNavigation = to.name.split("-")[1];
|
||||||
navigation.activeLink = to.name.split("-")[2];
|
navigation.activeLink = to.name.split("-")[2];
|
||||||
|
|
|
@ -55,6 +55,7 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
|
||||||
// check jwt expiry
|
// check jwt expiry
|
||||||
const exp = decoded.exp ?? 0;
|
const exp = decoded.exp ?? 0;
|
||||||
const correctedLocalTime = new Date().getTime();
|
const correctedLocalTime = new Date().getTime();
|
||||||
|
let failedRefresh = false;
|
||||||
if (exp < Math.floor(correctedLocalTime / 1000) || forceRefresh) {
|
if (exp < Math.floor(correctedLocalTime / 1000) || forceRefresh) {
|
||||||
await refreshToken()
|
await refreshToken()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
@ -63,13 +64,16 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
|
||||||
.catch((err: string) => {
|
.catch((err: string) => {
|
||||||
console.log("expired");
|
console.log("expired");
|
||||||
auth.setFailed();
|
auth.setFailed();
|
||||||
|
failedRefresh = true;
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (failedRefresh) return;
|
||||||
|
|
||||||
var { userId, firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
var { userId, firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
||||||
|
|
||||||
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
|
if (Object.keys(permissions ?? {}).filter((p) => p != "adminByOwner").length === 0 && !isOwner) {
|
||||||
auth.setFailed();
|
auth.setFailed();
|
||||||
reject("nopermissions");
|
reject("nopermissions");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useBackupStore } from "../stores/admin/management/backup";
|
import { useBackupStore } from "@/stores/admin/management/backup";
|
||||||
|
|
||||||
export async function setBackupPage(to: any, from: any, next: any) {
|
export async function setBackupPage(to: any, from: any, next: any) {
|
||||||
const backup = useBackupStore();
|
const backup = useBackupStore();
|
||||||
|
|
|
@ -2,14 +2,12 @@ import { createRouter, createWebHistory } from "vue-router";
|
||||||
import Login from "@/views/Login.vue";
|
import Login from "@/views/Login.vue";
|
||||||
|
|
||||||
import { isAuthenticated } from "./authGuard";
|
import { isAuthenticated } from "./authGuard";
|
||||||
import { loadAccountData } from "./accountGuard";
|
|
||||||
import { isSetup } from "./setupGuard";
|
import { isSetup } from "./setupGuard";
|
||||||
import { abilityAndNavUpdate } from "./adminGuard";
|
import { abilityAndNavUpdate } from "./adminGuard";
|
||||||
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
||||||
import { resetMemberStores, setMemberId } from "./memberGuard";
|
import { resetMemberStores, setMemberId } from "./memberGuard";
|
||||||
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
||||||
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
||||||
import { config } from "../config";
|
|
||||||
import { setBackupPage } from "./backupGuard";
|
import { setBackupPage } from "./backupGuard";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
@ -17,7 +15,7 @@ const router = createRouter({
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
redirect: { name: "admin" },
|
redirect: { name: "admin-default" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
@ -78,12 +76,13 @@ const router = createRouter({
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
name: "admin",
|
name: "admin",
|
||||||
component: () => import("@/views/admin/View.vue"),
|
component: () => import("@/views/admin/View.vue"),
|
||||||
beforeEnter: [isAuthenticated],
|
beforeEnter: [isAuthenticated, abilityAndNavUpdate],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
name: "admin-default",
|
name: "admin-default",
|
||||||
component: () => import("@/views/admin/ViewSelect.vue"),
|
component: () => import("@/views/admin/ViewSelect.vue"),
|
||||||
|
beforeEnter: [abilityAndNavUpdate],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "club",
|
path: "club",
|
||||||
|
@ -642,6 +641,13 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "settings",
|
||||||
|
name: "admin-management-setting",
|
||||||
|
component: () => import("@/views/admin/management/setting/Setting.vue"),
|
||||||
|
meta: { type: "read", section: "management", module: "setting" },
|
||||||
|
beforeEnter: [abilityAndNavUpdate],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "backup",
|
path: "backup",
|
||||||
name: "admin-management-backup-route",
|
name: "admin-management-backup-route",
|
||||||
|
@ -777,10 +783,6 @@ const router = createRouter({
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.afterEach((to, from) => {
|
|
||||||
document.title = config.app_name_overwrite || "FF Admin";
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
||||||
declare module "vue-router" {
|
declare module "vue-router" {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
|
||||||
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
import { useNewsletterDatesStore } from "@/stores/admin/club/newsletter/newsletterDates";
|
||||||
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/newsletterRecipients";
|
||||||
import { useNewsletterPrintoutStore } from "../stores/admin/club/newsletter/newsletterPrintout";
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
|
||||||
export async function setNewsletterId(to: any, from: any, next: any) {
|
export async function setNewsletterId(to: any, from: any, next: any) {
|
||||||
const newsletter = useNewsletterStore();
|
const newsletter = useNewsletterStore();
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAge
|
||||||
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
||||||
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||||
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
||||||
import { useProtocolPrintoutStore } from "../stores/admin/club/protocol/protocolPrintout";
|
import { useProtocolPrintoutStore } from "@/stores/admin/club/protocol/protocolPrintout";
|
||||||
|
|
||||||
export async function setProtocolId(to: any, from: any, next: any) {
|
export async function setProtocolId(to: any, from: any, next: any) {
|
||||||
const protocol = useProtocolStore();
|
const protocol = useProtocolStore();
|
||||||
|
|
|
@ -135,4 +135,4 @@ async function* streamingFetch(path: string, abort?: AbortController) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { http, newEventSource, streamingFetch, host };
|
export { http, newEventSource, streamingFetch, host, url };
|
||||||
|
|
|
@ -11,21 +11,18 @@ export const useAbilityStore = defineStore("ability", {
|
||||||
getters: {
|
getters: {
|
||||||
can:
|
can:
|
||||||
(state) =>
|
(state) =>
|
||||||
(type: PermissionType | "admin", section: PermissionSection, module?: PermissionModule): boolean => {
|
(type: PermissionType | "admin", section: PermissionSection, module: PermissionModule): boolean => {
|
||||||
const permissions = state.permissions;
|
const permissions = state.permissions;
|
||||||
if (state.isOwner) return true;
|
if (state.isOwner) return true;
|
||||||
if (type == "admin") return permissions?.admin ?? false;
|
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||||
if (permissions?.admin) return true;
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
if (
|
if (
|
||||||
(!module &&
|
|
||||||
permissions[section] != undefined &&
|
|
||||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
|
|
||||||
permissions[section]?.all == "*" ||
|
permissions[section]?.all == "*" ||
|
||||||
permissions[section]?.all?.includes(type)
|
permissions[section]?.all?.includes(type) ||
|
||||||
|
permissions[section]?.[module] == "*" ||
|
||||||
|
permissions[section]?.[module]?.includes(type)
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
|
|
||||||
return true;
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
canSection:
|
canSection:
|
||||||
|
@ -33,16 +30,24 @@ export const useAbilityStore = defineStore("ability", {
|
||||||
(type: PermissionType | "admin", section: PermissionSection): boolean => {
|
(type: PermissionType | "admin", section: PermissionSection): boolean => {
|
||||||
const permissions = state.permissions;
|
const permissions = state.permissions;
|
||||||
if (state.isOwner) return true;
|
if (state.isOwner) return true;
|
||||||
if (type == "admin") return permissions?.admin ?? false;
|
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||||
if (permissions?.admin) return true;
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
if (
|
if (
|
||||||
permissions[section]?.all == "*" ||
|
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
|
||||||
permissions[section]?.all?.includes(type) ||
|
|
||||||
permissions[section] != undefined
|
permissions[section] != undefined
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
canAccessSection:
|
||||||
|
(state) =>
|
||||||
|
(section: PermissionSection): boolean => {
|
||||||
|
const permissions = state.permissions;
|
||||||
|
if (state.isOwner) return true;
|
||||||
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
|
if (permissions[section] != undefined) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
isAdmin: (state) => (): boolean => {
|
isAdmin: (state) => (): boolean => {
|
||||||
const permissions = state.permissions;
|
const permissions = state.permissions;
|
||||||
if (state.isOwner) return true;
|
if (state.isOwner) return true;
|
||||||
|
@ -54,23 +59,41 @@ export const useAbilityStore = defineStore("ability", {
|
||||||
permissions: PermissionObject,
|
permissions: PermissionObject,
|
||||||
type: PermissionType | "admin",
|
type: PermissionType | "admin",
|
||||||
section: PermissionSection,
|
section: PermissionSection,
|
||||||
module?: PermissionModule
|
module: PermissionModule
|
||||||
): boolean => {
|
): boolean => {
|
||||||
// ignores ownership
|
// ignores ownership
|
||||||
if (type == "admin") return permissions?.admin ?? false;
|
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||||
if (permissions?.admin) return true;
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
if (
|
if (
|
||||||
(!module &&
|
|
||||||
permissions[section] != undefined &&
|
|
||||||
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
|
|
||||||
permissions[section]?.all == "*" ||
|
permissions[section]?.all == "*" ||
|
||||||
permissions[section]?.all?.includes(type)
|
permissions[section]?.all?.includes(type) ||
|
||||||
|
permissions[section]?.[module] == "*" ||
|
||||||
|
permissions[section]?.[module]?.includes(type)
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
|
return false;
|
||||||
|
},
|
||||||
|
_canSection:
|
||||||
|
() =>
|
||||||
|
(permissions: PermissionObject, type: PermissionType | "admin", section: PermissionSection): boolean => {
|
||||||
|
// ignores ownership
|
||||||
|
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
|
||||||
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
|
if (
|
||||||
|
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
|
||||||
|
permissions[section] != undefined
|
||||||
|
)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
_canAccessSection:
|
||||||
|
() =>
|
||||||
|
(permissions: PermissionObject, section: PermissionSection): boolean => {
|
||||||
|
// ignores ownership
|
||||||
|
if (permissions?.admin || permissions?.adminByOwner) return true;
|
||||||
|
if (permissions[section] != undefined) return true;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setAbility(permissions: PermissionObject, isOwner: boolean) {
|
setAbility(permissions: PermissionObject, isOwner: boolean) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue