Compare commits

..

No commits in common. "main" and "v1.5.1" have entirely different histories.
main ... v1.5.1

108 changed files with 1427 additions and 2003 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.7.0", "version": "1.5.1",
"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.8", "@tailwindcss/vite": "^4.1.5",
"@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.3", "pinia": "^3.0.2",
"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.16", "vue": "^3.5.13",
"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.8", "@tailwindcss/postcss": "^4.1.5",
"@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.30", "@types/node": "^22.15.12",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.3",
"@types/qrcode": "^1.5.5", "@types/qrcode": "^1.5.5",
"@types/qs": "^6.14.0", "@types/qs": "^6.9.18",
"@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.4", "@vitejs/plugin-vue": "^5.2.3",
"@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.28.0", "eslint": "^9.26.0",
"eslint-plugin-vue": "^10.2.0", "eslint-plugin-vue": "^10.1.0",
"npm-run-all2": "^8.0.4", "npm-run-all2": "^8.0.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.5",
"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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -12,7 +12,6 @@
<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>
@ -30,7 +29,6 @@ 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">
@ -40,7 +38,6 @@ 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,13 +1,5 @@
<template> <template>
<img v-if="useFallback" ref="fallback" src="/icon.png" alt="LOGO" class="h-full w-auto" /> <img ref="icon" :src="url + '/api/public/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">
@ -24,11 +16,6 @@ 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,13 +1,5 @@
<template> <template>
<img v-if="useFallback" ref="fallback" src="/admin-logo.png" alt="LOGO" class="h-full w-auto" /> <img ref="logo" :src="url + '/api/public/applogo.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">
@ -24,11 +16,6 @@ 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

@ -1,64 +0,0 @@
<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,10 +80,6 @@
<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" />
@ -158,7 +154,6 @@ 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

@ -1,164 +0,0 @@
<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

@ -1,82 +0,0 @@
<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

@ -1,198 +0,0 @@
<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

@ -1,54 +0,0 @@
<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

@ -3,17 +3,15 @@
: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 class="bg-primary p-2 text-white flex flex-row justify-between items-center"> <div
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"> <p v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}, da {{member.lastMembershipEntry?.terminationReason ?? '- kein Grund angegeben'}}</p>
ausgetreten: {{ member.lastMembershipEntry?.end }}, da
{{ member.lastMembershipEntry?.terminationReason ?? "- kein Grund angegeben" }}
</p>
</div> </div>
</RouterLink> </RouterLink>
</template> </template>

View file

@ -63,12 +63,11 @@ export default defineComponent({
}; };
this.status = "loading"; this.status = "loading";
this.createNewsletter(createNewsletter) this.createNewsletter(createNewsletter)
.then((res) => { .then(() => {
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,15 +4,7 @@
: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> <p>{{ newsletter.title }}</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,12 +66,11 @@ export default defineComponent({
}; };
this.status = "loading"; this.status = "loading";
this.createProtocol(createProtocol) this.createProtocol(createProtocol)
.then((res) => { .then(() => {
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

@ -1,81 +0,0 @@
<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

@ -1,75 +0,0 @@
<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

@ -1,55 +0,0 @@
<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, value: formData.show_link_to_calendar.checked || null,
}, },
]); ]);
}, },

View file

@ -149,6 +149,7 @@ 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,14 +12,7 @@ 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 (to.name == "admin-default") { if ((admin && ability.isAdmin()) || ability.can(type, section, module)) {
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-default" }, redirect: { name: "admin" },
}, },
{ {
path: "/login", path: "/login",
@ -76,13 +76,12 @@ 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, abilityAndNavUpdate], beforeEnter: [isAuthenticated],
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",
@ -142,12 +141,6 @@ 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",
@ -367,30 +360,6 @@ 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,7 +4,6 @@ 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();
@ -15,7 +14,6 @@ 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();
} }
@ -30,7 +28,6 @@ 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,21 +33,13 @@ 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?.includes(type)) && permissions[section]?.all == "*" ||
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;
@ -80,20 +72,13 @@ 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?.includes(type)) && permissions[section]?.all == "*" ||
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,7 +106,6 @@ 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;
@ -119,7 +118,6 @@ 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

