#15-messages #22
14 changed files with 941 additions and 14 deletions
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter erstellen</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<form ref="form" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||||
|
<div>
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input type="text" id="title" required autocomplete="false" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row gap-2">
|
||||||
|
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
|
||||||
|
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||||
|
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-end">
|
||||||
|
<div class="flex flex-row gap-4 py-2">
|
||||||
|
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
|
||||||
|
abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
import { useNewsletterStore } from "../../../../stores/admin/newsletter";
|
||||||
|
import type { CreateNewsletterViewModel } from "../../../../viewmodels/admin/newsletter.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||||
|
timeout: undefined as any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
try {
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useModalStore, ["closeModal"]),
|
||||||
|
...mapActions(useNewsletterStore, ["createNewsletter"]),
|
||||||
|
triggerCreate(e: any) {
|
||||||
|
let formData = e.target.elements;
|
||||||
|
let createNewsletter: CreateNewsletterViewModel = {
|
||||||
|
title: formData.title.value,
|
||||||
|
};
|
||||||
|
this.createNewsletter(createNewsletter)
|
||||||
|
.then(() => {
|
||||||
|
this.status = { status: "success" };
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
(this.$refs.form as HTMLFormElement).reset();
|
||||||
|
this.closeModal();
|
||||||
|
}, 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.status = { status: "failed" };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<template>
|
||||||
|
<div class="w-full md:max-w-md">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<p class="text-xl font-medium">Newsletter wird noch synchronisiert</p>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<p>Es gibt noch Daten, welche synchronisiert werden müssen.</p>
|
||||||
|
<p>Dieses PopUp entfernt sich von selbst nach erfolgreicher Synchronisierung.</p>
|
||||||
|
</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 SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
import { useProtocolStore } from "@/stores/admin/protocol";
|
||||||
|
import type { CreateProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({});
|
||||||
|
</script>
|
29
src/components/admin/club/newsletter/NewsletterListItem.vue
Normal file
29
src/components/admin/club/newsletter/NewsletterListItem.vue
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
|
||||||
|
<RouterLink
|
||||||
|
:to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }"
|
||||||
|
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
|
||||||
|
>
|
||||||
|
<p>{{ newsletter.title }}</p>
|
||||||
|
<PaperAirplaneIcon v-if="newsletter.isSent" class="w-5 h-5" />
|
||||||
|
</RouterLink>
|
||||||
|
<div class="p-2 max-h-48 overflow-y-auto">
|
||||||
|
<p v-html="newsletter.description"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent, type PropType } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import type { NewsletterViewModel } from "@/viewmodels/admin/newsletter.models";
|
||||||
|
import { PaperAirplaneIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletter: { type: Object as PropType<NewsletterViewModel>, default: {} },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
123
src/components/admin/club/newsletter/NewsletterSyncing.vue
Normal file
123
src/components/admin/club/newsletter/NewsletterSyncing.vue
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<CloudIcon v-if="syncing == 'synced'" class="w-5 h-5" />
|
||||||
|
<CloudArrowUpIcon
|
||||||
|
v-else-if="syncing == 'detectedChanges'"
|
||||||
|
class="w-5 h-5 cursor-pointer animate-bounce"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
<ArrowPathIcon v-else-if="syncing == 'syncing'" class="w-5 h-5 animate-spin" />
|
||||||
|
<ExclamationTriangleIcon
|
||||||
|
v-else
|
||||||
|
class="w-5 h-5 animate-[ping_1s_ease-in-out_3] text-red-500 cursor-pointer"
|
||||||
|
@click="syncAll"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapState, mapActions } from "pinia";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { ArrowPathIcon, CloudArrowUpIcon, CloudIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useNewsletterDatesStore } from "@/stores/admin/newsletterDates";
|
||||||
|
import { useNewsletterRecipientsStore } from "@/stores/admin/newsletterRecipients";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: ["executeSyncAll"],
|
||||||
|
watch: {
|
||||||
|
executeSyncAll() {
|
||||||
|
this.syncAll();
|
||||||
|
},
|
||||||
|
syncing() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletter() {
|
||||||
|
clearTimeout(this.newsletterTimer);
|
||||||
|
this.setNewsletterSyncingState("synced");
|
||||||
|
if (this.detectedChangeNewsletter == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterSyncingState("detectedChanges");
|
||||||
|
this.newsletterTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletter();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletterDates() {
|
||||||
|
clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (this.detectedChangeNewsletterDates == false) {
|
||||||
|
this.setNewsletterDatesSyncingState("synced");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterDatesSyncingState("detectedChanges");
|
||||||
|
this.newsletterDatesTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletterDates();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
detectedChangeNewsletterRecipients() {
|
||||||
|
clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
this.setNewsletterRecipientsSyncingState("synced");
|
||||||
|
if (this.detectedChangeNewsletterRecipients == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setNewsletterRecipientsSyncingState("detectedChanges");
|
||||||
|
this.newsletterRecipientsTimer = setTimeout(() => {
|
||||||
|
this.synchronizeActiveNewsletterRecipients();
|
||||||
|
}, 10000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: {
|
||||||
|
syncState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||||
|
return typeof state == "string";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newsletterTimer: undefined as undefined | any,
|
||||||
|
newsletterDatesTimer: undefined as undefined | any,
|
||||||
|
newsletterRecipientsTimer: undefined as undefined | any,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$emit("syncState", this.syncing);
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (!this.newsletterTimer) clearTimeout(this.newsletterTimer);
|
||||||
|
if (!this.newsletterDatesTimer) clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (!this.newsletterRecipientsTimer) clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useNewsletterStore, ["syncingNewsletter", "detectedChangeNewsletter"]),
|
||||||
|
...mapState(useNewsletterDatesStore, ["syncingNewsletterDates", "detectedChangeNewsletterDates"]),
|
||||||
|
...mapState(useNewsletterRecipientsStore, ["syncingNewsletterRecipients", "detectedChangeNewsletterRecipients"]),
|
||||||
|
|
||||||
|
syncing(): "synced" | "syncing" | "detectedChanges" | "failed" {
|
||||||
|
let states = [
|
||||||
|
this.syncingNewsletter,
|
||||||
|
this.syncingNewsletterDates,
|
||||||
|
this.syncingNewsletterRecipients,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (states.includes("failed")) return "failed";
|
||||||
|
else if (states.includes("syncing")) return "syncing";
|
||||||
|
else if (states.includes("detectedChanges")) return "detectedChanges";
|
||||||
|
else return "synced";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterStore, ["synchronizeActiveNewsletter", "setNewsletterSyncingState"]),
|
||||||
|
...mapActions(useNewsletterDatesStore, ["synchronizeActiveNewsletterDates", "setNewsletterDatesSyncingState"]),
|
||||||
|
...mapActions(useNewsletterRecipientsStore, ["synchronizeActiveNewsletterRecipients", "setNewsletterRecipientsSyncingState"]),
|
||||||
|
|
||||||
|
syncAll() {
|
||||||
|
if (!this.newsletterTimer) clearTimeout(this.newsletterTimer);
|
||||||
|
if (!this.newsletterDatesTimer) clearTimeout(this.newsletterDatesTimer);
|
||||||
|
if (!this.newsletterRecipientsTimer) clearTimeout(this.newsletterRecipientsTimer);
|
||||||
|
|
||||||
|
this.synchronizeActiveNewsletter();
|
||||||
|
this.synchronizeActiveNewsletterDates();
|
||||||
|
this.synchronizeActiveNewsletterRecipients();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -8,6 +8,7 @@ import { abilityAndNavUpdate } from "./adminGuard";
|
||||||
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
||||||
import { resetMemberStores, setMemberId } from "./memberGuard";
|
import { resetMemberStores, setMemberId } from "./memberGuard";
|
||||||
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
||||||
|
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -174,10 +175,51 @@ const router = createRouter({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "newsletter",
|
path: "newsletter",
|
||||||
name: "admin-club-newsletter",
|
name: "admin-club-newsletter-route",
|
||||||
component: () => import("@/views/admin/ViewSelect.vue"),
|
component: () => import("@/views/RouterView.vue"),
|
||||||
meta: { type: "read", section: "club", module: "newsletter" },
|
meta: { type: "read", section: "club", module: "newsletter" },
|
||||||
beforeEnter: [abilityAndNavUpdate],
|
beforeEnter: [abilityAndNavUpdate],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "admin-club-newsletter",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/Newsletter.vue"),
|
||||||
|
beforeEnter: [resetNewsletterStores],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ":newsletterId",
|
||||||
|
name: "admin-club-newsletter-routing",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/NewsletterRouting.vue"),
|
||||||
|
beforeEnter: [setNewsletterId],
|
||||||
|
props: true,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "overview",
|
||||||
|
name: "admin-club-newsletter-overview",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/NewsletterOverview.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "recipients",
|
||||||
|
name: "admin-club-newsletter-recipients",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/NewsletterRecipients.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "dates",
|
||||||
|
name: "admin-club-newsletter-dates",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/NewsletterDates.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "printout",
|
||||||
|
name: "admin-club-newsletter-printout",
|
||||||
|
component: () => import("@/views/admin/club/newsletter/NewsletterPrintout.vue"),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "protocol",
|
path: "protocol",
|
||||||
|
|
24
src/router/newsletterGuard.ts
Normal file
24
src/router/newsletterGuard.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { useNewsletterDatesStore } from "@/stores/admin/newsletterDates";
|
||||||
|
import { useNewsletterRecipientsStore } from "@/stores/admin/newsletterRecipients";
|
||||||
|
|
||||||
|
export async function setNewsletterId(to: any, from: any, next: any) {
|
||||||
|
const newsletter = useNewsletterStore();
|
||||||
|
newsletter.activeNewsletter = to.params?.newsletterId ?? null;
|
||||||
|
|
||||||
|
useNewsletterDatesStore().$reset();
|
||||||
|
useNewsletterRecipientsStore().$reset();
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetNewsletterStores(to: any, from: any, next: any) {
|
||||||
|
const newsletter = useNewsletterStore();
|
||||||
|
newsletter.activeNewsletter = null;
|
||||||
|
newsletter.activeNewsletterObj = null;
|
||||||
|
|
||||||
|
useNewsletterDatesStore().$reset();
|
||||||
|
useNewsletterRecipientsStore().$reset();
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { http } from "@/serverCom";
|
import { http } from "@/serverCom";
|
||||||
import type { NewsletterDatesViewModel, SyncNewsletterDatesViewModel } from "@/viewmodels/admin/newsletterDates.models";
|
import type { NewsletterDatesViewModel, SyncNewsletterDatesViewModel } from "@/viewmodels/admin/newsletterDates.models";
|
||||||
import { useProtocolStore } from "./protocol";
|
import { useNewsletterStore } from "./newsletter";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
import differenceWith from "lodash.differencewith";
|
import differenceWith from "lodash.differencewith";
|
||||||
|
@ -36,16 +36,16 @@ export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchNewsletterDatesPromise() {
|
fetchNewsletterDatesPromise() {
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
return http.get(`/admin/protocol/${protocolId}/agenda`);
|
return http.get(`/admin/newsletter/${newsletterId}/dates`);
|
||||||
},
|
},
|
||||||
async synchronizeActiveNewsletterDates() {
|
async synchronizeActiveNewsletterDates() {
|
||||||
this.syncingNewsletterDates = "syncing";
|
this.syncingNewsletterDates = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
|
|
||||||
await http
|
await http
|
||||||
.patch(`/admin/protocol/${protocolId}/synchronize/agenda`, {
|
.patch(`/admin/newsletter/${newsletterId}/synchronize/dates`, {
|
||||||
agenda: differenceWith(this.dates, this.origin, isEqual),
|
dates: differenceWith(this.dates, this.origin, isEqual),
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.syncingNewsletterDates = "synced";
|
this.syncingNewsletterDates = "synced";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type {
|
||||||
NewsletterRecipientsViewModel,
|
NewsletterRecipientsViewModel,
|
||||||
SyncNewsletterRecipientsViewModel,
|
SyncNewsletterRecipientsViewModel,
|
||||||
} from "@/viewmodels/admin/newsletterRecipients.models";
|
} from "@/viewmodels/admin/newsletterRecipients.models";
|
||||||
import { useProtocolStore } from "./protocol";
|
import { useNewsletterStore } from "./newsletter";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import isEqual from "lodash.isequal";
|
import isEqual from "lodash.isequal";
|
||||||
|
|
||||||
|
@ -38,15 +38,15 @@ export const useNewsletterRecipientsStore = defineStore("newsletterRecipients",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchNewsletterRecipientsPromise() {
|
fetchNewsletterRecipientsPromise() {
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
return http.get(`/admin/protocol/${protocolId}/presence`);
|
return http.get(`/admin/newsletter/${newsletterId}/recipients`);
|
||||||
},
|
},
|
||||||
async synchronizeActiveNewsletterRecipients() {
|
async synchronizeActiveNewsletterRecipients() {
|
||||||
this.syncingNewsletterRecipients = "syncing";
|
this.syncingNewsletterRecipients = "syncing";
|
||||||
const protocolId = useProtocolStore().activeProtocol;
|
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||||
await http
|
await http
|
||||||
.put(`/admin/protocol/${protocolId}/synchronize/presence`, {
|
.patch(`/admin/newsletter/${newsletterId}/synchronize/recipients`, {
|
||||||
presence: this.recipients,
|
recipients: this.recipients,
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.syncingNewsletterRecipients = "synced";
|
this.syncingNewsletterRecipients = "synced";
|
||||||
|
|
69
src/views/admin/club/newsletter/Newsletter.vue
Normal file
69
src/views/admin/club/newsletter/Newsletter.vue
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<MainTemplate>
|
||||||
|
<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">Newsletter</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #diffMain>
|
||||||
|
<div class="flex flex-col w-full h-full gap-2 justify-center px-7">
|
||||||
|
<Pagination
|
||||||
|
:items="newsletters"
|
||||||
|
:totalCount="totalCount"
|
||||||
|
:indicateLoading="loading == 'loading'"
|
||||||
|
@load-data="(offset, count, search) => fetchNewsletters(offset, count)"
|
||||||
|
@search="(search) => fetchNewsletters(0, 25, true)"
|
||||||
|
>
|
||||||
|
<template #pageRow="{ row }: { row: NewsletterViewModel }">
|
||||||
|
<NewsletterListItem :newsletter="row" />
|
||||||
|
</template>
|
||||||
|
</Pagination>
|
||||||
|
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<button v-if="can('create', 'club', 'newsletter')" primary class="!w-fit" @click="openCreateModal">
|
||||||
|
Newsletter erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MainTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import Pagination from "@/components/Pagination.vue";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import type { NewsletterViewModel } from "@/viewmodels/admin/newsletter.models";
|
||||||
|
import NewsletterListItem from "../../../../components/admin/club/newsletter/NewsletterListItem.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentPage: 0,
|
||||||
|
maxEntriesPerPage: 25,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useNewsletterStore, ["newsletters", "totalCount", "loading"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletters(0, this.maxEntriesPerPage, true);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterStore, ["fetchNewsletters"]),
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
openCreateModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/newsletter/CreateNewsletterModal.vue")))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
84
src/views/admin/club/newsletter/NewsletterDates.vue
Normal file
84
src/views/admin/club/newsletter/NewsletterDates.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchNewsletterDates" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<details
|
||||||
|
v-for="item in dates"
|
||||||
|
class="flex flex-col gap-2 rounded-lg w-full justify-between border border-primary overflow-hidden min-h-fit"
|
||||||
|
>
|
||||||
|
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||||
|
<svg
|
||||||
|
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path d="M12.95 10.707l.707-.707L8 4.343 6.586 5.757 10.828 10l-4.242 4.243L8 15.657l4.95-4.95z" />
|
||||||
|
</svg>
|
||||||
|
<p>{{ item.calendar.title }} {{ item.calendar.starttime }}</p>
|
||||||
|
</summary>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
id="title"
|
||||||
|
placeholder="Einscheidung"
|
||||||
|
autocomplete="off"
|
||||||
|
v-model="item.diffTitle"
|
||||||
|
@keyup.prevent
|
||||||
|
:disabled="!can('create', 'club', 'newsletter')"
|
||||||
|
/>
|
||||||
|
<QuillEditor
|
||||||
|
id="top"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="Entscheidung Inhalt..."
|
||||||
|
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="item.diffDescription"
|
||||||
|
:enable="can('create', 'club', 'newsletter')"
|
||||||
|
:style="!can('create', 'club', 'newsletter') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button v-if="can('create', 'club', 'newsletter')" primary class="!w-fit" @click="addEntry">
|
||||||
|
Eintrag hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
import { useNewsletterDatesStore } from "@/stores/admin/newsletterDates";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletterId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useNewsletterDatesStore, ["dates", "loading"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletterDates();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterDatesStore, ["fetchNewsletterDates"]),
|
||||||
|
addEntry(){
|
||||||
|
// modal to select date
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
63
src/views/admin/club/newsletter/NewsletterOverview.vue
Normal file
63
src/views/admin/club/newsletter/NewsletterOverview.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<div v-if="activeNewsletterObj != null" class="flex flex-col gap-2 w-full">
|
||||||
|
<div class="w-full">
|
||||||
|
<label for="title">Titel</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="title"
|
||||||
|
v-model="activeNewsletterObj.title"
|
||||||
|
:disabled="!can('create', 'club', 'newsletter')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col h-1/2">
|
||||||
|
<label for="summary">Zusammenfassung</label>
|
||||||
|
<QuillEditor
|
||||||
|
id="summary"
|
||||||
|
theme="snow"
|
||||||
|
placeholder="Zusammenfassung zur Sitzung..."
|
||||||
|
style="height: 250px; max-height: 250px; min-height: 250px"
|
||||||
|
contentType="html"
|
||||||
|
:toolbar="toolbarOptions"
|
||||||
|
v-model:content="activeNewsletterObj.description"
|
||||||
|
:enable="can('create', 'club', 'newsletter')"
|
||||||
|
:style="!can('create', 'club', 'newsletter') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loadingActive == 'failed'" @click="fetchNewsletterByActiveId" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { QuillEditor } from "@vueup/vue-quill";
|
||||||
|
import "@vueup/vue-quill/dist/vue-quill.snow.css";
|
||||||
|
import { toolbarOptions } from "@/helpers/quillConfig";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletterId: String,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useNewsletterStore, ["loadingActive", "activeNewsletterObj"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletterByActiveId();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterStore, ["fetchNewsletterByActiveId"]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
102
src/views/admin/club/newsletter/NewsletterPrintout.vue
Normal file
102
src/views/admin/club/newsletter/NewsletterPrintout.vue
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<!-- <div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchProtocolPrintout" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 h-full overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="print in printout"
|
||||||
|
:key="print.id"
|
||||||
|
class="flex flex-col h-fit w-full border border-primary rounded-md"
|
||||||
|
>
|
||||||
|
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||||
|
<p>{{ print.title }}</p>
|
||||||
|
<div class="flex flex-row">
|
||||||
|
<div>
|
||||||
|
<ViewfinderCircleIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="openPdfShow(print.id)" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ArrowDownTrayIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="downloadPdf(print.id)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<p>Ausdruck Nummer: {{ print.iteration }}</p>
|
||||||
|
<p>Ausdruck erstellt: {{ print.createdAt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-start gap-2">
|
||||||
|
<button
|
||||||
|
v-if="can('create', 'club', 'newsletter')"
|
||||||
|
primary
|
||||||
|
class="!w-fit"
|
||||||
|
:disabled="printing != undefined"
|
||||||
|
@click="createProtocolPrintout"
|
||||||
|
>
|
||||||
|
Ausdruck erstellen
|
||||||
|
</button>
|
||||||
|
<Spinner v-if="printing == 'loading'" class="my-auto" />
|
||||||
|
<SuccessCheckmark v-else-if="printing == 'success'" />
|
||||||
|
<FailureXMark v-else-if="printing == 'failed'" />
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||||
|
import FailureXMark from "@/components/FailureXMark.vue";
|
||||||
|
// import { useProtocolPrintoutStore } from "@/stores/admin/newsletterPrintout";
|
||||||
|
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletterId: String,
|
||||||
|
},
|
||||||
|
// computed: {
|
||||||
|
// ...mapState(useProtocolPrintoutStore, ["printout", "loading", "printing"]),
|
||||||
|
// ...mapState(useAbilityStore, ["can"]),
|
||||||
|
// },
|
||||||
|
// mounted() {
|
||||||
|
// this.fetchProtocolPrintout();
|
||||||
|
// },
|
||||||
|
// methods: {
|
||||||
|
// ...mapActions(useModalStore, ["openModal"]),
|
||||||
|
// ...mapActions(useProtocolPrintoutStore, [
|
||||||
|
// "fetchProtocolPrintout",
|
||||||
|
// "createProtocolPrintout",
|
||||||
|
// "fetchProtocolPrintoutById",
|
||||||
|
// ]),
|
||||||
|
// openPdfShow(id: number) {
|
||||||
|
// this.openModal(
|
||||||
|
// markRaw(defineAsyncComponent(() => import("@/components/admin/club/newsletter/ProtocolPrintoutViewerModal.vue"))),
|
||||||
|
// id
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// downloadPdf(id: number) {
|
||||||
|
// let clickedOn = this.printout.find((p) => p.id == id);
|
||||||
|
// this.fetchProtocolPrintoutById(id)
|
||||||
|
// .then((response) => {
|
||||||
|
// const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
// const fileLink = document.createElement("a");
|
||||||
|
// fileLink.href = fileURL;
|
||||||
|
// fileLink.setAttribute("download", clickedOn?.title ? clickedOn.title + ".pdf" : "Protokoll.pdf");
|
||||||
|
// document.body.appendChild(fileLink);
|
||||||
|
// fileLink.click();
|
||||||
|
// fileLink.remove();
|
||||||
|
// })
|
||||||
|
// .catch(() => {});
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
</script>
|
164
src/views/admin/club/newsletter/NewsletterRecipients.vue
Normal file
164
src/views/admin/club/newsletter/NewsletterRecipients.vue
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
|
||||||
|
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||||
|
<p v-else-if="loading == 'failed'" @click="fetchNewsletterRecipients" class="cursor-pointer">
|
||||||
|
↺ laden fehlgeschlagen
|
||||||
|
</p>
|
||||||
|
|
||||||
|
-- select members by query
|
||||||
|
|
||||||
|
<div class="w-full">
|
||||||
|
<Combobox v-model="recipients" :disabled="!can('create', 'club', 'newsletter')" multiple>
|
||||||
|
<ComboboxLabel>Empfänger suchen</ComboboxLabel>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<ComboboxInput
|
||||||
|
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
|
||||||
|
@input="query = $event.target.value"
|
||||||
|
/>
|
||||||
|
<ComboboxButton class="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
<TransitionRoot
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
@after-leave="query = ''"
|
||||||
|
>
|
||||||
|
<ComboboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-md ring-1 ring-black/5 focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<ComboboxOption v-if="filtered.length === 0" as="template" disabled>
|
||||||
|
<li class="text-text relative cursor-default select-none py-2 pl-3 pr-4">
|
||||||
|
<span class="font-normal block truncate"> Keine Auswahl</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
|
||||||
|
<ComboboxOption
|
||||||
|
v-for="member in filtered"
|
||||||
|
as="template"
|
||||||
|
:key="member.id"
|
||||||
|
:value="member.id"
|
||||||
|
v-slot="{ selected, active }"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="relative cursor-default select-none py-2 pl-10 pr-4"
|
||||||
|
:class="{
|
||||||
|
'bg-primary text-white': active,
|
||||||
|
'text-gray-900': !active,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="block truncate" :class="{ 'font-medium': selected, 'font-normal': !selected }">
|
||||||
|
{{ member.firstname }} {{ member.lastname }} {{ member.nameaffix }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-3"
|
||||||
|
:class="{ 'text-white': active, 'text-primary': !active }"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</TransitionRoot>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<p>Ausgewählte Empfänger</p>
|
||||||
|
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||||
|
<div
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||||
|
import Spinner from "@/components/Spinner.vue";
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxLabel,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
TransitionRoot,
|
||||||
|
} from "@headlessui/vue";
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
|
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { useMemberStore } from "@/stores/admin/member";
|
||||||
|
import type { MemberViewModel } from "@/viewmodels/admin/member.models";
|
||||||
|
import { useNewsletterRecipientsStore } from "@/stores/admin/newsletterRecipients";
|
||||||
|
import { useAbilityStore } from "@/stores/ability";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletterId: String,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: "" as String,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapWritableState(useNewsletterRecipientsStore, ["recipients", "loading"]),
|
||||||
|
...mapState(useMemberStore, ["members"]),
|
||||||
|
...mapState(useAbilityStore, ["can"]),
|
||||||
|
filtered(): Array<MemberViewModel> {
|
||||||
|
return this.query === ""
|
||||||
|
? this.members
|
||||||
|
: this.members.filter((member) =>
|
||||||
|
(member.firstname + " " + member.lastname)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "")
|
||||||
|
.includes(this.query.toLowerCase().replace(/\s+/g, ""))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sorted(): Array<MemberViewModel> {
|
||||||
|
return this.selected.sort((a, b) => {
|
||||||
|
if (a.lastname < b.lastname) return -1;
|
||||||
|
if (a.lastname > b.lastname) return 1;
|
||||||
|
if (a.firstname < b.firstname) return -1;
|
||||||
|
if (a.firstname > b.firstname) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
selected(): Array<MemberViewModel> {
|
||||||
|
return this.members.filter((m) => this.recipients.includes(m.id));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchMembers();
|
||||||
|
this.fetchNewsletterRecipients();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||||
|
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
||||||
|
removeSelected(id: number) {
|
||||||
|
let index = this.recipients.findIndex((s) => s == id);
|
||||||
|
if (index != -1) {
|
||||||
|
this.recipients.splice(index, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
123
src/views/admin/club/newsletter/NewsletterRouting.vue
Normal file
123
src/views/admin/club/newsletter/NewsletterRouting.vue
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<MainTemplate>
|
||||||
|
<template #headerInsert>
|
||||||
|
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
|
||||||
|
</template>
|
||||||
|
<template #topBar>
|
||||||
|
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
|
||||||
|
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}</h1>
|
||||||
|
<NewsletterSyncing
|
||||||
|
:executeSyncAll="executeSyncAll"
|
||||||
|
@syncState="
|
||||||
|
(state) => {
|
||||||
|
syncState = state;
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #diffMain>
|
||||||
|
<div class="flex flex-col gap-2 grow px-7 overflow-hidden">
|
||||||
|
<div class="flex flex-col grow gap-2 overflow-hidden">
|
||||||
|
<div class="w-full flex flex-row max-lg:flex-wrap justify-center">
|
||||||
|
<RouterLink
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.route"
|
||||||
|
v-slot="{ isActive }"
|
||||||
|
:to="{ name: tab.route }"
|
||||||
|
class="w-1/2 md:w-1/3 lg:w-full p-0.5 first:pl-0 last:pr-0"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
:class="[
|
||||||
|
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-none',
|
||||||
|
isActive ? 'bg-red-200 shadow border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ tab.title }}
|
||||||
|
</p>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MainTemplate>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||||
|
import { mapActions, mapState } from "pinia";
|
||||||
|
import MainTemplate from "@/templates/Main.vue";
|
||||||
|
import { RouterLink, RouterView } from "vue-router";
|
||||||
|
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||||
|
import { useModalStore } from "@/stores/modal";
|
||||||
|
import NewsletterSyncing from "@/components/admin/club/newsletter/NewsletterSyncing.vue";
|
||||||
|
import { PrinterIcon } from "@heroicons/vue/24/outline";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
newsletterId: String,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
syncState() {
|
||||||
|
if (this.wantToClose && this.syncState == "synced") {
|
||||||
|
this.wantToClose = false;
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tabs: [
|
||||||
|
{ route: "admin-club-newsletter-overview", title: "Übersicht" },
|
||||||
|
{ route: "admin-club-newsletter-dates", title: "Termine" },
|
||||||
|
{ route: "admin-club-newsletter-recipients", title: "Empfänger" },
|
||||||
|
{ route: "admin-club-newsletter-printout", title: "Druck" },
|
||||||
|
],
|
||||||
|
wantToClose: false as boolean,
|
||||||
|
executeSyncAll: undefined as any,
|
||||||
|
syncState: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(useNewsletterStore, ["origin"]),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchNewsletterByActiveId();
|
||||||
|
},
|
||||||
|
// this.syncState is undefined, so it will never work
|
||||||
|
// beforeRouteLeave(to, from, next) {
|
||||||
|
// const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
|
||||||
|
// if (answer) {
|
||||||
|
// next()
|
||||||
|
// } else {
|
||||||
|
// next(false)
|
||||||
|
// }
|
||||||
|
// if (this.syncState != "synced") {
|
||||||
|
// this.executeSyncAll = Date.now();
|
||||||
|
// this.wantToClose = true;
|
||||||
|
// this.openInfoModal();
|
||||||
|
// next(false);
|
||||||
|
// } else {
|
||||||
|
// next();
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
methods: {
|
||||||
|
...mapActions(useNewsletterStore, ["fetchNewsletterByActiveId"]),
|
||||||
|
...mapActions(useModalStore, ["openModal"]),
|
||||||
|
openInfoModal() {
|
||||||
|
this.openModal(
|
||||||
|
markRaw(defineAsyncComponent(() => import("@/components/admin/club/newsletter/CurrentlySyncingModal.vue")))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
openDeleteModal() {
|
||||||
|
// this.openModal(
|
||||||
|
// markRaw(defineAsyncComponent(() => import("@/components/admin/club/newsletter/DeleteNewsletterModal.vue"))),
|
||||||
|
// parseInt(this.newsletterId ?? "")
|
||||||
|
// );
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
Reference in a new issue