Merge pull request '#23-cleanup-&-enhancements' (#28) from #23-cleanup-&-enhancements into main
Reviewed-on: Ehrenamt/member-administration-ui#28
This commit is contained in:
commit
01b744175f
37 changed files with 490 additions and 62 deletions
26
docs/calendar.md
Normal file
26
docs/calendar.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# Kalender
|
||||||
|
|
||||||
|
Der Kalender bietet eine Möglichkeit der Organisation von Terminen in unterschiedlichen Kategorien.
|
||||||
|
|
||||||
|
Die Kategorien können in den Einstellungen gesetzt werden. Dabei gibt es folgende Einstellungsmöglichkeiten:
|
||||||
|
- Bezeichnung
|
||||||
|
- Farbe
|
||||||
|
- Standard Kalender Auslieferung
|
||||||
|
- Passphrase
|
||||||
|
|
||||||
|
Die Standard Kalender Auslieferung gibt immer Termine dieser Art in der Öffentlichen Ansicht, als auch in der externen Kalendern an, sofern der Link nicht weiter spezifiziert ist.
|
||||||
|
|
||||||
|
Die Passphrase kann zugangsbeschränkend für Termintypen angewandt werden.
|
||||||
|
|
||||||
|
## öffentlicher Kalender
|
||||||
|
|
||||||
|
Der öffentliche Kalender ist unter dem Pfad `/public/calendar` erreichbar. Dieser Kalender zeigt immer alle Termine an, welche einem Typ mit Standard-Auslieferung zugewiesen sind.
|
||||||
|
|
||||||
|
## WebCal
|
||||||
|
|
||||||
|
Der Kalender kann auch in den Kalender von zum Beispiel Google, Apple und co eingebunden werden, damit die Termine auch direkt im Kalender des Smartphones oder Outlook... verfügbar sind.
|
||||||
|
Hierfür kann der Link konfiguriert werden. Der Link kann dann so eingestellt werden, dass passwort-geschützte oder nicht standard Typen ausgeliefert werden. Zusätzlich können unter diesen Links auch die Standard-Typen hinzugefügt werden.
|
||||||
|
|
||||||
|
Genutzt werden kann das zum Beispiel, dass Vorstands-Interne Termine oder Mitglieds-Spezifische Termine über einen personalisierten Link ausgegeben werden können.
|
||||||
|
|
||||||
|
Wird der erstellte Link in einem Browser geöffnet, sollte automatisch eine ICS-Datei mit den Terminen heruntergeladen werden, oder die Möglichkeit vorgeschlagen werden, den Link in einen Kalender zu integrieren.
|
|
@ -1 +1,25 @@
|
||||||
# FF Admin
|
# FF Admin
|
||||||
|
|
||||||
|
## FF Admin ist eine Verwaltungsoberfläche für die Feuerwehr:
|
||||||
|
|
||||||
|
FF Admin bietet folgende Module:
|
||||||
|
- Mitgliederverwaltung
|
||||||
|
- Kalender
|
||||||
|
- Newsletter-Versand
|
||||||
|
- Protokolle
|
||||||
|
- Datenabfrage
|
||||||
|
- Templating-Engine
|
||||||
|
- Benutzerverwaltung
|
||||||
|
- Rollenverwaltung
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
-------
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Struktur
|
||||||
|
|
||||||
|
FF Admin ist in Verein, Wehr, Einstellungen und Nutzerverwaltung getrennt.
|
||||||
|
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.
|
||||||
|
|
51
docs/member.md
Normal file
51
docs/member.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Mitgliederverwaltung
|
||||||
|
|
||||||
|
Die Mitgliederverwaltung bietet eine Startansicht, in welcher alle Mitglieder durch Pagination angezeigt werden. Die Suche ermöglicht eine Full-Text-Suche nach Vor- und Nachnamen.
|
||||||
|
|
||||||
|
Ist ein Mitglied ausgewählt, lassen sich innerhalb dessen alle Daten zu einem Mitglied verwalten.
|
||||||
|
- Allgemeine Daten des Mitglieds
|
||||||
|
- Mitgliedschaft
|
||||||
|
- Adressen bzw. Kommunikationswege
|
||||||
|
- Auszeichnungen
|
||||||
|
- Qualifikationen
|
||||||
|
- Vereinsämter
|
||||||
|
|
||||||
|
Jedes dieser Verwaltungsmöglichkeiten benötigt vorher eingestellte Werte, welche dann einem Mitglied hinzugefügt werden können.
|
||||||
|
|
||||||
|
## Allgemeine Daten des Mitglieds
|
||||||
|
|
||||||
|
Die allgemeinen Daten des Mitglieds umfassen die interne Id, Anrede, Vorname, Nachname, Nameaffix und das Geburtsdatum. Diese Daten können über den Stift oben rechts im Eck geändert werden.
|
||||||
|
|
||||||
|
Weiterhin zeigt die Übersicht des Mitglieds auch Informationen zu den Einträgen der übrigen Kategorien.
|
||||||
|
|
||||||
|
## Mitgliedschaft
|
||||||
|
|
||||||
|
Die auswählbaren Mitgliedsarten können in den Einstellungen gesetzt werden.
|
||||||
|
|
||||||
|
Im Mitglied können dann Zeiträume einer bestimmten Mitgliedschafts-Art angelegt werden. Wird ein neuer Zeitraum hinzugefügt, wird ein aktuell laufender Zeitraum mit dem Vortag des neuen Startdatums beendet.
|
||||||
|
Weiterhin kann bei manuellem setzen des Enddatums ein Grund angegeben werden.
|
||||||
|
|
||||||
|
## Adressen bzw. Kommunikationswege
|
||||||
|
|
||||||
|
Die auswählbaren Kommunikationsarten können in den Einstellungen erstellt werden. Hierfür muss zu jeder Kommunikationsart ausgewählt werden, welche Felder ausgefüllt werden sollen.
|
||||||
|
|
||||||
|
Im Miglied kann dann bei jedem kommunikationstyp gesetzt werden, ob dieser bevorzugt wird, und ob der Newsletter dorthin versandt werden soll.
|
||||||
|
Ist eine Telefonnummer in der Auswahl enthalten, besteht zusätzlich die Möglichkeit, diesen Kommunikationsweg für den Versand der SMS Alarmierung auszuwählen.
|
||||||
|
|
||||||
|
## Auszeichnungen
|
||||||
|
|
||||||
|
Die auswählbaren Auszeichnungen können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Auszeichnungen mit dem Vergabedatum hinzugefügt werden. Wird eine Annahme verweigert oder Ausgabe verwehrt, kann ein Grund hierfür angegeben werden.
|
||||||
|
|
||||||
|
## Qualifikation
|
||||||
|
|
||||||
|
Die auswählbaren Qualifikationen können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Qualifikationen mit einem Start und Enddatum hinzugefügt werden. Eine Notiz kann auch hinzugefügt werden. Zusätzlich zum Enddatum kann ein Grund für das Ende gesetzt werden.
|
||||||
|
|
||||||
|
## Vereinsämter
|
||||||
|
|
||||||
|
Die auswählbaren Vereinsämter können in den Einstellungen erstellt werden.
|
||||||
|
|
||||||
|
Im Mitglied können Qualifikationen mit einem Start und Enddatum hinzugefügt werden. Eine Notiz kann auch hinzugefügt werden.
|
143
docs/newsletter.md
Normal file
143
docs/newsletter.md
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
# Newsletter
|
||||||
|
|
||||||
|
Das Newsletter erlaubt den Druck und Versand von Inhalten zum Verein. Zu einem Newsletter können öffentliche Kalendereinträge hinzugefügt werden.
|
||||||
|
|
||||||
|
## Newsletter erstellen
|
||||||
|
|
||||||
|
Ein Newsletter besteht aus Titel und Zusammenfassung, um einen schnelleren Überblick in der Pagination zu erhalten.
|
||||||
|
|
||||||
|
Im Newsletter können Überschrift, Einleitung/Text und Signatur hinzugefügt werden.
|
||||||
|
Es können Daten ausgewählt werden, welche dann automatisch nach dem Text und vor der Signatur im Standard-Template angezeigt.
|
||||||
|
Und es können Empfänger über eine Vordefinierte Datenabfrage oder manuelles hinzufügen festgelegt werden.
|
||||||
|
Im Tab Druck und Versand wird eine Datei mit allen Hinzugefügten Kalendereinträgen angezeigt, wie auch alle erstellten pdfs wie auch eine pdf, die alle anderen pdfs enthält.
|
||||||
|
|
||||||
|
## Versand
|
||||||
|
In den Einstellungen kann festgelegt werden, welcher Kommunikationstyp wie versandt werden soll. Dies wird zusätzlich nochmals vor dem finalen Versand geprüft.
|
||||||
|
|
||||||
|
Es wird beim Druck unterschieden in Ausgaben mit und ohne Adresse. In der Fußzeile wird dann entweder nur der Name oder auch mit Adresse gedruckt.
|
||||||
|
|
||||||
|
Die Auswahl des Typs Mail versendet nur den Hauptteil des pdfs mit einer ics-Datei im Anhang.
|
||||||
|
|
||||||
|
## Template
|
||||||
|
Über die Templating-Engine können für den Newsletter abweichende Kopf- und Fußzeilen und ein abweichender Hauptteil festgelegt werden.
|
||||||
|
|
||||||
|
Ein Newsletter-Template erhält folgende Daten:
|
||||||
|
``` ts
|
||||||
|
// interface:
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
newsletterTitle: string;
|
||||||
|
newsletterText: string;
|
||||||
|
newsletterSignatur: string;
|
||||||
|
dates: Array<
|
||||||
|
{
|
||||||
|
title: string; // enthält alternativen Titel bzw. Titel des Kalendereintrags
|
||||||
|
content: string; // enthält alternative Beschreibung bzw. Beschreibung des Kalendereintrags
|
||||||
|
starttime: string;
|
||||||
|
endtime: string;
|
||||||
|
location: string;
|
||||||
|
formattedStarttime: string;
|
||||||
|
formattedFullStarttime: string;
|
||||||
|
formattedEndtime: string;
|
||||||
|
formattedFullEndtime: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
recipient: {
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
salutation: Salutation; // (sir | madam | divers | none)
|
||||||
|
nameaffix: string;
|
||||||
|
street: string;
|
||||||
|
streetNumber: string;
|
||||||
|
streetNumberAdd: string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// beispieldaten
|
||||||
|
|
||||||
|
{
|
||||||
|
title: "Beispiel Newsletter Daten",
|
||||||
|
description: "Zusammenfassung der Demodaten.",
|
||||||
|
newsletterTitle: "<h1>Sehr geehrtes Feuerwehrmitglied</h1>",
|
||||||
|
newsletterText: "<p>zu folgenden Terminen möchten wir recht herzlich zur Teilnahme einladen:</p>",
|
||||||
|
newsletterSignatur: "<p>Mit freundlichen Grüßen</p><p>...</p>",
|
||||||
|
dates: [
|
||||||
|
{
|
||||||
|
title: "Termin 1",
|
||||||
|
content: "<p>Beschreibung eines Termins</p>",
|
||||||
|
starttime: new Date(),
|
||||||
|
formattedStarttime: "Montag 20. Januar",
|
||||||
|
formattedFullStarttime: "Montag 20. Januar um 19:00",
|
||||||
|
endtime: new Date(),
|
||||||
|
formattedEndtime: "Montag 20. Januar",
|
||||||
|
formattedFullEndtime: "Montag 20. Januar um 21:00",
|
||||||
|
location: "Feuerwehrhaus",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recipient: {
|
||||||
|
firstname: "Julian",
|
||||||
|
lastname: "Krauser",
|
||||||
|
salutation: "sir",
|
||||||
|
nameaffix: "",
|
||||||
|
street: "Straße",
|
||||||
|
streetNumber: "Hausnummer",
|
||||||
|
streetNumberAdd: "Adresszusatz",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Template ist als HTML definiert und beinhaltet Platzhalter, welche durch `handlebarsjs` ausgetauscht werden.
|
||||||
|
``` html
|
||||||
|
<!-- Standard-Template -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Newsletter</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{{newsletterTitle}}}</h1>
|
||||||
|
<p>{{{newsletterText}}}</p>
|
||||||
|
<br />
|
||||||
|
{{#each dates}}
|
||||||
|
<div>
|
||||||
|
<h2><b>{{this.formattedStarttime}}: {{this.title}}</b></h2>
|
||||||
|
<span>{{{this.content}}}</span>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
{{/each}}
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<p>{{{newsletterSignatur}}}</p>
|
||||||
|
</body>
|
||||||
|
<style>
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p,
|
||||||
|
span,
|
||||||
|
ul,
|
||||||
|
li {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
color: #990b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<!--Footer Template-->
|
||||||
|
<div style="font-size: 10pt; width: 100%; margin: 0 20px; padding-top: 5px; color: #888; border-top: 0.5px solid black">
|
||||||
|
{{recipient.lastname}}, {{recipient.firstname}}{{#if recipient.street}},{{/if}} {{recipient.street}}
|
||||||
|
{{recipient.streetNumber}} {{recipient.streetNumberAdd}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
```
|
1
docs/protocol.md
Normal file
1
docs/protocol.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Protokoll
|
1
docs/query.md
Normal file
1
docs/query.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Query Builder & Query Store
|
1
docs/role.md
Normal file
1
docs/role.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Rollenverwaltung
|
1
docs/templating.md
Normal file
1
docs/templating.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Templating Engine
|
1
docs/user.md
Normal file
1
docs/user.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# Benutzerverwaltung
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<footer
|
<footer
|
||||||
v-if="authCheck && (routeName.includes('admin-') || routeName.includes('account-'))"
|
v-if="authCheck && (routeName.includes('admin-') || routeName.includes('account-') || routeName.includes('docs-'))"
|
||||||
class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
|
class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
|
||||||
>
|
>
|
||||||
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
:disableSubLink="true"
|
:disableSubLink="true"
|
||||||
/>
|
/>
|
||||||
<TopLevelLink
|
<TopLevelLink
|
||||||
v-else-if="routeName == 'account' || routeName.includes('account-')"
|
v-else-if="routeName == 'account' || routeName.includes('account-') || routeName == 'docs' || routeName.includes('docs-')"
|
||||||
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||||
:disableSubLink="true"
|
:disableSubLink="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:link="item"
|
:link="item"
|
||||||
/>
|
/>
|
||||||
<TopLevelLink
|
<TopLevelLink
|
||||||
v-else-if="routeName == 'account' || routeName.includes('account-')"
|
v-else-if="routeName == 'account' || routeName.includes('account-') || routeName == 'docs' || routeName.includes('docs-')"
|
||||||
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
:link="{ key: 'club', title: 'Zur Verwaltung', levelDefault: '' }"
|
||||||
:disable-sub-link="true"
|
:disable-sub-link="true"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="fixed right-0 flex flex-col gap-4 p-2 w-full md:w-80"
|
class="fixed right-0 flex flex-col gap-4 p-2 w-full md:w-80 z-50"
|
||||||
:class="position == 'bottom' ? 'bottom-0' : 'top-0'"
|
:class="position == 'bottom' ? 'bottom-0' : 'top-0'"
|
||||||
>
|
>
|
||||||
<TransitionGroup
|
<TransitionGroup
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
leave-to-class="transform scale-95 opacity-0"
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
>
|
>
|
||||||
<MenuItems
|
<MenuItems
|
||||||
class="absolute right-0 mt-2 w-56 z-10 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
class="absolute right-0 mt-2 w-56 z-20 origin-top-right divide-y divide-gray-100 rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1 pt-2">
|
<div class="px-3 py-1 pt-2">
|
||||||
<p class="text-xs">Angemeldet als</p>
|
<p class="text-xs">Angemeldet als</p>
|
||||||
|
@ -25,6 +25,11 @@
|
||||||
<button button primary @click="close">Mein Account</button>
|
<button button primary @click="close">Mein Account</button>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem v-slot="{ close }">
|
||||||
|
<RouterLink to="/docs">
|
||||||
|
<button button primary @click="close">Dokumentation</button>
|
||||||
|
</RouterLink>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<span>
|
<span>
|
||||||
<button primary-outline @click="logoutAccount">ausloggen</button>
|
<button primary-outline @click="logoutAccount">ausloggen</button>
|
||||||
|
|
60
src/components/admin/club/member/MemberNameListModal.vue
Normal file
60
src/components/admin/club/member/MemberNameListModal.vue
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col gap-2">
|
||||||
|
<Spinner v-if="status == 'loading'" />
|
||||||
|
<div class="grow">
|
||||||
|
<iframe ref="viewer" class="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-2 justify-end">
|
||||||
|
<a ref="download" button primary class="!w-fit">download</a>
|
||||||
|
<button primary-outline class="!w-fit" @click="closeModal">schließen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
import { useMemberStore } from "@/stores/admin/club/member/member";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useModalStore, ["data"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchItem();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useMemberStore, ["printMemberList"]),
|
||||||
|
fetchItem() {
|
||||||
|
this.status = "loading";
|
||||||
|
this.printMemberList()
|
||||||
|
.then((response) => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
const blob = new Blob([response.data], { type: "application/pdf" });
|
||||||
|
(this.$refs.viewer as HTMLIFrameElement).src = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const fileLink = (this.$refs.download as HTMLAnchorElement)
|
||||||
|
fileLink.href = fileURL;
|
||||||
|
fileLink.setAttribute("download", "Mitgliederliste.pdf");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -45,7 +45,6 @@ const router = createRouter({
|
||||||
path: "/reset",
|
path: "/reset",
|
||||||
name: "reset",
|
name: "reset",
|
||||||
component: () => import("@/views/RouterView.vue"),
|
component: () => import("@/views/RouterView.vue"),
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
|
@ -655,6 +654,7 @@ const router = createRouter({
|
||||||
path: "/docs",
|
path: "/docs",
|
||||||
name: "docs",
|
name: "docs",
|
||||||
component: () => import("@/views/docs/View.vue"),
|
component: () => import("@/views/docs/View.vue"),
|
||||||
|
beforeEnter: [isAuthenticated],
|
||||||
props: true,
|
props: true,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -84,5 +84,10 @@ export const useMemberStore = defineStore("member", {
|
||||||
this.fetchMembers();
|
this.fetchMembers();
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
async printMemberList(){
|
||||||
|
return http.get(`/admin/member/print/namelist`, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { NewsletterDatesViewModel, SyncNewsletterDatesViewModel } from "@/viewmodels/admin/club/newsletter/newsletterDates.models";
|
import type {
|
||||||
|
NewsletterDatesViewModel,
|
||||||
|
SyncNewsletterDatesViewModel,
|
||||||
|
} from "@/viewmodels/admin/club/newsletter/newsletterDates.models";
|
||||||
import { useNewsletterStore } from "./newsletter";
|
import { useNewsletterStore } from "./newsletter";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
|
@ -9,6 +12,7 @@ import differenceWith from "lodash.differencewith";
|
||||||
export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
initialized: false as boolean,
|
||||||
dates: [] as Array<NewsletterDatesViewModel>,
|
dates: [] as Array<NewsletterDatesViewModel>,
|
||||||
origin: [] as Array<NewsletterDatesViewModel>,
|
origin: [] as Array<NewsletterDatesViewModel>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
@ -38,6 +42,7 @@ export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data;
|
this.origin = result.data;
|
||||||
this.dates = cloneDeep(this.origin);
|
this.dates = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -49,6 +54,8 @@ export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||||
return http.get(`/admin/newsletter/${newsletterId}/dates`);
|
return http.get(`/admin/newsletter/${newsletterId}/dates`);
|
||||||
},
|
},
|
||||||
async synchronizeActiveNewsletterDates() {
|
async synchronizeActiveNewsletterDates() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingNewsletterDates = "syncing";
|
this.syncingNewsletterDates = "syncing";
|
||||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import isEqual from "lodash.isequal";
|
||||||
export const useNewsletterRecipientsStore = defineStore("newsletterRecipients", {
|
export const useNewsletterRecipientsStore = defineStore("newsletterRecipients", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
initialized: false as boolean,
|
||||||
recipients: [] as Array<number>,
|
recipients: [] as Array<number>,
|
||||||
origin: [] as Array<number>,
|
origin: [] as Array<number>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
@ -31,6 +32,7 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data.map((d: NewsletterRecipientsViewModel) => d.memberId);
|
this.origin = result.data.map((d: NewsletterRecipientsViewModel) => d.memberId);
|
||||||
this.recipients = cloneDeep(this.origin);
|
this.recipients = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -42,8 +44,11 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
||||||
return http.get(`/admin/newsletter/${newsletterId}/recipients`);
|
return http.get(`/admin/newsletter/${newsletterId}/recipients`);
|
||||||
},
|
},
|
||||||
async synchronizeActiveNewsletterRecipients() {
|
async synchronizeActiveNewsletterRecipients() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingNewsletterRecipients = "syncing";
|
this.syncingNewsletterRecipients = "syncing";
|
||||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
|
|
||||||
await http
|
await http
|
||||||
.patch(`/admin/newsletter/${newsletterId}/synchronize/recipients`, {
|
.patch(`/admin/newsletter/${newsletterId}/synchronize/recipients`, {
|
||||||
recipients: this.recipients,
|
recipients: this.recipients,
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { ProtocolAgendaViewModel, SyncProtocolAgendaViewModel } from "@/viewmodels/admin/club/protocol/protocolAgenda.models";
|
import type {
|
||||||
|
ProtocolAgendaViewModel,
|
||||||
|
SyncProtocolAgendaViewModel,
|
||||||
|
} from "@/viewmodels/admin/club/protocol/protocolAgenda.models";
|
||||||
import { useProtocolStore } from "./protocol";
|
import { useProtocolStore } from "./protocol";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
|
@ -9,6 +12,7 @@ import differenceWith from "lodash.differencewith";
|
||||||
export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
initialized: false as boolean,
|
||||||
agenda: [] as Array<ProtocolAgendaViewModel>,
|
agenda: [] as Array<ProtocolAgendaViewModel>,
|
||||||
origin: [] as Array<ProtocolAgendaViewModel>,
|
origin: [] as Array<ProtocolAgendaViewModel>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
@ -29,6 +33,7 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data;
|
this.origin = result.data;
|
||||||
this.agenda = cloneDeep(this.origin);
|
this.agenda = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -55,6 +60,8 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
},
|
},
|
||||||
async synchronizeActiveProtocolAgenda() {
|
async synchronizeActiveProtocolAgenda() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingProtocolAgenda = "syncing";
|
this.syncingProtocolAgenda = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import differenceWith from "lodash.differencewith";
|
||||||
export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
initialized: false as boolean,
|
||||||
decision: [] as Array<ProtocolDecisionViewModel>,
|
decision: [] as Array<ProtocolDecisionViewModel>,
|
||||||
origin: [] as Array<ProtocolDecisionViewModel>,
|
origin: [] as Array<ProtocolDecisionViewModel>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
@ -33,9 +34,11 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data;
|
this.origin = result.data;
|
||||||
this.decision = cloneDeep(this.origin);
|
this.decision = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -59,6 +62,8 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
},
|
},
|
||||||
async synchronizeActiveProtocolDecision() {
|
async synchronizeActiveProtocolDecision() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingProtocolDecision = "syncing";
|
this.syncingProtocolDecision = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
|
|
@ -12,15 +12,19 @@ import isEqual from "lodash.isequal";
|
||||||
export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
presence: [] as Array<number>,
|
initialized: false as boolean,
|
||||||
origin: [] as Array<number>,
|
presence: [] as Array<ProtocolPresenceViewModel>,
|
||||||
|
origin: [] as Array<ProtocolPresenceViewModel>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
syncingProtocolPresence: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
syncingProtocolPresence: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
detectedChangeProtocolPresence: (state) =>
|
detectedChangeProtocolPresence: (state) =>
|
||||||
!isEqual(state.origin, state.presence) && state.syncingProtocolPresence != "syncing",
|
!isEqual(
|
||||||
|
state.origin.sort((a: ProtocolPresenceViewModel, b: ProtocolPresenceViewModel) => a.memberId - b.memberId),
|
||||||
|
state.presence.sort((a: ProtocolPresenceViewModel, b: ProtocolPresenceViewModel) => a.memberId - b.memberId)
|
||||||
|
) && state.syncingProtocolPresence != "syncing",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setProtocolPresenceSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
setProtocolPresenceSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
@ -30,8 +34,9 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||||
this.loading = "loading";
|
this.loading = "loading";
|
||||||
this.fetchProtocolPresencePromise()
|
this.fetchProtocolPresencePromise()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
this.origin = result.data;
|
||||||
this.presence = cloneDeep(this.origin);
|
this.presence = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -43,8 +48,11 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||||
return http.get(`/admin/protocol/${protocolId}/presence`);
|
return http.get(`/admin/protocol/${protocolId}/presence`);
|
||||||
},
|
},
|
||||||
async synchronizeActiveProtocolPresence() {
|
async synchronizeActiveProtocolPresence() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingProtocolPresence = "syncing";
|
this.syncingProtocolPresence = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
await http
|
await http
|
||||||
.put(`/admin/protocol/${protocolId}/synchronize/presence`, {
|
.put(`/admin/protocol/${protocolId}/synchronize/presence`, {
|
||||||
presence: this.presence,
|
presence: this.presence,
|
||||||
|
@ -57,7 +65,7 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||||
});
|
});
|
||||||
this.fetchProtocolPresencePromise()
|
this.fetchProtocolPresencePromise()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
this.origin = result.data;
|
||||||
if (this.detectedChangeProtocolPresence) this.syncingProtocolPresence = "detectedChanges";
|
if (this.detectedChangeProtocolPresence) this.syncingProtocolPresence = "detectedChanges";
|
||||||
})
|
})
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
|
|
|
@ -19,7 +19,7 @@ export const useProtocolPrintoutStore = defineStore("protocolPrintout", {
|
||||||
http
|
http
|
||||||
.get(`/admin/protocol/${protocolId}/printouts`)
|
.get(`/admin/protocol/${protocolId}/printouts`)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.printout = result.data;
|
this.printout = result.data.reverse();
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import type { ProtocolVotingViewModel, SyncProtocolVotingViewModel } from "@/viewmodels/admin/club/protocol/protocolVoting.models";
|
import type {
|
||||||
|
ProtocolVotingViewModel,
|
||||||
|
SyncProtocolVotingViewModel,
|
||||||
|
} from "@/viewmodels/admin/club/protocol/protocolVoting.models";
|
||||||
import { useProtocolStore } from "./protocol";
|
import { useProtocolStore } from "./protocol";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
|
@ -10,6 +13,7 @@ import differenceWith from "lodash.differencewith";
|
||||||
export const useProtocolVotingStore = defineStore("protocolVoting", {
|
export const useProtocolVotingStore = defineStore("protocolVoting", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
initialized: false as boolean,
|
||||||
voting: [] as Array<ProtocolVotingViewModel>,
|
voting: [] as Array<ProtocolVotingViewModel>,
|
||||||
origin: [] as Array<ProtocolVotingViewModel>,
|
origin: [] as Array<ProtocolVotingViewModel>,
|
||||||
loading: "loading" as "loading" | "fetched" | "failed",
|
loading: "loading" as "loading" | "fetched" | "failed",
|
||||||
|
@ -30,6 +34,7 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.origin = result.data;
|
this.origin = result.data;
|
||||||
this.voting = cloneDeep(this.origin);
|
this.voting = cloneDeep(this.origin);
|
||||||
|
this.initialized = true;
|
||||||
this.loading = "fetched";
|
this.loading = "fetched";
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
@ -59,6 +64,8 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
|
||||||
.catch((err) => {});
|
.catch((err) => {});
|
||||||
},
|
},
|
||||||
async synchronizeActiveProtocolVoting() {
|
async synchronizeActiveProtocolVoting() {
|
||||||
|
if (!this.initialized) return;
|
||||||
|
|
||||||
this.syncingProtocolVoting = "syncing";
|
this.syncingProtocolVoting = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const protocolId = useProtocolStore().activeProtocol;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import type { MemberViewModel } from "../member/member.models";
|
|
||||||
|
|
||||||
export interface ProtocolPresenceViewModel {
|
export interface ProtocolPresenceViewModel {
|
||||||
memberId: number;
|
memberId: number;
|
||||||
member: MemberViewModel;
|
absent: boolean;
|
||||||
protocolId: number;
|
protocolId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
<template #topBar>
|
<template #topBar>
|
||||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||||
<h1 class="font-bold text-xl h-8">Mitglieder</h1>
|
<h1 class="font-bold text-xl h-8">Mitglieder</h1>
|
||||||
|
<div title="Mitgliederliste drucken" @click="openPrintModal">
|
||||||
|
<DocumentTextIcon class="w-5 h-5 cursor-pointer" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #diffMain>
|
<template #diffMain>
|
||||||
|
@ -40,6 +43,7 @@ import { useModalStore } from "@/stores/modal";
|
||||||
import Pagination from "@/components/Pagination.vue";
|
import Pagination from "@/components/Pagination.vue";
|
||||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { DocumentTextIcon, PencilIcon } from "@heroicons/vue/24/outline";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -65,6 +69,11 @@ export default defineComponent({
|
||||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/CreateMemberModal.vue")))
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/CreateMemberModal.vue")))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
openPrintModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberNameListModal.vue")))
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
<div v-if="activeMemberObj != null" class="flex flex-col gap-2 w-full">
|
<div v-if="activeMemberObj != null" class="flex flex-col gap-2 w-full">
|
||||||
|
<div>
|
||||||
|
<label for="id">Interne Id</label>
|
||||||
|
<input type="text" id="id" :value="activeMemberObj.internalId" readonly />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="salutation">Anrede</label>
|
<label for="salutation">Anrede</label>
|
||||||
<input type="text" id="salutation" :value="activeMemberObj.salutation" readonly />
|
<input type="text" id="salutation" :value="activeMemberObj.salutation" readonly />
|
||||||
|
|
|
@ -140,7 +140,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchNewsletterDates();
|
// this.fetchNewsletterDates();
|
||||||
this.fetchCalendars();
|
this.fetchCalendars();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchNewsletterByActiveId();
|
// this.fetchNewsletterByActiveId();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useNewsletterStore, ["fetchNewsletterByActiveId"]),
|
...mapActions(useNewsletterStore, ["fetchNewsletterByActiveId"]),
|
||||||
|
|
|
@ -205,8 +205,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchMembers(0, 1000, true);
|
this.fetchMembers(0, 1000, "", true);
|
||||||
this.fetchNewsletterRecipients();
|
// this.fetchNewsletterRecipients();
|
||||||
this.fetchQueries();
|
this.fetchQueries();
|
||||||
this.loadQuery();
|
this.loadQuery();
|
||||||
},
|
},
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default defineComponent({
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolAgenda();
|
// this.fetchProtocolAgenda();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
|
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
|
||||||
|
|
|
@ -73,7 +73,7 @@ export default defineComponent({
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolDecision();
|
// this.fetchProtocolDecision();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
|
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default defineComponent({
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolByActiveId();
|
// this.fetchProtocolByActiveId();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Combobox v-model="presence" :disabled="!can('create', 'club', 'protocol')" multiple>
|
<Combobox v-model="presence" :disabled="!can('create', 'club', 'protocol')" multiple by="memberId">
|
||||||
<ComboboxLabel>Anwesende suchen</ComboboxLabel>
|
<ComboboxLabel>Anwesende suchen</ComboboxLabel>
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<ComboboxInput
|
<ComboboxInput
|
||||||
|
@ -34,8 +34,8 @@
|
||||||
<ComboboxOption
|
<ComboboxOption
|
||||||
v-for="member in filtered"
|
v-for="member in filtered"
|
||||||
as="template"
|
as="template"
|
||||||
:key="member.id"
|
:key="member.memberId"
|
||||||
:value="member.id"
|
:value="member"
|
||||||
v-slot="{ selected, active }"
|
v-slot="{ selected, active }"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||||
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
{{ getMember(member.memberId)?.firstname }} {{ getMember(member.memberId)?.lastname }} {{ getMember(member.memberId)?.nameaffix }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="selected"
|
v-if="selected"
|
||||||
|
@ -63,18 +63,24 @@
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<p>Ausgewählte Anwesende</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">
|
||||||
<div
|
<div
|
||||||
v-for="member in selected"
|
v-for="member in presence"
|
||||||
:key="member.id"
|
:key="member.memberId"
|
||||||
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"
|
||||||
>
|
>
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
<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>
|
||||||
|
<label class="flex flex-row gap-2 items-center">
|
||||||
|
<input type="checkbox" v-model="member.absent" />
|
||||||
|
war abwesend
|
||||||
|
</label>
|
||||||
|
</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"
|
||||||
@click="removeSelected(member.id)"
|
@click="removeSelected(member.memberId)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -117,38 +123,31 @@ export default defineComponent({
|
||||||
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
||||||
...mapState(useMemberStore, ["members"]),
|
...mapState(useMemberStore, ["members"]),
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
filtered(): Array<MemberViewModel> {
|
filtered(): Array<{memberId:number, absent:boolean; protocolId:number}> {
|
||||||
return this.query === ""
|
return (this.query === ""
|
||||||
? this.members
|
? this.members
|
||||||
: this.members.filter((member) =>
|
: this.members.filter((member) =>
|
||||||
(member.firstname + " " + member.lastname)
|
(member.firstname + " " + member.lastname)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s+/g, "")
|
.replace(/\s+/g, "")
|
||||||
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
||||||
);
|
)).map(m =>({memberId: m.id, absent:false, protocolId:parseInt(this.protocolId ?? "")}));
|
||||||
},
|
|
||||||
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> {
|
|
||||||
return this.members.filter((m) => this.presence.includes(m.id));
|
|
||||||
},
|
},
|
||||||
|
getMember(){
|
||||||
|
return (memberId:number) => {
|
||||||
|
return this.members.find(m => memberId == m.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchMembers(0, 1000, true);
|
this.fetchMembers(0, 1000, "", true);
|
||||||
this.fetchProtocolPresence();
|
// this.fetchProtocolPresence();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||||
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||||
removeSelected(id: number) {
|
removeSelected(id: number) {
|
||||||
let index = this.presence.findIndex((s) => s == id);
|
let index = this.presence.findIndex((s) => s.memberId == id);
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
this.presence.splice(index, 1);
|
this.presence.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ import { useProtocolStore } from "@/stores/admin/club/protocol/protocol";
|
||||||
import { useModalStore } from "@/stores/modal";
|
import { useModalStore } from "@/stores/modal";
|
||||||
import ProtocolSyncing from "@/components/admin/club/protocol/ProtocolSyncing.vue";
|
import ProtocolSyncing from "@/components/admin/club/protocol/ProtocolSyncing.vue";
|
||||||
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda";
|
||||||
|
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
|
||||||
|
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
|
||||||
|
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -88,6 +92,10 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolByActiveId();
|
this.fetchProtocolByActiveId();
|
||||||
|
this.fetchProtocolAgenda()
|
||||||
|
this.fetchProtocolDecision()
|
||||||
|
this.fetchProtocolPresence()
|
||||||
|
this.fetchProtocolVoting()
|
||||||
},
|
},
|
||||||
// this.syncState is undefined, so it will never work
|
// this.syncState is undefined, so it will never work
|
||||||
// beforeRouteLeave(to, from, next) {
|
// beforeRouteLeave(to, from, next) {
|
||||||
|
@ -108,6 +116,10 @@ export default defineComponent({
|
||||||
// },
|
// },
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||||
|
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda"]),
|
||||||
|
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision"]),
|
||||||
|
...mapActions(useProtocolPresenceStore,["fetchProtocolPresence"]),
|
||||||
|
...mapActions(useProtocolVotingStore,["fetchProtocolVoting"]),
|
||||||
...mapActions(useModalStore, ["openModal"]),
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
openInfoModal() {
|
openInfoModal() {
|
||||||
this.openModal(
|
this.openModal(
|
||||||
|
|
|
@ -90,7 +90,7 @@ export default defineComponent({
|
||||||
...mapState(useAbilityStore, ["can"]),
|
...mapState(useAbilityStore, ["can"]),
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchProtocolVoting();
|
// this.fetchProtocolVoting();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
|
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<MainTemplate :useStagedOverviewLink="false">
|
<MainTemplate :useStagedOverviewLink="false">
|
||||||
<template #topBar>
|
<!-- <template #topBar>
|
||||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||||
<h1 class="font-bold text-xl h-8">{{page}}</h1>
|
<h1 class="font-bold text-xl h-8">{{page}}</h1>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template> -->
|
||||||
<template #main>
|
<template #diffMain>
|
||||||
<div class="markdown-container">
|
<div class="flex flex-col gap-2 h-full px-4 overflow-hidden">
|
||||||
<component v-if="markdownComponent" :is="markdownComponent" />
|
<div class="markdown-container overflow-y-scroll">
|
||||||
<p v-else>Diese Seite existiert nicht.</p>
|
<component v-if="markdownComponent" :is="markdownComponent" />
|
||||||
|
<p v-else>Diese Seite existiert nicht.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</MainTemplate>
|
</MainTemplate>
|
||||||
|
@ -40,8 +42,46 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
loadPage(){
|
loadPage(){
|
||||||
|
this.markdownComponent = null
|
||||||
this.markdownComponent = markRaw(defineAsyncComponent(() => import(`$/${this.page?.toLowerCase()}.md`)));
|
this.markdownComponent = markRaw(defineAsyncComponent(() => import(`$/${this.page?.toLowerCase()}.md`)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.markdown-container {
|
||||||
|
font-family: 'Open Sans', sans-serif;
|
||||||
|
color: #555;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 2px solid gray;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
height: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container h1 {
|
||||||
|
font-size: 2rem !important;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container h2 {
|
||||||
|
font-size: 1.25rem !important;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container hr {
|
||||||
|
background-color: #222;
|
||||||
|
margin: 8px 0;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container p {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-container ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,6 +4,14 @@
|
||||||
<SidebarTemplate mainTitle="Dokumentation">
|
<SidebarTemplate mainTitle="Dokumentation">
|
||||||
<template #list>
|
<template #list>
|
||||||
<RoutingLink title="FF Admin" :link="{ name: 'docs-page', params: { page: 'ff-admin' } }" :active="page == 'ff-admin'" />
|
<RoutingLink title="FF Admin" :link="{ name: 'docs-page', params: { page: 'ff-admin' } }" :active="page == 'ff-admin'" />
|
||||||
|
<RoutingLink title="Mitgliederverwaltung" :link="{ name: 'docs-page', params: { page: 'member' } }" :active="page == 'member'" />
|
||||||
|
<RoutingLink title="Kalendar" :link="{ name: 'docs-page', params: { page: 'calendar' } }" :active="page == 'calendar'" />
|
||||||
|
<RoutingLink title="Newsletter-Versand" :link="{ name: 'docs-page', params: { page: 'newsletter' } }" :active="page == 'newsletter'" />
|
||||||
|
<RoutingLink title="Protokolle" :link="{ name: 'docs-page', params: { page: 'protocol' } }" :active="page == 'protocol'" />
|
||||||
|
<RoutingLink title="Datenabfrage" :link="{ name: 'docs-page', params: { page: 'query' } }" :active="page == 'query'" />
|
||||||
|
<RoutingLink title="Templating-Engine" :link="{ name: 'docs-page', params: { page: 'templating' } }" :active="page == 'templating'" />
|
||||||
|
<RoutingLink title="Benutzerverwaltung" :link="{ name: 'docs-page', params: { page: 'user' } }" :active="page == 'user'" />
|
||||||
|
<RoutingLink title="Rollenverwaltung" :link="{ name: 'docs-page', params: { page: 'role' } }" :active="page == 'role'" />
|
||||||
</template>
|
</template>
|
||||||
</SidebarTemplate>
|
</SidebarTemplate>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in a new issue