@ -1,67 +0,0 @@
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,7 +7,6 @@ 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";
@ -17,7 +16,6 @@ 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",
}; };
}, },
@ -44,15 +42,6 @@ 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,10 +1,11 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { CreateNewsletterViewModel } from "@/viewmodels/admin/club/newsletter/newsletter.models"; import type { CreateNewsletterViewModel, SyncNewsletterViewModel } 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: () => {
@ -71,6 +72,7 @@ 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,10 +1,11 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import type { CreateProtocolViewModel } from "@/viewmodels/admin/club/protocol/protocol.models"; import type { CreateProtocolViewModel, SyncProtocolViewModel } 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: () => {
@ -72,6 +73,7 @@ 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: this.agenda, agenda: differenceWith(this.agenda, this.origin, isEqual),
}) })
.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: this.decision, decisions: differenceWith(this.decision, this.origin, isEqual),
}) })
.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: this.voting, votings: differenceWith(this.voting, this.origin, isEqual),
}) })
.then((res) => { .then((res) => {
this.syncingProtocolVoting = "synced"; this.syncingProtocolVoting = "synced";

View file

@ -1,55 +0,0 @@
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.canAccessSection("club") ...(abilityStore.canSection("read", "club")
? [ ? [
{ {
key: "club", key: "club",
@ -57,7 +57,7 @@ export const useNavigationStore = defineStore("navigation", {
} as topLevelNavigationModel, } as topLevelNavigationModel,
] ]
: []), : []),
...(abilityStore.canAccessSection("configuration") ...(abilityStore.canSection("read", "configuration")
? [ ? [
{ {
key: "configuration", key: "configuration",
@ -66,7 +66,7 @@ export const useNavigationStore = defineStore("navigation", {
} as topLevelNavigationModel, } as topLevelNavigationModel,
] ]
: []), : []),
...(abilityStore.canAccessSection("management") ...(abilityStore.canSection("read", "management")
? [ ? [
{ {
key: "management", key: "management",
@ -112,9 +112,6 @@ 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" }]
: []), : []),
@ -150,8 +147,7 @@ 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.filter((m) => !m.key.startsWith("divider"))[0].key; let link = this.activeNavigationObject.main[0].key;
this.activeLink = link;
router.push({ name: `admin-${this.activeNavigation}-${link}` }); router.push({ name: `admin-${this.activeNavigation}-${link}` });
} }
}, },

View file

@ -16,13 +16,10 @@
<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"
> >
<div v-if="topBar || title" class="flex flex-row items-center justify-between pt-5 pb-3 px-7"> <slot name="topBar"></slot>
<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-auto"> <div v-if="!diffMain" class="flex flex-col gap-2 grow px-7 overflow-y-scroll">
<slot name="main"></slot> <slot name="main"></slot>
</div> </div>
</div> </div>
@ -54,10 +51,6 @@ 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"]),
@ -67,9 +60,6 @@ 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,7 +13,6 @@ export type PermissionModule =
| "communication_type" | "communication_type"
| "membership_status" | "membership_status"
| "salutation" | "salutation"
| "education"
| "calendar_type" | "calendar_type"
| "user" | "user"
| "role" | "role"
@ -71,7 +70,6 @@ export const permissionModules: Array<PermissionModule> = [
"communication_type", "communication_type",
"membership_status", "membership_status",
"salutation", "salutation",
"education",
"calendar_type", "calendar_type",
"user", "user",
"role", "role",
@ -93,7 +91,6 @@ 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,7 +15,6 @@ 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 {
@ -37,7 +36,6 @@ export interface CreateMemberViewModel {
nameaffix: string; nameaffix: string;
birthdate: Date; birthdate: Date;
internalId?: string; internalId?: string;
note?: string;
} }
export interface UpdateMemberViewModel { export interface UpdateMemberViewModel {
@ -48,5 +46,4 @@ export interface UpdateMemberViewModel {
nameaffix: string; nameaffix: string;
birthdate: Date; birthdate: Date;
internalId?: string; internalId?: string;
note?: string;
} }

View file

@ -1,26 +0,0 @@
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,18 +21,6 @@ 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,7 +7,6 @@ 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

@ -1,16 +0,0 @@
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,11 +65,6 @@
</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>
@ -81,6 +76,7 @@ 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";
@ -100,9 +96,10 @@ export default defineComponent({
}; };
}, },
computed: { computed: {
...mapState(useConfigurationStore, ["clubName", "appShow_link_to_calendar"]), ...mapState(useConfigurationStore, ["clubName"]),
}, },
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,5 +1,10 @@
<template> <template>
<MainTemplate title="Administration übertragen" :useStagedOverviewLink="false"> <MainTemplate :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,5 +1,10 @@
<template> <template>
<MainTemplate title="Meine Anmeldedaten" :useStagedOverviewLink="false"> <MainTemplate :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,5 +1,10 @@
<template> <template>
<MainTemplate title="Mein Account" :useStagedOverviewLink="false"> <MainTemplate :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,13 +1,18 @@
<template> <template>
<MainTemplate title="Meine Berechtigungen" :useStagedOverviewLink="false"> <MainTemplate :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 /> <Permission :permissions="permissions" :disableEdit="true" />
</template> </template>
</MainTemplate> </MainTemplate>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent, markRaw, defineAsyncComponent } 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,9 +1,15 @@
<template> <template>
<MainTemplate title="Kalender"> <MainTemplate>
<template #topBar> <template #topBar>
<div class="flex flex-row gap-2"> <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<PlusIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="select({ start: '', end: '', allDay: false })" /> <h1 class="font-bold text-xl h-8">Kalender</h1>
<LinkIcon class="text-gray-500 h-5 w-5 cursor-pointer" @click="openLinkModal" /> <div class="flex flex-row gap-2">
<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,77 +1,84 @@
<template> <template>
<MainTemplate title="Liste Drucken"> <MainTemplate>
<template #main> <template #topBar>
<form <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2" <h1 class="font-bold text-xl h-8">Liste Drucken</h1>
@submit.prevent="sendPrintJob" </div>
> </template>
<div class="flex flex-row gap-2 items-center"> <template #diffMain>
<p class="min-w-16">Titel:</p> <div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto">
<input id="title" type="text" required /> <form
</div> class="flex flex-col h-fit w-full border border-primary rounded-md p-2 gap-2"
<div class="flex flex-row gap-2 items-center"> @submit.prevent="sendPrintJob"
<p class="min-w-16">Query:</p> >
<select id="query" value="member"> <div class="flex flex-row gap-2 items-center">
<option value="member">(system) alle Mitglieder</option> <p class="min-w-16">Titel:</p>
<option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option> <input id="title" type="text" required />
<option v-for="query in queries" :key="query.id" :value="query.id"> </div>
{{ query.title }} <div class="flex flex-row gap-2 items-center">
</option> <p class="min-w-16">Query:</p>
</select> <select id="query" value="member">
</div> <option value="member">(system) alle Mitglieder</option>
<div class="flex flex-col md:flex-row gap-2 md:items-center"> <option value="memberByRunningMembership">(system) alle Mitglieder mit laufender Mitgliedschaft</option>
<div class="flex flex-row w-full gap-2 items-center"> <option v-for="query in queries" :key="query.id" :value="query.id">
<p class="min-w-16">Kopfzeile:</p> {{ query.title }}
<select id="header" value="def"> </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">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-row gap-2 items-center"> <div class="flex flex-col md:flex-row gap-2 md:items-center">
<p class="whitespace-nowrap">Höhe [mm]:</p> <div class="flex flex-row w-full gap-2 items-center">
<input id="headerHeight" type="number" :min="15" class="w-24!" placeholder="15" /> <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 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> <div class="flex flex-row gap-2">
<div class="flex flex-row gap-2 items-center"> <button type="submit" primary class="w-fit!">Liste drucken</button>
<p class="min-w-16">Hauptteil:</p> <button type="reset" primary-outline class="w-fit!">zurücksetzen</button>
<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>
<div class="flex flex-row gap-2 items-center"> </form>
<p class="whitespace-nowrap">Höhe [mm]:</p> <div class="w-full grow min-h-[50%] flex flex-col gap-2">
<input id="footerHeight" type="number" :min="15" class="w-24!" placeholder="15" /> <Spinner v-if="status == 'loading'" />
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Mitglieder"> <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">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,10 +75,6 @@
<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
@ -97,6 +93,7 @@
<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";
@ -166,7 +163,6 @@ 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

@ -1,51 +0,0 @@
<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,19 +25,9 @@
<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> <div v-if="membershipStatistics.length != 0">
<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 rounded-md overflow-hidden divide-y divide-white"> <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>
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"
@ -159,20 +149,16 @@ export default defineComponent({
}, },
computed: { computed: {
...mapState(useMemberStore, ["activeMemberObj", "activeMemberStatistics", "loadingActive"]), ...mapState(useMemberStore, ["activeMemberObj", "activeMemberStatistics", "loadingActive"]),
...mapState(useMembershipStore, ["membershipStatistics", "totalMembershipStatistics"]), ...mapState(useMembershipStore, ["membershipStatistics"]),
}, },
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, [ ...mapActions(useMembershipStore, ["fetchMembershipStatisticsForMember"]),
"fetchMembershipStatisticsForMember",
"fetchMembershipTotalStatisticsForMember",
]),
}, },
}); });
</script> </script>

View file

@ -4,11 +4,12 @@
<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>
<h1 class="font-bold text-xl h-8 min-h-fit"> <div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
{{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }} <h1 class="font-bold text-xl h-8 min-h-fit grow">
{{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }} {{ activeMemberObj?.lastname }}, {{ activeMemberObj?.firstname }}
</h1> {{ activeMemberObj?.nameaffix ? `- ${activeMemberObj?.nameaffix}` : "" }}
<div class="flex flex-row gap-2"> </h1>
<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>
@ -21,7 +22,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 items-stretch"> <div class="w-full flex flex-row max-lg:flex-wrap justify-center">
<RouterLink <RouterLink
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.route" :key="tab.route"
@ -31,7 +32,7 @@
> >
<p <p
:class="[ :class="[
'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', 'w-full 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',
]" ]"
> >
@ -68,9 +69,8 @@ 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 / Ehrungen" }, { route: "admin-club-member-awards", title: "Auszeichnungen" },
{ route: "admin-club-member-educations", title: "Aus- / Fortbildungen" }, { route: "admin-club-member-qualifications", title: "Qualifikationen" },
{ 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

@ -0,0 +1,24 @@
<template>
<MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Ü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,5 +1,10 @@
<template> <template>
<MainTemplate title="Newsletter"> <MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Newsletter</h1>
</div>
</template>
<template #diffMain> <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,15 +37,11 @@
}) })
}} }}
</p> </p>
<DoubleConfirmClick <TrashIcon
v-if="can('create', 'club', 'newsletter')" v-if="can('create', 'club', 'newsletter')"
light class="w-5 h-5 p-1 box-content cursor-pointer text-white"
v-slot="{ isSensitive }" @click.prevent="removeSelected(item.calendarId)"
@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
@ -115,10 +111,8 @@ 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,15 +42,11 @@
<p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p> <p>Newsletter senden an Typ: {{ member.sendNewsletter?.type.type ?? "---" }}</p>
</div> </div>
<DoubleConfirmClick <TrashIcon
v-if="can('create', 'club', 'newsletter') && showMemberSelect" v-if="can('create', 'club', 'newsletter') && showMemberSelect"
light class="w-5 h-5 p-1 box-content cursor-pointer"
v-slot="{ isSensitive }" @click="removeSelected(member.id)"
@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>
@ -65,8 +61,17 @@
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";
@ -74,9 +79,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,17 +1,20 @@
<template> <template>
<MainTemplate :title="origin?.title"> <MainTemplate>
<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>
<NewsletterSyncing <div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
:executeSyncAll="executeSyncAll" <h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}</h1>
@syncState=" <NewsletterSyncing
(state) => { :executeSyncAll="executeSyncAll"
syncState = state; @syncState="
} (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,5 +1,10 @@
<template> <template>
<MainTemplate title="Protokolle"> <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">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,16 +31,6 @@
: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,9 +73,7 @@ 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, TrashIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon } 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">
@ -121,9 +109,6 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.agenda = this.agenda.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -31,16 +31,6 @@
: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,9 +73,7 @@ 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, TrashIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon } 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">
@ -121,9 +109,6 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.decision = this.decision.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -42,15 +42,11 @@
</label> </label>
</div> </div>
</div> </div>
<DoubleConfirmClick <TrashIcon
v-if="can('create', 'club', 'protocol')" v-if="can('create', 'club', 'protocol')"
light class="w-5 h-5 p-1 box-content cursor-pointer"
v-slot="{ isSensitive }" @click="removeSelected(member.memberId)"
@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>
@ -61,12 +57,10 @@ 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,17 +1,20 @@
<template> <template>
<MainTemplate :title="`${origin?.title}, ${origin?.date}`"> <MainTemplate>
<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>
<ProtocolSyncing <div class="flex flex-row gap-2 items-center justify-between pt-5 pb-3 px-7">
:executeSyncAll="executeSyncAll" <h1 class="font-bold text-xl h-8 min-h-fit grow">{{ origin?.title }}, {{ origin?.date }}</h1>
@syncState=" <ProtocolSyncing
(state) => { :executeSyncAll="executeSyncAll"
syncState = state; @syncState="
} (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">
@ -89,10 +92,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) {
@ -115,8 +118,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,16 +31,6 @@
: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"
@ -93,16 +83,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia"; import { mapActions, mapState } 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, TrashIcon } from "@heroicons/vue/24/outline"; import { ChevronDownIcon, ChevronUpIcon } 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">
@ -111,7 +99,7 @@ export default defineComponent({
protocolId: String, protocolId: String,
}, },
computed: { computed: {
...mapWritableState(useProtocolVotingStore, ["voting", "loading"]), ...mapState(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);
@ -138,9 +126,6 @@ export default defineComponent({
}); });
} }
}, },
removeFromArray(thisId: number) {
this.voting = this.voting.filter((item) => item.id !== thisId);
},
}, },
}); });
</script> </script>

View file

@ -1,50 +1,57 @@
<template> <template>
<MainTemplate title="Query Builder"> <MainTemplate>
<template #main> <template #topBar>
<BuilderHost <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
v-model="query" <h1 class="font-bold text-xl h-8">Query Builder</h1>
allow-predefined-select </div>
@query:run="sendQuery" </template>
@query:save="triggerSave" <template #diffMain>
@results:clear="clearResults" <div class="flex flex-col w-full h-full gap-2 px-7 overflow-y-auto">
@results:export="exportData" <BuilderHost
/> v-model="query"
<p>Ergebnisse:</p> allow-predefined-select
<div @query:run="sendQuery"
v-if="loadingData == 'failed'" @query:save="triggerSave"
class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none" @results:clear="clearResults"
> @results:export="exportData"
<p v-if="typeof queryError == 'string'"> />
{{ queryError }} <p>Ergebnisse:</p>
</p> <div
<div v-else> v-if="loadingData == 'failed'"
<p> class="flex flex-col p-2 border border-red-600 bg-red-200 rounded-md select-none"
CODE: <br /> >
{{ queryError.code }} <p v-if="typeof queryError == 'string'">
</p> {{ queryError }}
<br /> </p>
<p> <div v-else>
MSG: <br /> <p>
{{ queryError.msg }} CODE: <br />
</p> {{ queryError.code }}
<br /> </p>
<p> <br />
RESULTING SQL: <br /> <p>
{{ queryError.sql }} MSG: <br />
</p> {{ queryError.msg }}
</div> </p>
<br />
<p>
RESULTING SQL: <br />
{{ queryError.sql }}
</p>
</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,5 +1,10 @@
<template> <template>
<MainTemplate title="Auszeichnungen"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Auszeichnung ${origin?.award} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Termintyp"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Termintyp ${origin?.type} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Kommunikationsarten"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Kommunikationsart ${origin?.type} - Daten bearbeiten`"> <MainTemplate>
<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

@ -1,49 +0,0 @@
<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

@ -1,120 +0,0 @@
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Vereinsämter"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Vereinsamt ${origin?.position} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Mitgliedsstatus"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Mitgliedsstatus ${origin?.status} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Newsletter Konfiguration"> <MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Newsletter Konfiguration</h1>
</div>
</template>
<template #main> <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,5 +1,10 @@
<template> <template>
<MainTemplate title="Qualifikationen"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Qualifikation ${origin?.qualification} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="gespeicherte Abfragen"> <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">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,5 +1,10 @@
<template> <template>
<MainTemplate title="Anrede"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Anrede ${origin?.salutation} - Daten bearbeiten`"> <MainTemplate>
<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,9 +1,12 @@
<template> <template>
<MainTemplate title="Templates"> <MainTemplate>
<template #topBar> <template #topBar>
<RouterLink :to="{ name: 'admin-configuration-template-info' }"> <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<InformationCircleIcon class="text-gray-500 h-5 w-5" /> <h1 class="font-bold text-xl h-8">Templates</h1>
</RouterLink> <RouterLink :to="{ name: 'admin-configuration-template-info' }">
<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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Template ${origin?.template} - Daten bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Templates - Verwendungsinformation"> <MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Templates - Verwendungsinformation</h1>
</div>
</template>
<template #main> <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,5 +1,10 @@
<template> <template>
<MainTemplate title="Template-Verwendung"> <MainTemplate>
<template #topBar>
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<h1 class="font-bold text-xl h-8">Template-Verwendung</h1>
</div>
</template>
<template #main> <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,5 +1,10 @@
<template> <template>
<MainTemplate title="Backups"> <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">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"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<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"> <div class="flex flex-col gap-4 h-full pl-7">
<div class="flex flex-col gap-2 grow overflow-y-scroll"> <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Rollen"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Rolle ${origin?.role} - Daten bearbeiten`"> <MainTemplate>
<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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Rolle ${role?.role} - Berechtigungen bearbeiten`"> <MainTemplate>
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Einstellungen"> <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">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,10 +1,17 @@
<template> <template>
<MainTemplate title="offene Einladungen"> <MainTemplate>
<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 #main> <template #topBar>
<InviteListItem v-for="invite in invites" :key="invite.username" :invite="invite" /> <div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
<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,5 +1,10 @@
<template> <template>
<MainTemplate title="Benutzer"> <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">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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Nutzer ${origin?.username} - Daten bearbeiten`"> <MainTemplate>
<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,8 +1,13 @@
<template> <template>
<MainTemplate :title="`Nutzer ${user?.username} - Berechtigungen bearbeiten`"> <MainTemplate>
<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