Merge branch 'milestone/ff-admin-unit' into unit/#103-base-management

This commit is contained in:
Julian Krauser 2025-06-08 08:02:38 +02:00
commit 50fa0128ea
53 changed files with 1757 additions and 950 deletions

View file

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

View file

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

View file

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

View file

@ -80,6 +80,10 @@
<input type="text" id="internalId" />
</div>
<div>
<label for="note">Notiz (optional)</label>
<textarea 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" />
@ -154,6 +158,7 @@ export default defineComponent({
nameaffix: formData.nameaffix.value,
birthdate: formData.birthdate.value,
internalId: formData.internalId.value,
note: formData.note.value,
};
this.status = "loading";
this.createMember(createMember)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +1,19 @@
<template>
<RouterLink
<RouterLink
:to="{ name: 'admin-club-member-overview', params: { memberId: member.id } }"
class="flex flex-col h-fit w-full border border-primary rounded-md"
>
<div
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
>
<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>
</div>
<div class="p-2">
<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 v-if="member.lastMembershipEntry?.end">ausgetreten: {{ member.lastMembershipEntry?.end }}, da {{member.lastMembershipEntry?.terminationReason ?? '- kein Grund angegeben'}}</p>
<p v-if="member.lastMembershipEntry?.end">
ausgetreten: {{ member.lastMembershipEntry?.end }}, da
{{ member.lastMembershipEntry?.terminationReason ?? "- kein Grund angegeben" }}
</p>
</div>
</RouterLink>
</template>

View file

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

View file

@ -4,7 +4,15 @@
:to="{ name: 'admin-club-newsletter-overview', params: { newsletterId: newsletter.id } }"
class="bg-primary p-2 text-white flex flex-row justify-between items-center"
>
<p>{{ newsletter.title }}</p>
<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" />
</RouterLink>
<div class="p-2 max-h-48 overflow-y-auto">

View file

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

View file

@ -0,0 +1,81 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Aus-/Fortbildung erstellen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<label for="education">Bezeichnung</label>
<input type="text" id="education" required />
</div>
<div>
<label for="description">Beschreibung (optional)</label>
<input type="text" id="description" />
</div>
<div class="flex flex-row gap-2">
<button primary type="submit" :disabled="status == 'loading' || status?.status == 'success'">erstellen</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
</form>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEducationStore } from "@/stores/admin/configuration/education";
import type { CreateEducationViewModel } from "@/viewmodels/admin/configuration/education.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEducationStore, ["createEducation"]),
triggerCreate(e: any) {
let formData = e.target.elements;
let createEducation: CreateEducationViewModel = {
education: formData.education.value,
description: formData.description.value,
};
this.status = "loading";
this.createEducation(createEducation)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,75 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Aus-/Fortbildung {{ education?.education }} löschen?</p>
</div>
<br />
<div class="flex flex-row gap-2">
<button primary :disabled="status == 'loading' || status?.status == 'success'" @click="triggerDelete">
unwiederuflich löschen
</button>
<Spinner v-if="status == 'loading'" class="my-auto" />
<SuccessCheckmark v-else-if="status?.status == 'success'" />
<FailureXMark v-else-if="status?.status == 'failed'" />
</div>
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { mapState, mapActions } from "pinia";
import { useModalStore } from "@/stores/modal";
import Spinner from "@/components/Spinner.vue";
import SuccessCheckmark from "@/components/SuccessCheckmark.vue";
import FailureXMark from "@/components/FailureXMark.vue";
import { useEducationStore } from "@/stores/admin/configuration/education";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
};
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
computed: {
...mapState(useModalStore, ["data"]),
...mapState(useEducationStore, ["educations"]),
education() {
return this.educations.find((r) => r.id == this.data);
},
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useEducationStore, ["deleteEducation"]),
triggerDelete() {
this.status = "loading";
this.deleteEducation(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,55 @@
<template>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
<p>{{ education.education }}</p>
<div class="flex flex-row">
<RouterLink
v-if="can('update', 'configuration', 'education')"
:to="{ name: 'admin-configuration-education-edit', params: { id: education.id } }"
>
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</RouterLink>
<div v-if="can('delete', 'configuration', 'education')" @click="openDeleteModal">
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
</div>
</div>
</div>
<div class="flex flex-col p-2">
<div class="flex flex-row gap-2">
<p class="min-w-16">Beschreibung:</p>
<p class="grow overflow-hidden">{{ education.description }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useAbilityStore } from "@/stores/ability";
import { useModalStore } from "@/stores/modal";
import type { EducationViewModel } from "@/viewmodels/admin/configuration/education.models";
</script>
<script lang="ts">
export default defineComponent({
props: {
education: { type: Object as PropType<EducationViewModel>, default: {} },
},
computed: {
...mapState(useAbilityStore, ["can"]),
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/configuration/education/DeleteEducationModal.vue"))
),
this.education.id
);
},
},
});
</script>

View file

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