Compare commits
40 commits
Author | SHA1 | Date | |
---|---|---|---|
6e82675557 | |||
1b531b1152 | |||
131b3747de | |||
883559d8a5 | |||
626a355c5c | |||
4ecb39ceff | |||
45ad07a906 | |||
5bcb76a60e | |||
c40b53b200 | |||
c9c6df20e0 | |||
05e464e825 | |||
4dc183f52b | |||
363b5bb541 | |||
c2b495f8a7 | |||
8a85cc054d | |||
afa834739c | |||
8d2e0deee6 | |||
9e50d95d7b | |||
020c7c6cb9 | |||
065b0aa6d5 | |||
cd6c9bfd93 | |||
557ee051ab | |||
0ee5a92c95 | |||
22359c3bea | |||
3da0f4cd49 | |||
a8e2b05d8e | |||
52a35be6c5 | |||
3f3aa040d9 | |||
260478af69 | |||
64806953b2 | |||
059b0fa9f2 | |||
3be63061a8 | |||
6e69e84316 | |||
fbc039c35a | |||
9215ac6fd8 | |||
c83b4670cc | |||
a749bfb45e | |||
2176c6c3cf | |||
f74c10d4aa | |||
fa45559127 |
|
@ -1 +1,5 @@
|
||||||
SERVER_ADDRESS = serveradress
|
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
|
||||||
|
|
5
.env.production
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
VITE_SERVER_ADDRESS = __SERVERADDRESS__
|
||||||
|
VITE_APP_NAME_OVERWRITE = __APPNAMEOVERWRITE__
|
||||||
|
VITE_IMPRINT_LINK = __IMPRINTLINK__
|
||||||
|
VITE_PRIVACY_LINK = __PRIVACYLINK__
|
||||||
|
VITE_CUSTOM_LOGIN_MESSAGE = __CUSTOMLOGINMESSAGE__
|
|
@ -19,4 +19,7 @@ COPY ./nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
COPY ./entrypoint.sh /entrypoint.sh
|
||||||
|
RUN apk add --no-cache dos2unix
|
||||||
|
RUN dos2unix /entrypoint.sh && chmod +x /entrypoint.sh
|
||||||
|
ENTRYPOINT [ "/entrypoint.sh" ]
|
33
README.md
|
@ -1,12 +1,12 @@
|
||||||
# member-administration-ui
|
# FF Admin
|
||||||
|
|
||||||
Mitgliederverwaltung für Feuerwehren und Vereine.
|
Administration für Feuerwehren und Vereine.
|
||||||
|
|
||||||
## Einleitung
|
## Einleitung
|
||||||
|
|
||||||
Dieses Repository dient zur Verwaltung der Mitgliederdaten. Es ist ein Frontend-Client, der auf die Daten des [member-administration-server Backends](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server) zugreift. Die Webapp bietet eine Möglichkeit Mitgliederdaten zu verwalten, Protokolle zu schreiben und Kaledereinträge zu erstellen. Benutzer können eingeladen und Rollen zugewiesen werden.
|
Dieses Repository dient hauptsächlich zur Verwaltung der Mitgliederdaten, aber auch zur Verwaltung weiterer Daten der Feuerwehr oder eines Vereins. Es ist ein Frontend-Client, der auf die Daten des [ff-admin-server Backends](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server) zugreift. Die Webapp bietet eine Möglichkeit Mitgliederdaten zu verwalten, Protokolle zu schreiben und Kaledereinträge zu erstellen. Benutzer können eingeladen und Rollen zugewiesen werden.
|
||||||
|
|
||||||
Eine Demo dieser Seite finden Sie unter [https://ff-admin-demo.jk-effects.cloud](https://ff-admin-demo.jk-effects.cloud).
|
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.
|
Für die Verwendung muss ein TOTP-Code eingegeben werden.
|
||||||
|
|
||||||
|
@ -25,16 +25,21 @@ Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
ff-member-administration-app:
|
ff-admin-app:
|
||||||
image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/app:latest
|
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/app:latest
|
||||||
container_name: ff_member_administration_ui
|
container_name: ff_admin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
#environment:
|
#environment:
|
||||||
# - SERVER_ADRESS=<backend_host> # wichtig: ohne https:// bzw http://
|
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
||||||
|
# - APPNAMEOVERWRITE=Mitgliederverwaltung # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
|
||||||
|
# - IMPRINTLINK=https://mywebsite-imprint-url
|
||||||
|
# - PRIVACYLINK=https://mywebsite-privacy-url
|
||||||
|
# - CUSTOMLOGINMESSAGE=betrieben von xy
|
||||||
#volumes:
|
#volumes:
|
||||||
# - <volume|local path>/myfavicon.png:/app/public/favicon.png
|
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
||||||
# - <volume|local path>/mylogo.png:/app/public/logo.png
|
# - <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.
|
||||||
|
@ -50,8 +55,8 @@ docker-compose up -d
|
||||||
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
|
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui.git
|
git clone https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin.git
|
||||||
cd member-administration-ui
|
cd ff-admin
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
npm run start
|
npm run start
|
||||||
|
@ -59,11 +64,11 @@ npm run start
|
||||||
|
|
||||||
### Konfiguration
|
### Konfiguration
|
||||||
|
|
||||||
Ein eigenes favicon und Logo kann über ein volume ausgetauscht werden.
|
Ein eigenes Favicon und Logo kann über das verwenden Volume ausgetauscht werden. Es dürfen jedoch nur einzelne Dateien ausgetauscht werden.
|
||||||
|
|
||||||
## Einrichtung
|
## Einrichtung
|
||||||
|
|
||||||
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad [/setup](https://ff-admin-demo.jk-effects.cloud/setup), um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
|
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
|
||||||
|
|
||||||
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
|
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# FF Admin
|
# FF Admin
|
||||||
|
|
||||||
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr:
|
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr oder andere Vereine:
|
||||||
|
|
||||||
FF Admin bietet folgende Module:
|
FF Admin bietet folgende Module:
|
||||||
- Mitgliederverwaltung
|
- Mitgliederverwaltung
|
||||||
|
@ -23,3 +23,6 @@ FF Admin ist in Verein, Wehr, Einstellungen und Nutzerverwaltung getrennt.
|
||||||
Die den Modulen zugrunde liegenden Daten können in den Einstellungen gesetzt werden.
|
Die den Modulen zugrunde liegenden Daten können in den Einstellungen gesetzt werden.
|
||||||
|
|
||||||
Fast alle Daten lassen sich einstellen, damit es keine Einschränkungen in der Auswahl von Werten... gibt. Diese Modularität muss allerdings bei einigen Modulen gesondert eingestellt werden.
|
Fast alle Daten lassen sich einstellen, damit es keine Einschränkungen in der Auswahl von Werten... gibt. Diese Modularität muss allerdings bei einigen Modulen gesondert eingestellt werden.
|
||||||
|
|
||||||
|
## Verwendung
|
||||||
|
Damit FF Admin auch für andere Vereine genutzt werden kann, muss keine erweiterte Konfiguration vorgenommen werden. Am besten ist es alle nicht benötigten Module in der Berechtigungsverwaltung zu deaktivieren. So wird normalerweise der Abschnitt Wehr nicht außerhalb der Feuerwehr benötigt. So müssen hier lediglich keine Berechtigungen vergeben werden und das Modul ist außer für Administratoren oder Owner nicht sichtbar.
|
||||||
|
|
27
entrypoint.sh
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
keys="SERVERADDRESS APPNAMEOVERWRITE IMPRINTLINK PRIVACYLINK CUSTOMLOGINMESSAGE"
|
||||||
|
files="/usr/share/nginx/html/assets/config-*.js /usr/share/nginx/html/manifest.webmanifest"
|
||||||
|
|
||||||
|
# Replace env vars in files served by NGINX
|
||||||
|
for file in $files
|
||||||
|
do
|
||||||
|
echo "Processing $file ...";
|
||||||
|
for key in $keys
|
||||||
|
do
|
||||||
|
# Get environment variable
|
||||||
|
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"
|
||||||
|
|
||||||
|
# replace __[variable_name]__ value with environment variable
|
||||||
|
sed -i 's|__'"$key"'__|'"$value"'|g' $file
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
nginx -g 'daemon off;'
|
|
@ -4,7 +4,6 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Mitgliederverwaltung</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
8
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "member-administration-ui",
|
"name": "ff-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.1.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "member-administration-ui",
|
"name": "ff-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.1.2",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.15",
|
"@fullcalendar/core": "^6.1.15",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "member-administration-ui",
|
"name": "ff-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.1.2",
|
||||||
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
"description": "Feuerwehr/Verein Mitgliederverwaltung UI",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -12,11 +12,11 @@
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write src/",
|
||||||
"bnp": "npm run build-only && npm run preview",
|
"bnp": "npm run build-only && npm run preview",
|
||||||
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/CM.svg"
|
"generate-pwa-assets": "pwa-assets-generator --preset minimal-2023 public/fw-wappen.png"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui.git"
|
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"Feuerwehr"
|
"Feuerwehr"
|
||||||
|
|
1921
public/ChartDB(administration-db).json
Normal file
BIN
public/Logo.png
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 641 KiB After Width: | Height: | Size: 650 KiB |
Before Width: | Height: | Size: 10 MiB After Width: | Height: | Size: 11 MiB |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 516 KiB After Width: | Height: | Size: 29 KiB |
|
@ -1,9 +1,20 @@
|
||||||
<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">
|
<div class="flex flex-row gap-2 justify-center">
|
||||||
<a href="https://jk-effects.com/privacy" target="_blank">Datenschutz</a>
|
<a v-if="config.imprint_link" :href="config.imprint_link" target="_blank">Datenschutz</a>
|
||||||
<a href="https://jk-effects.com/imprint" target="_blank">Impressum</a>
|
<a v-if="config.privacy_link" :href="config.privacy_link" target="_blank">Impressum</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="https://jk-effects.com" target="_blank"> © Admin-Portal by JK Effects </a>
|
<p v-if="config.custom_login_message">{{ config.custom_login_message }}</p>
|
||||||
|
<p>
|
||||||
|
©
|
||||||
|
<a href="https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin" target="_blank">Admin-Portal</a>
|
||||||
|
by
|
||||||
|
<a href="https://jk-effects.com" target="_blank">JK Effects</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { config } from '@/config'
|
||||||
|
</script>
|
|
@ -2,7 +2,7 @@
|
||||||
<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-sm">
|
||||||
<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" />
|
<img src="/Logo.png" alt="LOGO" class="h-full w-auto" />
|
||||||
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">Mitgliederverwaltung</h1>
|
<h1 v-if="false" class="font-bold text-3xl w-fit whitespace-nowrap">{{config.app_name_overwrite || "FF Admin"}}</h1>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div class="flex flex-row gap-2 items-center">
|
<div class="flex flex-row gap-2 items-center">
|
||||||
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
|
<div v-if="authCheck" class="hidden md:flex flex-row gap-2 h-full align-middle">
|
||||||
|
@ -30,6 +30,7 @@ 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">
|
||||||
|
|
|
@ -113,10 +113,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useNotificationStore, ["revoke"]),
|
...mapActions(useNotificationStore, ["revoke"]),
|
||||||
close(id: number) {
|
close(id: string) {
|
||||||
this.revoke(id);
|
this.revoke(id);
|
||||||
},
|
},
|
||||||
hovering(id: number, value: boolean, timeout?: number) {
|
hovering(id: string, value: boolean, timeout?: number) {
|
||||||
if (value) {
|
if (value) {
|
||||||
clearTimeout(this.timeouts[id]);
|
clearTimeout(this.timeouts[id]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -159,7 +159,7 @@ const loadPage = (newPage: number | ".") => {
|
||||||
if (pageEnd > entryCount.value) pageEnd = entryCount.value;
|
if (pageEnd > entryCount.value) pageEnd = entryCount.value;
|
||||||
|
|
||||||
let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length;
|
let loadedElementCount = filterData(props.items, searchString.value, pageStart, pageEnd).length;
|
||||||
console.log(loadedElementCount, props.maxEntriesPerPage, pageStart, pageEnd)
|
|
||||||
if (loadedElementCount < props.maxEntriesPerPage && (pageEnd != props.totalCount || loadedElementCount == 0))
|
if (loadedElementCount < props.maxEntriesPerPage && (pageEnd != props.totalCount || loadedElementCount == 0))
|
||||||
emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value);
|
emit("loadData", pageStart, props.maxEntriesPerPage, searchString.value);
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem v-slot="{ close }">
|
<MenuItem v-slot="{ close }">
|
||||||
<RouterLink to="/docs">
|
<RouterLink to="/docs" target="_blank">
|
||||||
<button button primary @click="close">Dokumentation</button>
|
<button button primary @click="close">Dokumentation</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
182
src/components/admin/MemberSearchSelect.vue
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<Combobox v-model="selected" :disabled="disabled" multiple>
|
||||||
|
<ComboboxLabel>{{ title }}</ComboboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<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"
|
||||||
|
@input="query = $event.target.value"
|
||||||
|
/>
|
||||||
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
<TransitionRoot
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
@after-leave="query = ''"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
<Spinner />
|
||||||
|
<span class="font-normal block truncate">suche</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<ComboboxOption v-else-if="filtered.length === 0 && query == ''" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate">tippe, um zu suchen...</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
<ComboboxOption v-else-if="filtered.length === 0" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate">Keine Auswahl gefunden.</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
|
||||||
|
<ComboboxOption
|
||||||
|
v-if="!(loading || deferingSearch)"
|
||||||
|
v-for="member in filtered"
|
||||||
|
as="template"
|
||||||
|
:key="member.id"
|
||||||
|
:value="member.id"
|
||||||
|
v-slot="{ selected, active }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white': active,
|
||||||
|
'text-gray-900': !active,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||||
|
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
:class="{ 'text-white': active, 'text-primary': !active }"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</TransitionRoot>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxLabel,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
TransitionRoot,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
|
import difference from "lodash.difference";
|
||||||
|
import Spinner from "../Spinner.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Array as PropType<Array<number>>,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
title: String,
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["update:model-value", "add:difference", "remove:difference", "add:member", "add:memberByArray"],
|
||||||
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
if (this.initialLoaded) return;
|
||||||
|
this.initialLoaded = true;
|
||||||
|
this.loadMembersInitial();
|
||||||
|
},
|
||||||
|
query() {
|
||||||
|
this.deferingSearch = true;
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
this.timer = setTimeout(() => {
|
||||||
|
this.deferingSearch = false;
|
||||||
|
this.search();
|
||||||
|
}, 600);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
initialLoaded: false as boolean,
|
||||||
|
loading: false as boolean,
|
||||||
|
deferingSearch: false as boolean,
|
||||||
|
timer: undefined as any,
|
||||||
|
query: "" as string,
|
||||||
|
filtered: [] as Array<MemberViewModel>,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected: {
|
||||||
|
get() {
|
||||||
|
return this.modelValue;
|
||||||
|
},
|
||||||
|
set(val: Array<number>) {
|
||||||
|
this.$emit("update:model-value", val);
|
||||||
|
if (this.modelValue.length < val.length) {
|
||||||
|
let diff = difference(val, this.modelValue);
|
||||||
|
if (diff.length != 1) return;
|
||||||
|
this.$emit("add:difference", diff[0]);
|
||||||
|
this.$emit("add:member", this.getMemberFromSearch(diff[0]));
|
||||||
|
} else {
|
||||||
|
let diff = difference(this.modelValue, val);
|
||||||
|
if (diff.length != 1) return;
|
||||||
|
this.$emit("remove:difference", diff[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadMembersInitial();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useMemberStore, ["searchMembers", "getMembersByIds"]),
|
||||||
|
search() {
|
||||||
|
this.filtered = [];
|
||||||
|
if (this.query == "") return;
|
||||||
|
this.loading = true;
|
||||||
|
this.searchMembers(this.query)
|
||||||
|
.then((res) => {
|
||||||
|
this.filtered = res.data;
|
||||||
|
})
|
||||||
|
.catch((err) => {})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getMemberFromSearch(id: number) {
|
||||||
|
return this.filtered.find((f) => f.id == id);
|
||||||
|
},
|
||||||
|
loadMembersInitial() {
|
||||||
|
if (this.modelValue.length == 0) return;
|
||||||
|
this.getMembersByIds(this.modelValue)
|
||||||
|
.then((res) => {
|
||||||
|
this.$emit("add:memberByArray", res.data);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -24,7 +24,7 @@ export default defineComponent({
|
||||||
default: "LINK",
|
default: "LINK",
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
type: Object as PropType<string | { name: string }>,
|
type: Object as PropType<string | { name: string, params?:{[key:string]:string} }>,
|
||||||
default: "/",
|
default: "/",
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-full md:max-w-md">
|
<div class="w-full md:max-w-md">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<p class="text-xl font-medium">Termintyp erstellen</p>
|
<p class="text-xl font-medium">Termin erstellen</p>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
@ -124,11 +124,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
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
primary-outline
|
|
||||||
@click="closeModal"
|
|
||||||
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
|
|
||||||
>
|
|
||||||
abbrechen
|
abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -194,6 +190,7 @@ export default defineComponent({
|
||||||
location: formData.location.value,
|
location: formData.location.value,
|
||||||
allDay: this.allDay,
|
allDay: this.allDay,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createCalendar(createCalendar)
|
this.createCalendar(createCalendar)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -38,7 +38,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +80,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useCalendarStore, ["deleteCalendar"]),
|
...mapActions(useCalendarStore, ["deleteCalendar"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteCalendar(this.data)
|
this.deleteCalendar(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
@click="deleteCalendar"
|
@click="deleteCalendar"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<p class="text-xl font-medium">Termintyp erstellen</p>
|
<p class="text-xl font-medium">Termin erstellen</p>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
@ -166,11 +166,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
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
primary-outline
|
|
||||||
@click="closeModal"
|
|
||||||
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
|
|
||||||
>
|
|
||||||
abbrechen / schließen
|
abbrechen / schließen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -264,6 +260,7 @@ export default defineComponent({
|
||||||
location: formData.location.value,
|
location: formData.location.value,
|
||||||
allDay: this.calendar.allDay,
|
allDay: this.calendar.allDay,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateCalendar(updateCalendar)
|
this.updateCalendar(updateCalendar)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -133,6 +133,7 @@ export default defineComponent({
|
||||||
birthdate: formData.birthdate.value,
|
birthdate: formData.birthdate.value,
|
||||||
internalId: formData.internalId.value,
|
internalId: formData.internalId.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMember(createMember)
|
this.createMember(createMember)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -72,6 +72,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMemberStore, ["deleteMember"]),
|
...mapActions(useMemberStore, ["deleteMember"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMember(this.data)
|
this.deleteMember(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -140,6 +140,7 @@ export default defineComponent({
|
||||||
given: formData.given.checked,
|
given: formData.given.checked,
|
||||||
awardId: this.selectedAward.id,
|
awardId: this.selectedAward.id,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMemberAward(createMemberAward)
|
this.createMemberAward(createMemberAward)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -65,6 +65,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMemberAwardStore, ["deleteMemberAward"]),
|
...mapActions(useMemberAwardStore, ["deleteMemberAward"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMemberAward(this.data)
|
this.deleteMemberAward(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -83,7 +83,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">schließen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -167,6 +169,7 @@ export default defineComponent({
|
||||||
given: formData.given.checked,
|
given: formData.given.checked,
|
||||||
awardId: this.memberAward.awardId,
|
awardId: this.memberAward.awardId,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateMemberAward(updateMemberAward)
|
this.updateMemberAward(updateMemberAward)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default defineComponent({
|
||||||
preferred: formData.preferred.checked,
|
preferred: formData.preferred.checked,
|
||||||
mobile: formData.mobile?.value,
|
mobile: formData.mobile?.value,
|
||||||
email: formData.email?.value,
|
email: formData.email?.value,
|
||||||
postalCode: formData.postalCode.value,
|
postalCode: formData.postalCode?.value,
|
||||||
city: formData.city?.value,
|
city: formData.city?.value,
|
||||||
street: formData.street?.value,
|
street: formData.street?.value,
|
||||||
streetNumber: formData.streetNumber?.value,
|
streetNumber: formData.streetNumber?.value,
|
||||||
|
@ -175,6 +175,7 @@ export default defineComponent({
|
||||||
isSMSAlarming: formData.isSMSAlarming?.checked,
|
isSMSAlarming: formData.isSMSAlarming?.checked,
|
||||||
typeId: this.selectedCommunicationType.id,
|
typeId: this.selectedCommunicationType.id,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createCommunication(createCommunication)
|
this.createCommunication(createCommunication)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -68,6 +68,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useCommunicationStore, ["deleteCommunication"]),
|
...mapActions(useCommunicationStore, ["deleteCommunication"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteCommunication(this.data)
|
this.deleteCommunication(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -62,7 +62,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">schließen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,7 +138,7 @@ export default defineComponent({
|
||||||
preferred: formData.preferred.checked,
|
preferred: formData.preferred.checked,
|
||||||
mobile: formData.mobile?.value,
|
mobile: formData.mobile?.value,
|
||||||
email: formData.email?.value,
|
email: formData.email?.value,
|
||||||
postalCode: formData.postalCode.value,
|
postalCode: formData.postalCode?.value,
|
||||||
city: formData.city?.value,
|
city: formData.city?.value,
|
||||||
street: formData.street?.value,
|
street: formData.street?.value,
|
||||||
streetNumber: formData.streetNumber?.value,
|
streetNumber: formData.streetNumber?.value,
|
||||||
|
@ -144,6 +146,7 @@ export default defineComponent({
|
||||||
isNewsletterMain: formData.isNewsletterMain.checked,
|
isNewsletterMain: formData.isNewsletterMain.checked,
|
||||||
isSMSAlarming: formData.isSMSAlarming?.checked,
|
isSMSAlarming: formData.isSMSAlarming?.checked,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateCommunication(updateCommunication)
|
this.updateCommunication(updateCommunication)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -141,6 +141,7 @@ export default defineComponent({
|
||||||
note: formData.note.value,
|
note: formData.note.value,
|
||||||
executivePositionId: this.selectedExecutivePosition.id,
|
executivePositionId: this.selectedExecutivePosition.id,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMemberExecutivePosition(createMemberExecutivePosition)
|
this.createMemberExecutivePosition(createMemberExecutivePosition)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -65,6 +65,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMemberExecutivePositionStore, ["deleteMemberExecutivePosition"]),
|
...mapActions(useMemberExecutivePositionStore, ["deleteMemberExecutivePosition"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMemberExecutivePosition(this.data)
|
this.deleteMemberExecutivePosition(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">schließen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -176,6 +178,7 @@ export default defineComponent({
|
||||||
note: formData.note.value,
|
note: formData.note.value,
|
||||||
executivePositionId: this.memberExecutivePosition.executivePositionId,
|
executivePositionId: this.memberExecutivePosition.executivePositionId,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateMemberExecutivePosition(updateMemberExecutivePosition)
|
this.updateMemberExecutivePosition(updateMemberExecutivePosition)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -148,6 +148,7 @@ export default defineComponent({
|
||||||
note: formData.note.value,
|
note: formData.note.value,
|
||||||
qualificationId: this.selectedQualification.id,
|
qualificationId: this.selectedQualification.id,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMemberQualification(createMemberQualification)
|
this.createMemberQualification(createMemberQualification)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -69,6 +69,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMemberQualificationStore, ["deleteMemberQualification"]),
|
...mapActions(useMemberQualificationStore, ["deleteMemberQualification"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMemberQualification(this.data)
|
this.deleteMemberQualification(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -90,7 +90,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">schließen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -175,6 +177,7 @@ export default defineComponent({
|
||||||
terminationReason: formData.terminationReason.value,
|
terminationReason: formData.terminationReason.value,
|
||||||
qualificationId: this.memberQualification.qualificationId,
|
qualificationId: this.memberQualification.qualificationId,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateMemberQualification(updateMemberQualification)
|
this.updateMemberQualification(updateMemberQualification)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -131,6 +131,7 @@ export default defineComponent({
|
||||||
start: formData.start.value,
|
start: formData.start.value,
|
||||||
statusId: this.selectedStatus.id,
|
statusId: this.selectedStatus.id,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMembership(createMember)
|
this.createMembership(createMember)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -67,6 +67,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMembershipStore, ["deleteMembership"]),
|
...mapActions(useMembershipStore, ["deleteMembership"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMembership(this.data)
|
this.deleteMembership(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -86,7 +86,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">schließen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
schließen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,6 +172,7 @@ export default defineComponent({
|
||||||
terminationReason: formData.terminationReason.value,
|
terminationReason: formData.terminationReason.value,
|
||||||
statusId: this.membership.statusId,
|
statusId: this.membership.statusId,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateMembership(updateMembership)
|
this.updateMembership(updateMembership)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.fetchItem();
|
this.fetchItem();
|
||||||
|
|
|
@ -61,6 +61,7 @@ export default defineComponent({
|
||||||
let createNewsletter: CreateNewsletterViewModel = {
|
let createNewsletter: CreateNewsletterViewModel = {
|
||||||
title: formData.title.value,
|
title: formData.title.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createNewsletter(createNewsletter)
|
this.createNewsletter(createNewsletter)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter Mail-Versand Logs</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="h-96 overflow-y-scroll">
|
||||||
|
<p v-for="entry in mailSourceMessages">
|
||||||
|
{{ entry }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed:{
|
||||||
|
...mapState(useNewsletterPrintoutStore, ["mailSourceMessages"])
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter Druck-Prozess Logs</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-96 overflow-y-scroll">
|
||||||
|
<div
|
||||||
|
v-for="entry in pdfSourceMessages"
|
||||||
|
class="flex flex-row gap-2 border border-gray-200 rounded-md p-1 items-center"
|
||||||
|
>
|
||||||
|
<SuccessCheckmark v-if="entry.factor == 'success'" class="w-5 h-5" />
|
||||||
|
<InformationCircleIcon v-else-if="entry.factor == 'info'" class="w-5 h-5 min-h-5 min-w-5 text-gray-500" />
|
||||||
|
<FailureXMark v-else-if="entry.factor == 'failed'" class="w-5 h-5" />
|
||||||
|
<p>{{ entry.iteration }}/{{ entry.total }}: {{ entry.msg }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal">abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useNewsletterPrintoutStore } from "@/stores/admin/club/newsletter/newsletterPrintout";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import { InformationCircleIcon } from "@heroicons/vue/24/solid";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useNewsletterPrintoutStore, ["pdfSourceMessages"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -64,6 +64,7 @@ export default defineComponent({
|
||||||
title: formData.title.value,
|
title: formData.title.value,
|
||||||
date: formData.date.value,
|
date: formData.date.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createProtocol(createProtocol)
|
this.createProtocol(createProtocol)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
let createAward: CreateAwardViewModel = {
|
let createAward: CreateAwardViewModel = {
|
||||||
award: formData.award.value,
|
award: formData.award.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createAward(createAward)
|
this.createAward(createAward)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useAwardStore, ["deleteAward"]),
|
...mapActions(useAwardStore, ["deleteAward"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteAward(this.data)
|
this.deleteAward(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -32,11 +32,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
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
primary-outline
|
|
||||||
@click="closeModal"
|
|
||||||
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
|
|
||||||
>
|
|
||||||
abbrechen
|
abbrechen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,8 +49,6 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
import { useCalendarTypeStore } from "@/stores/admin/settings/calendarType";
|
||||||
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -80,8 +74,9 @@ export default defineComponent({
|
||||||
type: formData.type.value,
|
type: formData.type.value,
|
||||||
color: formData.color.value,
|
color: formData.color.value,
|
||||||
nscdr: formData.nscdr.checked,
|
nscdr: formData.nscdr.checked,
|
||||||
passphrase: formData.passphrase.value,
|
passphrase: formData.passphrase?.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createCalendarType(createCalendarType)
|
this.createCalendarType(createCalendarType)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
|
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteCalendarType(this.data)
|
this.deleteCalendarType(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -65,7 +65,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,6 +116,7 @@ export default defineComponent({
|
||||||
type: formData.communicationType.value,
|
type: formData.communicationType.value,
|
||||||
fields: this.selectedFields,
|
fields: this.selectedFields,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createCommunicationType(createCommunicationType)
|
this.createCommunicationType(createCommunicationType)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]),
|
...mapActions(useCommunicationTypeStore, ["deleteCommunicationType"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteCommunicationType(this.data)
|
this.deleteCommunicationType(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
let createExecutivePosition: CreateExecutivePositionViewModel = {
|
let createExecutivePosition: CreateExecutivePositionViewModel = {
|
||||||
position: formData.executivePosition.value,
|
position: formData.executivePosition.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createExecutivePosition(createExecutivePosition)
|
this.createExecutivePosition(createExecutivePosition)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useExecutivePositionStore, ["deleteExecutivePosition"]),
|
...mapActions(useExecutivePositionStore, ["deleteExecutivePosition"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteExecutivePosition(this.data)
|
this.deleteExecutivePosition(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
let createMembershipStatus: CreateMembershipStatusViewModel = {
|
let createMembershipStatus: CreateMembershipStatusViewModel = {
|
||||||
status: formData.membershipStatus.value,
|
status: formData.membershipStatus.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createMembershipStatus(createMembershipStatus)
|
this.createMembershipStatus(createMembershipStatus)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useMembershipStatusStore, ["deleteMembershipStatus"]),
|
...mapActions(useMembershipStatusStore, ["deleteMembershipStatus"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteMembershipStatus(this.data)
|
this.deleteMembershipStatus(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +64,7 @@ export default defineComponent({
|
||||||
qualification: formData.qualification.value,
|
qualification: formData.qualification.value,
|
||||||
description: formData.description.value,
|
description: formData.description.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createQualification(createQualification)
|
this.createQualification(createQualification)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useQualificationStore, ["deleteQualification"]),
|
...mapActions(useQualificationStore, ["deleteQualification"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteQualification(this.data)
|
this.deleteQualification(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +64,7 @@ export default defineComponent({
|
||||||
title: formData.title.value,
|
title: formData.title.value,
|
||||||
query: this.query ?? "",
|
query: this.query ?? "",
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createQueryStore(createAward)
|
this.createQueryStore(createAward)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useQueryStoreStore, ["deleteQueryStore"]),
|
...mapActions(useQueryStoreStore, ["deleteQueryStore"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteQueryStore(this.data)
|
this.deleteQueryStore(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -21,7 +21,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,6 +75,7 @@ export default defineComponent({
|
||||||
id: this.data,
|
id: this.data,
|
||||||
query: this.query ?? "",
|
query: this.query ?? "",
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.updateActiveQueryStore(updateQuery)
|
this.updateActiveQueryStore(updateQuery)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -23,7 +23,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -62,6 +64,7 @@ export default defineComponent({
|
||||||
template: formData.template.value,
|
template: formData.template.value,
|
||||||
description: formData.description.value,
|
description: formData.description.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createTemplate(createTemplate)
|
this.createTemplate(createTemplate)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,6 +59,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useTemplateStore, ["deleteTemplate"]),
|
...mapActions(useTemplateStore, ["deleteTemplate"]),
|
||||||
triggerDelete() {
|
triggerDelete() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteTemplate(this.data)
|
this.deleteTemplate(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,6 +55,7 @@ export default defineComponent({
|
||||||
...mapActions(useRoleStore, ["createRole"]),
|
...mapActions(useRoleStore, ["createRole"]),
|
||||||
triggerCreateRole(e: any) {
|
triggerCreateRole(e: any) {
|
||||||
let formData = e.target.elements;
|
let formData = e.target.elements;
|
||||||
|
this.status = "loading";
|
||||||
this.createRole(formData.role.value)
|
this.createRole(formData.role.value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useRoleStore, ["deleteRole"]),
|
...mapActions(useRoleStore, ["deleteRole"]),
|
||||||
triggerDeleteRole() {
|
triggerDeleteRole() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteRole(this.data)
|
this.deleteRole(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +58,7 @@ export default defineComponent({
|
||||||
...mapActions(useModalStore, ["closeModal"]),
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
...mapActions(useUserStore, ["deleteUser"]),
|
...mapActions(useUserStore, ["deleteUser"]),
|
||||||
triggerDeleteUser() {
|
triggerDeleteUser() {
|
||||||
|
this.status = "loading";
|
||||||
this.deleteUser(this.data)
|
this.deleteUser(this.data)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
|
|
|
@ -32,7 +32,9 @@
|
||||||
|
|
||||||
<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" :disabled="status != null">abbrechen</button>
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,24 +72,22 @@ export default defineComponent({
|
||||||
...mapActions(useInviteStore, ["createInvite"]),
|
...mapActions(useInviteStore, ["createInvite"]),
|
||||||
invite(e: any) {
|
invite(e: any) {
|
||||||
let formData = e.target.elements;
|
let formData = e.target.elements;
|
||||||
this.status = "loading";
|
|
||||||
let createInvite: CreateInviteViewModel = {
|
let createInvite: CreateInviteViewModel = {
|
||||||
username: formData.username.value,
|
username: formData.username.value,
|
||||||
mail: formData.mail.value,
|
mail: formData.mail.value,
|
||||||
firstname: formData.firstname.value,
|
firstname: formData.firstname.value,
|
||||||
lastname: formData.lastname.value,
|
lastname: formData.lastname.value,
|
||||||
};
|
};
|
||||||
|
this.status = "loading";
|
||||||
this.createInvite(createInvite)
|
this.createInvite(createInvite)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.status = { status: "success" };
|
this.status = { status: "success" };
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
this.status = { status: "failed", reason: err.response.data };
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
this.status = { status: "failed", reason: err.response.data };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<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>
|
<p>
|
||||||
{{ user.firstname }} {{ user.lastname }} <small v-if="user.permissions_total.admin">(Admin)</small
|
{{ user.firstname }} {{ user.lastname }} <small v-if="user.permissions_total.admin">(Admin)</small
|
||||||
><small v-if="isOwner"> (Owner)</small>
|
><small v-if="user.isOwner"> (Owner)</small>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
|
@ -65,7 +65,7 @@ export default defineComponent({
|
||||||
user: { type: Object as PropType<UserViewModel>, default: {} },
|
user: { type: Object as PropType<UserViewModel>, default: {} },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useAbilityStore, ["can", "isOwner"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
|
97
src/components/public/calendar/ShowCalendarEntryModal.vue
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Termin</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="flex flex-col gap-4 py-2">
|
||||||
|
<div>
|
||||||
|
<label for="title">Terminart</label>
|
||||||
|
<input type="text" id="title" readonly :value="data.type?.type" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" readonly :value="data.title" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="content">Beschreibung</label>
|
||||||
|
<textarea id="content" class="h-18" readonly :value="data.content"></textarea>
|
||||||
|
</div>
|
||||||
|
<div v-if="data.allDay" class="flex flex-row gap-2 items-center">Der Termin findet ganztägig statt.</div>
|
||||||
|
<div v-if="data.allDay == false" class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="starttime">Startzeit</label>
|
||||||
|
<input type="datetime-local" id="starttime" readonly :value="formatForDateTimeLocalInput(data.starttime)" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="endtime">Endzeit</label>
|
||||||
|
<input
|
||||||
|
ref="endtime"
|
||||||
|
type="datetime-local"
|
||||||
|
id="endtime"
|
||||||
|
readonly
|
||||||
|
:value="formatForDateTimeLocalInput(data.endtime)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-row gap-2">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="startdate">Startdatum</label>
|
||||||
|
<input type="date" id="startdate" readonly :value="formatForDateInput(data.starttime)" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="enddate">Enddatum</label>
|
||||||
|
<input ref="enddate" type="date" id="enddate" readonly :value="formatForDateInput(data.endtime)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="location">Ort</label>
|
||||||
|
<input type="text" id="location" readonly :value="data.location" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
formatForDateTimeLocalInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(localDate.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(localDate.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
formatForDateInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -46,18 +46,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow max-lg:hidden"></div>
|
<div class="grow max-lg:hidden"></div>
|
||||||
|
<div class="p-1 border border-gray-400 bg-gray-100 rounded-md" title="Schema-Struktur" @click="showStructure">
|
||||||
|
<SparklesIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
||||||
|
</div>
|
||||||
<div class="flex flex-row min-w-fit overflow-hidden border border-gray-400 rounded-md">
|
<div class="flex flex-row min-w-fit overflow-hidden border border-gray-400 rounded-md">
|
||||||
<div
|
<div
|
||||||
class="p-1"
|
class="p-1"
|
||||||
:class="queryMode == 'structure' ? 'bg-gray-200' : ''"
|
:class="typeof value == 'object' ? 'bg-gray-200' : ''"
|
||||||
title="Schema-Struktur"
|
|
||||||
@click="queryMode = 'structure'"
|
|
||||||
>
|
|
||||||
<SparklesIcon class="text-gray-500 h-6 w-6 cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="p-1"
|
|
||||||
:class="typeof value == 'object' && queryMode != 'structure' ? 'bg-gray-200' : ''"
|
|
||||||
title="Visual Builder"
|
title="Visual Builder"
|
||||||
@click="queryMode = 'builder'"
|
@click="queryMode = 'builder'"
|
||||||
>
|
>
|
||||||
|
@ -65,7 +60,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="p-1"
|
class="p-1"
|
||||||
:class="typeof value == 'string' && queryMode != 'structure' ? 'bg-gray-200' : ''"
|
:class="typeof value == 'string' ? 'bg-gray-200' : ''"
|
||||||
title="SQL Editor"
|
title="SQL Editor"
|
||||||
@click="queryMode = 'editor'"
|
@click="queryMode = 'editor'"
|
||||||
>
|
>
|
||||||
|
@ -74,10 +69,7 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<div v-if="queryMode == 'structure'">
|
<textarea v-if="typeof value == 'string'" v-model="value" placeholder="SQL Query" class="h-full w-full" />
|
||||||
<img src="/administration-db.png" class="h-full w-full cursor-pointer" @click="showStructure" />
|
|
||||||
</div>
|
|
||||||
<textarea v-else-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" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,7 +145,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
autoChangeFlag: false as boolean,
|
autoChangeFlag: false as boolean,
|
||||||
queryMode: "builder" as "builder" | "editor" | "structure",
|
queryMode: "builder" as "builder" | "editor",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -11,7 +11,10 @@
|
||||||
|
|
||||||
<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">schnließen</button>
|
<a href="/administration-db.png" button primary-outline download="Datenbank-Schema" class="!whitespace-nowrap"
|
||||||
|
>Bild herunterladen</a
|
||||||
|
>
|
||||||
|
<button primary-outline @click="closeModal">schließen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
15
src/config.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export interface Config {
|
||||||
|
server_address: string;
|
||||||
|
app_name_overwrite: string;
|
||||||
|
imprint_link: string;
|
||||||
|
privacy_link: string;
|
||||||
|
custom_login_message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const config: Config = {
|
||||||
|
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,
|
||||||
|
};
|
|
@ -14,7 +14,6 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
|
||||||
const newKey = prefix ? `${prefix}_${key}` : key;
|
const newKey = prefix ? `${prefix}_${key}` : key;
|
||||||
|
|
||||||
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
||||||
console.log(value, newKey);
|
|
||||||
const arrayResults: Array<{ [key: string]: FieldType }> = [];
|
const arrayResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
value.forEach((item) => {
|
value.forEach((item) => {
|
||||||
const flattenedItems = flatten(item, newKey);
|
const flattenedItems = flatten(item, newKey);
|
||||||
|
@ -29,7 +28,6 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
|
||||||
});
|
});
|
||||||
results = tempResults;
|
results = tempResults;
|
||||||
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||||
console.log(value, newKey);
|
|
||||||
const objResults = flatten(value as QueryResult, newKey);
|
const objResults = flatten(value as QueryResult, newKey);
|
||||||
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
const tempResults: Array<{ [key: string]: FieldType }> = [];
|
||||||
results.forEach((res) => {
|
results.forEach((res) => {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import type { PermissionType, PermissionSection, PermissionModule } from "@/type
|
||||||
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";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -683,6 +684,10 @@ 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" {
|
||||||
|
|
|
@ -9,8 +9,6 @@ export async function setNewsletterId(to: any, from: any, next: any) {
|
||||||
|
|
||||||
useNewsletterDatesStore().$reset();
|
useNewsletterDatesStore().$reset();
|
||||||
useNewsletterRecipientsStore().$reset();
|
useNewsletterRecipientsStore().$reset();
|
||||||
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
|
|
||||||
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
|
|
||||||
useNewsletterPrintoutStore().$reset();
|
useNewsletterPrintoutStore().$reset();
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
@ -23,8 +21,6 @@ export async function resetNewsletterStores(to: any, from: any, next: any) {
|
||||||
|
|
||||||
useNewsletterDatesStore().$reset();
|
useNewsletterDatesStore().$reset();
|
||||||
useNewsletterRecipientsStore().$reset();
|
useNewsletterRecipientsStore().$reset();
|
||||||
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
|
|
||||||
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
|
|
||||||
useNewsletterPrintoutStore().$reset();
|
useNewsletterPrintoutStore().$reset();
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|
|
@ -3,11 +3,12 @@ import { isAuthenticatedPromise, type Payload } from "./router/authGuard";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { useNotificationStore } from "./stores/notification";
|
import { useNotificationStore } from "./stores/notification";
|
||||||
import { EventSourcePolyfill } from "event-source-polyfill";
|
import { EventSourcePolyfill } from "event-source-polyfill";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
let devMode = process.env.NODE_ENV === "development";
|
let devMode = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
let host = devMode ? "localhost:5000" : process.env.SERVER_ADDRESS || "";
|
let host = devMode ? "localhost:5000" : (config.server_address ?? "").replace(/(^\w+:|^)\/\//, "");
|
||||||
let url = devMode ? "http://" + host : (host ? "https://" : "") + host;
|
let url = devMode ? "http://" + host : config.server_address;
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: url + "/api",
|
baseURL: url + "/api",
|
||||||
|
@ -27,6 +28,15 @@ http.interceptors.request.use(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPWA =
|
||||||
|
window.matchMedia("(display-mode: standalone)").matches ||
|
||||||
|
window.matchMedia("(display-mode: fullscreen)").matches;
|
||||||
|
if (isPWA) {
|
||||||
|
if (config.headers) {
|
||||||
|
config.headers["X-PWA-Client"] = isPWA ? "true" : "false";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -52,11 +62,15 @@ http.interceptors.response.use(
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return http(originalRequest);
|
return http(originalRequest);
|
||||||
})
|
})
|
||||||
.catch();
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
notificationStore.push("Fehler", error.response.data, "error");
|
if (error.toString().includes("Network Error")) {
|
||||||
|
notificationStore.push("Netzwerkfehler", "Server nicht erreichbar!", "error");
|
||||||
|
} else {
|
||||||
|
notificationStore.push("Fehler", error.response.data, "error");
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
@ -98,4 +112,25 @@ function newEventSource(path: string) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { http, newEventSource, host };
|
async function* streamingFetch(path: string, abort?: AbortController) {
|
||||||
|
await refreshToken()
|
||||||
|
.then(() => {})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
const token = localStorage.getItem("accessToken");
|
||||||
|
const response = await fetch(url + "/api" + path, {
|
||||||
|
signal: abort?.signal,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const reader = response.body?.getReader();
|
||||||
|
while (true && reader) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
yield new TextDecoder().decode(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { http, newEventSource, streamingFetch, host };
|
||||||
|
|
|
@ -40,6 +40,21 @@ export const useMemberStore = defineStore("member", {
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async getAllMembers(): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.get(`/admin/member?noLimit=true`).then((res) => {
|
||||||
|
return { ...res, data: res.data.members };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async getMembersByIds(ids: Array<number>): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.get(`/admin/member?ids=${ids.join(",")}&noLimit=true`).then((res) => {
|
||||||
|
return { ...res, data: res.data.members };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async searchMembers(search: string): Promise<AxiosResponse<any, any>> {
|
||||||
|
return await http.get(`/admin/member?search=${search}&noLimit=true`).then((res) => {
|
||||||
|
return { ...res, data: res.data.members };
|
||||||
|
});
|
||||||
|
},
|
||||||
fetchMemberByActiveId() {
|
fetchMemberByActiveId() {
|
||||||
this.loadingActive = "loading";
|
this.loadingActive = "loading";
|
||||||
http
|
http
|
||||||
|
@ -84,10 +99,10 @@ export const useMemberStore = defineStore("member", {
|
||||||
this.fetchMembers();
|
this.fetchMembers();
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
async printMemberList(){
|
async printMemberList() {
|
||||||
return http.get(`/admin/member/print/namelist`, {
|
return http.get(`/admin/member/print/namelist`, {
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http, newEventSource } from "@/serverCom";
|
import { http, newEventSource, streamingFetch } from "@/serverCom";
|
||||||
import { useNewsletterStore } from "./newsletter";
|
import { useNewsletterStore } from "./newsletter";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import type { EventSourcePolyfill } from "event-source-polyfill";
|
import type { EventSourcePolyfill } from "event-source-polyfill";
|
||||||
|
import { useNotificationStore, type NotificationType } from "../../../notification";
|
||||||
|
|
||||||
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||||
state: () => {
|
state: () => {
|
||||||
|
@ -12,10 +13,10 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||||
printing: undefined as undefined | "loading" | "success" | "failed",
|
printing: undefined as undefined | "loading" | "success" | "failed",
|
||||||
sending: undefined as undefined | "loading" | "success" | "failed",
|
sending: undefined as undefined | "loading" | "success" | "failed",
|
||||||
sendingPreview: undefined as undefined | "loading" | "success" | "failed",
|
sendingPreview: undefined as undefined | "loading" | "success" | "failed",
|
||||||
pdfProgessSource: undefined as undefined | EventSourcePolyfill,
|
pdfSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
|
||||||
mailProgessSource: undefined as undefined | EventSourcePolyfill,
|
mailSourceMessages: [] as Array<{ kind: string; factor: string; [key: string]: string }>,
|
||||||
pdfSourceMessages: [] as Array<Object>,
|
pdfPrintingAbort: undefined as undefined | AbortController,
|
||||||
mailSourceMessages: [] as Array<Object>,
|
mailSendingAbort: undefined as undefined | AbortController,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -63,6 +64,7 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createNewsletterPrintout() {
|
createNewsletterPrintout() {
|
||||||
|
this.subscribePdfPrintingProgress();
|
||||||
this.printing = "loading";
|
this.printing = "loading";
|
||||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
if (newsletterId == null) return;
|
if (newsletterId == null) return;
|
||||||
|
@ -78,10 +80,12 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.printing = undefined;
|
this.printing = undefined;
|
||||||
|
this.pdfPrintingAbort?.abort();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
createNewsletterSend() {
|
createNewsletterSend() {
|
||||||
|
this.subscribeMailSendingProgress();
|
||||||
this.sending = "loading";
|
this.sending = "loading";
|
||||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
if (newsletterId == null) return;
|
if (newsletterId == null) return;
|
||||||
|
@ -96,32 +100,55 @@ export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.sending = undefined;
|
this.sending = undefined;
|
||||||
|
this.mailSendingAbort?.abort();
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
subscribePdfPrintingProgress() {
|
async subscribePdfPrintingProgress() {
|
||||||
// const newsletterId = useNewsletterStore().activeNewsletter;
|
this.pdfSourceMessages = [];
|
||||||
// if (this.pdfProgessSource != undefined) return;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
// this.pdfProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/printoutprogress`);
|
const notificationStore = useNotificationStore();
|
||||||
// this.pdfProgessSource.onmessage = (event) => {
|
this.pdfPrintingAbort = new AbortController();
|
||||||
// console.log("pdf", event);
|
for await (let chunk of streamingFetch(
|
||||||
// };
|
`/admin/newsletter/${newsletterId}/printoutprogress`,
|
||||||
|
this.pdfPrintingAbort
|
||||||
|
)) {
|
||||||
|
chunk.split("//").forEach((r) => {
|
||||||
|
if (r.trim() != "") {
|
||||||
|
let data = JSON.parse(r);
|
||||||
|
this.pdfSourceMessages.push(data);
|
||||||
|
let type: NotificationType = "info";
|
||||||
|
let timeout = undefined;
|
||||||
|
if (data.factor == "failed") {
|
||||||
|
type = "error";
|
||||||
|
timeout = 0;
|
||||||
|
}
|
||||||
|
notificationStore.push(`Druck: ${data.iteration}/${data.total}`, `${data.msg}`, type, timeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.fetchNewsletterPrintout();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
subscribeMailSendingProgress() {
|
async subscribeMailSendingProgress() {
|
||||||
// const newsletterId = useNewsletterStore().activeNewsletter;
|
this.mailSourceMessages = [];
|
||||||
// if (this.mailProgessSource != undefined) return;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
// this.mailProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/sendprogress`);
|
const notificationStore = useNotificationStore();
|
||||||
// this.mailProgessSource.onmessage = (event) => {
|
this.mailSendingAbort = new AbortController();
|
||||||
// console.log("mail", event);
|
for await (let chunk of streamingFetch(`/admin/newsletter/${newsletterId}/sendprogress`, this.mailSendingAbort)) {
|
||||||
// };
|
chunk.split("//").forEach((r) => {
|
||||||
},
|
if (r.trim() != "") {
|
||||||
unsubscribePdfPrintingProgress() {
|
let data = JSON.parse(r);
|
||||||
this.pdfProgessSource?.close();
|
this.mailSourceMessages.push(data);
|
||||||
this.pdfProgessSource = undefined;
|
let type: NotificationType = "info";
|
||||||
},
|
let timeout = undefined;
|
||||||
unsubscribeMailSendingProgress() {
|
if (data.factor == "failed") {
|
||||||
this.mailProgessSource?.close();
|
type = "error";
|
||||||
this.mailProgessSource = undefined;
|
timeout = 0;
|
||||||
|
}
|
||||||
|
notificationStore.push(`Mailversand: ${data.iteration}/${data.total}`, `${data.msg}`, type, timeout);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,7 +38,6 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const useNavigationStore = defineStore("navigation", {
|
||||||
{
|
{
|
||||||
key: "settings",
|
key: "settings",
|
||||||
title: "Einstellungen",
|
title: "Einstellungen",
|
||||||
levelDefault: "qualification",
|
levelDefault: "award",
|
||||||
} as topLevelNavigationModel,
|
} as topLevelNavigationModel,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
@ -98,30 +98,32 @@ export const useNavigationStore = defineStore("navigation", {
|
||||||
settings: {
|
settings: {
|
||||||
mainTitle: "Einstellungen",
|
mainTitle: "Einstellungen",
|
||||||
main: [
|
main: [
|
||||||
...(abilityStore.can("read", "settings", "qualification")
|
{ key: "divider1", title: "Mitgliederdaten" },
|
||||||
? [{ key: "qualification", title: "Qualifikationen" }]
|
|
||||||
: []),
|
|
||||||
...(abilityStore.can("read", "settings", "award") ? [{ key: "award", title: "Auszeichnungen" }] : []),
|
...(abilityStore.can("read", "settings", "award") ? [{ key: "award", title: "Auszeichnungen" }] : []),
|
||||||
...(abilityStore.can("read", "settings", "executive_position")
|
|
||||||
? [{ key: "executive_position", title: "Vereinsämter" }]
|
|
||||||
: []),
|
|
||||||
...(abilityStore.can("read", "settings", "communication_type")
|
...(abilityStore.can("read", "settings", "communication_type")
|
||||||
? [{ key: "communication_type", title: "Kommunikationsarten" }]
|
? [{ key: "communication_type", title: "Kommunikationsarten" }]
|
||||||
: []),
|
: []),
|
||||||
...(abilityStore.can("read", "settings", "membership_status")
|
...(abilityStore.can("read", "settings", "membership_status")
|
||||||
? [{ key: "membership_status", title: "Mitgliedsstatus" }]
|
? [{ key: "membership_status", title: "Mitgliedsstatus" }]
|
||||||
: []),
|
: []),
|
||||||
...(abilityStore.can("read", "settings", "calendar_type")
|
...(abilityStore.can("read", "settings", "qualification")
|
||||||
? [{ key: "calendar_type", title: "Terminarten" }]
|
? [{ key: "qualification", title: "Qualifikationen" }]
|
||||||
|
: []),
|
||||||
|
...(abilityStore.can("read", "settings", "executive_position")
|
||||||
|
? [{ key: "executive_position", title: "Vereinsämter" }]
|
||||||
|
: []),
|
||||||
|
{ key: "divider2", title: "Einstellungen" },
|
||||||
|
...(abilityStore.can("read", "settings", "newsletter_config")
|
||||||
|
? [{ key: "newsletter_config", title: "Newsletter Konfiguration" }]
|
||||||
: []),
|
: []),
|
||||||
...(abilityStore.can("read", "settings", "query") ? [{ key: "query_store", title: "Query Store" }] : []),
|
|
||||||
...(abilityStore.can("read", "settings", "template") ? [{ key: "template", title: "Templates" }] : []),
|
...(abilityStore.can("read", "settings", "template") ? [{ key: "template", title: "Templates" }] : []),
|
||||||
...(abilityStore.can("read", "settings", "template_usage")
|
...(abilityStore.can("read", "settings", "template_usage")
|
||||||
? [{ key: "template_usage", title: "Template-Verwendung" }]
|
? [{ key: "template_usage", title: "Template-Verwendung" }]
|
||||||
: []),
|
: []),
|
||||||
...(abilityStore.can("read", "settings", "newsletter_config")
|
...(abilityStore.can("read", "settings", "calendar_type")
|
||||||
? [{ key: "newsletter_config", title: "Newsletter Konfiguration" }]
|
? [{ key: "calendar_type", title: "Terminarten" }]
|
||||||
: []),
|
: []),
|
||||||
|
...(abilityStore.can("read", "settings", "query") ? [{ key: "query_store", title: "Query Store" }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: number;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
type: NotificationType;
|
type: NotificationType;
|
||||||
|
@ -19,7 +19,7 @@ export const useNotificationStore = defineStore("notification", {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
push(title: string, text: string, type: NotificationType, timeout: number = 5000) {
|
push(title: string, text: string, type: NotificationType, timeout: number = 5000) {
|
||||||
let id = Date.now();
|
let id = `${Date.now()}_${Math.random()}`;
|
||||||
this.notifications.push({
|
this.notifications.push({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
@ -27,14 +27,16 @@ export const useNotificationStore = defineStore("notification", {
|
||||||
type,
|
type,
|
||||||
indicator: false,
|
indicator: false,
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
if (timeout != 0) {
|
||||||
this.notifications[this.notifications.findIndex((n) => n.id === id)].indicator = true;
|
setTimeout(() => {
|
||||||
}, 100);
|
this.notifications[this.notifications.findIndex((n) => n.id === id)].indicator = true;
|
||||||
this.timeouts[id] = setTimeout(() => {
|
}, 100);
|
||||||
this.revoke(id);
|
this.timeouts[id] = setTimeout(() => {
|
||||||
}, timeout);
|
this.revoke(id);
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
revoke(id: number) {
|
revoke(id: string) {
|
||||||
this.notifications.splice(
|
this.notifications.splice(
|
||||||
this.notifications.findIndex((n) => n.id === id),
|
this.notifications.findIndex((n) => n.id === id),
|
||||||
1
|
1
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export interface ProtocolPresenceViewModel {
|
export interface ProtocolPresenceViewModel {
|
||||||
memberId: number;
|
memberId: number;
|
||||||
absent: boolean;
|
absent: boolean;
|
||||||
|
excused: boolean;
|
||||||
protocolId: number;
|
protocolId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
<div class="grow flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||||
<div class="max-w-md w-full space-y-8 pb-20">
|
<div class="max-w-md w-full space-y-8 pb-20">
|
||||||
<div class="flex flex-col items-center gap-4">
|
<div class="flex flex-col items-center gap-4">
|
||||||
<img src="/Logo.png" alt="LOGO" class="h-36" />
|
<img src="/Logo.png" alt="LOGO" class="h-auto w-full" />
|
||||||
<h2 class="text-center text-4xl font-extrabold text-gray-900">Mitgliederverwaltung</h2>
|
<h2 class="text-center text-4xl font-extrabold text-gray-900">
|
||||||
|
{{ config.app_name_overwrite || "FF Admin" }}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="login">
|
<form class="flex flex-col gap-2" @submit.prevent="login">
|
||||||
|
@ -48,6 +50,7 @@ import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
import FailureXMark from "@/components/FailureXMark.vue";
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
import { resetAllPiniaStores } from "@/helpers/piniaReset";
|
||||||
import FormBottomBar from "@/components/FormBottomBar.vue";
|
import FormBottomBar from "@/components/FormBottomBar.vue";
|
||||||
|
import { config } from "@/config";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -10,16 +10,7 @@
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
<img :src="image" alt="totp" class="w-56 h-56 self-center" />
|
||||||
|
|
||||||
<div class="flex relative">
|
<TextCopy :copyText="otp" />
|
||||||
<input type="text" :value="otp" />
|
|
||||||
<ClipboardIcon
|
|
||||||
class="w-5 h-5 p-2 box-content absolute right-1 top-1/2 -translate-y-1/2 bg-white cursor-pointer"
|
|
||||||
@click="copyToClipboard"
|
|
||||||
/>
|
|
||||||
<div v-if="copySuccess" class="absolute w-5 h-5 right-3 top-[10px]">
|
|
||||||
<SuccessCheckmark />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<form class="flex flex-col gap-2" @submit.prevent="verify">
|
<form class="flex flex-col gap-2" @submit.prevent="verify">
|
||||||
<div class="-space-y-px">
|
<div class="-space-y-px">
|
||||||
|
@ -44,13 +35,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { mapActions, mapState } from "pinia";
|
import { mapActions, mapState } from "pinia";
|
||||||
import MainTemplate from "@/templates/Main.vue";
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
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 { ClipboardIcon } from "@heroicons/vue/24/outline";
|
import TextCopy from "@/components/TextCopy.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -62,8 +53,6 @@ export default defineComponent({
|
||||||
otp: undefined as undefined | string,
|
otp: undefined as undefined | string,
|
||||||
verifyStatus: undefined as undefined | "loading" | "success" | "failed",
|
verifyStatus: undefined as undefined | "loading" | "success" | "failed",
|
||||||
verifyError: "" as string,
|
verifyError: "" as string,
|
||||||
copySuccess: false,
|
|
||||||
timeoutCopy: undefined as any,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -100,13 +89,6 @@ export default defineComponent({
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
copyToClipboard() {
|
|
||||||
navigator.clipboard.writeText(this.otp ?? "");
|
|
||||||
this.copySuccess = true;
|
|
||||||
this.timeoutCopy = setTimeout(() => {
|
|
||||||
this.copySuccess = false;
|
|
||||||
}, 2000);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<SidebarLayout>
|
<SidebarLayout>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<SidebarTemplate mainTitle="Mein Account" topTitle="Mitgliederverwaltung" :showTopList="isOwner">
|
<SidebarTemplate mainTitle="Mein Account" :topTitle="config.app_name_overwrite || 'FF Admin'" :showTopList="isOwner">
|
||||||
<template v-if="isOwner" #topList>
|
<template v-if="isOwner" #topList>
|
||||||
<RoutingLink
|
<RoutingLink
|
||||||
title="Administration"
|
title="Administration"
|
||||||
|
@ -38,6 +38,7 @@ import SidebarTemplate from "@/templates/Sidebar.vue";
|
||||||
import RoutingLink from "@/components/admin/RoutingLink.vue";
|
import RoutingLink from "@/components/admin/RoutingLink.vue";
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { config } from "@/config"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
|
@ -16,13 +16,15 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #list>
|
<template #list>
|
||||||
<RoutingLink
|
<div v-for="item in activeNavigationObject.main" :key="item.key">
|
||||||
v-for="item in activeNavigationObject.main"
|
<RoutingLink
|
||||||
:key="item.key"
|
v-if="!item.key.includes('divider')"
|
||||||
:title="item.title"
|
:title="item.title"
|
||||||
:link="{ name: `admin-${activeNavigation}-${item.key}` }"
|
:link="{ name: `admin-${activeNavigation}-${item.key}` }"
|
||||||
:active="activeLink == item.key"
|
:active="activeLink == item.key"
|
||||||
/>
|
/>
|
||||||
|
<p v-else class="pt-4 border-b border-gray-300">{{ item.title }}</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SidebarTemplate>
|
</SidebarTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -61,6 +61,12 @@
|
||||||
<SuccessCheckmark v-else-if="sendingPreview == 'success'" />
|
<SuccessCheckmark v-else-if="sendingPreview == 'success'" />
|
||||||
<FailureXMark v-else-if="sendingPreview == 'failed'" />
|
<FailureXMark v-else-if="sendingPreview == 'failed'" />
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="pdfSourceMessages.length != 0" primary-outline class="!w-fit" @click="openPdfLogs">
|
||||||
|
Druck Logs
|
||||||
|
</button>
|
||||||
|
<button v-if="mailSourceMessages.length != 0" primary-outline class="!w-fit" @click="openMailLogs">
|
||||||
|
Versand Logs
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -84,13 +90,19 @@ export default defineComponent({
|
||||||
newsletterId: String,
|
newsletterId: String,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useNewsletterPrintoutStore, ["printout", "loading", "printing", "sending", "sendingPreview"]),
|
...mapState(useNewsletterPrintoutStore, [
|
||||||
|
"printout",
|
||||||
|
"loading",
|
||||||
|
"printing",
|
||||||
|
"sending",
|
||||||
|
"sendingPreview",
|
||||||
|
"mailSourceMessages",
|
||||||
|
"pdfSourceMessages",
|
||||||
|
]),
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchNewsletterPrintout();
|
this.fetchNewsletterPrintout();
|
||||||
this.subscribeMailSendingProgress();
|
|
||||||
this.subscribePdfPrintingProgress();
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
@ -100,8 +112,6 @@ export default defineComponent({
|
||||||
"fetchNewsletterPrintoutById",
|
"fetchNewsletterPrintoutById",
|
||||||
"createNewsletterMailPreview",
|
"createNewsletterMailPreview",
|
||||||
"createNewsletterSend",
|
"createNewsletterSend",
|
||||||
"subscribeMailSendingProgress",
|
|
||||||
"subscribePdfPrintingProgress",
|
|
||||||
]),
|
]),
|
||||||
openPdfShow(filename?: string) {
|
openPdfShow(filename?: string) {
|
||||||
this.openModal(
|
this.openModal(
|
||||||
|
@ -122,6 +132,20 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
|
openPdfLogs() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(
|
||||||
|
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterPrintingProgressModal.vue"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
openMailLogs() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(
|
||||||
|
defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterMailProgressModal.vue"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -23,63 +23,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full">
|
<MemberSearchSelect
|
||||||
<Combobox v-model="recipients" :disabled="!can('create', 'club', 'newsletter')" multiple>
|
title="weitere Empfänger suchen"
|
||||||
<ComboboxLabel>weitere Empfänger suchen</ComboboxLabel>
|
v-model="recipients"
|
||||||
<div class="relative mt-1">
|
:disabled="!can('create', 'club', 'newsletter')"
|
||||||
<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"
|
|
||||||
@input="query = $event.target.value"
|
|
||||||
/>
|
|
||||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
|
||||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
|
||||||
</ComboboxButton>
|
|
||||||
<TransitionRoot
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
@after-leave="query = ''"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<ComboboxOption v-if="filtered.length === 0" as="template" disabled>
|
|
||||||
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
|
||||||
<span class="font-normal block truncate"> Keine Auswahl</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
|
|
||||||
<ComboboxOption
|
|
||||||
v-for="member in filtered"
|
|
||||||
as="template"
|
|
||||||
:key="member.id"
|
|
||||||
:value="member.id"
|
|
||||||
v-slot="{ selected, active }"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
|
||||||
:class="{
|
|
||||||
'bg-primary text-white': active,
|
|
||||||
'text-gray-900': !active,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
|
||||||
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
|
||||||
:class="{ 'text-white': active, 'text-primary': !active }"
|
|
||||||
>
|
|
||||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
</ComboboxOptions>
|
|
||||||
</TransitionRoot>
|
|
||||||
</div>
|
|
||||||
</Combobox>
|
|
||||||
</div>
|
|
||||||
<p>Ausgewählte Empfänger</p>
|
<p>Ausgewählte Empfänger</p>
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
<div
|
<div
|
||||||
|
@ -125,6 +74,7 @@ import { useAbilityStore } from "@/stores/ability";
|
||||||
import { useQueryStoreStore } from "@/stores/admin/settings/queryStore";
|
import { useQueryStoreStore } from "@/stores/admin/settings/queryStore";
|
||||||
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
|
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -140,36 +90,25 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: "" as String,
|
query: "" as String,
|
||||||
|
members: [] as Array<MemberViewModel>,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapWritableState(useNewsletterRecipientsStore, ["recipients", "loading"]),
|
...mapWritableState(useNewsletterRecipientsStore, ["recipients", "loading"]),
|
||||||
...mapWritableState(useNewsletterStore, ["activeNewsletterObj"]),
|
...mapWritableState(useNewsletterStore, ["activeNewsletterObj"]),
|
||||||
...mapState(useMemberStore, ["members"]),
|
|
||||||
...mapState(useQueryStoreStore, ["queries"]),
|
...mapState(useQueryStoreStore, ["queries"]),
|
||||||
...mapState(useQueryBuilderStore, ["data"]),
|
...mapState(useQueryBuilderStore, ["data"]),
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
filtered(): Array<MemberViewModel> {
|
|
||||||
return this.query === ""
|
|
||||||
? this.members
|
|
||||||
: this.members.filter((member) =>
|
|
||||||
(member.firstname + " " + member.lastname)
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, "")
|
|
||||||
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
|
||||||
);
|
|
||||||
},
|
|
||||||
sorted(): Array<MemberViewModel> {
|
|
||||||
return this.selected.sort((a, b) => {
|
|
||||||
if (a.lastname < b.lastname) return -1;
|
|
||||||
if (a.lastname > b.lastname) return 1;
|
|
||||||
if (a.firstname < b.firstname) return -1;
|
|
||||||
if (a.firstname > b.firstname) return 1;
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
selected(): Array<MemberViewModel> {
|
selected(): Array<MemberViewModel> {
|
||||||
return this.members.filter((m) => this.recipients.includes(m.id));
|
return this.members
|
||||||
|
.filter((m) => this.recipients.includes(m.id))
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.lastname < b.lastname) return -1;
|
||||||
|
if (a.lastname > b.lastname) return 1;
|
||||||
|
if (a.firstname < b.firstname) return -1;
|
||||||
|
if (a.firstname > b.firstname) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
queried(): Array<MemberViewModel> {
|
queried(): Array<MemberViewModel> {
|
||||||
if (this.recipientsByQueryId == "def") return [];
|
if (this.recipientsByQueryId == "def") return [];
|
||||||
|
@ -205,13 +144,13 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchMembers(0, 1000, "", true);
|
|
||||||
// this.fetchNewsletterRecipients();
|
// this.fetchNewsletterRecipients();
|
||||||
this.fetchQueries();
|
this.fetchQueries();
|
||||||
this.loadQuery();
|
this.loadQuery();
|
||||||
|
this.loadMembers();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
...mapActions(useMemberStore, ["getAllMembers"]),
|
||||||
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
||||||
...mapActions(useQueryStoreStore, ["fetchQueries"]),
|
...mapActions(useQueryStoreStore, ["fetchQueries"]),
|
||||||
...mapActions(useQueryBuilderStore, ["sendQuery"]),
|
...mapActions(useQueryBuilderStore, ["sendQuery"]),
|
||||||
|
@ -221,6 +160,13 @@ export default defineComponent({
|
||||||
this.recipients.splice(index, 1);
|
this.recipients.splice(index, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
loadMembers() {
|
||||||
|
this.getAllMembers()
|
||||||
|
.then((res) => {
|
||||||
|
this.members = res.data;
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
},
|
||||||
loadQuery() {
|
loadQuery() {
|
||||||
if (this.recipientsByQuery) {
|
if (this.recipientsByQuery) {
|
||||||
this.sendQuery(0, 1000, this.recipientsByQuery.query);
|
this.sendQuery(0, 1000, this.recipientsByQuery.query);
|
||||||
|
|
|
@ -5,63 +5,19 @@
|
||||||
↺ laden fehlgeschlagen
|
↺ laden fehlgeschlagen
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="w-full">
|
<MemberSearchSelect
|
||||||
<Combobox v-model="presence" :disabled="!can('create', 'club', 'protocol')" multiple by="memberId">
|
title="Anwesende suchen"
|
||||||
<ComboboxLabel>Anwesende suchen</ComboboxLabel>
|
:model-value="presence.map((p) => p.memberId)"
|
||||||
<div class="relative mt-1">
|
:disabled="!can('create', 'club', 'protocol')"
|
||||||
<ComboboxInput
|
@add:difference="
|
||||||
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"
|
(id: number) =>
|
||||||
@input="query = $event.target.value"
|
presence.push({ memberId: id, absent: false, excused: true, protocolId: parseInt(protocolId ?? '') })
|
||||||
/>
|
"
|
||||||
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
@add:member="(s) => members.push(s)"
|
||||||
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
@add:member-by-array="(s) => members.push(...s)"
|
||||||
</ComboboxButton>
|
@remove:difference="removeSelected"
|
||||||
<TransitionRoot
|
/>
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
leaveTo="opacity-0"
|
|
||||||
@after-leave="query = ''"
|
|
||||||
>
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
<ComboboxOption v-if="filtered.length === 0" as="template" disabled>
|
|
||||||
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
|
||||||
<span class="font-normal block truncate"> Keine Auswahl</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
|
|
||||||
<ComboboxOption
|
|
||||||
v-for="member in filtered"
|
|
||||||
as="template"
|
|
||||||
:key="member.memberId"
|
|
||||||
:value="member"
|
|
||||||
v-slot="{ selected, active }"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
|
||||||
:class="{
|
|
||||||
'bg-primary text-white': active,
|
|
||||||
'text-gray-900': !active,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
|
||||||
{{ getMember(member.memberId)?.firstname }} {{ getMember(member.memberId)?.lastname }} {{ getMember(member.memberId)?.nameaffix }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="selected"
|
|
||||||
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
|
||||||
:class="{ 'text-white': active, 'text-primary': !active }"
|
|
||||||
>
|
|
||||||
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ComboboxOption>
|
|
||||||
</ComboboxOptions>
|
|
||||||
</TransitionRoot>
|
|
||||||
</div>
|
|
||||||
</Combobox>
|
|
||||||
</div>
|
|
||||||
<br />
|
<br />
|
||||||
<p>Anwesenheit</p>
|
<p>Anwesenheit</p>
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
|
@ -71,12 +27,21 @@
|
||||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-start">
|
<div class="flex flex-col items-start">
|
||||||
<p>{{ getMember(member.memberId)?.lastname }}, {{ getMember(member.memberId)?.firstname }} {{ getMember(member.memberId)?.nameaffix ? `- ${getMember(member.memberId)?.nameaffix}` : "" }}</p>
|
<p>
|
||||||
<label class="flex flex-row gap-2 items-center">
|
{{ getMember(member.memberId)?.lastname }}, {{ getMember(member.memberId)?.firstname }}
|
||||||
<input type="checkbox" v-model="member.absent" />
|
{{ getMember(member.memberId)?.nameaffix ? `- ${getMember(member.memberId)?.nameaffix}` : "" }}
|
||||||
war abwesend
|
</p>
|
||||||
</label>
|
<div class="flex flex-row gap-4">
|
||||||
</div>
|
<label class="flex flex-row gap-2 items-center">
|
||||||
|
<input type="checkbox" v-model="member.absent" />
|
||||||
|
war abwesend
|
||||||
|
</label>
|
||||||
|
<label v-if="member.absent" class="flex flex-row gap-2 items-center">
|
||||||
|
<input type="checkbox" v-model="member.excused" />
|
||||||
|
ist entschuldigt
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<TrashIcon
|
<TrashIcon
|
||||||
v-if="can('create', 'club', 'protocol')"
|
v-if="can('create', 'club', 'protocol')"
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
|
@ -91,22 +56,11 @@
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
import Spinner from "@/components/Spinner.vue";
|
import Spinner from "@/components/Spinner.vue";
|
||||||
import {
|
|
||||||
Combobox,
|
|
||||||
ComboboxLabel,
|
|
||||||
ComboboxInput,
|
|
||||||
ComboboxButton,
|
|
||||||
ComboboxOptions,
|
|
||||||
ComboboxOption,
|
|
||||||
TransitionRoot,
|
|
||||||
} from "@headlessui/vue";
|
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
|
||||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
|
||||||
import { useMemberStore } from "@/stores/admin/club/member/member";
|
|
||||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
|
||||||
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -117,34 +71,20 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: "" as String,
|
query: "" as String,
|
||||||
|
members: [] as Array<MemberViewModel>,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
||||||
...mapState(useMemberStore, ["members"]),
|
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
filtered(): Array<{memberId:number, absent:boolean; protocolId:number}> {
|
getMember() {
|
||||||
return (this.query === ""
|
return (memberId: number) => {
|
||||||
? this.members
|
return this.members.find((m) => memberId == m.id);
|
||||||
: this.members.filter((member) =>
|
};
|
||||||
(member.firstname + " " + member.lastname)
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, "")
|
|
||||||
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
|
||||||
)).map(m =>({memberId: m.id, absent:false, protocolId:parseInt(this.protocolId ?? "")}));
|
|
||||||
},
|
},
|
||||||
getMember(){
|
|
||||||
return (memberId:number) => {
|
|
||||||
return this.members.find(m => memberId == m.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.fetchMembers(0, 1000, "", true);
|
|
||||||
// this.fetchProtocolPresence();
|
|
||||||
},
|
},
|
||||||
|
mounted() {},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
|
||||||
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||||
removeSelected(id: number) {
|
removeSelected(id: number) {
|
||||||
let index = this.presence.findIndex((s) => s.memberId == id);
|
let index = this.presence.findIndex((s) => s.memberId == id);
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<AwardListItem v-for="award in awards" :key="award.id" :award="award" />
|
<AwardListItem v-for="award in awards" :key="award.id" :award="award" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<CalendarTypeListItem
|
<CalendarTypeListItem
|
||||||
v-for="calendarType in calendarTypes"
|
v-for="calendarType in calendarTypes"
|
||||||
|
|
|
@ -58,8 +58,10 @@ 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 { RouterLink } from "vue-router";
|
import { RouterLink } from "vue-router";
|
||||||
import type { CalendarTypeViewModel, UpdateCalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models";
|
import type {
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
CalendarTypeViewModel,
|
||||||
|
UpdateCalendarTypeViewModel,
|
||||||
|
} from "@/viewmodels/admin/settings/calendarType.models";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
</script>
|
</script>
|
||||||
|
@ -115,7 +117,7 @@ export default defineComponent({
|
||||||
type: formData.type.value,
|
type: formData.type.value,
|
||||||
color: formData.color.value,
|
color: formData.color.value,
|
||||||
nscdr: formData.nscdr.checked,
|
nscdr: formData.nscdr.checked,
|
||||||
passphrase: formData.passphrase.value,
|
passphrase: formData.passphrase?.value,
|
||||||
};
|
};
|
||||||
this.status = "loading";
|
this.status = "loading";
|
||||||
this.updateActiveCalendarType(updateCalendarType)
|
this.updateActiveCalendarType(updateCalendarType)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<CommunicationTypeListItem
|
<CommunicationTypeListItem
|
||||||
v-for="communicationType in communicationTypes"
|
v-for="communicationType in communicationTypes"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<ExecutivePositionListItem
|
<ExecutivePositionListItem
|
||||||
v-for="executivePosition in executivePositions"
|
v-for="executivePosition in executivePositions"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<MembershipStatusListItem v-for="status in membershipStatus" :key="status.id" :membershipStatus="status" />
|
<MembershipStatusListItem v-for="status in membershipStatus" :key="status.id" :membershipStatus="status" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<QualificationListItem
|
<QualificationListItem
|
||||||
v-for="qualification in qualifications"
|
v-for="qualification in qualifications"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<QueryStoreListItem v-for="query in queries" :key="query.id" :queryItem="query" />
|
<QueryStoreListItem v-for="query in queries" :key="query.id" :queryItem="query" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<TemplateListItem v-for="template in templates" :key="template.id" :template="template" />
|
<TemplateListItem v-for="template in templates" :key="template.id" :template="template" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -139,7 +139,6 @@ export default defineComponent({
|
||||||
if (!fromSave) this.loadDesign();
|
if (!fromSave) this.loadDesign();
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err);
|
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
<div class="flex flex-col gap-4 grow pl-7">
|
<div class="flex flex-col gap-4 h-full pl-7">
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||||
<RoleListItem v-for="role in roles" :key="role.id" :role="role" />
|
<RoleListItem v-for="role in roles" :key="role.id" :role="role" />
|
||||||
</div>
|
</div>
|
||||||
|
|