#15-messages #22
84 changed files with 3168 additions and 80 deletions
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mitgliederverwaltung</title>
|
||||
</head>
|
||||
|
|
124
package-lock.json
generated
124
package-lock.json
generated
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "fireportal-ui",
|
||||
"name": "member-administration-ui",
|
||||
"version": "0.0.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "fireportal-ui",
|
||||
"name": "member-administration-ui",
|
||||
"version": "0.0.11",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
@ -18,6 +18,9 @@
|
|||
"@heroicons/vue": "^2.1.5",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"axios": "^0.26.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"grapesjs": "^0.22.4",
|
||||
"grapesjs-preset-newsletter": "^1.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
|
@ -37,6 +40,7 @@
|
|||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/eslint": "~9.6.0",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"@types/lodash.difference": "^4.5.9",
|
||||
"@types/lodash.differencewith": "^4.5.9",
|
||||
|
@ -3073,6 +3077,16 @@
|
|||
"integrity": "sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/backbone": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/backbone/-/backbone-1.4.15.tgz",
|
||||
"integrity": "sha512-WWeKtYlsIMtDyLbbhkb96taJMEbfQBnuz7yw1u0pkphCOtksemoWhIXhK74VRCY9hbjnsH3rsJu2uUiFtnsEYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jquery": "*",
|
||||
"@types/underscore": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.0.tgz",
|
||||
|
@ -3089,6 +3103,22 @@
|
|||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/event-source-polyfill": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz",
|
||||
"integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jquery": {
|
||||
"version": "3.5.32",
|
||||
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
|
||||
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/sizzle": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
@ -3178,12 +3208,24 @@
|
|||
"integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/sizzle": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
|
||||
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/underscore": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz",
|
||||
"integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||
|
@ -4019,6 +4061,26 @@
|
|||
"@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/backbone": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.1.tgz",
|
||||
"integrity": "sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"underscore": ">=1.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/backbone-undo": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/backbone-undo/-/backbone-undo-0.2.6.tgz",
|
||||
"integrity": "sha512-AsfpNiljLXlk7TcffDUu3EAUq7CxWbyTNwARWrql5XTzN4vh6WzEEBZYaKK4kTTz+iW1tSzqUooaGRIwO83kWA==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"backbone": ">=1.0.0",
|
||||
"underscore": ">=1.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
@ -4383,6 +4445,18 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.63.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.63.0.tgz",
|
||||
"integrity": "sha512-KlLWRPggDg2rBD1Mx7/EqEhaBdy+ybBCVh/efgjBDsPpMeEu6MbTAJzIT4TuCzvmbTEgvKOGzVT6wdBTNusqrg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/codemirror-formatting": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/codemirror-formatting/-/codemirror-formatting-1.0.0.tgz",
|
||||
"integrity": "sha512-br9yM6eJI3pJHekEnoyHaBEb1B7XxxDjju+vRyBe8QGLp5saTIXXkZ+eFCTqXSAtI8QEZDFVEX2/SOjH2sVWRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
|
@ -5335,6 +5409,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-source-polyfill": {
|
||||
"version": "1.0.31",
|
||||
"resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz",
|
||||
"integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
|
||||
|
@ -5889,6 +5969,28 @@
|
|||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/grapesjs": {
|
||||
"version": "0.22.4",
|
||||
"resolved": "https://registry.npmjs.org/grapesjs/-/grapesjs-0.22.4.tgz",
|
||||
"integrity": "sha512-4ea7T5FguyPC2fLytpSBgPXcSGreRKKisknXUbsgHBCzv4G11Z0oBJNM5jRucupBr2CRxt/3U2zixeEHEisfbw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@types/backbone": "1.4.15",
|
||||
"backbone": "1.4.1",
|
||||
"backbone-undo": "0.2.6",
|
||||
"codemirror": "5.63.0",
|
||||
"codemirror-formatting": "1.0.0",
|
||||
"html-entities": "~1.4.0",
|
||||
"promise-polyfill": "8.3.0",
|
||||
"underscore": "1.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/grapesjs-preset-newsletter": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/grapesjs-preset-newsletter/-/grapesjs-preset-newsletter-1.0.2.tgz",
|
||||
"integrity": "sha512-z8KJ1ZrTXfASSJZ/tHOcnpcWu4AMr2F/ZfQit+QjimNi3UGowwl7+Yjefuh3R7lbDTrXMMaxhCannCaJo/kPJw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/graphemer": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||
|
@ -5987,6 +6089,12 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz",
|
||||
"integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
|
||||
|
@ -7825,6 +7933,12 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/promise-polyfill": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz",
|
||||
"integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
|
@ -9330,6 +9444,12 @@
|
|||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz",
|
||||
"integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
|
|
|
@ -33,6 +33,9 @@
|
|||
"@heroicons/vue": "^2.1.5",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"axios": "^0.26.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"grapesjs": "^0.22.4",
|
||||
"grapesjs-preset-newsletter": "^1.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.difference": "^4.5.0",
|
||||
|
@ -52,6 +55,7 @@
|
|||
"@rushstack/eslint-patch": "^1.8.0",
|
||||
"@tsconfig/node20": "^20.1.4",
|
||||
"@types/eslint": "~9.6.0",
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/lodash.clonedeep": "^4.5.9",
|
||||
"@types/lodash.difference": "^4.5.9",
|
||||
"@types/lodash.differencewith": "^4.5.9",
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
</div>
|
||||
<div class="px-1 py-1 w-full flex flex-col gap-2">
|
||||
<MenuItem v-slot="{ close }">
|
||||
<RouterLink to="/account">
|
||||
<RouterLink to="/account/me">
|
||||
<button button primary @click="close">Mein Account</button>
|
||||
</RouterLink>
|
||||
</MenuItem>
|
||||
|
|
|
@ -72,6 +72,14 @@
|
|||
</p>
|
||||
<br />
|
||||
<TextCopy :copyText="generatedLink" />
|
||||
<div v-if="selectedTypes.length != 0" class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="nscdr"
|
||||
v-model="provideNSCDR"
|
||||
/>
|
||||
<label for="nscdr">Standard-Typen trotz Auswahl ausliefern</label>
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
|
@ -111,6 +119,7 @@ export default defineComponent({
|
|||
data() {
|
||||
return {
|
||||
selectedTypes: [] as Array<CalendarTypeViewModel>,
|
||||
provideNSCDR: false as boolean
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -124,7 +133,7 @@ export default defineComponent({
|
|||
},
|
||||
generatedLink() {
|
||||
let extend = this.selectedTypes.map((t) => [t.type, t.passphrase].filter((at) => at).join(":"));
|
||||
return `webcal://${host || window.location.host}/api/public/calendar${extend.length == 0 ? "" : "?types=" + extend.join("&types=")}`;
|
||||
return `webcal://${host || window.location.host}/api/public/calendar${extend.length == 0 ? "" : "?types=" + extend.join("&types=")}${this.provideNSCDR && extend.length != 0 ? '&nscdr=true':''}`;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,57 @@
|
|||
<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>
|
||||
|
||||
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
||||
</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 { useNewsletterPrintoutStore } from "@/stores/admin/newsletterPrintout";
|
||||
</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(useNewsletterPrintoutStore, ["fetchNewsletterPrintoutPreview", "fetchNewsletterPrintoutById"]),
|
||||
fetchItem() {
|
||||
this.status = "loading";
|
||||
let query: Promise<AxiosResponse<any, any>>;
|
||||
if (this.data) {
|
||||
query = this.fetchNewsletterPrintoutById(this.data);
|
||||
} else {
|
||||
query = this.fetchNewsletterPrintoutPreview();
|
||||
}
|
||||
query
|
||||
.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);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</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>
|
|
@ -15,7 +15,7 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import type { ProtocolViewModel } from "../../../../viewmodels/admin/protocol.models";
|
||||
import type { ProtocolViewModel } from "@/viewmodels/admin/protocol.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -20,8 +20,8 @@ import { useProtocolStore } from "@/stores/admin/protocol";
|
|||
import { ArrowPathIcon, CloudArrowUpIcon, CloudIcon, ExclamationTriangleIcon } from "@heroicons/vue/24/outline";
|
||||
import { useProtocolAgendaStore } from "@/stores/admin/protocolAgenda";
|
||||
import { useProtocolPresenceStore } from "@/stores/admin/protocolPresence";
|
||||
import { useProtocolDecisionStore } from "../../../../stores/admin/protocolDecision";
|
||||
import { useProtocolVotingStore } from "../../../../stores/admin/protocolVoting";
|
||||
import { useProtocolDecisionStore } from "@/stores/admin/protocolDecision";
|
||||
import { useProtocolVotingStore } from "@/stores/admin/protocolVoting";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
<p>{{ communicationType.type }}</p>
|
||||
<div class="flex flex-row">
|
||||
<RouterLink
|
||||
v-if="can('update', 'settings', 'communication')"
|
||||
:to="{ name: 'admin-settings-communication-edit', params: { id: communicationType.id } }"
|
||||
v-if="can('update', 'settings', 'communication_type')"
|
||||
:to="{ name: 'admin-settings-communication_type-edit', params: { id: communicationType.id } }"
|
||||
>
|
||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</RouterLink>
|
||||
<div v-if="can('delete', 'settings', 'communication')" @click="openDeleteModal">
|
||||
<div v-if="can('delete', 'settings', 'communication_type')" @click="openDeleteModal">
|
||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<form ref="form" class="flex flex-col h-fit w-full border border-primary rounded-md" @submit.prevent="updateUsage">
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<p>Newsletter bei Type "{{ comType.type }}" versenden/exportieren als</p>
|
||||
<div v-if="can('create','settings','newsletter_config')" class="flex flex-row justify-end w-16">
|
||||
<button v-if="status == null" type="submit" class="!p-0 !h-fit !w-fit" title="speichern">
|
||||
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
<button type="button" class="!p-0 !h-fit !w-fit" title="zurücksetzen" @click="resetForm">
|
||||
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col p-2 gap-2">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<select ref="config" id="config" :value="newsletterConfig?.config ?? 'def'">
|
||||
<option value="def">Standard (pdf nur mit Name)</option>
|
||||
<option v-for="config in configs" :key="config" :value="config">{{ config == "pdf" ? "pdf mit Adresse":config }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon } from "@heroicons/vue/24/outline";
|
||||
import { useNewsletterConfigStore } from "@/stores/admin/newsletterConfig";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/communicationType.models";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
comType: { type: Object as PropType<CommunicationTypeViewModel>, default: {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
configs: [] as Array<string>,
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
...mapState(useNewsletterConfigStore, ["config"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
newsletterConfig() {
|
||||
return this.config.find(c => c.comTypeId == this.comType.id)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.configs = Object.values(NewsletterConfigType);
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useNewsletterConfigStore, ["setNewsletterConfig", "deleteNewsletterConfig"]),
|
||||
updateUsage(e: any) {
|
||||
const fromData = e.target.elements;
|
||||
const config = fromData.config.value === "def" ? null : fromData.config.value;
|
||||
|
||||
this.status = "loading"
|
||||
let request: Promise<AxiosResponse<any, any>>
|
||||
if(config){
|
||||
request = this.setNewsletterConfig({
|
||||
comTypeId: this.comType.id,
|
||||
config: config
|
||||
})
|
||||
} else {
|
||||
request = this.deleteNewsletterConfig(this.comType.id)
|
||||
}
|
||||
request.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
(this.$refs.config as HTMLSelectElement).value = String(this.newsletterConfig?.config ?? "def");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Template erstellen</p>
|
||||
</div>
|
||||
<br />
|
||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<label for="template">Bezeichnung</label>
|
||||
<input type="text" id="template" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Beschreibung (optional)</label>
|
||||
<input type="text" id="description" />
|
||||
</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 != null">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 { useTemplateStore } from "@/stores/admin/template";
|
||||
import type { CreateTemplateViewModel } from "@/viewmodels/admin/template.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(useTemplateStore, ["createTemplate"]),
|
||||
triggerCreate(e: any) {
|
||||
let formData = e.target.elements;
|
||||
let createTemplate: CreateTemplateViewModel = {
|
||||
template: formData.template.value,
|
||||
description: formData.description.value,
|
||||
};
|
||||
this.createTemplate(createTemplate)
|
||||
.then((res) => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
this.$router.push({ name: "admin-settings-template-edit", params: { id: res.data } });
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,73 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Auszeichnung {{ template?.template }} löschen?</p>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
|
||||
unwiederuflich löschen
|
||||
</button>
|
||||
<Spinner v-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button primary-outline @click="closeModal" :disabled="status != null">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 { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
</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) {}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useModalStore, ["data"]),
|
||||
...mapState(useTemplateStore, ["templates"]),
|
||||
template() {
|
||||
return this.templates.find((t) => t.id == this.data);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useTemplateStore, ["deleteTemplate"]),
|
||||
triggerDelete() {
|
||||
this.deleteTemplate(this.data)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
88
src/components/admin/settings/template/TemplateListItem.vue
Normal file
88
src/components/admin/settings/template/TemplateListItem.vue
Normal file
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<div 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>{{ template.template }}</p>
|
||||
<div class="flex flex-row justify-end w-24">
|
||||
<RouterLink
|
||||
v-if="can('update', 'settings', 'template')"
|
||||
:to="{ name: 'admin-settings-template-edit', params: { id: template.id } }"
|
||||
>
|
||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</RouterLink>
|
||||
<button v-if="status == null" class="!p-0 !h-fit !w-fit" title="duplizieren" @click="cloneElement">
|
||||
<DocumentDuplicateIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
<div v-if="can('delete', 'settings', 'template')" @click="openDeleteModal">
|
||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col p-2">
|
||||
<div class="flex flex-row gap-2">
|
||||
<p class="min-w-16">Beschreibung:</p>
|
||||
<p class="grow overflow-hidden">{{ template.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { PencilIcon, TrashIcon, DocumentDuplicateIcon } from "@heroicons/vue/24/outline";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import type { TemplateViewModel } from "@/viewmodels/admin/template.models";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
template: { type: Object as PropType<TemplateViewModel>, default: {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useTemplateStore,["cloneTemplate"]),
|
||||
openDeleteModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/settings/template/DeleteTemplateModal.vue"))),
|
||||
this.template.id
|
||||
);
|
||||
},
|
||||
cloneElement(){
|
||||
this.cloneTemplate(this.template.id)
|
||||
.then((res) => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
this.$router.push({ name: "admin-settings-template-edit", params: { id: res.data } });
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<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>
|
||||
|
||||
<button primary-outline class="!w-fit self-end" @click="closeModal">schließen</button>
|
||||
</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 { useTemplateUsageStore } from "@/stores/admin/templateUsage";
|
||||
</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(useTemplateUsageStore, ["previewTemplateUsage"]),
|
||||
fetchItem() {
|
||||
this.status = "loading"
|
||||
this.previewTemplateUsage(this.data)
|
||||
.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);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<form ref="form" class="flex flex-col h-fit w-full border border-primary rounded-md" @submit.prevent="updateUsage">
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<p>Templates zu "{{ templateUsage.scope }}" zuweisen</p>
|
||||
<div class="flex flex-row justify-end w-16">
|
||||
<button type="button" class="!p-0 !h-fit !w-fit" title="Vorschau erzeugen" @click="previewUsage">
|
||||
<EyeIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
<button v-if="status == null && can('create','settings','newsletter_config')" type="submit" class="!p-0 !h-fit !w-fit" title="speichern">
|
||||
<ArchiveBoxArrowDownIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
<Spinner v-else-if="status == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="status?.status == 'success'" />
|
||||
<FailureXMark v-else-if="status?.status == 'failed'" />
|
||||
<button type="button" v-if="can('create','settings','newsletter_config')" class="!p-0 !h-fit !w-fit" title="zurücksetzen" @click="resetForm">
|
||||
<ArchiveBoxXMarkIcon class="w-5 h-5 p-1 box-content pointer-events-none" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col p-2 gap-2">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Kopfzeile:</p>
|
||||
<select ref="header" id="header" :value="templateUsage.header?.id ?? 'def'">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">{{ template.template }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Hauptteil:</p>
|
||||
<select ref="body" id="body" :value="templateUsage.body?.id ?? 'def'">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">{{ template.template }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<p class="min-w-16">Fußzeile:</p>
|
||||
<select ref="footer" id="footer" :value="templateUsage.footer?.id ?? 'def'">
|
||||
<option value="def">Standard-Vorlage verwenden</option>
|
||||
<option v-for="template in templates" :key="template.id" :value="template.id">{{ template.template }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { ArchiveBoxArrowDownIcon, ArchiveBoxXMarkIcon, EyeIcon } from "@heroicons/vue/24/outline";
|
||||
import type { TemplateUsageViewModel } from "@/viewmodels/admin/templateUsage.models";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
import { useTemplateUsageStore } from "@/stores/admin/templateUsage";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
templateUsage: { type: Object as PropType<TemplateUsageViewModel>, default: {} },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useTemplateStore, ["templates"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useTemplateUsageStore, ["updateTemplateUsage"]),
|
||||
previewUsage() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/settings/templateUsage/TemplatePreviewModal.vue"))),
|
||||
this.templateUsage.scope
|
||||
)
|
||||
},
|
||||
updateUsage(e: any) {
|
||||
const fromData = e.target.elements;
|
||||
const headerId = fromData.header.value === "def" ? null : fromData.header.value;
|
||||
const bodyId = fromData.body.value === "def" ? null : fromData.body.value;
|
||||
const footerId = fromData.footer.value === "def" ? null : fromData.footer.value;
|
||||
|
||||
this.status = "loading"
|
||||
this.updateTemplateUsage({
|
||||
scope: this.templateUsage.scope,
|
||||
headerId: headerId,
|
||||
bodyId: bodyId,
|
||||
footerId: footerId,
|
||||
})
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
resetForm() {
|
||||
(this.$refs.header as HTMLSelectElement).value = String(this.templateUsage.header?.id ?? "def");
|
||||
(this.$refs.body as HTMLSelectElement).value = String(this.templateUsage.body?.id ?? "def");
|
||||
(this.$refs.footer as HTMLSelectElement).value = String(this.templateUsage.footer?.id ?? "def");
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -86,7 +86,7 @@
|
|||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||
import {
|
||||
ArchiveBoxArrowDownIcon,
|
||||
CommandLineIcon,
|
||||
|
@ -97,8 +97,8 @@ import {
|
|||
TrashIcon,
|
||||
SparklesIcon,
|
||||
} from "@heroicons/vue/24/outline";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import { useModalStore } from "../../stores/modal";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import Table from "./Table.vue";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -60,8 +60,8 @@ import {
|
|||
type ConditionValue,
|
||||
type WhereOperation,
|
||||
type WhereType,
|
||||
} from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
} from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import { PlusIcon } from "@heroicons/vue/24/outline";
|
||||
import JoinTable from "./JoinTable.vue";
|
||||
</script>
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import Table from "./Table.vue";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import { joinTableName } from "@/helpers/queryFormatter";
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { ConditionStructure, WhereType } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { ConditionStructure, WhereType } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import NestedWhere from "./NestedWhere.vue";
|
||||
</script>
|
||||
|
|
|
@ -32,8 +32,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { ConditionStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { ConditionStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import NestedCondition from "./NestedCondition.vue";
|
||||
import Condition from "./Condition.vue";
|
||||
import { PlusIcon, RectangleStackIcon } from "@heroicons/vue/24/outline";
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { OrderByStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { OrderByStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import OrderStructure from "./OrderStructure.vue";
|
||||
import { PlusIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
|
|
@ -21,8 +21,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { OrderByStructure, OrderByType } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { OrderByStructure, OrderByType } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { ConditionStructure, DynamicQueryStructure, OrderByStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { ConditionStructure, DynamicQueryStructure, OrderByStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import ColumnSelect from "./ColumnSelect.vue";
|
||||
import Where from "./Where.vue";
|
||||
import Order from "./Order.vue";
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState } from "pinia";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
|
@ -33,8 +33,8 @@
|
|||
<script setup lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import type { ConditionStructure } from "../../types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "../../stores/admin/queryBuilder";
|
||||
import type { ConditionStructure } from "@/types/dynamicQueries";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import NestedCondition from "./NestedCondition.vue";
|
||||
import Condition from "./Condition.vue";
|
||||
import { PlusIcon, RectangleStackIcon } from "@heroicons/vue/24/outline";
|
||||
|
|
4
src/enums/newsletterConfigType.ts
Normal file
4
src/enums/newsletterConfigType.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export enum NewsletterConfigType {
|
||||
pdf = "pdf",
|
||||
mail = "mail",
|
||||
}
|
144
src/helpers/grapesEditor.ts
Normal file
144
src/helpers/grapesEditor.ts
Normal file
|
@ -0,0 +1,144 @@
|
|||
import type { AddComponentTypeOptions, BlockProperties, Editor } from "grapesjs";
|
||||
|
||||
export function configureEditor(editor: Editor): void {
|
||||
//editor.Panels.getPanel("devices-c")?.set("visible", false);
|
||||
editor.Panels.removeButton("devices-c", "set-device-mobile");
|
||||
editor.Panels.removeButton("devices-c", "set-device-desktop");
|
||||
editor.Panels.removeButton("views", "open-tm");
|
||||
editor.Panels.removeButton("views", "open-layers");
|
||||
editor.Panels.removeButton("options", "export-template");
|
||||
editor.Panels.removeButton("options", "preview");
|
||||
// editor.Panels.removeButton("options", "fullscreen");
|
||||
editor.Panels.removeButton("options", "gjs-open-import-template");
|
||||
editor.Panels.removeButton("options", "gjs-toggle-images");
|
||||
editor.BlockManager.remove("button");
|
||||
editor.BlockManager.remove("image");
|
||||
editor.BlockManager.remove("link-block");
|
||||
editor.BlockManager.remove("list-items");
|
||||
editor.BlockManager.remove("grid-items");
|
||||
editor.BlockManager.remove("sect37");
|
||||
editor.BlockManager.remove("text-sect");
|
||||
|
||||
editor.BlockManager.get("text").set("category", "Text");
|
||||
editor.BlockManager.get("quote").set("category", "Text");
|
||||
editor.BlockManager.get("link").set("category", "Text");
|
||||
editor.BlockManager.get("sect100").set("category", "Struktur");
|
||||
editor.BlockManager.get("sect50").set("category", "Struktur");
|
||||
editor.BlockManager.get("sect30").set("category", "Struktur");
|
||||
editor.BlockManager.get("divider").set("category", "Struktur");
|
||||
editor.BlockManager.add("heading1-block", heading1_block);
|
||||
editor.BlockManager.add("heading2-block", heading2_block);
|
||||
editor.BlockManager.add("heading3-block", heading3_block);
|
||||
editor.BlockManager.add("list_start", list_start_block);
|
||||
editor.DomComponents.addType("list_end", list_end_block.type);
|
||||
editor.BlockManager.add("list_end", list_end_block.block);
|
||||
editor.BlockManager.add("list-block", list_block);
|
||||
editor.BlockManager.add("list-inner-block", list_inner_block);
|
||||
}
|
||||
|
||||
const heading1_block: BlockProperties = {
|
||||
label: "Heading1",
|
||||
category: "Text",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.243 4.493v7.5m0 0v7.502m0-7.501h10.5m0-7.5v7.5m0 0v7.501m4.501-8.627 2.25-1.5v10.126m0 0h-2.25m2.25 0h2.25" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<h1 class="heading">Heading 1</h1>
|
||||
`,
|
||||
};
|
||||
|
||||
const heading2_block: BlockProperties = {
|
||||
label: "Heading2",
|
||||
category: "Text",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" >
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.75 19.5H16.5v-1.609a2.25 2.25 0 0 1 1.244-2.012l2.89-1.445c.651-.326 1.116-.955 1.116-1.683 0-.498-.04-.987-.118-1.463-.135-.825-.835-1.422-1.668-1.489a15.202 15.202 0 0 0-3.464.12M2.243 4.492v7.5m0 0v7.502m0-7.501h10.5m0-7.5v7.5m0 0v7.501" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<h2 class="heading">Heading 2</h2>
|
||||
`,
|
||||
};
|
||||
|
||||
const heading3_block: BlockProperties = {
|
||||
label: "Heading3",
|
||||
category: "Text",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.905 14.626a4.52 4.52 0 0 1 .738 3.603c-.154.695-.794 1.143-1.504 1.208a15.194 15.194 0 0 1-3.639-.104m4.405-4.707a4.52 4.52 0 0 0 .738-3.603c-.154-.696-.794-1.144-1.504-1.209a15.19 15.19 0 0 0-3.639.104m4.405 4.708H18M2.243 4.493v7.5m0 0v7.502m0-7.501h10.5m0-7.5v7.5m0 0v7.501" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<h3 class="heading">Heading 3</h3>
|
||||
`,
|
||||
};
|
||||
|
||||
const list_start_block: BlockProperties = {
|
||||
label: "Anfang WDH",
|
||||
category: "Struktur",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<liststart style="font-style: italic; display: block;">
|
||||
WDH Start: LISTENNAME
|
||||
</liststart>
|
||||
`,
|
||||
};
|
||||
|
||||
const list_end_block: { type: AddComponentTypeOptions; block: BlockProperties } = {
|
||||
type: {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: "listend",
|
||||
content: "WDH ENDE",
|
||||
editable: false,
|
||||
},
|
||||
},
|
||||
isComponent(el) {
|
||||
return el.tagName === "listend";
|
||||
},
|
||||
},
|
||||
block: {
|
||||
label: "Ende WDH",
|
||||
content: { type: "list_end" },
|
||||
category: "Struktur",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15m-3 0-3-3m0 0 3-3m-3 3H15" />
|
||||
</svg>
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
const list_block: BlockProperties = {
|
||||
label: "Liste",
|
||||
category: "Struktur",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<ul>
|
||||
<li>Element</li>
|
||||
</ul>
|
||||
`,
|
||||
};
|
||||
|
||||
const list_inner_block: BlockProperties = {
|
||||
label: "Listenelement",
|
||||
category: "Text",
|
||||
media: `
|
||||
<svg fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||
</svg>
|
||||
`,
|
||||
content: `
|
||||
<li>Element</li>
|
||||
`,
|
||||
};
|
|
@ -11,7 +11,7 @@ export function flattenQueryResult(result: Array<QueryResult>): Array<{ [key: st
|
|||
|
||||
for (const key in row) {
|
||||
const value = row[key];
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
const newKey = prefix ? `${prefix}_${key}` : key;
|
||||
|
||||
if (Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null)) {
|
||||
console.log(value, newKey);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export const toolbarOptions = [
|
||||
[/*{ header: [1, 2, false] },*/ { font: [] }],
|
||||
[{ header: [1, 2, 3, 4, false] }, { font: [] }],
|
||||
//[{ header: 1 }, { header: 2 }],
|
||||
["bold", "italic", "underline", "strike"],
|
||||
["blockquote", "code-block", "link"],
|
||||
|
|
|
@ -98,12 +98,12 @@ select[disabled] {
|
|||
|
||||
details {
|
||||
user-select: none;
|
||||
& summary svg {
|
||||
& summary svg[indicator] {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
details[open] {
|
||||
& summary svg {
|
||||
& summary svg[indicator] {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
|
|||
next();
|
||||
} else {
|
||||
NProgress.done();
|
||||
next(false);
|
||||
next({ name: "admin-default" });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { abilityAndNavUpdate } from "./adminGuard";
|
|||
import type { PermissionType, PermissionSection, PermissionModule } from "@/types/permissionTypes";
|
||||
import { resetMemberStores, setMemberId } from "./memberGuard";
|
||||
import { resetProtocolStores, setProtocolId } from "./protocolGuard";
|
||||
import { resetNewsletterStores, setNewsletterId } from "./newsletterGuard";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
|
@ -174,10 +175,57 @@ const router = createRouter({
|
|||
},
|
||||
{
|
||||
path: "newsletter",
|
||||
name: "admin-club-newsletter",
|
||||
component: () => import("@/views/admin/ViewSelect.vue"),
|
||||
name: "admin-club-newsletter-route",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
meta: { type: "read", section: "club", module: "newsletter" },
|
||||
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: "data",
|
||||
name: "admin-club-newsletter-data",
|
||||
component: () => import("@/views/admin/club/newsletter/NewsletterData.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",
|
||||
|
@ -403,6 +451,48 @@ const router = createRouter({
|
|||
meta: { type: "read", section: "settings", module: "query_store" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: "template",
|
||||
name: "admin-settings-template-route",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
meta: { type: "read", section: "settings", module: "template" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "admin-settings-template",
|
||||
component: () => import("@/views/admin/settings/template/Template.vue"),
|
||||
},
|
||||
{
|
||||
path: "info",
|
||||
name: "admin-settings-template-info",
|
||||
component: () => import("@/views/admin/settings/template/UsageInfo.vue"),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
name: "admin-settings-template-edit",
|
||||
component: () => import("@/views/admin/settings/template/TemplateEdit.vue"),
|
||||
meta: { type: "update", section: "settings", module: "template" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "template-usage",
|
||||
name: "admin-settings-template_usage",
|
||||
component: () => import("@/views/admin/settings/templateUsage/TemplateUsage.vue"),
|
||||
meta: { type: "read", section: "settings", module: "template_usage" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: "newsletter-config",
|
||||
name: "admin-settings-newsletter_config",
|
||||
component: () => import("@/views/admin/settings/newsletterConfig/NewsletterConfig.vue"),
|
||||
meta: { type: "read", section: "settings", module: "newsletter_config" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
31
src/router/newsletterGuard.ts
Normal file
31
src/router/newsletterGuard.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||
import { useNewsletterDatesStore } from "@/stores/admin/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "@/stores/admin/newsletterRecipients";
|
||||
import { useNewsletterPrintoutStore } from "../stores/admin/newsletterPrintout";
|
||||
|
||||
export async function setNewsletterId(to: any, from: any, next: any) {
|
||||
const newsletter = useNewsletterStore();
|
||||
newsletter.activeNewsletter = to.params?.newsletterId ?? null;
|
||||
|
||||
useNewsletterDatesStore().$reset();
|
||||
useNewsletterRecipientsStore().$reset();
|
||||
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
|
||||
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
|
||||
useNewsletterPrintoutStore().$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();
|
||||
useNewsletterPrintoutStore().unsubscribePdfPrintingProgress();
|
||||
useNewsletterPrintoutStore().unsubscribeMailSendingProgress();
|
||||
useNewsletterPrintoutStore().$reset();
|
||||
|
||||
next();
|
||||
}
|
|
@ -2,6 +2,7 @@ import axios from "axios";
|
|||
import { isAuthenticatedPromise, type Payload } from "./router/authGuard";
|
||||
import router from "./router";
|
||||
import { useNotificationStore } from "./stores/notification";
|
||||
import { EventSourcePolyfill } from "event-source-polyfill";
|
||||
|
||||
let devMode = process.env.NODE_ENV === "development";
|
||||
|
||||
|
@ -88,4 +89,13 @@ export async function refreshToken(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export { http, host };
|
||||
function newEventSource(path: string) {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
return new EventSourcePolyfill(url + "/api" + path, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { http, newEventSource, host };
|
||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
|||
} from "@/viewmodels/admin/communicationType.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { CommunicationFieldType } from "../../types/fieldTypes";
|
||||
import type { CommunicationFieldType } from "@/types/fieldTypes";
|
||||
|
||||
export const useCommunicationTypeStore = defineStore("communicationType", {
|
||||
state: () => {
|
||||
|
|
|
@ -46,7 +46,7 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
resetNavigation() {
|
||||
this.$reset();
|
||||
},
|
||||
updateTopLevel(first: boolean = false) {
|
||||
updateTopLevel() {
|
||||
const abilityStore = useAbilityStore();
|
||||
this.topLevel = [
|
||||
...(abilityStore.canSection("read", "club")
|
||||
|
@ -77,10 +77,12 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
]
|
||||
: []),
|
||||
];
|
||||
if (this.topLevel.findIndex((e) => e.key == this.activeNavigation) == -1 && !first)
|
||||
if (this.topLevel.findIndex((e) => e.key == this.activeNavigation) == -1) {
|
||||
this.activeNavigation = this.topLevel[0]?.key ?? "club";
|
||||
router.push({ name: `admin-${this.topLevel[0]?.key ?? "club"}-default` });
|
||||
}
|
||||
},
|
||||
updateNavigation(first: boolean = false) {
|
||||
updateNavigation() {
|
||||
const abilityStore = useAbilityStore();
|
||||
this.navigation = {
|
||||
club: {
|
||||
|
@ -113,6 +115,13 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
? [{ key: "calendar_type", title: "Terminarten" }]
|
||||
: []),
|
||||
...(abilityStore.can("read", "settings", "query") ? [{ key: "query_store", title: "Query Store" }] : []),
|
||||
...(abilityStore.can("read", "settings", "template") ? [{ key: "template", title: "Templates" }] : []),
|
||||
...(abilityStore.can("read", "settings", "template_usage")
|
||||
? [{ key: "template_usage", title: "Template-Verwendung" }]
|
||||
: []),
|
||||
...(abilityStore.can("read", "settings", "newsletter_config")
|
||||
? [{ key: "newsletter_config", title: "Newsletter Konfiguration" }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
user: {
|
||||
|
@ -123,8 +132,13 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
],
|
||||
},
|
||||
} as navigationModel;
|
||||
if (this.activeNavigationObject.main.findIndex((e) => e.key == this.activeLink) == -1 && !first)
|
||||
router.push({ name: `admin-${this.activeNavigation}-default` });
|
||||
if (
|
||||
this.activeNavigationObject.main.findIndex((e) => e.key == this.activeLink) == -1 ||
|
||||
this.activeLink == "default"
|
||||
) {
|
||||
let link = this.activeNavigationObject.main[0].key;
|
||||
router.push({ name: `admin-${this.activeNavigation}-${link}` });
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
105
src/stores/admin/newsletter.ts
Normal file
105
src/stores/admin/newsletter.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type { CreateNewsletterViewModel, SyncNewsletterViewModel } from "@/viewmodels/admin/newsletter.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { NewsletterViewModel } from "@/viewmodels/admin/newsletter.models";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import difference from "lodash.difference";
|
||||
|
||||
export const useNewsletterStore = defineStore("newsletter", {
|
||||
state: () => {
|
||||
return {
|
||||
newsletters: [] as Array<NewsletterViewModel & { tab_pos: number }>,
|
||||
totalCount: 0 as number,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
activeNewsletter: null as number | null,
|
||||
activeNewsletterObj: null as NewsletterViewModel | null,
|
||||
origin: null as NewsletterViewModel | null,
|
||||
loadingActive: "loading" as "loading" | "fetched" | "failed",
|
||||
syncingNewsletter: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
detectedChangeNewsletter: (state) =>
|
||||
!isEqual(state.origin, state.activeNewsletterObj) && state.syncingNewsletter != "syncing",
|
||||
},
|
||||
actions: {
|
||||
setNewsletterSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||
this.syncingNewsletter = state;
|
||||
},
|
||||
fetchNewsletters(offset = 0, count = 25, clear = false) {
|
||||
if (clear) this.newsletters = [];
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get(`/admin/newsletter?offset=${offset}&count=${count}`)
|
||||
.then((result) => {
|
||||
this.totalCount = result.data.total;
|
||||
result.data.newsletters
|
||||
.filter((elem: NewsletterViewModel) => this.newsletters.findIndex((m) => m.id == elem.id) == -1)
|
||||
.map((elem: NewsletterViewModel, index: number): NewsletterViewModel & { tab_pos: number } => {
|
||||
return {
|
||||
...elem,
|
||||
tab_pos: index + offset,
|
||||
};
|
||||
})
|
||||
.forEach((elem: NewsletterViewModel & { tab_pos: number }) => {
|
||||
this.newsletters.push(elem);
|
||||
});
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterByActiveId() {
|
||||
this.loadingActive = "loading";
|
||||
http
|
||||
.get(`/admin/newsletter/${this.activeNewsletter}`)
|
||||
.then((res) => {
|
||||
this.origin = res.data;
|
||||
this.activeNewsletterObj = cloneDeep(this.origin);
|
||||
this.loadingActive = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loadingActive = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterById(id: number) {
|
||||
return http.get(`/admin/newsletter/${id}`);
|
||||
},
|
||||
async createNewsletter(newsletter: CreateNewsletterViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/newsletter`, {
|
||||
title: newsletter.title,
|
||||
});
|
||||
this.fetchNewsletters();
|
||||
return result;
|
||||
},
|
||||
async synchronizeActiveNewsletter(): Promise<void> {
|
||||
if (this.origin == null || this.activeNewsletterObj == null) return;
|
||||
|
||||
this.syncingNewsletter = "syncing";
|
||||
await http
|
||||
.patch(`/admin/newsletter/${this.origin.id}/synchronize`, {
|
||||
title: this.activeNewsletterObj.title,
|
||||
description: this.activeNewsletterObj.description,
|
||||
newsletterTitle: this.activeNewsletterObj.newsletterTitle,
|
||||
newsletterText: this.activeNewsletterObj.newsletterText,
|
||||
newsletterSignatur: this.activeNewsletterObj.newsletterSignatur,
|
||||
recipientsByQueryId: this.activeNewsletterObj.recipientsByQueryId,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingNewsletter = "synced";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.syncingNewsletter = "failed";
|
||||
});
|
||||
this.fetchNewsletterById(this.origin.id)
|
||||
.then((res) => {
|
||||
this.origin = res.data;
|
||||
if (this.detectedChangeNewsletter) this.syncingNewsletter = "detectedChanges";
|
||||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
},
|
||||
});
|
46
src/stores/admin/newsletterConfig.ts
Normal file
46
src/stores/admin/newsletterConfig.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type {
|
||||
SetNewsletterConfigViewModel,
|
||||
NewsletterConfigViewModel,
|
||||
} from "@/viewmodels/admin/newsletterConfig.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const useNewsletterConfigStore = defineStore("newsletterConfi", {
|
||||
state: () => {
|
||||
return {
|
||||
config: [] as Array<NewsletterConfigViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchNewsletterConfigs() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/newsletterconfig")
|
||||
.then((result) => {
|
||||
this.config = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterConfigById(id: number): Promise<AxiosResponse<any, any>> {
|
||||
return http.get(`/admin/newsletterconfig/${id}`);
|
||||
},
|
||||
async setNewsletterConfig(setConfig: SetNewsletterConfigViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.put(`/admin/newsletterconfig`, {
|
||||
comTypeId: setConfig.comTypeId,
|
||||
config: setConfig.config,
|
||||
});
|
||||
this.fetchNewsletterConfigs();
|
||||
return result;
|
||||
},
|
||||
async deleteNewsletterConfig(newsletterConfigStore: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.delete(`/admin/newsletterconfig/${newsletterConfigStore}`);
|
||||
this.fetchNewsletterConfigs();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
73
src/stores/admin/newsletterDates.ts
Normal file
73
src/stores/admin/newsletterDates.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { NewsletterDatesViewModel, SyncNewsletterDatesViewModel } from "@/viewmodels/admin/newsletterDates.models";
|
||||
import { useNewsletterStore } from "./newsletter";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import differenceWith from "lodash.differencewith";
|
||||
|
||||
export const useNewsletterDatesStore = defineStore("newsletterDates", {
|
||||
state: () => {
|
||||
return {
|
||||
dates: [] as Array<NewsletterDatesViewModel>,
|
||||
origin: [] as Array<NewsletterDatesViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
syncingNewsletterDates: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
detectedChangeNewsletterDates: (state) =>
|
||||
!isEqual(
|
||||
state.origin.sort(
|
||||
(a: NewsletterDatesViewModel, b: NewsletterDatesViewModel) =>
|
||||
new Date(a.calendar.starttime).getTime() - new Date(b.calendar.starttime).getTime()
|
||||
),
|
||||
state.dates.sort(
|
||||
(a: NewsletterDatesViewModel, b: NewsletterDatesViewModel) =>
|
||||
new Date(a.calendar.starttime).getTime() - new Date(b.calendar.starttime).getTime()
|
||||
)
|
||||
) && state.syncingNewsletterDates != "syncing",
|
||||
},
|
||||
actions: {
|
||||
setNewsletterDatesSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||
this.syncingNewsletterDates = state;
|
||||
},
|
||||
fetchNewsletterDates() {
|
||||
this.loading = "loading";
|
||||
this.fetchNewsletterDatesPromise()
|
||||
.then((result) => {
|
||||
this.origin = result.data;
|
||||
this.dates = cloneDeep(this.origin);
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterDatesPromise() {
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
return http.get(`/admin/newsletter/${newsletterId}/dates`);
|
||||
},
|
||||
async synchronizeActiveNewsletterDates() {
|
||||
this.syncingNewsletterDates = "syncing";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
|
||||
await http
|
||||
.patch(`/admin/newsletter/${newsletterId}/synchronize/dates`, {
|
||||
dates: this.dates,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingNewsletterDates = "synced";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.syncingNewsletterDates = "failed";
|
||||
});
|
||||
this.fetchNewsletterDatesPromise()
|
||||
.then((res) => {
|
||||
this.origin = res.data;
|
||||
if (this.detectedChangeNewsletterDates) this.syncingNewsletterDates = "detectedChanges";
|
||||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
},
|
||||
});
|
127
src/stores/admin/newsletterPrintout.ts
Normal file
127
src/stores/admin/newsletterPrintout.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http, newEventSource } from "@/serverCom";
|
||||
import { useNewsletterStore } from "./newsletter";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { EventSourcePolyfill } from "event-source-polyfill";
|
||||
|
||||
export const useNewsletterPrintoutStore = defineStore("newsletterPrintout", {
|
||||
state: () => {
|
||||
return {
|
||||
printout: [] as Array<string>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
printing: undefined as undefined | "loading" | "success" | "failed",
|
||||
sending: undefined as undefined | "loading" | "success" | "failed",
|
||||
sendingPreview: undefined as undefined | "loading" | "success" | "failed",
|
||||
pdfProgessSource: undefined as undefined | EventSourcePolyfill,
|
||||
mailProgessSource: undefined as undefined | EventSourcePolyfill,
|
||||
pdfSourceMessages: [] as Array<Object>,
|
||||
mailSourceMessages: [] as Array<Object>,
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchNewsletterPrintout() {
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get(`/admin/newsletter/${newsletterId}/printouts`)
|
||||
.then((result) => {
|
||||
this.printout = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterPrintoutById(printout: string): Promise<AxiosResponse<any, any>> {
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
return http.get(`/admin/newsletter/${newsletterId}/printout/${printout}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
fetchNewsletterPrintoutPreview(): Promise<AxiosResponse<any, any>> {
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
return http.get(`/admin/newsletter/${newsletterId}/printoutpreview`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
createNewsletterMailPreview() {
|
||||
this.sendingPreview = "loading";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
if (newsletterId == null) return;
|
||||
return http
|
||||
.post(`/admin/newsletter/${newsletterId}/mailpreview`)
|
||||
.then((res) => {
|
||||
this.sendingPreview = "success";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.sendingPreview = "failed";
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.sendingPreview = undefined;
|
||||
}, 1500);
|
||||
});
|
||||
},
|
||||
createNewsletterPrintout() {
|
||||
this.printing = "loading";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
if (newsletterId == null) return;
|
||||
return http
|
||||
.post(`/admin/newsletter/${newsletterId}/printout`)
|
||||
.then((res) => {
|
||||
this.fetchNewsletterPrintout();
|
||||
this.printing = "success";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.printing = "failed";
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.printing = undefined;
|
||||
}, 1500);
|
||||
});
|
||||
},
|
||||
createNewsletterSend() {
|
||||
this.sending = "loading";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
if (newsletterId == null) return;
|
||||
return http
|
||||
.post(`/admin/newsletter/${newsletterId}/send`)
|
||||
.then((res) => {
|
||||
this.sending = "success";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.sending = "failed";
|
||||
})
|
||||
.finally(() => {
|
||||
setTimeout(() => {
|
||||
this.sending = undefined;
|
||||
}, 1500);
|
||||
});
|
||||
},
|
||||
subscribePdfPrintingProgress() {
|
||||
// const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
// if (this.pdfProgessSource != undefined) return;
|
||||
// this.pdfProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/printoutprogress`);
|
||||
// this.pdfProgessSource.onmessage = (event) => {
|
||||
// console.log("pdf", event);
|
||||
// };
|
||||
},
|
||||
subscribeMailSendingProgress() {
|
||||
// const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
// if (this.mailProgessSource != undefined) return;
|
||||
// this.mailProgessSource = newEventSource(`/admin/newsletter/${newsletterId}/sendprogress`);
|
||||
// this.mailProgessSource.onmessage = (event) => {
|
||||
// console.log("mail", event);
|
||||
// };
|
||||
},
|
||||
unsubscribePdfPrintingProgress() {
|
||||
this.pdfProgessSource?.close();
|
||||
this.pdfProgessSource = undefined;
|
||||
},
|
||||
unsubscribeMailSendingProgress() {
|
||||
this.mailProgessSource?.close();
|
||||
this.mailProgessSource = undefined;
|
||||
},
|
||||
},
|
||||
});
|
65
src/stores/admin/newsletterRecipients.ts
Normal file
65
src/stores/admin/newsletterRecipients.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type {
|
||||
NewsletterRecipientsViewModel,
|
||||
SyncNewsletterRecipientsViewModel,
|
||||
} from "@/viewmodels/admin/newsletterRecipients.models";
|
||||
import { useNewsletterStore } from "./newsletter";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
||||
export const useNewsletterRecipientsStore = defineStore("newsletterRecipients", {
|
||||
state: () => {
|
||||
return {
|
||||
recipients: [] as Array<number>,
|
||||
origin: [] as Array<number>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
syncingNewsletterRecipients: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
detectedChangeNewsletterRecipients: (state) =>
|
||||
!isEqual(state.origin, state.recipients) && state.syncingNewsletterRecipients != "syncing",
|
||||
},
|
||||
actions: {
|
||||
setNewsletterRecipientsSyncingState(state: "synced" | "syncing" | "detectedChanges" | "failed") {
|
||||
this.syncingNewsletterRecipients = state;
|
||||
},
|
||||
fetchNewsletterRecipients() {
|
||||
this.loading = "loading";
|
||||
this.fetchNewsletterRecipientsPromise()
|
||||
.then((result) => {
|
||||
this.origin = result.data.map((d: NewsletterRecipientsViewModel) => d.memberId);
|
||||
this.recipients = cloneDeep(this.origin);
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchNewsletterRecipientsPromise() {
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
return http.get(`/admin/newsletter/${newsletterId}/recipients`);
|
||||
},
|
||||
async synchronizeActiveNewsletterRecipients() {
|
||||
this.syncingNewsletterRecipients = "syncing";
|
||||
const newsletterId = useNewsletterStore().activeNewsletter;
|
||||
await http
|
||||
.patch(`/admin/newsletter/${newsletterId}/synchronize/recipients`, {
|
||||
recipients: this.recipients,
|
||||
})
|
||||
.then((res) => {
|
||||
this.syncingNewsletterRecipients = "synced";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.syncingNewsletterRecipients = "failed";
|
||||
});
|
||||
this.fetchNewsletterRecipientsPromise()
|
||||
.then((result) => {
|
||||
this.origin = result.data.map((d: NewsletterRecipientsViewModel) => d.memberId);
|
||||
if (this.detectedChangeNewsletterRecipients) this.syncingNewsletterRecipients = "detectedChanges";
|
||||
})
|
||||
.catch((err) => {});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,9 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type {
|
||||
ProtocolAgendaViewModel,
|
||||
SyncProtocolAgendaViewModel,
|
||||
} from "../../viewmodels/admin/protocolAgenda.models";
|
||||
import type { ProtocolAgendaViewModel, SyncProtocolAgendaViewModel } from "@/viewmodels/admin/protocolAgenda.models";
|
||||
import { useProtocolStore } from "./protocol";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { AxiosResponse } from "axios";
|
|||
import type {
|
||||
ProtocolDecisionViewModel,
|
||||
SyncProtocolDecisionViewModel,
|
||||
} from "../../viewmodels/admin/protocolDecision.models";
|
||||
} from "@/viewmodels/admin/protocolDecision.models";
|
||||
import { useProtocolStore } from "./protocol";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { AxiosResponse } from "axios";
|
|||
import type {
|
||||
ProtocolPresenceViewModel,
|
||||
SyncProtocolPresenceViewModel,
|
||||
} from "../../viewmodels/admin/protocolPresence.models";
|
||||
} from "@/viewmodels/admin/protocolPresence.models";
|
||||
import { useProtocolStore } from "./protocol";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { ProtocolPrintoutViewModel } from "../../viewmodels/admin/protocolPrintout.models";
|
||||
import type { ProtocolPrintoutViewModel } from "@/viewmodels/admin/protocolPrintout.models";
|
||||
import { useProtocolStore } from "./protocol";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type {
|
||||
ProtocolVotingViewModel,
|
||||
SyncProtocolVotingViewModel,
|
||||
} from "../../viewmodels/admin/protocolVoting.models";
|
||||
import type { ProtocolVotingViewModel, SyncProtocolVotingViewModel } from "@/viewmodels/admin/protocolVoting.models";
|
||||
import { useProtocolStore } from "./protocol";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { TableMeta } from "../../viewmodels/admin/query.models";
|
||||
import type { DynamicQueryStructure, FieldType } from "../../types/dynamicQueries";
|
||||
import { flattenQueryResult } from "../../helpers/queryFormatter";
|
||||
import type { TableMeta } from "@/viewmodels/admin/query.models";
|
||||
import type { DynamicQueryStructure, FieldType } from "@/types/dynamicQueries";
|
||||
|
||||
export const useQueryBuilderStore = defineStore("queryBuilder", {
|
||||
state: () => {
|
||||
|
@ -31,23 +30,21 @@ export const useQueryBuilderStore = defineStore("queryBuilder", {
|
|||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
sendQuery(offset = 0, count = 25) {
|
||||
sendQuery(offset = 0, count = 25, query?: DynamicQueryStructure | string) {
|
||||
this.queryError = "";
|
||||
this.data = [];
|
||||
this.totalLength = 0;
|
||||
if (this.query == undefined || this.query == "" || (typeof this.query != "string" && this.query.table == ""))
|
||||
let queryToSend = query ?? this.query;
|
||||
if (queryToSend == undefined || queryToSend == "" || (typeof queryToSend != "string" && queryToSend.table == ""))
|
||||
return;
|
||||
this.loadingData = "loading";
|
||||
http
|
||||
.post(`/admin/querybuilder/query?offset=${offset}&count=${count}`, {
|
||||
query: this.query,
|
||||
query: queryToSend,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data.stats == "success") {
|
||||
this.data = flattenQueryResult(result.data.rows).map((row) => ({
|
||||
id: row.id ?? "", // Ensure id is present
|
||||
...row,
|
||||
}));
|
||||
this.data = result.data.rows;
|
||||
this.totalLength = result.data.total;
|
||||
this.loadingData = "fetched";
|
||||
} else {
|
||||
|
|
64
src/stores/admin/template.ts
Normal file
64
src/stores/admin/template.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type {
|
||||
CreateTemplateViewModel,
|
||||
TemplateViewModel,
|
||||
UpdateTemplateViewModel,
|
||||
} from "@/viewmodels/admin/template.models";
|
||||
|
||||
export const useTemplateStore = defineStore("template", {
|
||||
state: () => {
|
||||
return {
|
||||
templates: [] as Array<TemplateViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchTemplates() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/template")
|
||||
.then((result) => {
|
||||
this.templates = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchTemplateById(id: number): Promise<AxiosResponse<any, any>> {
|
||||
return http.get(`/admin/template/${id}`);
|
||||
},
|
||||
async createTemplate(template: CreateTemplateViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/template`, {
|
||||
template: template.template,
|
||||
description: template.description,
|
||||
});
|
||||
this.fetchTemplates();
|
||||
return result;
|
||||
},
|
||||
async updateActiveTemplate(template: UpdateTemplateViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.patch(`/admin/template/${template.id}`, {
|
||||
template: template.template,
|
||||
description: template.description,
|
||||
design: template.design,
|
||||
html: template.html,
|
||||
});
|
||||
this.fetchTemplates();
|
||||
return result;
|
||||
},
|
||||
async cloneTemplate(cloneId: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/template/clone`, {
|
||||
cloneId: cloneId,
|
||||
});
|
||||
this.fetchTemplates();
|
||||
return result;
|
||||
},
|
||||
async deleteTemplate(template: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.delete(`/admin/template/${template}`);
|
||||
this.fetchTemplates();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
43
src/stores/admin/templateUsage.ts
Normal file
43
src/stores/admin/templateUsage.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import type { CreateTemplateViewModel, UpdateTemplateViewModel } from "@/viewmodels/admin/template.models";
|
||||
import type { TemplateUsageViewModel, UpdateTemplateUsageViewModel } from "@/viewmodels/admin/templateUsage.models";
|
||||
import type { PermissionModule } from "@/types/permissionTypes";
|
||||
|
||||
export const useTemplateUsageStore = defineStore("templateUsage", {
|
||||
state: () => {
|
||||
return {
|
||||
templateUsages: [] as Array<TemplateUsageViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchTemplateUsages() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/templateusage")
|
||||
.then((result) => {
|
||||
this.templateUsages = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
async previewTemplateUsage(scope: PermissionModule): Promise<AxiosResponse<any, any>> {
|
||||
return await http.get(`/admin/templateusage/${scope}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
},
|
||||
async updateTemplateUsage(templateUsage: UpdateTemplateUsageViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.patch(`/admin/templateusage/${templateUsage.scope}`, {
|
||||
headerId: templateUsage.headerId,
|
||||
bodyId: templateUsage.bodyId,
|
||||
footerId: templateUsage.footerId,
|
||||
});
|
||||
this.fetchTemplateUsages();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -4,6 +4,7 @@ export type PermissionModule =
|
|||
| "member"
|
||||
| "calendar"
|
||||
| "newsletter"
|
||||
| "newsletter_config"
|
||||
| "protocol"
|
||||
| "qualification"
|
||||
| "award"
|
||||
|
@ -14,7 +15,9 @@ export type PermissionModule =
|
|||
| "user"
|
||||
| "role"
|
||||
| "query"
|
||||
| "query_store";
|
||||
| "query_store"
|
||||
| "template"
|
||||
| "template_usage";
|
||||
|
||||
export type PermissionType = "read" | "create" | "update" | "delete";
|
||||
|
||||
|
@ -42,6 +45,7 @@ export const permissionModules: Array<PermissionModule> = [
|
|||
"member",
|
||||
"calendar",
|
||||
"newsletter",
|
||||
"newsletter_config",
|
||||
"protocol",
|
||||
"qualification",
|
||||
"award",
|
||||
|
@ -53,6 +57,8 @@ export const permissionModules: Array<PermissionModule> = [
|
|||
"role",
|
||||
"query",
|
||||
"query_store",
|
||||
"template",
|
||||
"template_usage",
|
||||
];
|
||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||
|
@ -65,6 +71,9 @@ export const sectionsAndModules: SectionsAndModulesObject = {
|
|||
"membership_status",
|
||||
"calendar_type",
|
||||
"query_store",
|
||||
"template",
|
||||
"template_usage",
|
||||
"newsletter_config",
|
||||
],
|
||||
user: ["user", "role"],
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { CommunicationFieldType } from "../../types/fieldTypes";
|
||||
import type { CommunicationFieldType } from "@/types/fieldTypes";
|
||||
|
||||
export interface CommunicationTypeViewModel {
|
||||
id: number;
|
||||
|
|
31
src/viewmodels/admin/newsletter.models.ts
Normal file
31
src/viewmodels/admin/newsletter.models.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type { QueryViewModel } from "./query.models";
|
||||
|
||||
export interface NewsletterViewModel {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
newsletterTitle: string;
|
||||
newsletterText: string;
|
||||
newsletterSignatur: string;
|
||||
isSent: boolean;
|
||||
recipientsByQueryId?: number | null;
|
||||
recipientsByQuery?: QueryViewModel | null;
|
||||
}
|
||||
|
||||
export interface CreateNewsletterViewModel {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface SyncNewsletterViewModel {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
newsletterTitle: string;
|
||||
newsletterText: string;
|
||||
newsletterSignatur: string;
|
||||
recipientsByQueryId?: number;
|
||||
}
|
||||
|
||||
export interface SendNewsletterViewModel {
|
||||
id: number;
|
||||
}
|
13
src/viewmodels/admin/newsletterConfig.models.ts
Normal file
13
src/viewmodels/admin/newsletterConfig.models.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import type { NewsletterConfigType } from "@/enums/newsletterConfigType";
|
||||
import type { CommunicationTypeViewModel } from "./communicationType.models";
|
||||
|
||||
export interface NewsletterConfigViewModel {
|
||||
comTypeId: number;
|
||||
config: NewsletterConfigType;
|
||||
comType: CommunicationTypeViewModel;
|
||||
}
|
||||
|
||||
export interface SetNewsletterConfigViewModel {
|
||||
comTypeId: number;
|
||||
config: NewsletterConfigType;
|
||||
}
|
15
src/viewmodels/admin/newsletterDates.models.ts
Normal file
15
src/viewmodels/admin/newsletterDates.models.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { CalendarViewModel } from "./calendar.models";
|
||||
|
||||
export interface NewsletterDatesViewModel {
|
||||
newsletterId: number;
|
||||
calendarId: string;
|
||||
diffTitle: string | null;
|
||||
diffDescription: string | null;
|
||||
calendar: CalendarViewModel;
|
||||
}
|
||||
|
||||
export interface SyncNewsletterDatesViewModel {
|
||||
calendarId: string;
|
||||
diffTitle?: string;
|
||||
diffDescription?: string;
|
||||
}
|
11
src/viewmodels/admin/newsletterRecipients.models.ts
Normal file
11
src/viewmodels/admin/newsletterRecipients.models.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { MemberViewModel } from "./member.models";
|
||||
|
||||
export interface NewsletterRecipientsViewModel {
|
||||
newsletterId: number;
|
||||
memberId: number;
|
||||
member: MemberViewModel;
|
||||
}
|
||||
|
||||
export interface SyncNewsletterRecipientsViewModel {
|
||||
memberId: number;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import type { DynamicQueryStructure } from "../../types/dynamicQueries";
|
||||
import type { DynamicQueryStructure } from "@/types/dynamicQueries";
|
||||
|
||||
export interface TableMeta {
|
||||
tableName: string;
|
||||
|
|
20
src/viewmodels/admin/template.models.ts
Normal file
20
src/viewmodels/admin/template.models.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export interface TemplateViewModel {
|
||||
id: number;
|
||||
template: string;
|
||||
description: string | null;
|
||||
design: object;
|
||||
html: string;
|
||||
}
|
||||
|
||||
export interface CreateTemplateViewModel {
|
||||
template: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateTemplateViewModel {
|
||||
id: number;
|
||||
template: string;
|
||||
description: string | null;
|
||||
design: object;
|
||||
html: string;
|
||||
}
|
15
src/viewmodels/admin/templateUsage.models.ts
Normal file
15
src/viewmodels/admin/templateUsage.models.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import type { PermissionModule } from "@/types/permissionTypes";
|
||||
|
||||
export interface TemplateUsageViewModel {
|
||||
scope: PermissionModule;
|
||||
header: { id: number; template: string } | null;
|
||||
body: { id: number; template: string } | null;
|
||||
footer: { id: number; template: string } | null;
|
||||
}
|
||||
|
||||
export interface UpdateTemplateUsageViewModel {
|
||||
scope: PermissionModule;
|
||||
headerId: number | null;
|
||||
bodyId: number | null;
|
||||
footerId: number | null;
|
||||
}
|
|
@ -58,8 +58,8 @@ export default defineComponent({
|
|||
this.updateTopLevel();
|
||||
this.updateNavigation();
|
||||
});
|
||||
this.updateTopLevel(true);
|
||||
this.updateNavigation(true);
|
||||
this.updateTopLevel();
|
||||
this.updateNavigation();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resetNavigation();
|
||||
|
|
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>
|
71
src/views/admin/club/newsletter/NewsletterData.vue
Normal file
71
src/views/admin/club/newsletter/NewsletterData.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<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">Überschrift</label>
|
||||
<QuillEditor
|
||||
id="summary"
|
||||
theme="snow"
|
||||
placeholder="Überschrift des Newsletters..."
|
||||
style="height: 150px; max-height: 150px; min-height: 150px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="activeNewsletterObj.newsletterTitle"
|
||||
:enable="can('create', 'club', 'newsletter')"
|
||||
:style="!can('create', 'club', 'newsletter') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col h-1/2">
|
||||
<label for="summary">Text</label>
|
||||
<QuillEditor
|
||||
id="summary"
|
||||
theme="snow"
|
||||
placeholder="Text zum Newsletter..."
|
||||
style="height: 150px; max-height: 150px; min-height: 150px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="activeNewsletterObj.newsletterText"
|
||||
:enable="can('create', 'club', 'newsletter')"
|
||||
:style="!can('create', 'club', 'newsletter') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col h-1/2">
|
||||
<label for="summary">Signatur</label>
|
||||
<QuillEditor
|
||||
id="summary"
|
||||
theme="snow"
|
||||
placeholder="Zusammenfassung zum Newsletter..."
|
||||
style="height: 150px; max-height: 150px; min-height: 150px"
|
||||
contentType="html"
|
||||
:toolbar="toolbarOptions"
|
||||
v-model:content="activeNewsletterObj.newsletterSignatur"
|
||||
:enable="can('create', 'club', 'newsletter')"
|
||||
:style="!can('create', 'club', 'newsletter') ? 'opacity: 75%; background: rgb(243 244 246)' : ''"
|
||||
/>
|
||||
</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 { 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"]),
|
||||
},
|
||||
});
|
||||
</script>
|
171
src/views/admin/club/newsletter/NewsletterDates.vue
Normal file
171
src/views/admin/club/newsletter/NewsletterDates.vue
Normal file
|
@ -0,0 +1,171 @@
|
|||
<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
|
||||
indicator
|
||||
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 class="w-full text-white">
|
||||
{{ item.calendar.title }}:
|
||||
{{
|
||||
item.calendar.allDay
|
||||
? new Date(item.calendar.starttime ?? "").toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: new Date(item.calendar.starttime ?? "").toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<TrashIcon
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
class="w-5 h-5 p-1 box-content cursor-pointer text-white"
|
||||
@click.prevent="removeSelected(item.calendarId)"
|
||||
/>
|
||||
</summary>
|
||||
<div class="flex flex-col gap-2 px-1">
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="alternativer Titel"
|
||||
autocomplete="off"
|
||||
v-model="item.diffTitle"
|
||||
@keyup.prevent
|
||||
:disabled="!can('create', 'club', 'newsletter')"
|
||||
/>
|
||||
<div>
|
||||
<QuillEditor
|
||||
id="top"
|
||||
theme="snow"
|
||||
placeholder="alternative Beschreibung..."
|
||||
style="height: 150px; max-height: 150px; min-height: 150px"
|
||||
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)' : ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-row md:flex-row gap-2" @submit.prevent="addEntry">
|
||||
<select id="date" ref="date" value="" required>
|
||||
<option value="" disabled>Datum wählen</option>
|
||||
<option v-for="cal in filteredCalendar" :key="cal.id" :value="cal.id">
|
||||
{{
|
||||
cal.allDay
|
||||
? new Date(cal.starttime).toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
: new Date(cal.starttime).toLocaleDateString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}}
|
||||
-
|
||||
{{ cal.title }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" primary class="!w-fit">hinzufügen</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapActions, mapState, mapWritableState } from "pinia";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
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";
|
||||
import { useCalendarStore } from "@/stores/admin/calendar";
|
||||
import type { CalendarViewModel } from "@/viewmodels/admin/calendar.models";
|
||||
import { TrashIcon } from "@heroicons/vue/24/outline";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import type { NewsletterDatesViewModel } from "@/viewmodels/admin/newsletterDates.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
newsletterId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useNewsletterDatesStore, ["dates", "loading"]),
|
||||
...mapState(useCalendarStore, ["calendars"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
filteredCalendar() {
|
||||
return this.calendars.filter(
|
||||
(c) => !this.dates.map((d) => d.calendarId).includes(c.id) && new Date(c.starttime) >= new Date()
|
||||
);
|
||||
},
|
||||
sortedDates() {
|
||||
return this.dates.sort(
|
||||
(a: NewsletterDatesViewModel, b: NewsletterDatesViewModel) =>
|
||||
new Date(a.calendar.starttime).getTime() - new Date(b.calendar.starttime).getTime()
|
||||
);
|
||||
},
|
||||
calendarData() {
|
||||
return (dateId: string) => this.calendars.find((c) => c.id == dateId);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchNewsletterDates();
|
||||
this.fetchCalendars();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useNewsletterDatesStore, ["fetchNewsletterDates"]),
|
||||
...mapActions(useCalendarStore, ["fetchCalendars"]),
|
||||
addEntry(e: any) {
|
||||
const formData = e.target.elements;
|
||||
const dateId = formData.date.value;
|
||||
|
||||
this.dates.push({
|
||||
newsletterId: parseInt(this.newsletterId ?? "0"),
|
||||
calendarId: dateId,
|
||||
diffTitle: "",
|
||||
diffDescription: "",
|
||||
calendar: cloneDeep(this.calendarData(dateId)) as CalendarViewModel,
|
||||
});
|
||||
|
||||
(this.$refs.date as HTMLSelectElement).value = "";
|
||||
},
|
||||
removeSelected(id: string) {
|
||||
let index = this.dates.findIndex((d) => d.calendarId == id);
|
||||
if (index != -1) {
|
||||
this.dates.splice(index, 1);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
67
src/views/admin/club/newsletter/NewsletterOverview.vue
Normal file
67
src/views/admin/club/newsletter/NewsletterOverview.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<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">
|
||||
<p class="italic">
|
||||
Titel und Zusammenfassung werden standardmäßig nicht im Newsletter angezeit, können aber bei Verwendung eines
|
||||
eigenen Templates verwendet werden.
|
||||
</p>
|
||||
<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 zum Newsletter..."
|
||||
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>
|
127
src/views/admin/club/newsletter/NewsletterPrintout.vue
Normal file
127
src/views/admin/club/newsletter/NewsletterPrintout.vue
Normal file
|
@ -0,0 +1,127 @@
|
|||
<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="fetchNewsletterPrintout" 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" 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 }}</p>
|
||||
<div class="flex flex-row">
|
||||
<div v-if="print.endsWith('.pdf')">
|
||||
<ViewfinderCircleIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="openPdfShow(print)" />
|
||||
</div>
|
||||
<div>
|
||||
<ArrowDownTrayIcon class="w-5 h-5 p-1 box-content cursor-pointer" @click="downloadPdf(print)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap justify-start gap-2">
|
||||
<button
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
primary
|
||||
class="!w-fit whitespace-nowrap flex flex-row gap-2"
|
||||
:disabled="printing != undefined"
|
||||
@click="createNewsletterPrintout"
|
||||
>
|
||||
Newsletter drucken
|
||||
<Spinner v-if="printing == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="printing == 'success'" />
|
||||
<FailureXMark v-else-if="printing == 'failed'" />
|
||||
</button>
|
||||
<button
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
primary
|
||||
class="!w-fit whitespace-nowrap flex flex-row gap-2"
|
||||
:disabled="sending != undefined"
|
||||
@click="createNewsletterSend"
|
||||
>
|
||||
Mails versenden
|
||||
<Spinner v-if="sending == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="sending == 'success'" />
|
||||
<FailureXMark v-else-if="sending == 'failed'" />
|
||||
</button>
|
||||
<button v-if="can('create', 'club', 'newsletter')" primary-outline class="!w-fit" @click="openPdfShow()">
|
||||
Newsletter Vorschau
|
||||
</button>
|
||||
<button
|
||||
v-if="can('create', 'club', 'newsletter')"
|
||||
primary-outline
|
||||
class="!w-fit whitespace-nowrap flex flex-row gap-2"
|
||||
:disabled="sendingPreview != undefined"
|
||||
@click="createNewsletterMailPreview"
|
||||
>
|
||||
Mail Vorschau
|
||||
<Spinner v-if="sendingPreview == 'loading'" class="my-auto" />
|
||||
<SuccessCheckmark v-else-if="sendingPreview == 'success'" />
|
||||
<FailureXMark v-else-if="sendingPreview == 'failed'" />
|
||||
</button>
|
||||
</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 { useNewsletterPrintoutStore } from "@/stores/admin/newsletterPrintout";
|
||||
import { ArrowDownTrayIcon, ViewfinderCircleIcon } from "@heroicons/vue/24/outline";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useNewsletterPrintoutStore } from "../../../../stores/admin/newsletterPrintout";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
newsletterId: String,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useNewsletterPrintoutStore, ["printout", "loading", "printing", "sending", "sendingPreview"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchNewsletterPrintout();
|
||||
this.subscribeMailSendingProgress();
|
||||
this.subscribePdfPrintingProgress();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useNewsletterPrintoutStore, [
|
||||
"fetchNewsletterPrintout",
|
||||
"createNewsletterPrintout",
|
||||
"fetchNewsletterPrintoutById",
|
||||
"createNewsletterMailPreview",
|
||||
"createNewsletterSend",
|
||||
"subscribeMailSendingProgress",
|
||||
"subscribePdfPrintingProgress",
|
||||
]),
|
||||
openPdfShow(filename?: string) {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/newsletter/NewsletterPreviewModal.vue"))),
|
||||
filename
|
||||
);
|
||||
},
|
||||
downloadPdf(filename: string) {
|
||||
this.fetchNewsletterPrintoutById(filename)
|
||||
.then((response) => {
|
||||
const fileURL = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const fileLink = document.createElement("a");
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute("download", filename);
|
||||
document.body.appendChild(fileLink);
|
||||
fileLink.click();
|
||||
fileLink.remove();
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
231
src/views/admin/club/newsletter/NewsletterRecipients.vue
Normal file
231
src/views/admin/club/newsletter/NewsletterRecipients.vue
Normal file
|
@ -0,0 +1,231 @@
|
|||
<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 v-model="recipientsByQueryId">
|
||||
<option value="def">Optional</option>
|
||||
<option v-for="query in queries" :key="query.id" :value="query.id">{{ query.title }}</option>
|
||||
</select>
|
||||
<p>Empfänger durch gespeicherte Abfrage</p>
|
||||
<div class="flex flex-col gap-2 grow overflow-y-auto">
|
||||
<div
|
||||
v-for="member in queried"
|
||||
: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>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<Combobox v-model="recipients" :disabled="!can('create', 'club', 'newsletter')" multiple>
|
||||
<ComboboxLabel>weitere 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>
|
||||
<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 { useMemberStore } from "@/stores/admin/member";
|
||||
import type { MemberViewModel } from "@/viewmodels/admin/member.models";
|
||||
import { useNewsletterStore } from "@/stores/admin/newsletter";
|
||||
import { useNewsletterRecipientsStore } from "@/stores/admin/newsletterRecipients";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
newsletterId: String,
|
||||
},
|
||||
watch: {
|
||||
recipientsByQuery() {
|
||||
this.loadQuery();
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: "" as String,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapWritableState(useNewsletterRecipientsStore, ["recipients", "loading"]),
|
||||
...mapWritableState(useNewsletterStore, ["activeNewsletterObj"]),
|
||||
...mapState(useMemberStore, ["members"]),
|
||||
...mapState(useQueryStoreStore, ["queries"]),
|
||||
...mapState(useQueryBuilderStore, ["data"]),
|
||||
...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));
|
||||
},
|
||||
queried(): Array<MemberViewModel> {
|
||||
if (this.recipientsByQueryId == "def") return [];
|
||||
let keys = Object.keys(this.data?.[0] ?? {});
|
||||
let memberKey = keys.find((k) => k.includes("member_id"));
|
||||
return this.members.filter((m) =>
|
||||
this.data
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
...(memberKey ? { memberId: t[memberKey] } : {}),
|
||||
}))
|
||||
.some((d) => (d.memberId ?? d.id) == m.id)
|
||||
);
|
||||
},
|
||||
recipientsByQueryId: {
|
||||
get() {
|
||||
return this.activeNewsletterObj?.recipientsByQueryId ?? "def";
|
||||
},
|
||||
set(val: string) {
|
||||
if (this.activeNewsletterObj == undefined) return;
|
||||
if (val == "def") {
|
||||
this.activeNewsletterObj.recipientsByQueryId = null;
|
||||
this.activeNewsletterObj.recipientsByQuery = null;
|
||||
} else if (this.queries.find((q) => q.id == parseInt(val))) {
|
||||
this.activeNewsletterObj.recipientsByQueryId = parseInt(val);
|
||||
this.activeNewsletterObj.recipientsByQuery = cloneDeep(this.queries.find((q) => q.id == parseInt(val)));
|
||||
this.sendQuery(0, 1000, this.recipientsByQuery?.query);
|
||||
}
|
||||
},
|
||||
},
|
||||
recipientsByQuery() {
|
||||
return this.activeNewsletterObj?.recipientsByQuery;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMembers(0, 1000, true);
|
||||
this.fetchNewsletterRecipients();
|
||||
this.fetchQueries();
|
||||
this.loadQuery();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useMemberStore, ["fetchMembers"]),
|
||||
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
||||
...mapActions(useQueryStoreStore, ["fetchQueries"]),
|
||||
...mapActions(useQueryBuilderStore, ["sendQuery"]),
|
||||
removeSelected(id: number) {
|
||||
let index = this.recipients.findIndex((s) => s == id);
|
||||
if (index != -1) {
|
||||
this.recipients.splice(index, 1);
|
||||
}
|
||||
},
|
||||
loadQuery() {
|
||||
if (this.recipientsByQuery) {
|
||||
this.sendQuery(0, 1000, this.recipientsByQuery.query);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
130
src/views/admin/club/newsletter/NewsletterRouting.vue
Normal file
130
src/views/admin/club/newsletter/NewsletterRouting.vue
Normal file
|
@ -0,0 +1,130 @@
|
|||
<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";
|
||||
import { useNewsletterDatesStore } from "../../../../stores/admin/newsletterDates";
|
||||
import { useNewsletterRecipientsStore } from "../../../../stores/admin/newsletterRecipients";
|
||||
</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-data", title: "Daten" },
|
||||
{ route: "admin-club-newsletter-dates", title: "Termine" },
|
||||
{ route: "admin-club-newsletter-recipients", title: "Empfänger" },
|
||||
{ route: "admin-club-newsletter-printout", title: "Druck/Versand" },
|
||||
],
|
||||
wantToClose: false as boolean,
|
||||
executeSyncAll: undefined as any,
|
||||
syncState: "synced" as "synced" | "syncing" | "detectedChanges" | "failed",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useNewsletterStore, ["origin"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchNewsletterByActiveId();
|
||||
this.fetchNewsletterDates();
|
||||
this.fetchNewsletterRecipients();
|
||||
},
|
||||
// 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(useNewsletterDatesStore, ["fetchNewsletterDates"]),
|
||||
...mapActions(useNewsletterRecipientsStore, ["fetchNewsletterRecipients"]),
|
||||
...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>
|
|
@ -12,6 +12,7 @@
|
|||
>
|
||||
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||
<svg
|
||||
indicator
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
>
|
||||
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||
<svg
|
||||
indicator
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -23,7 +24,7 @@
|
|||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Einscheidung"
|
||||
placeholder="Entscheidung"
|
||||
autocomplete="off"
|
||||
v-model="item.topic"
|
||||
@keyup.prevent
|
||||
|
|
|
@ -141,7 +141,7 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMembers();
|
||||
this.fetchMembers(0, 1000, true);
|
||||
this.fetchProtocolPresence();
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
>
|
||||
<summary class="flex flex-row gap-2 bg-primary p-2 w-full justify-between items-center cursor-pointer">
|
||||
<svg
|
||||
indicator
|
||||
class="fill-white stroke-white opacity-75 w-4 h-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
@ -23,7 +24,7 @@
|
|||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
placeholder="Einscheidung"
|
||||
placeholder="Abstimmung"
|
||||
autocomplete="off"
|
||||
v-model="item.topic"
|
||||
@keyup.prevent
|
||||
|
|
|
@ -62,7 +62,7 @@ import { mapActions, mapState, mapWritableState } from "pinia";
|
|||
import MainTemplate from "@/templates/Main.vue";
|
||||
import Pagination from "@/components/Pagination.vue";
|
||||
import { useQueryBuilderStore } from "@/stores/admin/queryBuilder";
|
||||
import BuilderHost from "../../../../components/queryBuilder/BuilderHost.vue";
|
||||
import BuilderHost from "@/components/queryBuilder/BuilderHost.vue";
|
||||
import type { DynamicQueryStructure, FieldType } from "@/types/dynamicQueries";
|
||||
import { useQueryStoreStore } from "@/stores/admin/queryStore";
|
||||
</script>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
<button v-if="can('create', 'settings', 'communication')" primary class="!w-fit" @click="openCreateModal">
|
||||
<button v-if="can('create', 'settings', 'communication_type')" primary class="!w-fit" @click="openCreateModal">
|
||||
Kommunikationsart erstellen
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<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 Konfiguration</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<p>
|
||||
Ein Newsletter kann als pdf exportiert oder per Mail versandt werden. <br>
|
||||
Die Entscheidung für den Export geschieht anhand der Einstellung "Newsletter hier hin versenden?". <br>
|
||||
Wird keine Adresse gefunden oder sind die Typen mit den falschen Versandoptionen konfiguriert,
|
||||
erstellt das System als Fallback pdfs mit nur dem Namen des Mitglieds.
|
||||
</p>
|
||||
<NewsletterConfigListItem v-for="comType in communicationTypes" :key="comType.id" :comType="comType" />
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useCommunicationTypeStore } from "@/stores/admin/communicationType";
|
||||
import { useNewsletterConfigStore } from "@/stores/admin/newsletterConfig";
|
||||
import NewsletterConfigListItem from "@/components/admin/settings/newsletterConfig/NewsletterConfigListItem.vue";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useCommunicationTypeStore, ["communicationTypes"]),
|
||||
...mapState(useNewsletterConfigStore,["config"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCommunicationTypes();
|
||||
this.fetchNewsletterConfigs()
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCommunicationTypeStore, ["fetchCommunicationTypes"]),
|
||||
...mapActions(useNewsletterConfigStore, ["fetchNewsletterConfigs"])
|
||||
},
|
||||
});
|
||||
</script>
|
57
src/views/admin/settings/template/Template.vue
Normal file
57
src/views/admin/settings/template/Template.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<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">Templates</h1>
|
||||
<RouterLink :to="{ name: 'admin-settings-template-info' }">
|
||||
<InformationCircleIcon class="text-gray-500 h-5 w-5" />
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-4 grow pl-7">
|
||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||
<TemplateListItem v-for="template in templates" :key="template.id" :template="template" />
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
<button v-if="can('create', 'settings', 'template')" primary class="!w-fit" @click="openCreateModal">
|
||||
Template erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import TemplateListItem from "@/components/admin/settings/template/TemplateListItem.vue";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { InformationCircleIcon } from "@heroicons/vue/24/outline";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useTemplateStore, ["templates"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchTemplates();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useTemplateStore, ["fetchTemplates"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openCreateModal() {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/settings/template/CreateTemplateModal.vue")))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
177
src/views/admin/settings/template/TemplateEdit.vue
Normal file
177
src/views/admin/settings/template/TemplateEdit.vue
Normal file
|
@ -0,0 +1,177 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #headerInsert>
|
||||
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
|
||||
</template>
|
||||
<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">Template {{ origin?.template }} - Daten bearbeiten</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||
<form
|
||||
v-show="loading == 'fetched' && template != null"
|
||||
class="flex flex-col gap-4 py-2 w-full h-full mx-auto"
|
||||
@submit.prevent="triggerUpdate"
|
||||
>
|
||||
<div v-if="template != null" class="flex flex-col xl:flex-row gap-4">
|
||||
<div class="w-full">
|
||||
<label for="template">Bezeichnung</label>
|
||||
<input type="text" id="template" required v-model="template.template" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="description">Beschreibung (optional)</label>
|
||||
<input type="text" id="description" v-model="template.description" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="contextLoading == 'loading'" class="flex flex-col gap-2 items-center">
|
||||
<p>Lade Template-Anzeige</p>
|
||||
<Spinner />
|
||||
</div>
|
||||
<div ref="grapesEditor" id="grapesEditor"></div>
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<button primary-outline type="reset" class="!w-fit" :disabled="status == 'loading'" @click="resetForm">
|
||||
verwerfen
|
||||
</button>
|
||||
<button primary type="submit" class="!w-fit" :disabled="status == 'loading'">speichern</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>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import Spinner from "@/components/Spinner.vue";
|
||||
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
|
||||
import FailureXMark from "@/components/FailureXMark.vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { configureEditor } from "@/helpers/grapesEditor";
|
||||
import type { TemplateViewModel, UpdateTemplateViewModel } from "@/viewmodels/admin/template.models";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isequal";
|
||||
import grapesjs, { Editor } from "grapesjs";
|
||||
import grapesNewsletter from "grapesjs-preset-newsletter";
|
||||
import "grapesjs/dist/css/grapes.min.css";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
id: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
origin: null as null | TemplateViewModel,
|
||||
template: null as null | TemplateViewModel,
|
||||
timeout: null as any,
|
||||
editor: null as null | Editor,
|
||||
contextLoading: "loading" as "loading" | "loaded" | "failed",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.origin, this.template);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchItem();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
localStorage.removeItem("gjsProject");
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useTemplateStore, ["fetchTemplateById", "updateActiveTemplate"]),
|
||||
initEditor() {
|
||||
this.editor = grapesjs.init({
|
||||
container: "#grapesEditor",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
fromElement: true,
|
||||
telemetry: false,
|
||||
showDevices: false,
|
||||
deviceManager: {
|
||||
devices: [
|
||||
{
|
||||
id: "tablet",
|
||||
name: "Tablet",
|
||||
width: "768px",
|
||||
widthMedia: "992px",
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [grapesNewsletter],
|
||||
});
|
||||
configureEditor(this.editor);
|
||||
},
|
||||
loadDesign() {
|
||||
this.contextLoading = "loading";
|
||||
this.editor?.destroy();
|
||||
localStorage.setItem("gjsProject", JSON.stringify(this.origin?.design ?? {}));
|
||||
setTimeout(() => {
|
||||
this.initEditor();
|
||||
this.contextLoading = "loaded";
|
||||
}, 1000);
|
||||
},
|
||||
resetForm() {
|
||||
this.template = cloneDeep(this.origin);
|
||||
this.loadDesign();
|
||||
},
|
||||
fetchItem(fromSave: boolean = false) {
|
||||
this.fetchTemplateById(parseInt(this.id ?? ""))
|
||||
.then((result) => {
|
||||
this.template = result.data;
|
||||
this.origin = cloneDeep(result.data);
|
||||
this.loading = "fetched";
|
||||
if (!fromSave) this.loadDesign();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
triggerUpdate(e: any) {
|
||||
if (this.template == null) return;
|
||||
let formData = e.target.elements;
|
||||
const htmlContent = this.editor?.getHtml();
|
||||
const cssContent = this.editor?.getCss();
|
||||
const html = `<style>${cssContent}</style>${htmlContent}`;
|
||||
|
||||
let updateTemplate: UpdateTemplateViewModel = {
|
||||
id: this.template.id,
|
||||
template: formData.template.value,
|
||||
description: formData.description.value,
|
||||
design: this.editor?.getProjectData() ?? {},
|
||||
html,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.updateActiveTemplate(updateTemplate)
|
||||
.then(() => {
|
||||
this.fetchItem(true);
|
||||
this.status = { status: "success" };
|
||||
})
|
||||
.catch((err) => {
|
||||
this.status = { status: "failed" };
|
||||
})
|
||||
.finally(() => {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
24
src/views/admin/settings/template/UsageInfo.vue
Normal file
24
src/views/admin/settings/template/UsageInfo.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<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">Templates - Verwendungsinformation</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<p>
|
||||
Mit diesem Editor können Vorlagen erstellt werden, welche später dafür genutzt werden können, um pdfs zu drucken
|
||||
oder Mails zu versenden.
|
||||
</p>
|
||||
<p>
|
||||
Es können Platzhalter in das Design integriert werden. Dort werden dann später Werte automatisch eingetragen.
|
||||
Diese Werte stammen zum Beispiel aus dem Kalender oder aus Abfragen, welche mit dem Query-Builder erstellt
|
||||
wurden.
|
||||
</p>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
</script>
|
37
src/views/admin/settings/templateUsage/TemplateUsage.vue
Normal file
37
src/views/admin/settings/templateUsage/TemplateUsage.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<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">Template-Verwendung</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<TemplateUsageListItem v-for="usage in templateUsages" :key="usage.scope" :templateUsage="usage" />
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import { useTemplateUsageStore } from "@/stores/admin/templateUsage";
|
||||
import TemplateUsageListItem from "@/components/admin/settings/templateUsage/TemplateUsageListItem.vue";
|
||||
import { useTemplateStore } from "@/stores/admin/template";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useTemplateUsageStore, ["templateUsages"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchTemplateUsages();
|
||||
this.fetchTemplates();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useTemplateUsageStore, ["fetchTemplateUsages"]),
|
||||
...mapActions(useTemplateStore, ["fetchTemplates"]),
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -8,5 +8,5 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from "vue-router";
|
||||
import FullContent from "../../layouts/FullContent.vue";
|
||||
import FullContent from "@/layouts/FullContent.vue";
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue