patches v1.3.1 #58
15 changed files with 174 additions and 119 deletions
|
@ -32,9 +32,9 @@ services:
|
||||||
|
|
||||||
#environment:
|
#environment:
|
||||||
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
# - SERVERADDRESS=<backend_url (https://... | http://...)> # wichtig: ohne Pfad
|
||||||
# - APPNAMEOVERWRITE=Mitgliederverwaltung # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
|
# - APPNAMEOVERWRITE=<appname> # ersetzt den Namen FF-Admin auf der Login-Seite und sonstigen Positionen in der Oberfläche
|
||||||
# - IMPRINTLINK=https://mywebsite-imprint-url
|
# - IMPRINTLINK=<imprint link>
|
||||||
# - PRIVACYLINK=https://mywebsite-privacy-url
|
# - PRIVACYLINK=<privacy link>
|
||||||
# - CUSTOMLOGINMESSAGE=betrieben von xy
|
# - CUSTOMLOGINMESSAGE=betrieben von xy
|
||||||
#volumes:
|
#volumes:
|
||||||
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
# - <volume|local path>/favicon.ico:/usr/share/nginx/html/favicon.ico # 48x48 px Auflösung
|
||||||
|
@ -68,7 +68,7 @@ Ein eigenes Favicon und Logo kann über das verwenden Volume ausgetauscht werden
|
||||||
|
|
||||||
## Einrichtung
|
## Einrichtung
|
||||||
|
|
||||||
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Migliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
|
1. **Admin Benutzer erstellen**: Erstellen Sie einen Admin Benutzer unter dem Pfad /setup, um auf die Mitgliederverwaltung Zugriff zu erhalten. Nach der Erstellung des ersten Benutzers wird der Pfad automatisch geblockt.
|
||||||
|
|
||||||
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
|
2. **Rollen und Berechtigungen**: Unter `Benutzer > Rollen` können die Rollen und Berechtigungen für die Benutzer erstellt und angepasst werden.
|
||||||
|
|
||||||
|
|
|
@ -79,11 +79,30 @@
|
||||||
<div v-if="!allDay" class="flex flex-row gap-2">
|
<div v-if="!allDay" class="flex flex-row gap-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label for="starttime">Startzeit</label>
|
<label for="starttime">Startzeit</label>
|
||||||
<input type="datetime-local" id="starttime" required :value="data.start.split(':00+')[0]" />
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
id="starttime"
|
||||||
|
required
|
||||||
|
:value="formatForDateTimeLocalInput(data.start)"
|
||||||
|
@change="
|
||||||
|
($event) => {
|
||||||
|
($refs.endtime as HTMLInputElement).min = formatForDateTimeLocalInput(
|
||||||
|
($event.target as HTMLInputElement).value
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label for="endtime">Endzeit</label>
|
<label for="endtime">Endzeit</label>
|
||||||
<input type="datetime-local" id="endtime" required :value="data.end.split(':00+')[0]" />
|
<input
|
||||||
|
ref="endtime"
|
||||||
|
type="datetime-local"
|
||||||
|
id="endtime"
|
||||||
|
required
|
||||||
|
:value="formatForDateTimeLocalInput(data.end)"
|
||||||
|
:min="formatForDateTimeLocalInput(data.start)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-row gap-2">
|
<div v-else class="flex flex-row gap-2">
|
||||||
|
@ -93,8 +112,12 @@
|
||||||
type="date"
|
type="date"
|
||||||
id="startdate"
|
id="startdate"
|
||||||
required
|
required
|
||||||
:value="data.start"
|
:value="formatForDateInput(data.start)"
|
||||||
@change="($event) => (($refs.enddate as HTMLInputElement).max = ($event.target as HTMLInputElement).value)"
|
@change="
|
||||||
|
($event) => {
|
||||||
|
($refs.enddate as HTMLInputElement).min = formatForDateInput(($event.target as HTMLInputElement).value);
|
||||||
|
}
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
@ -105,7 +128,7 @@
|
||||||
id="enddate"
|
id="enddate"
|
||||||
required
|
required
|
||||||
:value="decrementEndDate(data.end)"
|
:value="decrementEndDate(data.end)"
|
||||||
:min="data.start"
|
:min="formatForDateInput(data.start)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,11 +203,11 @@ export default defineComponent({
|
||||||
let createCalendar: CreateCalendarViewModel = {
|
let createCalendar: CreateCalendarViewModel = {
|
||||||
typeId: this.selectedType.id,
|
typeId: this.selectedType.id,
|
||||||
starttime: this.allDay
|
starttime: this.allDay
|
||||||
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0))
|
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0)).toISOString()
|
||||||
: formData.starttime.value,
|
: new Date(formData.starttime.value).toISOString(),
|
||||||
endtime: this.allDay
|
endtime: this.allDay
|
||||||
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999))
|
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999)).toISOString()
|
||||||
: formData.endtime.value,
|
: new Date(formData.endtime.value).toISOString(),
|
||||||
title: formData.title.value,
|
title: formData.title.value,
|
||||||
content: formData.content.value,
|
content: formData.content.value,
|
||||||
location: formData.location.value,
|
location: formData.location.value,
|
||||||
|
@ -209,6 +232,26 @@ export default defineComponent({
|
||||||
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
const day = String(localDate.getDate() - 1).padStart(2, "0");
|
const day = String(localDate.getDate() - 1).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
},
|
||||||
|
formatForDateTimeLocalInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
const hours = String(localDate.getHours()).padStart(2, "0");
|
||||||
|
const minutes = String(localDate.getMinutes()).padStart(2, "0");
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||||
|
},
|
||||||
|
formatForDateInput(utcDateString: string) {
|
||||||
|
const localDate = new Date(utcDateString);
|
||||||
|
|
||||||
|
const year = localDate.getFullYear();
|
||||||
|
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(localDate.getDate()).padStart(2, "0");
|
||||||
|
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -78,7 +78,7 @@
|
||||||
<input type="checkbox" id="allDay" v-model="calendar.allDay" />
|
<input type="checkbox" id="allDay" v-model="calendar.allDay" />
|
||||||
<label for="allDay">ganztägig</label>
|
<label for="allDay">ganztägig</label>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="calendar.allDay == false" class="flex flex-row gap-2">
|
<div v-if="!calendar.allDay" class="flex flex-row gap-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label for="starttime">Startzeit</label>
|
<label for="starttime">Startzeit</label>
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type { PermissionObject } from "@/types/permissionTypes";
|
||||||
import { useAbilityStore } from "@/stores/ability";
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
|
||||||
export type Payload = JwtPayload & {
|
export type Payload = JwtPayload & {
|
||||||
userId: number;
|
userId: string;
|
||||||
username: string;
|
username: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
|
@ -67,7 +67,7 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var { firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
var { userId, firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
||||||
|
|
||||||
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
|
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
|
||||||
auth.setFailed();
|
auth.setFailed();
|
||||||
|
@ -75,7 +75,7 @@ export async function isAuthenticatedPromise(forceRefresh: boolean = false): Pro
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.setSuccess();
|
auth.setSuccess();
|
||||||
account.setAccountData(firstname, lastname, mail, username);
|
account.setAccountData(userId, firstname, lastname, mail, username);
|
||||||
ability.setAbility(permissions, isOwner);
|
ability.setAbility(permissions, isOwner);
|
||||||
resolve(decoded);
|
resolve(decoded);
|
||||||
}
|
}
|
||||||
|
|
|
@ -661,6 +661,11 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "version",
|
||||||
|
name: "admin-user-version",
|
||||||
|
component: () => import("@/views/admin/user/version/VersionDisplay.vue"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -701,11 +706,6 @@ const router = createRouter({
|
||||||
name: "account-administration",
|
name: "account-administration",
|
||||||
component: () => import("@/views/account/Administration.vue"),
|
component: () => import("@/views/account/Administration.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "version",
|
|
||||||
name: "account-version",
|
|
||||||
component: () => import("@/views/account/VersionDisplay.vue"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ":pathMatch(.*)*",
|
path: ":pathMatch(.*)*",
|
||||||
name: "account-404",
|
name: "account-404",
|
||||||
|
|
|
@ -43,6 +43,11 @@ export const useAbilityStore = defineStore("ability", {
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
isAdmin: (state) => (): boolean => {
|
||||||
|
const permissions = state.permissions;
|
||||||
|
if (state.isOwner) return true;
|
||||||
|
return permissions?.admin ?? false;
|
||||||
|
},
|
||||||
_can:
|
_can:
|
||||||
() =>
|
() =>
|
||||||
(
|
(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { useAbilityStore } from "./ability";
|
||||||
export const useAccountStore = defineStore("account", {
|
export const useAccountStore = defineStore("account", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
|
id: "" as string,
|
||||||
firstname: "" as string,
|
firstname: "" as string,
|
||||||
lastname: "" as string,
|
lastname: "" as string,
|
||||||
mail: "" as string,
|
mail: "" as string,
|
||||||
|
@ -17,7 +18,8 @@ export const useAccountStore = defineStore("account", {
|
||||||
localStorage.removeItem("refreshToken");
|
localStorage.removeItem("refreshToken");
|
||||||
window.open("/login", "_self");
|
window.open("/login", "_self");
|
||||||
},
|
},
|
||||||
setAccountData(firstname: string, lastname: string, mail: string, alias: string) {
|
setAccountData(id: string, firstname: string, lastname: string, mail: string, alias: string) {
|
||||||
|
this.id = id;
|
||||||
this.firstname = firstname;
|
this.firstname = firstname;
|
||||||
this.lastname = lastname;
|
this.lastname = lastname;
|
||||||
this.mail = mail;
|
this.mail = mail;
|
||||||
|
|
|
@ -20,7 +20,10 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
detectedChangeNewsletterRecipients: (state) =>
|
detectedChangeNewsletterRecipients: (state) =>
|
||||||
!isEqual(state.origin, state.recipients) && state.syncingNewsletterRecipients != "syncing",
|
!isEqual(
|
||||||
|
state.origin.sort((a: string, b: string) => a.localeCompare(b)),
|
||||||
|
state.recipients.sort((a: string, b: string) => a.localeCompare(b))
|
||||||
|
) && state.syncingNewsletterRecipients != "syncing",
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setNewsletterRecipientsSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
setNewsletterRecipientsSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
this.loading = "failed";
|
this.loading = "failed";
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sendQuery(offset = 0, count = 25, query?: DynamicQueryStructure | string) {
|
async sendQuery(offset = 0, count = 25, query?: DynamicQueryStructure | string) {
|
||||||
this.queryError = "";
|
this.queryError = "";
|
||||||
if (offset == 0) {
|
if (offset == 0) {
|
||||||
this.data = [];
|
this.data = [];
|
||||||
|
@ -40,7 +40,7 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
if (queryToSend == undefined || queryToSend == "" || (typeof queryToSend != "string" && queryToSend.table == ""))
|
if (queryToSend == undefined || queryToSend == "" || (typeof queryToSend != "string" && queryToSend.table == ""))
|
||||||
return;
|
return;
|
||||||
this.loadingData = "loading";
|
this.loadingData = "loading";
|
||||||
http
|
await http
|
||||||
.post(`/admin/querybuilder/query?offset=${offset}&count=${count}`, {
|
.post(`/admin/querybuilder/query?offset=${offset}&count=${count}`, {
|
||||||
query: queryToSend,
|
query: queryToSend,
|
||||||
})
|
})
|
||||||
|
@ -65,7 +65,8 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||||
this.queryError = "";
|
this.queryError = "";
|
||||||
this.loadingData = "fetched";
|
this.loadingData = "fetched";
|
||||||
},
|
},
|
||||||
exportData() {
|
async exportData() {
|
||||||
|
await this.sendQuery(0, this.totalLength);
|
||||||
if (this.data.length == 0) return;
|
if (this.data.length == 0) return;
|
||||||
const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))]
|
const csvString = [Object.keys(this.data[0]), ...this.data.map((d) => Object.values(d))]
|
||||||
.map((e) => e.join(";"))
|
.map((e) => e.join(";"))
|
||||||
|
|
|
@ -134,6 +134,7 @@ export const useNavigationStore = defineStore("navigation", {
|
||||||
...(abilityStore.can("read", "user", "role") ? [{ key: "role", title: "Rollen" }] : []),
|
...(abilityStore.can("read", "user", "role") ? [{ key: "role", title: "Rollen" }] : []),
|
||||||
...(abilityStore.can("read", "user", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
|
...(abilityStore.can("read", "user", "webapi") ? [{ key: "webapi", title: "Webapi-Token" }] : []),
|
||||||
...(abilityStore.can("read", "user", "backup") ? [{ key: "backup", title: "Backups" }] : []),
|
...(abilityStore.can("read", "user", "backup") ? [{ key: "backup", title: "Backups" }] : []),
|
||||||
|
...(abilityStore.isAdmin() ? [{ key: "version", title: "Version" }] : []),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
} as navigationModel;
|
} as navigationModel;
|
||||||
|
|
|
@ -103,6 +103,7 @@ import {
|
||||||
} from "@headlessui/vue";
|
} from "@headlessui/vue";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import type { UserViewModel } from "@/viewmodels/admin/user/user.models";
|
import type { UserViewModel } from "@/viewmodels/admin/user/user.models";
|
||||||
|
import { useAccountStore } from "@/stores/account";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -116,15 +117,18 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(useUserStore, ["users", "loading"]),
|
...mapState(useUserStore, ["users", "loading"]),
|
||||||
|
...mapState(useAccountStore, ["id"]),
|
||||||
filtered(): Array<UserViewModel> {
|
filtered(): Array<UserViewModel> {
|
||||||
return this.query === ""
|
return (
|
||||||
? this.users
|
this.query === ""
|
||||||
: this.users.filter((user) =>
|
? this.users
|
||||||
(user.firstname + " " + user.lastname)
|
: this.users.filter((user) =>
|
||||||
.toLowerCase()
|
(user.firstname + " " + user.lastname)
|
||||||
.replace(/\s+/g, "")
|
.toLowerCase()
|
||||||
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
.replace(/\s+/g, "")
|
||||||
);
|
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
||||||
|
)
|
||||||
|
).filter((u) => u.id != this.id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
@ -12,11 +12,6 @@
|
||||||
:link="{ name: 'account-administration' }"
|
:link="{ name: 'account-administration' }"
|
||||||
:active="activeRouteName == 'account-administration'"
|
:active="activeRouteName == 'account-administration'"
|
||||||
/>
|
/>
|
||||||
<RoutingLink
|
|
||||||
title="Versions-Verwaltung"
|
|
||||||
:link="{ name: 'account-version' }"
|
|
||||||
:active="activeRouteName == 'account-version'"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #list>
|
<template #list>
|
||||||
<RoutingLink title="Mein Account" :link="{ name: 'account-me' }" :active="activeRouteName == 'account-me'" />
|
<RoutingLink title="Mein Account" :link="{ name: 'account-me' }" :active="activeRouteName == 'account-me'" />
|
||||||
|
|
|
@ -1,51 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
<div class="flex flex-col h-full w-full overflow-hidden">
|
||||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
||||||
↺ laden fehlgeschlagen
|
↺ laden fehlgeschlagen
|
||||||
</p>
|
</p>
|
||||||
|
<div class="flex flex-col gap-2 h-1/2">
|
||||||
<select v-model="recipientsByQueryId">
|
<select v-model="recipientsByQueryId">
|
||||||
<option value="def">Optional</option>
|
<option value="def">Optional</option>
|
||||||
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
||||||
</select>
|
</select>
|
||||||
<p>Empfänger durch gespeicherte Abfrage</p>
|
<p>Empfänger durch gespeicherte Abfrage</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 queried"
|
v-for="member in queried"
|
||||||
:key="member.id"
|
:key="member.id"
|
||||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 h-1/2">
|
||||||
|
<MemberSearchSelect
|
||||||
|
title="weitere Empfänger suchen"
|
||||||
|
v-model="recipients"
|
||||||
|
:disabled="!can('create', 'club', 'newsletter')"
|
||||||
|
/>
|
||||||
|
|
||||||
<MemberSearchSelect
|
<p>Ausgewählte Empfänger</p>
|
||||||
title="weitere Empfänger suchen"
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
v-model="recipients"
|
<div
|
||||||
:disabled="!can('create', 'club', 'newsletter')"
|
v-for="member in selected"
|
||||||
/>
|
:key="member.id"
|
||||||
|
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
||||||
|
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>Ausgewählte Empfänger</p>
|
<TrashIcon
|
||||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
v-if="can('create', 'club', 'newsletter')"
|
||||||
<div
|
class="w-5 h-5 p-1 box-content cursor-pointer"
|
||||||
v-for="member in selected"
|
@click="removeSelected(member.id)"
|
||||||
:key="member.id"
|
/>
|
||||||
class="flex flex-row h-fit w-full border border-primary rounded-md bg-primary p-2 text-white justify-between items-center"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
|
|
||||||
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TrashIcon
|
|
||||||
v-if="can('create', 'club', 'newsletter')"
|
|
||||||
class="w-5 h-5 p-1 box-content cursor-pointer"
|
|
||||||
@click="removeSelected(member.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,7 +10,17 @@
|
||||||
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-t-md">
|
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-t-md">
|
||||||
<div class="flex flex-row justify-between border-b-2 border-gray-300">
|
<div class="flex flex-row justify-between border-b-2 border-gray-300">
|
||||||
<h1 class="text-xl font-semibold">Client</h1>
|
<h1 class="text-xl font-semibold">Client</h1>
|
||||||
<p>V{{ clientVersion }}</p>
|
<p>
|
||||||
|
V{{ clientVersion }} ({{
|
||||||
|
new Date(clientVersionRelease).toLocaleDateString("de", {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
}})
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow flex flex-col gap-4 overflow-y-scroll">
|
<div class="grow flex flex-col gap-4 overflow-y-scroll">
|
||||||
<div v-for="version in newerClientVersions">
|
<div v-for="version in newerClientVersions">
|
||||||
|
@ -18,7 +28,7 @@
|
||||||
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
|
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
|
||||||
{{
|
{{
|
||||||
new Date(version.isoDate).toLocaleDateString("de", {
|
new Date(version.isoDate).toLocaleDateString("de", {
|
||||||
month: "long",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
|
@ -34,7 +44,17 @@
|
||||||
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-b-md">
|
<div class="h-1/2 flex flex-col gap-2 p-2 border border-gray-300 rounded-b-md">
|
||||||
<div class="flex flex-row justify-between border-b-2 border-gray-300">
|
<div class="flex flex-row justify-between border-b-2 border-gray-300">
|
||||||
<h1 class="text-xl font-semibold">Server</h1>
|
<h1 class="text-xl font-semibold">Server</h1>
|
||||||
<p>V{{ serverVersion }}</p>
|
<p>
|
||||||
|
V{{ serverVersion }} ({{
|
||||||
|
new Date(serverVersionRelease).toLocaleDateString("de", {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
}})
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow flex flex-col gap-2 overflow-y-scroll">
|
<div class="grow flex flex-col gap-2 overflow-y-scroll">
|
||||||
<div v-for="version in newerServerVersions">
|
<div v-for="version in newerServerVersions">
|
||||||
|
@ -42,7 +62,7 @@
|
||||||
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
|
<span class="font-semibold text-lg">V{{ version.title }}</span> vom
|
||||||
{{
|
{{
|
||||||
new Date(version.isoDate).toLocaleDateString("de", {
|
new Date(version.isoDate).toLocaleDateString("de", {
|
||||||
month: "long",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
})
|
})
|
||||||
|
@ -63,8 +83,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
import MainTemplate from "@/templates/Main.vue";
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
import clientPackage from "../../../package.json";
|
import clientPackage from "../../../../../package.json";
|
||||||
import type { Releases } from "../../viewmodels/version.models";
|
import type { Releases } from "@/viewmodels/version.models";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -80,11 +100,19 @@ export default defineComponent({
|
||||||
computed: {
|
computed: {
|
||||||
newerServerVersions() {
|
newerServerVersions() {
|
||||||
if (!this.serverRss) return [];
|
if (!this.serverRss) return [];
|
||||||
return this.serverRss.items.filter((i) => this.compareVersionStrings(this.serverVersion, i.title) < 0);
|
return this.serverRss.items.filter((i) => new Date(i.isoDate) > new Date(this.serverVersionRelease));
|
||||||
},
|
},
|
||||||
newerClientVersions() {
|
newerClientVersions() {
|
||||||
if (!this.clientRss) return [];
|
if (!this.clientRss) return [];
|
||||||
return this.clientRss.items.filter((i) => this.compareVersionStrings(this.clientVersion, i.title) < 0);
|
return this.clientRss.items.filter((i) => new Date(i.isoDate) > new Date(this.clientVersionRelease));
|
||||||
|
},
|
||||||
|
serverVersionRelease() {
|
||||||
|
if (!this.serverRss) return "";
|
||||||
|
return this.serverRss.items.find((i) => i.title == this.serverVersion)?.isoDate ?? "";
|
||||||
|
},
|
||||||
|
clientVersionRelease() {
|
||||||
|
if (!this.clientRss) return "";
|
||||||
|
return this.clientRss.items.find((i) => i.title == this.clientVersion)?.isoDate ?? "";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -102,7 +130,7 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
async getServerFeed() {
|
getServerFeed() {
|
||||||
this.$http
|
this.$http
|
||||||
.get("/server/serverrss")
|
.get("/server/serverrss")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
@ -110,7 +138,7 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
async getClientFeed() {
|
getClientFeed() {
|
||||||
this.$http
|
this.$http
|
||||||
.get("/server/clientrss")
|
.get("/server/clientrss")
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
@ -118,37 +146,6 @@ export default defineComponent({
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
compareVersionStrings(activeVersion: string, compareVersion: string) {
|
|
||||||
const parseVersion = (version: string) => {
|
|
||||||
const [main, tag] = version.split("-");
|
|
||||||
const [major, minor, patch] = main.split(".").map(Number);
|
|
||||||
return { major, minor, patch, tag };
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!activeVersion || !compareVersion) return 0;
|
|
||||||
|
|
||||||
const versionA = parseVersion(activeVersion);
|
|
||||||
const versionB = parseVersion(compareVersion);
|
|
||||||
|
|
||||||
if (versionA.major !== versionB.major) {
|
|
||||||
return versionA.major - versionB.major;
|
|
||||||
}
|
|
||||||
if (versionA.minor !== versionB.minor) {
|
|
||||||
return versionA.minor - versionB.minor;
|
|
||||||
}
|
|
||||||
if (versionA.patch !== versionB.patch) {
|
|
||||||
return versionA.patch - versionB.patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (versionA.tag && !versionB.tag) return -1;
|
|
||||||
if (!versionA.tag && versionB.tag) return 1;
|
|
||||||
if (versionA.tag && versionB.tag) {
|
|
||||||
const tags = ["alpha", "beta", ""];
|
|
||||||
return tags.indexOf(versionA.tag) - tags.indexOf(versionB.tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
|
@ -43,11 +43,13 @@ export default defineConfig({
|
||||||
name: "__APPNAMEOVERWRITE__",
|
name: "__APPNAMEOVERWRITE__",
|
||||||
short_name: "__APPNAMEOVERWRITE__",
|
short_name: "__APPNAMEOVERWRITE__",
|
||||||
theme_color: "#990b00",
|
theme_color: "#990b00",
|
||||||
|
display: "standalone",
|
||||||
|
start_url: "/",
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: "favicon.ico",
|
src: "favicon.ico",
|
||||||
sizes: "48x48",
|
sizes: "48x48",
|
||||||
type: "image/png",
|
type: "image/ico",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "favicon.png",
|
src: "favicon.png",
|
||||||
|
|
Loading…
Add table
Reference in a new issue