Compare commits

...

34 commits
v1.5.1 ... main

Author SHA1 Message Date
291d04182c 1.7.0 2025-06-07 15:56:43 +02:00
37d73d7c74 Merge pull request 'minor v1.7.0' (#108) from develop into main
Reviewed-on: #108
2025-06-07 13:55:06 +00:00
31c0b2a4c1 Merge branch 'main' into develop 2025-06-07 13:52:57 +00:00
defa732212 update packages 2025-06-07 15:44:41 +02:00
1452456138 1.6.0 2025-06-06 09:37:01 +02:00
0e0f86adce Merge pull request 'minor v1.6.0' (#106) from develop into main
Reviewed-on: #106
2025-06-06 07:36:01 +00:00
d9a24eb723 fix: spelling 2025-06-06 09:00:27 +02:00
d1ff313754 fix: protocol sync 2025-06-06 08:59:15 +02:00
87cb4252ec Merge branch 'main' into develop 2025-06-05 14:34:59 +00:00
90f5ef3b1a add: show count of newer versions 2025-06-05 07:53:03 +02:00
0db141cd13 add: logo and icon fallback on server error 2025-06-05 07:48:01 +02:00
583d4913d9 Merge pull request 'feature/#97-member-extend-data' (#105) from feature/#97-member-extend-data into develop
Reviewed-on: #105
2025-06-03 13:33:18 +00:00
f6252901cd spelling 2025-06-03 15:27:39 +02:00
ff53d2d4d9 fix view errors 2025-06-03 15:20:59 +02:00
05ec4afadb Education views in member and config 2025-06-02 13:57:06 +02:00
3b89262ce9 add note field to member 2025-06-02 13:30:16 +02:00
516c6a9e92 enhance: add calendar link to login screen 2025-05-31 07:34:19 +02:00
ec0222ff2f enhance: add membership total view in member 2025-05-30 15:13:50 +02:00
0defc9b0ba change: redirect to edit page after create of newsletter and protocol 2025-05-29 11:20:54 +02:00
caf8e71a51 enhance: double confirm deletion for manual added newsletter recipients 2025-05-21 10:36:01 +02:00
d11f0d50c6 enhance: double confirm deletion for newsletter dates 2025-05-21 10:33:24 +02:00
2ce66da1d1 enhance: enable deletion of protocol content 2025-05-21 10:32:56 +02:00
f3913a906c 1.5.3 2025-05-19 13:26:46 +02:00
cfe621debd Merge pull request 'patches v1.5.3' (#96) from develop into main
Reviewed-on: #96
2025-05-19 11:25:49 +00:00
5b7a9a3ace Merge branch 'main' into develop 2025-05-17 05:36:51 +00:00
12b1d08ea4 enhance: navigation optimization 2025-05-16 13:34:24 +02:00
04c01b6780 change: standardisation of UI 2025-05-16 13:32:40 +02:00
4ee16c624a enhance: permission handling 2025-05-16 11:12:18 +02:00
35fd8a8e82 enhance: unified ui 2025-05-16 10:27:38 +02:00
832f5053a0 1.5.2 2025-05-10 13:40:54 +02:00
362fc80891 Merge pull request 'patches v1.5.2' (#94) from develop into main
Reviewed-on: #94
2025-05-10 11:40:26 +00:00
2d35e2416b Merge branch 'main' into develop 2025-05-10 11:39:39 +00:00
738765bcb4 fix: possible reset of config store after redirect to login 2025-05-09 14:54:47 +02:00
a39044dffc change: remove logging 2025-05-08 08:17:14 +02:00
108 changed files with 2001 additions and 1425 deletions

1213
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "ff-admin", "name": "ff-admin",
"version": "1.5.1", "version": "1.7.0",
"description": "Feuerwehr/Verein Mitgliederverwaltung UI", "description": "Feuerwehr/Verein Mitgliederverwaltung UI",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -32,7 +32,7 @@
"@fullcalendar/vue3": "^6.1.17", "@fullcalendar/vue3": "^6.1.17",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.2.0", "@heroicons/vue": "^2.2.0",
"@tailwindcss/vite": "^4.1.5", "@tailwindcss/vite": "^4.1.8",
"@vueup/vue-quill": "^1.2.0", "@vueup/vue-quill": "^1.2.0",
"axios": "^1.9.0", "axios": "^1.9.0",
"event-source-polyfill": "^1.0.31", "event-source-polyfill": "^1.0.31",
@ -49,19 +49,19 @@
"markdown-it-prism": "^3.0.0", "markdown-it-prism": "^3.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"pdf-dist": "^1.0.0", "pdf-dist": "^1.0.0",
"pinia": "^3.0.2", "pinia": "^3.0.3",
"pwacompat": "^2.0.17", "pwacompat": "^2.0.17",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"qs": "^6.14.0", "qs": "^6.14.0",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"unplugin-vue-markdown": "^28.3.1", "unplugin-vue-markdown": "^28.3.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
"vue": "^3.5.13", "vue": "^3.5.16",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.11.0", "@rushstack/eslint-patch": "^1.11.0",
"@tailwindcss/postcss": "^4.1.5", "@tailwindcss/postcss": "^4.1.8",
"@tsconfig/node20": "^20.1.5", "@tsconfig/node20": "^20.1.5",
"@types/eslint": "~9.6.1", "@types/eslint": "~9.6.1",
"@types/event-source-polyfill": "^1.0.5", "@types/event-source-polyfill": "^1.0.5",
@ -70,21 +70,21 @@
"@types/lodash.differencewith": "^4.5.9", "@types/lodash.differencewith": "^4.5.9",
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/node": "^22.15.12", "@types/node": "^22.15.30",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.9.18", "@types/qs": "^6.14.0",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.0",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.4",
"@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-prettier": "^10.2.0",
"@vue/eslint-config-typescript": "^14.5.0", "@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"eslint": "^9.26.0", "eslint": "^9.28.0",
"eslint-plugin-vue": "^10.1.0", "eslint-plugin-vue": "^10.2.0",
"npm-run-all2": "^8.0.1", "npm-run-all2": "^8.0.4",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tailwindcss": "^4.1.5", "tailwindcss": "^4.1.8",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-pwa": "^1.0.0", "vite-plugin-pwa": "^1.0.0",

BIN
public/admin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -12,6 +12,7 @@
<Teleport to="head"> <Teleport to="head">
<title>{{ clubName }}</title> <title>{{ clubName }}</title>
<link rel="icon" type="image/ico" :href="config.server_address + '/api/public/favicon.ico'" /> <link rel="icon" type="image/ico" :href="config.server_address + '/api/public/favicon.ico'" />
<link rel="icon" type="image/png" href="/icon.png" />
<link rel="manifest" :href="config.server_address + '/api/public/manifest.webmanifest'" /> <link rel="manifest" :href="config.server_address + '/api/public/manifest.webmanifest'" />
</Teleport> </Teleport>
</template> </template>
@ -29,6 +30,7 @@ import Modal from "./components/Modal.vue";
import Notification from "./components/Notification.vue"; import Notification from "./components/Notification.vue";
import { config } from "./config"; import { config } from "./config";
import { useConfigurationStore } from "@/stores/configuration"; import { useConfigurationStore } from "@/stores/configuration";
import { resetAllPiniaStores } from "@/helpers/piniaReset";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -38,6 +40,7 @@ export default defineComponent({
...mapState(useConfigurationStore, ["clubName"]), ...mapState(useConfigurationStore, ["clubName"]),
}, },
mounted() { mounted() {
resetAllPiniaStores();
this.configure(); this.configure();
if (!this.authCheck && localStorage.getItem("access_token")) { if (!this.authCheck && localStorage.getItem("access_token")) {

View file

@ -1,5 +1,13 @@
<template> <template>
<img ref="icon" :src="url + '/api/public/icon.png'" alt="LOGO" class="h-full w-auto" /> <img v-if="useFallback" ref="fallback" src="/icon.png" alt="LOGO" class="h-full w-auto" />
<img
v-else
ref="icon"
:src="url + '/api/public/icon.png'"
alt="LOGO"
class="h-full w-auto"
@error="useFallback = true"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -16,6 +24,11 @@ export default defineComponent({
(this.$refs.icon as HTMLImageElement).src = url + "/api/public/icon.png?" + new Date().getTime(); (this.$refs.icon as HTMLImageElement).src = url + "/api/public/icon.png?" + new Date().getTime();
}, },
}, },
data() {
return {
useFallback: false,
};
},
computed: { computed: {
...mapState(useSettingStore, ["readSetting"]), ...mapState(useSettingStore, ["readSetting"]),
icon() { icon() {

View file

@ -1,5 +1,13 @@
<template> <template>
<img ref="logo" :src="url + '/api/public/applogo.png'" alt="LOGO" class="h-full w-auto" /> <img v-if="useFallback" ref="fallback" src="/admin-logo.png" alt="LOGO" class="h-full w-auto" />
<img
v-else
ref="logo"
:src="url + '/api/public/applogo.png'"
alt="LOGO"
class="h-full w-auto"
@error="useFallback = true"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -16,6 +24,11 @@ export default defineComponent({
(this.$refs.logo as HTMLImageElement).src = url + "/api/public/applogo.png?t=" + new Date().getTime(); (this.$refs.logo as HTMLImageElement).src = url + "/api/public/applogo.png?t=" + new Date().getTime();
}, },
}, },
data() {
return {
useFallback: false,
};
},
computed: { computed: {
...mapState(useSettingStore, ["readSetting"]), ...mapState(useSettingStore, ["readSetting"]),
logo() { logo() {

View file

@ -0,0 +1,64 @@
<template>
<div
class="cursor-pointer"
:class="{ 'text-white': light, 'animate-pulse': isSensitive }"
:title="`2 mal klicken für ${action}`"
@click.prevent="handleClick"
>
<slot :isSensitive="isSensitive">
<CursorArrowRaysIcon v-if="!isSensitive" class="h-5 w-5" />
<CursorArrowRippleIcon v-else class="h-5 w-5" />
</slot>
</div>
</template>
<script setup lang="ts">
import { CursorArrowRaysIcon, CursorArrowRippleIcon } from "@heroicons/vue/24/outline";
import { defineComponent } from "vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
light: {
type: Boolean,
default: false,
},
action: {
type: String,
default: "Bestätigung",
},
},
emits: ["click:first", "click:submit", "click:reset"],
data() {
return {
isSensitive: false as boolean,
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
handleClick() {
if (this.isSensitive) {
clearTimeout(this.timeout);
this.isSensitive = false;
this.$emit("click:submit");
} else {
this.timeout = setTimeout(() => {
this.isSensitive = true;
this.$emit("click:first");
this.timeout = setTimeout(() => {
this.isSensitive = false;
this.$emit("click:reset");
}, 2000);
}, 500);
}
},
},
});
</script>

View file

@ -80,6 +80,10 @@
<input type="text" id="internalId" /> <input type="text" id="internalId" />
</div> </div>
<div>
<label for="note">Notiz (optional)</label>
<textarea type="text" id="note" />
</div>
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button> <button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" /> <Spinner v-if="status == 'loading'" class="my-auto" />
@ -154,6 +158,7 @@ export default defineComponent({
nameaffix: formData.nameaffix.value, nameaffix: formData.nameaffix.value,
birthdate: formData.birthdate.value, birthdate: formData.birthdate.value,
internalId: formData.internalId.value, internalId: formData.internalId.value,
note: formData.note.value,
}; };
this.status = "loading"; this.status = "loading";
this.createMember(createMember) this.createMember(createMember)

View file

@ -0,0 +1,164 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedEducation" name="education">
<ListboxLabel>Aus-/Fortbildung</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-xs 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-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
educations.length != 0
? (selectedEducation?.education ?? "bitte auswählen")
: "keine Auswahl vorhanden"
}}</span
>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="educations.length == 0" disabled as="template">
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
</li>
</ListboxOption>
<ListboxOption
v-slot="{ active, selected }"
v-for="education in educations"
:key="education.id"
:value="education"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
education.education
}}</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div>
<label for="start">Start</label>
<input type="date" id="start" required />
</div>
<div>
<label for="end">Ende (optional)</label>
<input type="date" id="end" />
</div>
<div>
<label for="place">Ort (optional)</label>
<input type="text" id="place" />
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" />
</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 { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEducationStore } from "@/stores/admin/configuration/education";
import type { EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
import type { CreateMemberEducationViewModel } from "@/viewmodels/admin/club/member/memberEducation.models";
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedEducation: undefined as undefined | EducationViewModel,
};
},
computed: {
...mapState(useEducationStore, ["educations"]),
},
mounted() {
this.fetchEducations();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberEducationStore, ["createMemberEducation"]),
...mapActions(useEducationStore, ["fetchEducations"]),
triggerCreate(e: any) {
if (this.selectedEducation == undefined) return;
let formData = e.target.elements;
let createMemberEducation: CreateMemberEducationViewModel = {
start: formData.start.value,
end: formData.end.value,
note: formData.note.value,
place: formData.place.value,
educationId: this.selectedEducation.id,
};
this.status = "loading";
this.createMemberEducation(createMemberEducation)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,82 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung löschen</p>
</div>
<br />
<p class="text-center">Aus-/Fortbildung {{ memberEducation?.education }} löschen?</p>
<br />
<div class="flex flex-row gap-2">
<button
primary
type="submit"
:disabled="status == 'loading' || status?.status == 'success'"
@click="triggerDelete"
>
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 == '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 { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useMemberEducationStore, ["memberEducations"]),
memberEducation() {
return this.memberEducations.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberEducationStore, ["deleteMemberEducation"]),
triggerDelete() {
this.status = "loading";
this.deleteMemberEducation(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,198 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Aus-/Fortbildung bearbeiten</p>
</div>
<br />
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
<form v-else-if="memberEducation != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="memberEducation.educationId" name="education">
<ListboxLabel>Aus-/Fortbildung</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-xs 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-hidden focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
educations.length != 0 ? (selectedEducation ?? "bitte auswählen") : "keine Auswahl vorhanden"
}}</span
>
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
</span>
</ListboxButton>
<transition
leave-active-class="transition duration-100 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<ListboxOptions
class="absolute mt-1 max-h-60 z-20 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-hidden sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="educations.length == 0" disabled as="template">
<li :class="['relative cursor-default select-none py-2 pl-10 pr-4']">
<span :class="['font-normal', 'block truncate']">keine Auswahl vorhanden</span>
</li>
</ListboxOption>
<ListboxOption
v-slot="{ active, selected }"
v-for="education in educations"
:key="education.id"
:value="education.id"
as="template"
>
<li
:class="[
active ? 'bg-red-200 text-amber-900' : 'text-gray-900',
'relative cursor-default select-none py-2 pl-10 pr-4',
]"
>
<span :class="[selected ? 'font-medium' : 'font-normal', 'block truncate']">{{
education.education
}}</span>
<span v-if="selected" class="absolute inset-y-0 left-0 flex items-center pl-3 text-primary">
<CheckIcon class="h-5 w-5" aria-hidden="true" />
</span>
</li>
</ListboxOption>
</ListboxOptions>
</transition>
</div>
</Listbox>
</div>
<div>
<label for="start">Start</label>
<input type="date" id="start" required v-model="memberEducation.start" />
</div>
<div>
<label for="end">Ende (optional)</label>
<input type="date" id="end" v-model="memberEducation.end" />
</div>
<div>
<label for="place">Ort (optional)</label>
<input type="text" id="place" v-model="memberEducation.place" />
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" v-model="memberEducation.note" />
</div>
<div class="flex flex-row gap-2">
<button primary-outline type="reset" :disabled="canSaveOrReset" @click="resetForm">verwerfen</button>
<button primary type="submit" :disabled="status == 'loading' || canSaveOrReset">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>
<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'">
schließen
</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 { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
import { useEducationStore } from "@/stores/admin/configuration/education";
import type {
CreateMemberEducationViewModel,
MemberEducationViewModel,
UpdateMemberEducationViewModel,
} from "@/viewmodels/admin/club/member/memberEducation.models";
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
import isEqual from "lodash.isequal";
import cloneDeep from "lodash.clonedeep";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
loading: "loading" as "loading" | "fetched" | "failed",
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
origin: null as null | MemberEducationViewModel,
memberEducation: null as null | MemberEducationViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useEducationStore, ["educations"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.memberEducation);
},
selectedEducation() {
return this.educations.find((ms) => ms.id == this.memberEducation?.educationId)?.education;
},
},
mounted() {
this.fetchEducations();
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberEducationStore, ["updateMemberEducation", "fetchMemberEducationById"]),
...mapActions(useEducationStore, ["fetchEducations"]),
resetForm() {
this.memberEducation = cloneDeep(this.origin);
},
fetchItem() {
this.fetchMemberEducationById(this.data)
.then((result) => {
this.memberEducation = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.memberEducation == null) return;
let formData = e.target.elements;
let updateMemberEducation: UpdateMemberEducationViewModel = {
id: this.memberEducation.id,
start: formData.start.value,
end: formData.end.value,
note: formData.note.value,
place: formData.place.value,
educationId: this.memberEducation.educationId,
};
this.status = "loading";
this.updateMemberEducation(updateMemberEducation)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch((err) => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -0,0 +1,54 @@
<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 gap-2 justify-between items-center">
<p class="grow">{{ education.education }}</p>
<PencilIcon v-if="can('update', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon v-if="can('delete', 'club', 'member')" class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div class="p-2">
<p>
besucht: {{ education.start }} <span v-if="education.end">bis {{ education.end }}</span>
</p>
<p v-if="education.place">Ort: {{ education.place }}</p>
<p v-if="education.note">Notiz: {{ education.note }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { MemberEducationViewModel } from "@/viewmodels/admin/club/member/memberEducation.models";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
props: {
education: {
type: Object as PropType<MemberEducationViewModel>,
default: {},
},
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationEditModal.vue"))),
this.education.id
);
},
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationDeleteModal.vue"))),
this.education.id
);
},
},
});
</script>

View file

@ -1,17 +1,19 @@
<template> <template>
<RouterLink <RouterLink
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }" :to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md" class="flex flex-col h-fit w-full border border-primary rounded-md"
> >
<div <div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
>
<p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p> <p>{{ member.lastname }}, {{ member.firstname }} {{ member.nameaffix ? `- ${member.nameaffix}` : "" }}</p>
</div> </div>
<div class="p-2"> <div class="p-2">
<p v-if="member.internalId">Interne ID: {{ member.internalId }}</p> <p v-if="member.internalId">Interne ID: {{ member.internalId }}</p>
<p v-if="member.note">Notiz: {{ member.note }}</p>
<p>beigetreten: {{ member.firstMembershipEntry?.start }}</p> <p>beigetreten: {{ member.firstMembershipEntry?.start }}</p>
<p v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}, da {{member.lastMembershipEntry?.terminationReason ?? '- kein Grund angegeben'}}</p> <p v-if="member.lastMembershipEntry?.end">
ausgetreten: {{ member.lastMembershipEntry?.end }}, da
{{ member.lastMembershipEntry?.terminationReason ?? "- kein Grund angegeben" }}
</p>
</div> </div>
</RouterLink> </RouterLink>
</template> </template>

View file

@ -63,11 +63,12 @@ export default defineComponent({
}; };
this.status = "loading"; this.status = "loading";
this.createNewsletter(createNewsletter) this.createNewsletter(createNewsletter)
.then(() => { .then((res) => {
this.status = { status: "success" }; this.status = { status: "success" };
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
(this.$refs.form as HTMLFormElement).reset(); (this.$refs.form as HTMLFormElement).reset();
this.closeModal(); this.closeModal();
this.$router.push({ name: "admin-club-newsletter-overview", params: { newsletterId: res.data } });
}, 1500); }, 1500);
}) })
.catch(() => { .catch(() => {

View file

@ -4,7 +4,15 @@
:to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }" :to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }"
class="bg-primary p-2 text-white flex flex-row justify-between items-center" class="bg-primary p-2 text-white flex flex-row justify-between items-center"
> >
<p>{{ newsletter.title }}</p> <p>
{{ newsletter.title }} ({{
new Date(newsletter.createdAt).toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
})
}})
</p>
<PaperAirplaneIcon v-if="newsletter.isSent" class="w-5 h-5" /> <PaperAirplaneIcon v-if="newsletter.isSent" class="w-5 h-5" />
</RouterLink> </RouterLink>
<div class="p-2 max-h-48 overflow-y-auto"> <div class="p-2 max-h-48 overflow-y-auto">

View file

@ -66,11 +66,12 @@ export default defineComponent({
}; };
this.status = "loading"; this.status = "loading";
this.createProtocol(createProtocol) this.createProtocol(createProtocol)
.then(() => { .then((res) => {
this.status = { status: "success" }; this.status = { status: "success" };
this.timeout = setTimeout(() => { this.timeout = setTimeout(() => {
(this.$refs.form as HTMLFormElement).reset(); (this.$refs.form as HTMLFormElement).reset();
this.closeModal(); this.closeModal();
this.$router.push({ name: "admin-club-protocol-overview", params: { protocolId: res.data } });
}, 1500); }, 1500);
}) })
.catch(() => { .catch(() => {

View file

@ -0,0 +1,81 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Aus-/Fortbildung erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="education">Bezeichnung</label>
<input type="text" id="education" 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 == '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 { useEducationStore } from "@/stores/admin/configuration/education";
import type { CreateEducationViewModel } from "@/viewmodels/admin/configuration/education.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(useEducationStore, ["createEducation"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createEducation: CreateEducationViewModel = {
education: formData.education.value,
description: formData.description.value,
};
this.status = "loading";
this.createEducation(createEducation)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,75 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Aus-/Fortbildung {{ education?.education }} 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 == '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 { useEducationStore } from "@/stores/admin/configuration/education";
</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(useEducationStore, ["educations"]),
education() {
return this.educations.find((r) => r.id == this.data);
},
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEducationStore, ["deleteEducation"]),
triggerDelete() {
this.status = "loading";
this.deleteEducation(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,55 @@
<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>{{ education.education }}</p>
<div class="flex flex-row">
<RouterLink
v-if="can('update', 'configuration', 'education')"
:to="{ name: 'admin-configuration-education-edit', params: { id: education.id } }"
>
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</RouterLink>
<div v-if="can('delete', 'configuration', 'education')" @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">{{ education.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 } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability";
import { useModalStore } from "@/stores/modal";
import type { EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
education: { type: Object as PropType<EducationViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/education/DeleteEducationModal.vue"))
),
this.education.id
);
},
},
});
</script>

View file

@ -58,7 +58,7 @@ export default defineComponent({
}, },
{ {
key: "app.show_link_to_calendar", key: "app.show_link_to_calendar",
value: formData.show_link_to_calendar.checked || null, value: formData.show_link_to_calendar.checked,
}, },
]); ]);
}, },

