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 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>
|
||||
<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"
|
||||
>
|
||||
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
||||
|
@ -12,7 +12,7 @@
|
|||
:disableSubLink="true"
|
||||
/>
|
||||
<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: '' }"
|
||||
:disableSubLink="true"
|
||||
/>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:link="item"
|
||||
/>
|
||||
<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: '' }"
|
||||
:disable-sub-link="true"
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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'"
|
||||
>
|
||||
<TransitionGroup
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<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">
|
||||
<p class="text-xs">Angemeldet als</p>
|
||||
|
@ -25,6 +25,11 @@
|
|||
<button button primary @click="close">Mein Account</button>
|
||||
</RouterLink>
|
||||
</MenuItem>
|
||||
<MenuItem v-slot="{ close }">
|
||||
<RouterLink to="/docs">
|
||||
<button button primary @click="close">Dokumentation</button>
|
||||
</RouterLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<span>
|
||||
<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",
|
||||
name: "reset",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
|
@ -655,6 +654,7 @@ const router = createRouter({
|
|||
path: "/docs",
|
||||
name: "docs",
|
||||
component: () => import("@/views/docs/View.vue"),
|
||||
beforeEnter: [isAuthenticated],
|
||||
props: true,
|
||||
children: [
|
||||
{
|
||||
|
|
|
@ -84,5 +84,10 @@ export const useMemberStore = defineStore("member", {
|
|||
this.fetchMembers();
|
||||
return result;
|
||||
},
|
||||
async printMemberList(){
|
||||
return http.get(`/admin/member/print/namelist`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { defineStore } from "pinia";
|
||||
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 cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
@ -9,6 +12,7 @@ import differenceWith from "lodash.differencewith";
|
|||
export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||
state: () => {
|
||||
return {
|
||||
initialized: false as boolean,
|
||||
dates: [] as Array<NewsletterDatesViewModel>,
|
||||
origin: [] as Array<NewsletterDatesViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -38,6 +42,7 @@ export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
|||
.then((result) => {
|
||||
this.origin = result.data;
|
||||
this.dates = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -49,6 +54,8 @@ export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
|||
return http.get(`/admin/newsletter/${newsletterId}/dates`);
|
||||
},
|
||||
async synchronizeActiveNewsletterDates() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingNewsletterDates = "syncing";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import isEqual from "lodash.isequal";
|
|||
export const useNewsletterRecipientsStore = defineStore("newsletterRecipients", {
|
||||
state: () => {
|
||||
return {
|
||||
initialized: false as boolean,
|
||||
recipients: [] as Array<number>,
|
||||
origin: [] as Array<number>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -31,6 +32,7 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
|||
.then((result) => {
|
||||
this.origin = result.data.map((d: NewsletterRecipientsViewModel) => d.memberId);
|
||||
this.recipients = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -42,8 +44,11 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
|||
return http.get(`/admin/newsletter/${newsletterId}/recipients`);
|
||||
},
|
||||
async synchronizeActiveNewsletterRecipients() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingNewsletterRecipients = "syncing";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
|
||||
await http
|
||||
.patch(`/admin/newsletter/${newsletterId}/synchronize/recipients`, {
|
||||
recipients: this.recipients,
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { defineStore } from "pinia";
|
||||
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 cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
@ -9,6 +12,7 @@ import differenceWith from "lodash.differencewith";
|
|||
export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
||||
state: () => {
|
||||
return {
|
||||
initialized: false as boolean,
|
||||
agenda: [] as Array<ProtocolAgendaViewModel>,
|
||||
origin: [] as Array<ProtocolAgendaViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -29,6 +33,7 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
|||
.then((result) => {
|
||||
this.origin = result.data;
|
||||
this.agenda = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -55,6 +60,8 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
|
|||
.catch((err) => {});
|
||||
},
|
||||
async synchronizeActiveProtocolAgenda() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingProtocolAgenda = "syncing";
|
||||
const protocolId = useProtocolStore().activeProtocol;
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import differenceWith from "lodash.differencewith";
|
|||
export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
||||
state: () => {
|
||||
return {
|
||||
initialized: false as boolean,
|
||||
decision: [] as Array<ProtocolDecisionViewModel>,
|
||||
origin: [] as Array<ProtocolDecisionViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -33,9 +34,11 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
|||
.then((result) => {
|
||||
this.origin = result.data;
|
||||
this.decision = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
|
@ -59,6 +62,8 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
|
|||
.catch((err) => {});
|
||||
},
|
||||
async synchronizeActiveProtocolDecision() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingProtocolDecision = "syncing";
|
||||
const protocolId = useProtocolStore().activeProtocol;
|
||||
|
||||
|
|
|
@ -12,15 +12,19 @@ import isEqual from "lodash.isequal";
|
|||
export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
||||
state: () => {
|
||||
return {
|
||||
presence: [] as Array<number>,
|
||||
origin: [] as Array<number>,
|
||||
initialized: false as boolean,
|
||||
presence: [] as Array<ProtocolPresenceViewModel>,
|
||||
origin: [] as Array<ProtocolPresenceViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
syncingProtocolPresence: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
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: {
|
||||
setProtocolPresenceSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||
|
@ -30,8 +34,9 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
|||
this.loading = "loading";
|
||||
this.fetchProtocolPresencePromise()
|
||||
.then((result) => {
|
||||
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
||||
this.origin = result.data;
|
||||
this.presence = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -43,8 +48,11 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
|||
return http.get(`/admin/protocol/${protocolId}/presence`);
|
||||
},
|
||||
async synchronizeActiveProtocolPresence() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingProtocolPresence = "syncing";
|
||||
const protocolId = useProtocolStore().activeProtocol;
|
||||
|
||||
await http
|
||||
.put(`/admin/protocol/${protocolId}/synchronize/presence`, {
|
||||
presence: this.presence,
|
||||
|
@ -57,7 +65,7 @@ export const useProtocolPresenceStore = defineStore("protocolPresence", {
|
|||
});
|
||||
this.fetchProtocolPresencePromise()
|
||||
.then((result) => {
|
||||
this.origin = result.data.map((d: ProtocolPresenceViewModel) => d.memberId);
|
||||
this.origin = result.data;
|
||||
if (this.detectedChangeProtocolPresence) this.syncingProtocolPresence = "detectedChanges";
|
||||
})
|
||||
.catch((err) => {});
|
||||
|
|
|
@ -19,7 +19,7 @@ export const useProtocolPrintoutStore = defineStore("protocolPrintout", {
|
|||
http
|
||||
.get(`/admin/protocol/${protocolId}/printouts`)
|
||||
.then((result) => {
|
||||
this.printout = result.data;
|
||||
this.printout = result.data.reverse();
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
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 cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
@ -10,6 +13,7 @@ import differenceWith from "lodash.differencewith";
|
|||
export const useProtocolVotingStore = defineStore("protocolVoting", {
|
||||
state: () => {
|
||||
return {
|
||||
initialized: false as boolean,
|
||||
voting: [] as Array<ProtocolVotingViewModel>,
|
||||
origin: [] as Array<ProtocolVotingViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -30,6 +34,7 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
|
|||
.then((result) => {
|
||||
this.origin = result.data;
|
||||
this.voting = cloneDeep(this.origin);
|
||||
this.initialized = true;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
@ -59,6 +64,8 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
|
|||
.catch((err) => {});
|
||||
},
|
||||
async synchronizeActiveProtocolVoting() {
|
||||
if (!this.initialized) return;
|
||||
|
||||
this.syncingProtocolVoting = "syncing";
|
||||
const protocolId = useProtocolStore().activeProtocol;
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import type { MemberViewModel } from "../member/member.models";
|
||||
|
||||
export interface ProtocolPresenceViewModel {
|
||||
memberId: number;
|
||||
member: MemberViewModel;
|
||||
absent: boolean;
|
||||
protocolId: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
<template #topBar>
|
||||
<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>
|
||||
<div title="Mitgliederliste drucken" @click="openPrintModal">
|
||||
<DocumentTextIcon class="w-5 h-5 cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
|
@ -40,6 +43,7 @@ import { useModalStore } from "@/stores/modal";
|
|||
import Pagination from "@/components/Pagination.vue";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { DocumentTextIcon, PencilIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -65,6 +69,11 @@ export default defineComponent({
|
|||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/CreateMemberModal.vue")))
|
||||
);
|
||||
},
|
||||
openPrintModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberNameListModal.vue")))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<template>
|
||||
<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>
|
||||
<label for="id">Interne Id</label>
|
||||
<input type="text" id="id" :value="activeMemberObj.internalId" readonly />
|
||||
</div>
|
||||
<div>
|
||||
<label for="salutation">Anrede</label>
|
||||
<input type="text" id="salutation" :value="activeMemberObj.salutation" readonly />
|
||||
|
|
|
@ -140,7 +140,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchNewsletterDates();
|
||||
// this.fetchNewsletterDates();
|
||||
this.fetchCalendars();
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -58,7 +58,7 @@ export default defineComponent({
|
|||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchNewsletterByActiveId();
|
||||
// this.fetchNewsletterByActiveId();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useNewsletterStore, ["fetchNewsletterByActiveId"]),
|
||||
|
|
|
@ -205,8 +205,8 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMembers(0, 1000, true);
|
||||
this.fetchNewsletterRecipients();
|
||||
this.fetchMembers(0, 1000, "", true);
|
||||
// this.fetchNewsletterRecipients();
|
||||
this.fetchQueries();
|
||||
this.loadQuery();
|
||||
},
|
||||
|
|
|
@ -73,7 +73,7 @@ export default defineComponent({
|
|||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolAgenda();
|
||||
// this.fetchProtocolAgenda();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda", "createProtocolAgenda"]),
|
||||
|
|
|
@ -73,7 +73,7 @@ export default defineComponent({
|
|||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolDecision();
|
||||
// this.fetchProtocolDecision();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision", "createProtocolDecision"]),
|
||||
|
|
|
@ -80,7 +80,7 @@ export default defineComponent({
|
|||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolByActiveId();
|
||||
// this.fetchProtocolByActiveId();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</p>
|
||||
|
||||
<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>
|
||||
<div class="relative mt-1">
|
||||
<ComboboxInput
|
||||
|
@ -34,8 +34,8 @@
|
|||
<ComboboxOption
|
||||
v-for="member in filtered"
|
||||
as="template"
|
||||
:key="member.id"
|
||||
:value="member.id"
|
||||
:key="member.memberId"
|
||||
:value="member"
|
||||
v-slot="{ selected, active }"
|
||||
>
|
||||
<li
|
||||
|
@ -46,7 +46,7 @@
|
|||
}"
|
||||
>
|
||||
<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
|
||||
v-if="selected"
|
||||
|
@ -63,18 +63,24 @@
|
|||
</Combobox>
|
||||
</div>
|
||||
<br />
|
||||
<p>Ausgewählte Anwesende</p>
|
||||
<p>Anwesenheit</p>
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in selected"
|
||||
:key="member.id"
|
||||
v-for="member in presence"
|
||||
: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"
|
||||
>
|
||||
<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
|
||||
v-if="can('create', 'club', 'protocol')"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||
@click="removeSelected(member.id)"
|
||||
@click="removeSelected(member.memberId)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -117,38 +123,31 @@ export default defineComponent({
|
|||
...mapWritableState(useProtocolPresenceStore, ["presence", "loading"]),
|
||||
...mapState(useMemberStore, ["members"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
filtered(): Array<MemberViewModel> {
|
||||
return this.query === ""
|
||||
filtered(): Array<{memberId:number, absent:boolean; protocolId:number}> {
|
||||
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> {
|
||||
return this.members.filter((m) => this.presence.includes(m.id));
|
||||
)).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();
|
||||
this.fetchMembers(0, 1000, "", true);
|
||||
// this.fetchProtocolPresence();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||
...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
|
||||
removeSelected(id: number) {
|
||||
let index = this.presence.findIndex((s) => s == id);
|
||||
let index = this.presence.findIndex((s) => s.memberId == id);
|
||||
if (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 ProtocolSyncing from "@/components/admin/club/protocol/ProtocolSyncing.vue";
|
||||
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 lang="ts">
|
||||
|
@ -88,6 +92,10 @@ export default defineComponent({
|
|||
},
|
||||
mounted() {
|
||||
this.fetchProtocolByActiveId();
|
||||
this.fetchProtocolAgenda()
|
||||
this.fetchProtocolDecision()
|
||||
this.fetchProtocolPresence()
|
||||
this.fetchProtocolVoting()
|
||||
},
|
||||
// this.syncState is undefined, so it will never work
|
||||
// beforeRouteLeave(to, from, next) {
|
||||
|
@ -108,6 +116,10 @@ export default defineComponent({
|
|||
// },
|
||||
methods: {
|
||||
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
|
||||
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda"]),
|
||||
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision"]),
|
||||
...mapActions(useProtocolPresenceStore,["fetchProtocolPresence"]),
|
||||
...mapActions(useProtocolVotingStore,["fetchProtocolVoting"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openInfoModal() {
|
||||
this.openModal(
|
||||
|
|
|
@ -90,7 +90,7 @@ export default defineComponent({
|
|||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchProtocolVoting();
|
||||
// this.fetchProtocolVoting();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useProtocolVotingStore, ["fetchProtocolVoting", "createProtocolVoting"]),
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<template>
|
||||
<MainTemplate :useStagedOverviewLink="false">
|
||||
<template #topBar>
|
||||
<!-- <template #topBar>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<div class="markdown-container">
|
||||
</template> -->
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-2 h-full px-4 overflow-hidden">
|
||||
<div class="markdown-container overflow-y-scroll">
|
||||
<component v-if="markdownComponent" :is="markdownComponent" />
|
||||
<p v-else>Diese Seite existiert nicht.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
@ -40,8 +42,46 @@ export default defineComponent({
|
|||
},
|
||||
methods:{
|
||||
loadPage(){
|
||||
this.markdownComponent = null
|
||||
this.markdownComponent = markRaw(defineAsyncComponent(() => import(`$/${this.page?.toLowerCase()}.md`)));
|
||||
}
|
||||
}
|
||||
});
|
||||
</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">
|
||||
<template #list>
|
||||
<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>
|
||||
</SidebarTemplate>
|
||||
</template>
|
||||
|
|
Loading…
Reference in a new issue