View file

@ -149,7 +149,6 @@ export default defineComponent({
this.value.id = uuid(); this.value.id = uuid();
} }
if (!this.value.type) { if (!this.value.type) {
console.log("setting type");
this.type = "defined"; this.type = "defined";
} }
}, },

View file

@ -12,7 +12,14 @@ export async function abilityAndNavUpdate(to: any, from: any, next: any) {
let section = to.meta.section; let section = to.meta.section;
let module = to.meta.module; let module = to.meta.module;
if ((admin && ability.isAdmin()) || ability.can(type, section, module)) { if (to.name == "admin-default") {
navigation.activeNavigation = "club";
navigation.activeLink = null;
navigation.updateTopLevel();
navigation.updateNavigation();
NProgress.done();
next();
} else if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
NProgress.done(); NProgress.done();
navigation.activeNavigation = to.name.split("-")[1]; navigation.activeNavigation = to.name.split("-")[1];
navigation.activeLink = to.name.split("-")[2]; navigation.activeLink = to.name.split("-")[2];

View file

@ -15,7 +15,7 @@ const router = createRouter({
routes: [ routes: [
{ {
path: "/", path: "/",
redirect: { name: "admin" }, redirect: { name: "admin-default" },
}, },
{ {
path: "/login", path: "/login",
@ -76,12 +76,13 @@ const router = createRouter({
path: "/admin", path: "/admin",
name: "admin", name: "admin",
component: () => import("@/views/admin/View.vue"), component: () => import("@/views/admin/View.vue"),
beforeEnter: [isAuthenticated], beforeEnter: [isAuthenticated, abilityAndNavUpdate],
children: [ children: [
{ {
path: "", path: "",
name: "admin-default", name: "admin-default",
component: () => import("@/views/admin/ViewSelect.vue"), component: () => import("@/views/admin/ViewSelect.vue"),
beforeEnter: [abilityAndNavUpdate],
}, },
{ {
path: "club", path: "club",
@ -141,6 +142,12 @@ const router = createRouter({
component: () => import("@/views/admin/club/members/MemberAwards.vue"), component: () => import("@/views/admin/club/members/MemberAwards.vue"),
props: true, props: true,
}, },
{
path: "educations",
name: "admin-club-member-educations",
component: () => import("@/views/admin/club/members/MemberEducations.vue"),
props: true,
},
{ {
path: "qualifications", path: "qualifications",
name: "admin-club-member-qualifications", name: "admin-club-member-qualifications",
@ -360,6 +367,30 @@ const router = createRouter({
}, },
], ],
}, },
{
path: "education",
name: "admin-configuration-education-route",
component: () => import("@/views/RouterView.vue"),
meta: { type: "read", section: "configuration", module: "education" },
beforeEnter: [abilityAndNavUpdate],
children: [
{
path: "",
name: "admin-configuration-education",
component: () => import("@/views/admin/configuration/education/Education.vue"),
meta: { type: "read", section: "configuration", module: "education" },
beforeEnter: [abilityAndNavUpdate],
},
{
path: ":id/edit",
name: "admin-configuration-education-edit",
component: () => import("@/views/admin/configuration/education/EducationEdit.vue"),
meta: { type: "update", section: "configuration", module: "education" },
beforeEnter: [abilityAndNavUpdate],
props: true,
},
],
},
{ {
path: "executive-position", path: "executive-position",
name: "admin-configuration-executive_position-route", name: "admin-configuration-executive_position-route",

View file

@ -4,6 +4,7 @@ import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward";
import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition"; import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition";
import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification"; import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification";
import { useMembershipStore } from "@/stores/admin/club/member/membership"; import { useMembershipStore } from "@/stores/admin/club/member/membership";
import { useMemberEducationStore } from "../stores/admin/club/member/memberEducation";
export async function setMemberId(to: any, from: any, next: any) { export async function setMemberId(to: any, from: any, next: any) {
const member = useMemberStore(); const member = useMemberStore();
@ -14,6 +15,7 @@ export async function setMemberId(to: any, from: any, next: any) {
useMemberAwardStore().$reset(); useMemberAwardStore().$reset();
useMemberExecutivePositionStore().$reset(); useMemberExecutivePositionStore().$reset();
useMemberQualificationStore().$reset(); useMemberQualificationStore().$reset();
useMemberEducationStore().$reset();
next(); next();
} }
@ -28,6 +30,7 @@ export async function resetMemberStores(to: any, from: any, next: any) {
useMemberAwardStore().$reset(); useMemberAwardStore().$reset();
useMemberExecutivePositionStore().$reset(); useMemberExecutivePositionStore().$reset();
useMemberQualificationStore().$reset(); useMemberQualificationStore().$reset();
useMemberEducationStore().$reset();
next(); next();
} }

View file

@ -33,13 +33,21 @@ export const useAbilityStore = defineStore("ability", {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true; if (permissions?.admin || permissions?.adminByOwner) return true;
if ( if (
permissions[section]?.all == "*" || (permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
permissions[section]?.all?.includes(type) ||
permissions[section] != undefined permissions[section] != undefined
) )
return true; return true;
return false; return false;
}, },
canAccessSection:
(state) =>
(section: PermissionSection): boolean => {
const permissions = state.permissions;
if (state.isOwner) return true;
if (permissions?.admin || permissions?.adminByOwner) return true;
if (permissions[section] != undefined) return true;
return false;
},
isAdmin: (state) => (): boolean => { isAdmin: (state) => (): boolean => {
const permissions = state.permissions; const permissions = state.permissions;
if (state.isOwner) return true; if (state.isOwner) return true;
@ -72,13 +80,20 @@ export const useAbilityStore = defineStore("ability", {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true; if (permissions?.admin || permissions?.adminByOwner) return true;
if ( if (
permissions[section]?.all == "*" || (permissions[section]?.all == "*" || permissions[section]?.all?.includes(type)) &&
permissions[section]?.all?.includes(type) ||
permissions[section] != undefined permissions[section] != undefined
) )
return true; return true;
return false; return false;
}, },
_canAccessSection:
() =>
(permissions: PermissionObject, section: PermissionSection): boolean => {
// ignores ownership
if (permissions?.admin || permissions?.adminByOwner) return true;
if (permissions[section] != undefined) return true;
return false;
},
}, },
actions: { actions: {
setAbility(permissions: PermissionObject, isOwner: boolean) { setAbility(permissions: PermissionObject, isOwner: boolean) {

View file

@ -106,6 +106,7 @@ export const useMemberStore = defineStore("member", {
nameaffix: member.nameaffix, nameaffix: member.nameaffix,
birthdate: member.birthdate, birthdate: member.birthdate,
internalId: member.internalId, internalId: member.internalId,
note: member.note,
}); });
this.fetchMembers(); this.fetchMembers();
return result; return result;
@ -118,6 +119,7 @@ export const useMemberStore = defineStore("member", {
nameaffix: member.nameaffix, nameaffix: member.nameaffix,
birthdate: member.birthdate, birthdate: member.birthdate,
internalId: member.internalId, internalId: member.internalId,
note: member.note,
}); });
this.fetchMembers(); this.fetchMembers();
return result; return result;

View file

@ -0,0 +1,67 @@
import { defineStore } from "pinia";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
import { useMemberStore } from "./member";
import type {
CreateMemberEducationViewModel,
MemberEducationViewModel,
UpdateMemberEducationViewModel,
} from "@/viewmodels/admin/club/member/memberEducation.models";
export const useMemberEducationStore = defineStore("memberEducation", {
state: () => {
return {
memberEducations: [] as Array<MemberEducationViewModel>,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchMemberEducationsForMember() {
const memberId = useMemberStore().activeMember;
this.loading = "loading";
http
.get(`/admin/member/${memberId}/educations`)
.then((result) => {
this.memberEducations = result.data;
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
fetchMemberEducationById(id: number) {
const memberId = useMemberStore().activeMember;
return http.get(`/admin/member/${memberId}/education/${id}`);
},
async createMemberEducation(memberEducation: CreateMemberEducationViewModel): Promise<AxiosResponse<any, any>> {
const memberId = useMemberStore().activeMember;
const result = await http.post(`/admin/member/${memberId}/education`, {
start: memberEducation.start,
end: memberEducation.end,
place: memberEducation.place,
note: memberEducation.note,
educationId: memberEducation.educationId,
});
this.fetchMemberEducationsForMember();
return result;
},
async updateMemberEducation(memberEducation: UpdateMemberEducationViewModel): Promise<AxiosResponse<any, any>> {
const memberId = useMemberStore().activeMember;
const result = await http.patch(`/admin/member/${memberId}/education/${memberEducation.id}`, {
start: memberEducation.start,
end: memberEducation.end,
place: memberEducation.place,
note: memberEducation.note,
educationId: memberEducation.educationId,
});
this.fetchMemberEducationsForMember();
return result;
},
async deleteMemberEducation(memberEducation: number): Promise<AxiosResponse<any, any>> {
const memberId = useMemberStore().activeMember;
const result = await http.delete(`/admin/member/${memberId}/education/${memberEducation}`);
this.fetchMemberEducationsForMember();
return result;
},
},
});

View file

@ -7,6 +7,7 @@ import { useMemberStore } from "./member";
import type { import type {
CreateMembershipViewModel, CreateMembershipViewModel,
MembershipStatisticsViewModel, MembershipStatisticsViewModel,
MembershipTotalStatisticsViewModel,
MembershipViewModel, MembershipViewModel,
UpdateMembershipViewModel, UpdateMembershipViewModel,
} from "@/viewmodels/admin/club/member/membership.models"; } from "@/viewmodels/admin/club/member/membership.models";
@ -16,6 +17,7 @@ export const useMembershipStore = defineStore("membership", {
return { return {
memberships: [] as Array<MembershipViewModel>, memberships: [] as Array<MembershipViewModel>,
membershipStatistics: [] as Array<MembershipStatisticsViewModel>, membershipStatistics: [] as Array<MembershipStatisticsViewModel>,
totalMembershipStatistics: undefined as undefined | MembershipTotalStatisticsViewModel,
loading: "loading" as "loading" | "fetched" | "failed", loading: "loading" as "loading" | "fetched" | "failed",
}; };
}, },
@ -42,6 +44,15 @@ export const useMembershipStore = defineStore("membership", {
}) })
.catch((err) => {}); .catch((err) => {});
}, },
fetchMembershipTotalStatisticsForMember() {
const memberId = useMemberStore().activeMember;
http
.get(`/admin/member/${memberId}/memberships/totalstatistics`)
.then((result) => {
this.totalMembershipStatistics = result.data;
})
.catch((err) => {});
},
fetchMembershipById(id: number) { fetchMembershipById(id: number) {
const memberId = useMemberStore().activeMember; const memberId = useMemberStore().activeMember;
return http.get(`/admin/member/${memberId}/membership/${id}`); return http.get(`/admin/member/${memberId}/membership/${id}`);

View file

@ -1,11 +1,10 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { CreateNewsletterViewModel, SyncNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models"; import type { CreateNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
import { http } from "@/serverCom"; import { http } from "@/serverCom";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models"; import type { NewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import difference from "lodash.difference";
export const useNewsletterStore = defineStore("newsletter", { export const useNewsletterStore = defineStore("newsletter", {
state: () => { state: () => {
@ -72,7 +71,6 @@ export const useNewsletterStore = defineStore("newsletter", {
const result = await http.post(`/admin/newsletter`, { const result = await http.post(`/admin/newsletter`, {
title: newsletter.title, title: newsletter.title,
}); });
this.fetchNewsletters();
return result; return result;
}, },
async synchronizeActiveNewsletter(): Promise<void> { async synchronizeActiveNewsletter(): Promise<void> {

View file

@ -1,11 +1,10 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { CreateProtocolViewModel, SyncProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models"; import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
import { http } from "@/serverCom"; import { http } from "@/serverCom";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
import type { ProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models"; import type { ProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import difference from "lodash.difference";
export const useProtocolStore = defineStore("protocol", { export const useProtocolStore = defineStore("protocol", {
state: () => { state: () => {
@ -73,7 +72,6 @@ export const useProtocolStore = defineStore("protocol", {
title: protocol.title, title: protocol.title,
date: protocol.date, date: protocol.date,
}); });
this.fetchProtocols();
return result; return result;
}, },
async synchronizeActiveProtocol(): Promise<void> { async synchronizeActiveProtocol(): Promise<void> {

View file

@ -71,7 +71,7 @@ export const useProtocolAgendaStore = defineStore("protocolAgenda", {
await http await http
.patch(`/admin/protocol/${protocolId}/synchronize/agenda`, { .patch(`/admin/protocol/${protocolId}/synchronize/agenda`, {
agenda: differenceWith(this.agenda, this.origin, isEqual), agenda: this.agenda,
}) })
.then((res) => { .then((res) => {
this.syncingProtocolAgenda = "synced"; this.syncingProtocolAgenda = "synced";

View file

@ -72,7 +72,7 @@ export const useProtocolDecisionStore = defineStore("protocolDecision", {
await http await http
.patch(`/admin/protocol/${protocolId}/synchronize/decisions`, { .patch(`/admin/protocol/${protocolId}/synchronize/decisions`, {
decisions: differenceWith(this.decision, this.origin, isEqual), decisions: this.decision,
}) })
.then((res) => { .then((res) => {
this.syncingProtocolDecision = "synced"; this.syncingProtocolDecision = "synced";

View file

@ -75,7 +75,7 @@ export const useProtocolVotingStore = defineStore("protocolVoting", {
await http await http
.patch(`/admin/protocol/${protocolId}/synchronize/votings`, { .patch(`/admin/protocol/${protocolId}/synchronize/votings`, {
votings: differenceWith(this.voting, this.origin, isEqual), votings: this.voting,
}) })
.then((res) => { .then((res) => {
this.syncingProtocolVoting = "synced"; this.syncingProtocolVoting = "synced";

View file

@ -0,0 +1,55 @@
import { defineStore } from "pinia";
import type {
CreateEducationViewModel,
UpdateEducationViewModel,
EducationViewModel,
} from "@/viewmodels/admin/configuration/education.models";
import { http } from "@/serverCom";
import type { AxiosResponse } from "axios";
export const useEducationStore = defineStore("education", {
state: () => {
return {
educations: [] as Array<EducationViewModel>,
loading: "loading" as "loading" | "fetched" | "failed",
};
},
actions: {
fetchEducations() {
this.loading = "loading";
http
.get("/admin/education")
.then((result) => {
this.educations = result.data;
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
fetchEducationById(id: number): Promise<AxiosResponse<any, any>> {
return http.get(`/admin/education/${id}`);
},
async createEducation(education: CreateEducationViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.post(`/admin/education`, {
education: education.education,
description: education.description,
});
this.fetchEducations();
return result;
},
async updateActiveEducation(education: UpdateEducationViewModel): Promise<AxiosResponse<any, any>> {
const result = await http.patch(`/admin/education/${education.id}`, {
education: education.education,
description: education.description,
});
this.fetchEducations();
return result;
},
async deleteEducation(education: number): Promise<AxiosResponse<any, any>> {
const result = await http.delete(`/admin/education/${education}`);
this.fetchEducations();
return result;
},
},
});

View file

@ -48,7 +48,7 @@ export const useNavigationStore = defineStore("navigation", {
updateTopLevel() { updateTopLevel() {
const abilityStore = useAbilityStore(); const abilityStore = useAbilityStore();
this.topLevel = [ this.topLevel = [
...(abilityStore.canSection("read", "club") ...(abilityStore.canAccessSection("club")
? [ ? [
{ {
key: "club", key: "club",
@ -57,7 +57,7 @@ export const useNavigationStore = defineStore("navigation", {
} as topLevelNavigationModel, } as topLevelNavigationModel,
] ]
: []), : []),
...(abilityStore.canSection("read", "configuration") ...(abilityStore.canAccessSection("configuration")
? [ ? [
{ {
key: "configuration", key: "configuration",
@ -66,7 +66,7 @@ export const useNavigationStore = defineStore("navigation", {
} as topLevelNavigationModel, } as topLevelNavigationModel,
] ]
: []), : []),
...(abilityStore.canSection("read", "management") ...(abilityStore.canAccessSection("management")
? [ ? [
{ {
key: "management", key: "management",
@ -112,6 +112,9 @@ export const useNavigationStore = defineStore("navigation", {
...(abilityStore.can("read", "configuration", "qualification") ...(abilityStore.can("read", "configuration", "qualification")
? [{ key: "qualification", title: "Qualifikationen" }] ? [{ key: "qualification", title: "Qualifikationen" }]
: []), : []),
...(abilityStore.can("read", "configuration", "education")
? [{ key: "education", title: "Aus-/Fortbildungen" }]
: []),
...(abilityStore.can("read", "configuration", "executive_position") ...(abilityStore.can("read", "configuration", "executive_position")
? [{ key: "executive_position", title: "Vereinsämter" }] ? [{ key: "executive_position", title: "Vereinsämter" }]
: []), : []),
@ -147,7 +150,8 @@ export const useNavigationStore = defineStore("navigation", {
this.activeNavigationObject.main.findIndex((e) => e.key == this.activeLink) == -1 || this.activeNavigationObject.main.findIndex((e) => e.key == this.activeLink) == -1 ||
this.activeLink == "default" this.activeLink == "default"
) { ) {
let link = this.activeNavigationObject.main[0].key; let link = this.activeNavigationObject.main.filter((m) => !m.key.startsWith("divider"))[0].key;
this.activeLink = link;
router.push({ name: `admin-${this.activeNavigation}-${link}` }); router.push({ name: `admin-${this.activeNavigation}-${link}` });
} }
}, },

View file

@ -16,10 +16,13 @@
<div <div
class="max-w-full w-full grow flex flex-col divide-y-2 divide-gray-300 bg-white rounded-lg justify-center overflow-hidden" class="max-w-full w-full grow flex flex-col divide-y-2 divide-gray-300 bg-white rounded-lg justify-center overflow-hidden"
> >
<slot name="topBar"></slot> <div v-if="topBar || title" class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 v-if="title" class="font-bold text-xl h-8 min-h-fit">{{ title }}</h1>
<slot name="topBar"></slot>
</div>
<div class="flex flex-col gap-2 grow py-5 overflow-hidden"> <div class="flex flex-col gap-2 grow py-5 overflow-hidden">
<slot name="diffMain"></slot> <slot name="diffMain"></slot>
<div v-if="!diffMain" class="flex flex-col gap-2 grow px-7 overflow-y-scroll"> <div v-if="!diffMain" class="flex flex-col gap-2 grow px-7 overflow-y-auto">
<slot name="main"></slot> <slot name="main"></slot>
</div> </div>
</div> </div>
@ -51,6 +54,10 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
title: {
type: String,
default: "",
},
}, },
computed: { computed: {
...mapState(useNavigationStore, ["activeLink", "activeNavigation"]), ...mapState(useNavigationStore, ["activeLink", "activeNavigation"]),
@ -60,6 +67,9 @@ export default defineComponent({
rootRoute() { rootRoute() {
return ((this.$route?.name as string) ?? "").split("-")[0]; return ((this.$route?.name as string) ?? "").split("-")[0];
}, },
topBar() {
return this.$slots.topBar;
},
diffMain() { diffMain() {
return this.$slots.diffMain; return this.$slots.diffMain;
}, },

View file

@ -13,6 +13,7 @@ export type PermissionModule =
| "communication_type" | "communication_type"
| "membership_status" | "membership_status"
| "salutation" | "salutation"
| "education"
| "calendar_type" | "calendar_type"
| "user" | "user"
| "role" | "role"
@ -70,6 +71,7 @@ export const permissionModules: Array<PermissionModule> = [
"communication_type", "communication_type",
"membership_status", "membership_status",
"salutation", "salutation",
"education",
"calendar_type", "calendar_type",
"user", "user",
"role", "role",
@ -91,6 +93,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"communication_type", "communication_type",
"membership_status", "membership_status",
"salutation", "salutation",
"education",
"calendar_type", "calendar_type",
"query_store", "query_store",
"template", "template",

View file

@ -15,6 +15,7 @@ export interface MemberViewModel {
sendNewsletter?: CommunicationViewModel; sendNewsletter?: CommunicationViewModel;
smsAlarming?: Array<CommunicationViewModel>; smsAlarming?: Array<CommunicationViewModel>;
preferredCommunication?: Array<CommunicationViewModel>; preferredCommunication?: Array<CommunicationViewModel>;
note?: string;
} }
export interface MemberStatisticsViewModel { export interface MemberStatisticsViewModel {
@ -36,6 +37,7 @@ export interface CreateMemberViewModel {
nameaffix: string; nameaffix: string;
birthdate: Date; birthdate: Date;
internalId?: string; internalId?: string;
note?: string;
} }
export interface UpdateMemberViewModel { export interface UpdateMemberViewModel {
@ -46,4 +48,5 @@ export interface UpdateMemberViewModel {
nameaffix: string; nameaffix: string;
birthdate: Date; birthdate: Date;
internalId?: string; internalId?: string;
note?: string;
} }

View file

@ -0,0 +1,26 @@
export interface MemberEducationViewModel {
id: number;
start: Date;
end?: Date;
place?: string;
note?: string;
education: string;
educationId: number;
}
export interface CreateMemberEducationViewModel {
start: Date;
end?: Date;
place?: string;
note?: string;
educationId: number;
}
export interface UpdateMemberEducationViewModel {
id: number;
start: Date;
end?: Date;
place?: string;
note?: string;
educationId: number;
}

View file

@ -21,6 +21,18 @@ export interface MembershipStatisticsViewModel {
memberBirthdate: Date; memberBirthdate: Date;
} }
export interface MembershipTotalStatisticsViewModel {
durationInDays: number;
durationInYears: number;
exactDuration: string;
memberId: string;
memberSalutation: string;
memberFirstname: string;
memberLastname: string;
memberNameaffix: string;
memberBirthdate: Date;
}
export interface CreateMembershipViewModel { export interface CreateMembershipViewModel {
start: Date; start: Date;
statusId: number; statusId: number;

View file

@ -7,6 +7,7 @@ export interface NewsletterViewModel {
newsletterSignatur: string; newsletterSignatur: string;
isSent: boolean; isSent: boolean;
recipientsByQueryId?: string | null; recipientsByQueryId?: string | null;
createdAt: Date;
} }
export interface CreateNewsletterViewModel { export interface CreateNewsletterViewModel {

View file

@ -0,0 +1,16 @@
export interface EducationViewModel {
id: number;
education: string;
description: string | null;
}
export interface CreateEducationViewModel {
education: string;
description: string | null;
}
export interface UpdateEducationViewModel {
id: number;
education: string;
description: string | null;
}

View file

@ -65,6 +65,11 @@
</div> </div>
<p v-if="loginError" class="text-center">{{ loginError }}</p> <p v-if="loginError" class="text-center">{{ loginError }}</p>
</form> </form>
<div class="flex flex-col gap-2 empty:hidden">
<RouterLink v-if="appShow_link_to_calendar" :to="{ name: 'public-calendar' }" button primary-outline>
zum Kalender
</RouterLink>
</div>
<FormBottomBar /> <FormBottomBar />
</div> </div>
@ -76,7 +81,6 @@ import { defineComponent } from "vue";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue"; import FailureXMark from "@/components/FailureXMark.vue";
import { resetAllPiniaStores } from "@/helpers/piniaReset";
import FormBottomBar from "@/components/FormBottomBar.vue"; import FormBottomBar from "@/components/FormBottomBar.vue";
import AppLogo from "@/components/AppLogo.vue"; import AppLogo from "@/components/AppLogo.vue";
import { mapState } from "pinia"; import { mapState } from "pinia";
@ -96,10 +100,9 @@ export default defineComponent({
}; };
}, },
computed: { computed: {
...mapState(useConfigurationStore, ["clubName"]), ...mapState(useConfigurationStore, ["clubName", "appShow_link_to_calendar"]),
}, },
mounted() { mounted() {
resetAllPiniaStores();
this.username = localStorage.getItem("username") ?? ""; this.username = localStorage.getItem("username") ?? "";
this.routine = localStorage.getItem("routine") ?? ""; this.routine = localStorage.getItem("routine") ?? "";

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate :useStagedOverviewLink="false"> <MainTemplate title="Administration übertragen" :useStagedOverviewLink="false">
<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">Administration übertragen</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate :useStagedOverviewLink="false"> <MainTemplate title="Meine Anmeldedaten" :useStagedOverviewLink="false">
<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">Meine Anmeldedaten</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<Spinner v-if="loading" class="mx-auto" /> <Spinner v-if="loading" class="mx-auto" />
<div v-else class="flex flex-col w-full h-full gap-2 px-7 overflow-hidden"> <div v-else class="flex flex-col w-full h-full gap-2 px-7 overflow-hidden">

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate :useStagedOverviewLink="false"> <MainTemplate title="Mein Account" :useStagedOverviewLink="false">
<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">Mein Account</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,18 +1,13 @@
<template> <template>
<MainTemplate :useStagedOverviewLink="false"> <MainTemplate title="Meine Berechtigungen" :useStagedOverviewLink="false">
<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">Meine Berechtigungen</h1>
</div>
</template>
<template #main> <template #main>
<Permission :permissions="permissions" :disableEdit="true" /> <Permission :permissions="permissions" disableEdit />
</template> </template>
</MainTemplate> </MainTemplate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, markRaw, defineAsyncComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue"; import MainTemplate from "@/templates/Main.vue";
import Permission from "@/components/admin/Permission.vue"; import Permission from "@/components/admin/Permission.vue";

View file

@ -1,15 +1,9 @@
<template> <template>
<MainTemplate> <MainTemplate title="Kalender">
<template #topBar> <template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <div class="flex flex-row gap-2">
<h1 class="font-bold text-xl h-8">Kalender</h1> <PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select({ start: '', end: '', allDay: false })" />
<div class="flex flex-row gap-2"> <LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
<PlusIcon
class="text-gray-500 h-5 w-5 cursor-pointer"
@click="select({ start: '', end: '', allDay: false })"
/>
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" />
</div>
</div> </div>
</template> </template>
<template #diffMain> <template #diffMain>

View file

@ -1,84 +1,77 @@
<template> <template>
<MainTemplate> <MainTemplate title="Liste Drucken">
<template #topBar> <template #main>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <form
<h1 class="font-bold text-xl h-8">Liste Drucken</h1> class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2"
</div> @submit.prevent="sendPrintJob"
</template> >
<template #diffMain> <div class="flex flex-row gap-2 items-center">
<div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto"> <p class="min-w-16">Titel:</p>
<form <input id="title" type="text" required />
class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2" </div>
@submit.prevent="sendPrintJob" <div class="flex flex-row gap-2 items-center">
> <p class="min-w-16">Query:</p>
<div class="flex flex-row gap-2 items-center"> <select id="query" value="member">
<p class="min-w-16">Titel:</p> <option value="member">(system) alle Mitglieder</option>
<input id="title" type="text" required /> <option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option>
</div> <option v-for="query in queries" :key="query.id" :value="query.id">
<div class="flex flex-row gap-2 items-center"> {{ query.title }}
<p class="min-w-16">Query:</p> </option>
<select id="query" value="member"> </select>
<option value="member">(system) alle Mitglieder</option> </div>
<option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option> <div class="flex flex-col md:flex-row gap-2 md:items-center">
<option v-for="query in queries" :key="query.id" :value="query.id"> <div class="flex flex-row w-full gap-2 items-center">
{{ query.title }} <p class="min-w-16">Kopfzeile:</p>
</option> <select id="header" value="def">
</select>
</div>
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<div class="flex flex-row w-full gap-2 items-center">
<p class="min-w-16">Kopfzeile:</p>
<select id="header" value="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="whitespace-nowrap">Höhe [mm]:</p>
<input id="headerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
</div>
</div>
<div class="flex flex-row gap-2 items-center">
<p class="min-w-16">Hauptteil:</p>
<select id="body" value="def">
<option value="def">Standard-Vorlage verwenden</option> <option value="def">Standard-Vorlage verwenden</option>
<option value="listprint.member">(system) Mitgliederliste</option>
<option v-for="template in templates" :key="template.id" :value="template.id"> <option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.template }} {{ template.template }}
</option> </option>
</select> </select>
</div> </div>
<div class="flex flex-col md:flex-row gap-2 md:items-center"> <div class="flex flex-row gap-2 items-center">
<div class="flex flex-row w-full gap-2 items-center"> <p class="whitespace-nowrap">Höhe [mm]:</p>
<p class="min-w-16">Fußzeile:</p> <input id="headerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
<select id="footer" value="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="whitespace-nowrap">Höhe [mm]:</p>
<input id="footerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
</div>
</div> </div>
<div class="flex flex-row gap-2"> </div>
<button type="submit" primary class="w-fit!">Liste drucken</button> <div class="flex flex-row gap-2 items-center">
<button type="reset" primary-outline class="w-fit!">zurücksetzen</button> <p class="min-w-16">Hauptteil:</p>
<select id="body" value="def">
<option value="def">Standard-Vorlage verwenden</option>
<option value="listprint.member">(system) Mitgliederliste</option>
<option v-for="template in templates" :key="template.id" :value="template.id">
{{ template.template }}
</option>
</select>
</div>
<div class="flex flex-col md:flex-row gap-2 md:items-center">
<div class="flex flex-row w-full gap-2 items-center">
<p class="min-w-16">Fußzeile:</p>
<select id="footer" value="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> <div class="flex flex-row gap-2 items-center">
<div class="w-full grow min-h-[50%] flex flex-col gap-2"> <p class="whitespace-nowrap">Höhe [mm]:</p>
<Spinner v-if="status == 'loading'" /> <input id="footerHeight" type="number" :min="15" class="w-24!" placeholder="15" />
<div class="grow">
<iframe v-show="status == 'success'" ref="viewer" class="w-full h-full" />
</div> </div>
</div>
<div class="flex flex-row gap-2">
<button type="submit" primary class="w-fit!">Liste drucken</button>
<button type="reset" primary-outline class="w-fit!">zurücksetzen</button>
</div>
</form>
<div class="w-full grow min-h-[50%] flex flex-col gap-2">
<Spinner v-if="status == 'loading'" />
<div class="grow">
<iframe v-show="status == 'success'" ref="viewer" class="w-full h-full" />
</div>
<div v-show="status == 'success'" class="flex flex-row gap-2 justify-end"> <div v-show="status == 'success'" class="flex flex-row gap-2 justify-end">
<a ref="download" button primary class="w-fit!">download</a> <a ref="download" button primary class="w-fit!">download</a>
</div>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Mitglieder">
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Mitglieder</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7"> <div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination <Pagination

View file

@ -75,6 +75,10 @@
<label for="internalId">Interne ID (optional)</label> <label for="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" v-model="member.internalId" /> <input type="text" id="internalId" v-model="member.internalId" />
</div> </div>
<div>
<label for="note">Notiz (optional)</label>
<textarea type="text" id="note" v-model="member.note" />
</div>
<div class="flex flex-row justify-end gap-2"> <div class="flex flex-row justify-end gap-2">
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm"> <button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
verwerfen verwerfen
@ -93,7 +97,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useMemberStore } from "@/stores/admin/club/member/member"; import { useMemberStore } from "@/stores/admin/club/member/member";
import type { MemberViewModel, UpdateMemberViewModel } from "@/viewmodels/admin/club/member/member.models"; import type { MemberViewModel, UpdateMemberViewModel } from "@/viewmodels/admin/club/member/member.models";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
@ -163,6 +166,7 @@ export default defineComponent({
nameaffix: formData.nameaffix.value, nameaffix: formData.nameaffix.value,
birthdate: formData.birthdate.value, birthdate: formData.birthdate.value,
internalId: formData.internalId.value, internalId: formData.internalId.value,
note: formData.note.value,
}; };
this.status = "loading"; this.status = "loading";
this.updateActiveMember(updateMember) this.updateActiveMember(updateMember)

View file

@ -0,0 +1,51 @@
<template>
<div class="flex flex-col gap-2 h-full w-full overflow-y-auto">
<div v-if="memberEducations != null" class="flex flex-col gap-2 w-full">
<MemberEducationListItem v-for="education in memberEducations" :key="education.id" :education="education" />
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'club', 'member')" primary class="w-fit!" @click="openCreateModal">
Aus-/Fortbildung hinzufügen
</button>
</div>
</template>
<script setup lang="ts">
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useMemberEducationStore } from "@/stores/admin/club/member/memberEducation";
import MemberEducationListItem from "@/components/admin/club/member/MemberEducationListItem.vue";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMemberEducationStore, ["memberEducations", "loading"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchItem();
},
methods: {
...mapActions(useMemberEducationStore, ["fetchMemberEducationsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchMemberEducationsForMember();
},
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberEducationCreateModal.vue")))
);
},
},
});
</script>

View file

@ -25,9 +25,19 @@
<label for="birthdate">Geburtsdatum</label> <label for="birthdate">Geburtsdatum</label>
<input type="date" id="birthdate" :value="activeMemberObj.birthdate" readonly /> <input type="date" id="birthdate" :value="activeMemberObj.birthdate" readonly />
</div> </div>
<div v-if="membershipStatistics.length != 0"> <div>
<label for="note">Notiz</label>
<textarea type="text" id="note" v-model="activeMemberObj.note" readonly />
</div>
<div v-if="membershipStatistics.length != 0 || totalMembershipStatistics != undefined">
<p>Statistiken zur Mitgliedschaft</p> <p>Statistiken zur Mitgliedschaft</p>
<div class="flex flex-col h-fit w-full border border-primary rounded-md"> <div class="flex flex-col h-fit w-full rounded-md overflow-hidden divide-y divide-white">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>
gesamt {{ totalMembershipStatistics?.durationInDays }} Tage
<span class="whitespace-nowrap"> ~> {{ totalMembershipStatistics?.exactDuration }}</span>
</p>
</div>
<div <div
v-for="stat in membershipStatistics" v-for="stat in membershipStatistics"
class="bg-primary p-2 text-white flex flex-row justify-between items-center" class="bg-primary p-2 text-white flex flex-row justify-between items-center"
@ -149,16 +159,20 @@ export default defineComponent({
}, },
computed: { computed: {
...mapState(useMemberStore, ["activeMemberObj", "activeMemberStatistics", "loadingActive"]), ...mapState(useMemberStore, ["activeMemberObj", "activeMemberStatistics", "loadingActive"]),
...mapState(useMembershipStore, ["membershipStatistics"]), ...mapState(useMembershipStore, ["membershipStatistics", "totalMembershipStatistics"]),
}, },
mounted() { mounted() {
this.fetchMemberByActiveId(); this.fetchMemberByActiveId();
this.fetchMemberStatisticsByActiveId(); this.fetchMemberStatisticsByActiveId();
this.fetchMembershipStatisticsForMember(); this.fetchMembershipStatisticsForMember();
this.fetchMembershipTotalStatisticsForMember();
}, },
methods: { methods: {
...mapActions(useMemberStore, ["fetchMemberByActiveId", "fetchMemberStatisticsByActiveId"]), ...mapActions(useMemberStore, ["fetchMemberByActiveId", "fetchMemberStatisticsByActiveId"]),
...mapActions(useMembershipStore, ["fetchMembershipStatisticsForMember"]), ...mapActions(useMembershipStore, [
"fetchMembershipStatisticsForMember",
"fetchMembershipTotalStatisticsForMember",
]),
}, },
}); });
</script> </script>

View file

@ -4,12 +4,11 @@
<RouterLink to="../" class="text-primary">zurück zur Liste</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste</RouterLink>
</template> </template>
<template #topBar> <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">
<h1 class="font-bold text-xl h-8 min-h-fit grow"> {{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }}
{{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }} {{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }}
{{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }} </h1>
</h1> <div class="flex flex-row gap-2">
<div title="Mitgliederliste drucken" @click="openPrintModal"> <div title="Mitgliederliste drucken" @click="openPrintModal">
<DocumentTextIcon class="w-5 h-5 cursor-pointer" /> <DocumentTextIcon class="w-5 h-5 cursor-pointer" />
</div> </div>
@ -22,7 +21,7 @@
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col grow gap-2 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"> <div class="w-full flex flex-row max-lg:flex-wrap justify-center items-stretch">
<RouterLink <RouterLink
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.route" :key="tab.route"
@ -32,7 +31,7 @@
> >
<p <p
:class="[ :class="[
'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden', 'flex w-full h-full items-center justify-center rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden',
isActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200', isActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200',
]" ]"
> >
@ -69,8 +68,9 @@ export default defineComponent({
{ route: "admin-club-member-overview", title: "Übersicht" }, { route: "admin-club-member-overview", title: "Übersicht" },
{ route: "admin-club-member-membership", title: "Mitgliedschaft" }, { route: "admin-club-member-membership", title: "Mitgliedschaft" },
{ route: "admin-club-member-communication", title: "Kommunikation" }, { route: "admin-club-member-communication", title: "Kommunikation" },
{ route: "admin-club-member-awards", title: "Auszeichnungen" }, { route: "admin-club-member-awards", title: "Auszeichnungen / Ehrungen" },
{ route: "admin-club-member-qualifications", title: "Qualifikationen" }, { route: "admin-club-member-educations", title: "Aus- / Fortbildungen" },
{ route: "admin-club-member-qualifications", title: "Qualifikationen / Funktionen" },
{ route: "admin-club-member-positions", title: "Vereinsämter" }, { route: "admin-club-member-positions", title: "Vereinsämter" },
], ],
}; };

View file

@ -1,24 +0,0 @@
<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">Übersicht</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col gap-2 justify-center items-center h-full"></div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState } from "pinia";
import MainTemplate from "@/templates/Main.vue";
</script>
<script lang="ts">
export default defineComponent({
computed: {},
});
</script>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Newsletter">
<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> <template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7"> <div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination <Pagination

View file

@ -37,11 +37,15 @@
}) })
}} }}
</p> </p>
<TrashIcon <DoubleConfirmClick
v-if="can('create', 'club', 'newsletter')" v-if="can('create', 'club', 'newsletter')"
class="w-5 h-5 p-1 box-content cursor-pointer text-white" light
@click.prevent="removeSelected(item.calendarId)" v-slot="{ isSensitive }"
/> @click:submit="removeSelected(item.calendarId)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
</summary> </summary>
<div class="flex flex-col gap-2 px-1"> <div class="flex flex-col gap-2 px-1">
<input <input
@ -111,8 +115,10 @@ import { useAbilityStore } from "@/stores/ability";
import { useCalendarStore } from "@/stores/admin/club/calendar"; import { useCalendarStore } from "@/stores/admin/club/calendar";
import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models"; import type { CalendarViewModel } from "@/viewmodels/admin/club/calendar.models";
import { TrashIcon } from "@heroicons/vue/24/outline"; import { TrashIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import type { NewsletterDatesViewModel } from "@/viewmodels/admin/club/newsletter/newsletterDates.models"; import type { NewsletterDatesViewModel } from "@/viewmodels/admin/club/newsletter/newsletterDates.models";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -42,11 +42,15 @@
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p> <p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p>
</div> </div>
<TrashIcon <DoubleConfirmClick
v-if="can('create', 'club', 'newsletter') && showMemberSelect" v-if="can('create', 'club', 'newsletter') && showMemberSelect"
class="w-5 h-5 p-1 box-content cursor-pointer" light
@click="removeSelected(member.id)" v-slot="{ isSensitive }"
/> @click:submit="removeSelected(member.id)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
</div> </div>
</div> </div>
@ -61,17 +65,8 @@
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapActions, mapState, mapWritableState } from "pinia";
import Spinner from "@/components/Spinner.vue"; 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 { ArchiveBoxIcon, ExclamationTriangleIcon, TrashIcon, UserPlusIcon } from "@heroicons/vue/24/outline"; import { ArchiveBoxIcon, ExclamationTriangleIcon, TrashIcon, UserPlusIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import { useMemberStore } from "@/stores/admin/club/member/member"; import { useMemberStore } from "@/stores/admin/club/member/member";
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models"; import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter"; import { useNewsletterStore } from "@/stores/admin/club/newsletter/newsletter";
@ -79,9 +74,9 @@ import { useNewsletterRecipientsStore } from "@/stores/admin/club/newsletter/new
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore"; import { useQueryStoreStore } from "@/stores/admin/configuration/queryStore";
import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder"; import { useQueryBuilderStore } from "@/stores/admin/club/queryBuilder";
import cloneDeep from "lodash.clonedeep";
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue"; import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
import type { FieldType } from "@/types/dynamicQueries"; import type { FieldType } from "@/types/dynamicQueries";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -1,20 +1,17 @@
<template> <template>
<MainTemplate> <MainTemplate :title="origin?.title">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink> <RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
</template> </template>
<template #topBar> <template #topBar>
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7"> <NewsletterSyncing
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}</h1> :executeSyncAll="executeSyncAll"
<NewsletterSyncing @syncState="
:executeSyncAll="executeSyncAll" (state) => {
@syncState=" syncState = state;
(state) => { }
syncState = state; "
} />
"
/>
</div>
</template> </template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Protokolle">
<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">Protokolle</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col w-full h-full gap-2 justify-center px-7"> <div class="flex flex-col w-full h-full gap-2 justify-center px-7">
<Pagination <Pagination

View file

@ -31,6 +31,16 @@
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<DoubleConfirmClick
v-if="can('create', 'club', 'protocol')"
light
v-slot="{ isSensitive }"
@click:submit="removeFromArray(item.id)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
<div class="flex flex-col"> <div class="flex flex-col">
<ChevronUpIcon <ChevronUpIcon
v-if="index != 0" v-if="index != 0"
@ -73,7 +83,9 @@ import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda"; import { useProtocolAgendaStore } from "@/stores/admin/club/protocol/protocolAgenda";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -109,6 +121,9 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.agenda = this.agenda.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -31,6 +31,16 @@
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<DoubleConfirmClick
v-if="can('create', 'club', 'protocol')"
light
v-slot="{ isSensitive }"
@click:submit="removeFromArray(item.id)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
<div class="flex flex-col"> <div class="flex flex-col">
<ChevronUpIcon <ChevronUpIcon
v-if="index != 0" v-if="index != 0"
@ -73,7 +83,9 @@ import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision"; import { useProtocolDecisionStore } from "@/stores/admin/club/protocol/protocolDecision";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -109,6 +121,9 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.decision = this.decision.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -42,11 +42,15 @@
</label> </label>
</div> </div>
</div> </div>
<TrashIcon <DoubleConfirmClick
v-if="can('create', 'club', 'protocol')" v-if="can('create', 'club', 'protocol')"
class="w-5 h-5 p-1 box-content cursor-pointer" light
@click="removeSelected(member.memberId)" v-slot="{ isSensitive }"
/> @click:submit="removeSelected(member.memberId)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
</div> </div>
</div> </div>
</div> </div>
@ -57,10 +61,12 @@ import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapActions, mapState, mapWritableState } from "pinia";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import { TrashIcon } from "@heroicons/vue/24/outline"; import { TrashIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence"; import { useProtocolPresenceStore } from "@/stores/admin/club/protocol/protocolPresence";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue"; import MemberSearchSelect from "@/components/admin/MemberSearchSelect.vue";
import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models"; import type { MemberViewModel } from "@/viewmodels/admin/club/member/member.models";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">

View file

@ -1,20 +1,17 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`${origin?.title}, ${origin?.date}`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink> <RouterLink to="../" class="text-primary w-fit">zurück zur Liste</RouterLink>
</template> </template>
<template #topBar> <template #topBar>
<div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7"> <ProtocolSyncing
<h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}, {{ origin?.date }}</h1> :executeSyncAll="executeSyncAll"
<ProtocolSyncing @syncState="
:executeSyncAll="executeSyncAll" (state) => {
@syncState=" syncState = state;
(state) => { }
syncState = state; "
} />
"
/>
</div>
</template> </template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
@ -92,10 +89,10 @@ export default defineComponent({
}, },
mounted() { mounted() {
this.fetchProtocolByActiveId(); this.fetchProtocolByActiveId();
this.fetchProtocolAgenda() this.fetchProtocolAgenda();
this.fetchProtocolDecision() this.fetchProtocolDecision();
this.fetchProtocolPresence() this.fetchProtocolPresence();
this.fetchProtocolVoting() this.fetchProtocolVoting();
}, },
// this.syncState is undefined, so it will never work // this.syncState is undefined, so it will never work
// beforeRouteLeave(to, from, next) { // beforeRouteLeave(to, from, next) {
@ -118,8 +115,8 @@ export default defineComponent({
...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]), ...mapActions(useProtocolStore, ["fetchProtocolByActiveId"]),
...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda"]), ...mapActions(useProtocolAgendaStore, ["fetchProtocolAgenda"]),
...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision"]), ...mapActions(useProtocolDecisionStore, ["fetchProtocolDecision"]),
...mapActions(useProtocolPresenceStore,["fetchProtocolPresence"]), ...mapActions(useProtocolPresenceStore, ["fetchProtocolPresence"]),
...mapActions(useProtocolVotingStore,["fetchProtocolVoting"]), ...mapActions(useProtocolVotingStore, ["fetchProtocolVoting"]),
...mapActions(useModalStore, ["openModal"]), ...mapActions(useModalStore, ["openModal"]),
openInfoModal() { openInfoModal() {
this.openModal( this.openModal(

View file

@ -31,6 +31,16 @@
:disabled="!can('create', 'club', 'protocol')" :disabled="!can('create', 'club', 'protocol')"
/> />
<DoubleConfirmClick
v-if="can('create', 'club', 'protocol')"
light
v-slot="{ isSensitive }"
@click:submit="removeFromArray(item.id)"
>
<TrashIcon v-if="!isSensitive" class="h-5 w-5" />
<TrashIconSolid v-else class="h-5 w-5" />
</DoubleConfirmClick>
<div class="flex flex-col"> <div class="flex flex-col">
<ChevronUpIcon <ChevronUpIcon
v-if="index != 0" v-if="index != 0"
@ -83,14 +93,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState } from "pinia"; import { mapActions, mapState, mapWritableState } from "pinia";
import Spinner from "@/components/Spinner.vue"; import Spinner from "@/components/Spinner.vue";
import { QuillEditor } from "@vueup/vue-quill"; import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css"; import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { toolbarOptions } from "@/helpers/quillConfig"; import { toolbarOptions } from "@/helpers/quillConfig";
import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting"; import { useProtocolVotingStore } from "@/stores/admin/club/protocol/protocolVoting";
import { useAbilityStore } from "@/stores/ability"; import { useAbilityStore } from "@/stores/ability";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { TrashIcon as TrashIconSolid } from "@heroicons/vue/24/solid";
import DoubleConfirmClick from "@/components/DoubleConfirmClick.vue";
</script> </script>
<script lang="ts"> <script lang="ts">
@ -99,7 +111,7 @@ export default defineComponent({
protocolId: String, protocolId: String,
}, },
computed: { computed: {
...mapState(useProtocolVotingStore, ["voting", "loading"]), ...mapWritableState(useProtocolVotingStore, ["voting", "loading"]),
...mapState(useAbilityStore, ["can"]), ...mapState(useAbilityStore, ["can"]),
sortedVoting() { sortedVoting() {
return this.voting.slice().sort((a, b) => a.sort - b.sort); return this.voting.slice().sort((a, b) => a.sort - b.sort);
@ -126,6 +138,9 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.voting = this.voting.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -1,57 +1,50 @@
<template> <template>
<MainTemplate> <MainTemplate title="Query Builder">
<template #topBar> <template #main>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <BuilderHost
<h1 class="font-bold text-xl h-8">Query Builder</h1> v-model="query"
</div> allow-predefined-select
</template> @query:run="sendQuery"
<template #diffMain> @query:save="triggerSave"
<div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto"> @results:clear="clearResults"
<BuilderHost @results:export="exportData"
v-model="query" />
allow-predefined-select <p>Ergebnisse:</p>
@query:run="sendQuery" <div
@query:save="triggerSave" v-if="loadingData == 'failed'"
@results:clear="clearResults" class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none"
@results:export="exportData" >
/> <p v-if="typeof queryError == 'string'">
<p>Ergebnisse:</p> {{ queryError }}
<div </p>
v-if="loadingData == 'failed'" <div v-else>
class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none" <p>
> CODE: <br />
<p v-if="typeof queryError == 'string'"> {{ queryError.code }}
{{ queryError }} </p>
<br />
<p>
MSG: <br />
{{ queryError.msg }}
</p>
<br />
<p>
RESULTING SQL: <br />
{{ queryError.sql }}
</p> </p>
<div v-else>
<p>
CODE: <br />
{{ queryError.code }}
</p>
<br />
<p>
MSG: <br />
{{ queryError.msg }}
</p>
<br />
<p>
RESULTING SQL: <br />
{{ queryError.sql }}
</p>
</div>
</div> </div>
<Pagination
v-else
:items="data"
:totalCount="totalLength"
:indicateLoading="loadingData == 'loading'"
@load-data="(offset, count) => sendQuery(offset, count)"
>
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
<p>{{ row }}</p>
</template>
</Pagination>
</div> </div>
<Pagination
v-else
:items="data"
:totalCount="totalLength"
:indicateLoading="loadingData == 'loading'"
@load-data="(offset, count) => sendQuery(offset, count)"
>
<template #pageRow="{ row }: { row: { id: FieldType; [key: string]: FieldType } }">
<p>{{ row }}</p>
</template>
</Pagination>
</template> </template>
</MainTemplate> </MainTemplate>
</template> </template>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Auszeichnungen">
<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">Auszeichnungen</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Auszeichnung ${origin?.award} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Auszeichnung {{ origin?.award }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Termintyp">
<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">Termintyp</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Termintyp ${origin?.type} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Termintyp {{ origin?.type }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Kommunikationsarten">
<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">Kommunikationsarten</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Kommunikationsart ${origin?.type} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Kommunikationsart {{ origin?.type }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -0,0 +1,49 @@
<template>
<MainTemplate title="Aus-/Fortbildungen">
<template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<EducationListItem v-for="education in educations" :key="education.id" :education="education" />
</div>
<div class="flex flex-row gap-4">
<button v-if="can('create', 'configuration', 'education')" primary class="w-fit!" @click="openCreateModal">
Aus-/Fortbildung erstellen
</button>
</div>
</div>
</template>
</MainTemplate>
</template>
<script setup lang="ts">
import { defineComponent, defineAsyncComponent, markRaw } from "vue";
import { mapState, mapActions } from "pinia";
import MainTemplate from "@/templates/Main.vue";
import { useEducationStore } from "@/stores/admin/configuration/education";
import EducationListItem from "@/components/admin/configuration/education/EducationListItem.vue";
import { useModalStore } from "@/stores/modal";
import { useAbilityStore } from "@/stores/ability";
</script>
<script lang="ts">
export default defineComponent({
computed: {
...mapState(useEducationStore, ["educations"]),
...mapState(useAbilityStore, ["can"]),
},
mounted() {
this.fetchEducations();
},
methods: {
...mapActions(useEducationStore, ["fetchEducations"]),
...mapActions(useModalStore, ["openModal"]),
openCreateModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/education/CreateEducationModal.vue"))
)
);
},
},
});
</script>

View file

@ -0,0 +1,120 @@
<template>
<MainTemplate :title="`Aus-/Fortbildung ${origin?.education} - Daten bearbeiten`">
<template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template>
<template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<form
v-else-if="education != null"
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
@submit.prevent="triggerUpdate"
>
<div>
<label for="education">Bezeichnung</label>
<input type="text" id="education" required v-model="education.education" />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<input type="text" id="description" v-model="education.description" />
</div>
<div class="flex flex-row justify-end gap-2">
<button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm">
verwerfen
</button>
<button primary type="submit" class="w-fit!" :disabled="status == 'loading' || canSaveOrReset">
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 { useEducationStore } from "@/stores/admin/configuration/education";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { RouterLink } from "vue-router";
import type { UpdateEducationViewModel, EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
import cloneDeep from "lodash.clonedeep";
import isEqual from "lodash.isequal";
</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 | EducationViewModel,
education: null as null | EducationViewModel,
timeout: null as any,
};
},
computed: {
canSaveOrReset(): boolean {
return isEqual(this.origin, this.education);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useEducationStore, ["fetchEducationById", "updateActiveEducation"]),
resetForm() {
this.education = cloneDeep(this.origin);
},
fetchItem() {
this.fetchEducationById(parseInt(this.id ?? ""))
.then((result) => {
this.education = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerUpdate(e: any) {
if (this.education == null) return;
let formData = e.target.elements;
let updateEducation: UpdateEducationViewModel = {
id: this.education.id,
education: formData.education.value,
description: formData.description.value,
};
this.status = "loading";
this.updateActiveEducation(updateEducation)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch((err) => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Vereinsämter">
<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">Vereinsämter</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Vereinsamt ${origin?.position} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Vereinsamt {{ origin?.position }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Mitgliedsstatus">
<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">Mitgliedsstatus</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Mitgliedsstatus ${origin?.status} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Mitgliedsstatus {{ origin?.status }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Newsletter Konfiguration">
<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> <template #main>
<p> <p>
Ein Newsletter kann als pdf exportiert oder per Mail versandt werden. <br /> Ein Newsletter kann als pdf exportiert oder per Mail versandt werden. <br />

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Qualifikationen">
<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">Qualifikationen</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Qualifikation ${origin?.qualification} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Qualifikation {{ origin?.qualification }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="gespeicherte Abfragen">
<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">gespeicherte Abfragen</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Anrede">
<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">Anrede</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Anrede ${origin?.salutation} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Anrede {{ origin?.salutation }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,12 +1,9 @@
<template> <template>
<MainTemplate> <MainTemplate title="Templates">
<template #topBar> <template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <RouterLink :to="{ name: 'admin-configuration-template-info' }">
<h1 class="font-bold text-xl h-8">Templates</h1> <InformationCircleIcon class="text-gray-500 h-5 w-5" />
<RouterLink :to="{ name: 'admin-configuration-template-info' }"> </RouterLink>
<InformationCircleIcon class="text-gray-500 h-5 w-5" />
</RouterLink>
</div>
</template> </template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Template ${origin?.template} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Templates - Verwendungsinformation">
<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> <template #main>
<p> <p>
Mit diesem Editor können Vorlagen erstellt werden, welche später dafür genutzt werden können, um pdfs zu drucken Mit diesem Editor können Vorlagen erstellt werden, welche später dafür genutzt werden können, um pdfs zu drucken

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Template-Verwendung">
<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> <template #main>
<TemplateUsageListItem v-for="usage in templateUsages" :key="usage.scope" :templateUsage="usage" /> <TemplateUsageListItem v-for="usage in templateUsages" :key="usage.scope" :templateUsage="usage" />
</template> </template>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Backups">
<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">Backups</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col gap-2 grow px-7 overflow-hidden">
<div class="flex flex-col grow gap-2 overflow-hidden"> <div class="flex flex-col grow gap-2 overflow-hidden">

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll">
<BackupListItem v-for="backup in backups" :key="backup" :backup="backup" /> <BackupListItem v-for="backup in backups" :key="backup" :backup="backup" />
</div> </div>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll">
<BackupListItem v-for="backup in backups" :key="backup" :backup="backup" /> <BackupListItem v-for="backup in backups" :key="backup" :backup="backup" />
</div> </div>
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Rollen">
<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">Rollen</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Rolle ${origin?.role} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Rolle {{ origin?.role }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Rolle ${role?.role} - Berechtigungen bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Rolle {{ role?.role }} - Berechtigungen bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Einstellungen">
<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">Einstellungen</h1>
</div>
</template>
<template #main> <template #main>
<p>Hinweis: Optionale Felder können leer gelassen werden und nutzen dann einen Fallback-Werte.</p> <p>Hinweis: Optionale Felder können leer gelassen werden und nutzen dann einen Fallback-Werte.</p>
<ClubImageSetting /> <ClubImageSetting />

View file

@ -1,17 +1,10 @@
<template> <template>
<MainTemplate> <MainTemplate title="offene Einladungen">
<template #headerInsert> <template #headerInsert>
<RouterLink :to="{ name: 'admin-management-user' }" class="text-primary">zurück zur Nutzerliste</RouterLink> <RouterLink :to="{ name: 'admin-management-user' }" class="text-primary">zurück zur Nutzerliste</RouterLink>
</template> </template>
<template #topBar> <template #main>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <InviteListItem v-for="invite in invites" :key="invite.username" :invite="invite" />
<h1 class="font-bold text-xl h-8">offene Einladungen</h1>
</div>
</template>
<template #diffMain>
<div class="flex flex-col gap-2 grow overflow-y-scroll px-7">
<InviteListItem v-for="invite in invites" :key="invite.username" :invite="invite" />
</div>
</template> </template>
</MainTemplate> </MainTemplate>
</template> </template>

View file

@ -1,10 +1,5 @@
<template> <template>
<MainTemplate> <MainTemplate title="Benutzer">
<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">Benutzer</h1>
</div>
</template>
<template #diffMain> <template #diffMain>
<div class="flex flex-col gap-4 h-full pl-7"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Nutzer ${origin?.username} - Daten bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
</template> </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">Nutzer {{ origin?.username }} - Daten bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>

View file

@ -1,13 +1,8 @@
<template> <template>
<MainTemplate> <MainTemplate :title="`Nutzer ${user?.username} - Berechtigungen bearbeiten`">
<template #headerInsert> <template #headerInsert>
<RouterLink to="../" class="text-primary">zurück zur Liste</RouterLink> <RouterLink to="../" class="text-primary">zurück zur Liste</RouterLink>
</template> </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">Nutzer {{ user?.username }} - Berechtigungen bearbeiten</h1>
</div>
</template>
<template #main> <template #main>
<p>Hinweis: Berechtigungen von Nutzer und Rolle sind ergänzend.</p> <p>Hinweis: Berechtigungen von Nutzer und Rolle sind ergänzend.</p>
<Spinner v-if="loading == 'loading'" class="mx-auto" /> <Spinner v-if="loading == 'loading'" class="mx-auto" />

Some files were not shown because too many files have changed in this diff Show more