diff --git a/src/components/admin/club/member/CreateMemberModal.vue b/src/components/admin/club/member/CreateMemberModal.vue index b25aa1e..f0974e4 100644 --- a/src/components/admin/club/member/CreateMemberModal.vue +++ b/src/components/admin/club/member/CreateMemberModal.vue @@ -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) diff --git a/src/components/admin/club/member/MemberEducationCreateModal.vue b/src/components/admin/club/member/MemberEducationCreateModal.vue new file mode 100644 index 0000000..10add1d --- /dev/null +++ b/src/components/admin/club/member/MemberEducationCreateModal.vue @@ -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> diff --git a/src/components/admin/club/member/MemberEducationDeleteModal.vue b/src/components/admin/club/member/MemberEducationDeleteModal.vue new file mode 100644 index 0000000..fc4ffa0 --- /dev/null +++ b/src/components/admin/club/member/MemberEducationDeleteModal.vue @@ -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-Auzeichnung löschen</p> + </div> + <br /> + <p class="text-center">Auszeichnung {{ 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> diff --git a/src/components/admin/club/member/MemberEducationEditModal.vue b/src/components/admin/club/member/MemberEducationEditModal.vue new file mode 100644 index 0000000..3e10afe --- /dev/null +++ b/src/components/admin/club/member/MemberEducationEditModal.vue @@ -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-Auzeichnung bearbeiten</p> + </div> + <br /> + <Spinner v-if="loading == 'loading'" class="mx-auto" /> + <p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ 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>Auszeichnung</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> diff --git a/src/components/admin/club/member/MemberEducationListItem.vue b/src/components/admin/club/member/MemberEducationListItem.vue new file mode 100644 index 0000000..2ae27d9 --- /dev/null +++ b/src/components/admin/club/member/MemberEducationListItem.vue @@ -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> diff --git a/src/components/admin/club/member/MemberListItem.vue b/src/components/admin/club/member/MemberListItem.vue index e490fea..31b7d5d 100644 --- a/src/components/admin/club/member/MemberListItem.vue +++ b/src/components/admin/club/member/MemberListItem.vue @@ -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> diff --git a/src/components/admin/configuration/education/CreateEducationModal.vue b/src/components/admin/configuration/education/CreateEducationModal.vue new file mode 100644 index 0000000..149d9c5 --- /dev/null +++ b/src/components/admin/configuration/education/CreateEducationModal.vue @@ -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> diff --git a/src/components/admin/configuration/education/DeleteEducationModal.vue b/src/components/admin/configuration/education/DeleteEducationModal.vue new file mode 100644 index 0000000..604d347 --- /dev/null +++ b/src/components/admin/configuration/education/DeleteEducationModal.vue @@ -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> diff --git a/src/components/admin/configuration/education/EducationListItem.vue b/src/components/admin/configuration/education/EducationListItem.vue new file mode 100644 index 0000000..3b2541b --- /dev/null +++ b/src/components/admin/configuration/education/EducationListItem.vue @@ -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> diff --git a/src/router/index.ts b/src/router/index.ts index 02cbdc3..35b677a 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -142,6 +142,12 @@ const router = createRouter({ component: () => import("@/views/admin/club/members/MemberAwards.vue"), props: true, }, + { + path: "educations", + name: "admin-club-member-educations", + component: () => import("@/views/admin/club/members/MemberEducations.vue"), + props: true, + }, { path: "qualifications", name: "admin-club-member-qualifications", @@ -361,6 +367,30 @@ const router = createRouter({ }, ], }, + { + path: "education", + name: "admin-configuration-education-route", + component: () => import("@/views/RouterView.vue"), + meta: { type: "read", section: "configuration", module: "education" }, + beforeEnter: [abilityAndNavUpdate], + children: [ + { + path: "", + name: "admin-configuration-education", + component: () => import("@/views/admin/configuration/education/Education.vue"), + meta: { type: "read", section: "configuration", module: "education" }, + beforeEnter: [abilityAndNavUpdate], + }, + { + path: ":id/edit", + name: "admin-configuration-education-edit", + component: () => import("@/views/admin/configuration/education/EducationEdit.vue"), + meta: { type: "update", section: "configuration", module: "education" }, + beforeEnter: [abilityAndNavUpdate], + props: true, + }, + ], + }, { path: "executive-position", name: "admin-configuration-executive_position-route", diff --git a/src/router/memberGuard.ts b/src/router/memberGuard.ts index 498959a..de09386 100644 --- a/src/router/memberGuard.ts +++ b/src/router/memberGuard.ts @@ -4,6 +4,7 @@ import { useMemberAwardStore } from "@/stores/admin/club/member/memberAward"; import { useMemberExecutivePositionStore } from "@/stores/admin/club/member/memberExecutivePosition"; import { useMemberQualificationStore } from "@/stores/admin/club/member/memberQualification"; 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) { const member = useMemberStore(); @@ -14,6 +15,7 @@ export async function setMemberId(to: any, from: any, next: any) { useMemberAwardStore().$reset(); useMemberExecutivePositionStore().$reset(); useMemberQualificationStore().$reset(); + useMemberEducationStore().$reset(); next(); } @@ -28,6 +30,7 @@ export async function resetMemberStores(to: any, from: any, next: any) { useMemberAwardStore().$reset(); useMemberExecutivePositionStore().$reset(); useMemberQualificationStore().$reset(); + useMemberEducationStore().$reset(); next(); } diff --git a/src/stores/admin/club/member/member.ts b/src/stores/admin/club/member/member.ts index 26c8bd4..084343e 100644 --- a/src/stores/admin/club/member/member.ts +++ b/src/stores/admin/club/member/member.ts @@ -106,6 +106,7 @@ export const useMemberStore = defineStore("member", { nameaffix: member.nameaffix, birthdate: member.birthdate, internalId: member.internalId, + note: member.note, }); this.fetchMembers(); return result; @@ -118,6 +119,7 @@ export const useMemberStore = defineStore("member", { nameaffix: member.nameaffix, birthdate: member.birthdate, internalId: member.internalId, + note: member.note, }); this.fetchMembers(); return result; diff --git a/src/stores/admin/club/member/memberEducation.ts b/src/stores/admin/club/member/memberEducation.ts new file mode 100644 index 0000000..5b35787 --- /dev/null +++ b/src/stores/admin/club/member/memberEducation.ts @@ -0,0 +1,67 @@ +import { defineStore } from "pinia"; +import { http } from "@/serverCom"; +import type { AxiosResponse } from "axios"; +import { useMemberStore } from "./member"; +import type { + CreateMemberEducationViewModel, + MemberEducationViewModel, + UpdateMemberEducationViewModel, +} from "@/viewmodels/admin/club/member/memberEducation.models"; + +export const useMemberEducationStore = defineStore("memberEducation", { + state: () => { + return { + memberEducations: [] as Array<MemberEducationViewModel>, + loading: "loading" as "loading" | "fetched" | "failed", + }; + }, + actions: { + fetchMemberEducationsForMember() { + const memberId = useMemberStore().activeMember; + this.loading = "loading"; + http + .get(`/admin/member/${memberId}/educations`) + .then((result) => { + this.memberEducations = result.data; + this.loading = "fetched"; + }) + .catch((err) => { + this.loading = "failed"; + }); + }, + fetchMemberEducationById(id: number) { + const memberId = useMemberStore().activeMember; + return http.get(`/admin/member/${memberId}/education/${id}`); + }, + async createMemberEducation(memberEducation: CreateMemberEducationViewModel): Promise<AxiosResponse<any, any>> { + const memberId = useMemberStore().activeMember; + const result = await http.post(`/admin/member/${memberId}/education`, { + start: memberEducation.start, + end: memberEducation.end, + place: memberEducation.place, + note: memberEducation.note, + educationId: memberEducation.educationId, + }); + this.fetchMemberEducationsForMember(); + return result; + }, + async updateMemberEducation(memberEducation: UpdateMemberEducationViewModel): Promise<AxiosResponse<any, any>> { + const memberId = useMemberStore().activeMember; + const result = await http.patch(`/admin/member/${memberId}/education/${memberEducation.id}`, { + start: memberEducation.start, + end: memberEducation.end, + place: memberEducation.place, + note: memberEducation.note, + educationId: memberEducation.educationId, + }); + this.fetchMemberEducationsForMember(); + return result; + }, + async deleteMemberEducation(memberEducation: number): Promise<AxiosResponse<any, any>> { + const memberId = useMemberStore().activeMember; + const result = await http.delete(`/admin/member/${memberId}/education/${memberEducation}`); + this.fetchMemberEducationsForMember(); + return result; + }, + }, +}); diff --git a/src/stores/admin/configuration/education.ts b/src/stores/admin/configuration/education.ts new file mode 100644 index 0000000..3214aac --- /dev/null +++ b/src/stores/admin/configuration/education.ts @@ -0,0 +1,55 @@ +import { defineStore } from "pinia"; +import type { + CreateEducationViewModel, + UpdateEducationViewModel, + EducationViewModel, +} from "@/viewmodels/admin/configuration/education.models"; +import { http } from "@/serverCom"; +import type { AxiosResponse } from "axios"; + +export const useEducationStore = defineStore("education", { + state: () => { + return { + educations: [] as Array<EducationViewModel>, + loading: "loading" as "loading" | "fetched" | "failed", + }; + }, + actions: { + fetchEducations() { + this.loading = "loading"; + http + .get("/admin/education") + .then((result) => { + this.educations = result.data; + this.loading = "fetched"; + }) + .catch((err) => { + this.loading = "failed"; + }); + }, + fetchEducationById(id: number): Promise<AxiosResponse<any, any>> { + return http.get(`/admin/education/${id}`); + }, + async createEducation(education: CreateEducationViewModel): Promise<AxiosResponse<any, any>> { + const result = await http.post(`/admin/education`, { + education: education.education, + description: education.description, + }); + this.fetchEducations(); + return result; + }, + async updateActiveEducation(education: UpdateEducationViewModel): Promise<AxiosResponse<any, any>> { + const result = await http.patch(`/admin/education/${education.id}`, { + education: education.education, + description: education.description, + }); + this.fetchEducations(); + return result; + }, + async deleteEducation(education: number): Promise<AxiosResponse<any, any>> { + const result = await http.delete(`/admin/education/${education}`); + this.fetchEducations(); + return result; + }, + }, +}); diff --git a/src/stores/admin/navigation.ts b/src/stores/admin/navigation.ts index dfd93f4..3b14619 100644 --- a/src/stores/admin/navigation.ts +++ b/src/stores/admin/navigation.ts @@ -112,6 +112,9 @@ export const useNavigationStore = defineStore("navigation", { ...(abilityStore.can("read", "configuration", "qualification") ? [{ key: "qualification", title: "Qualifikationen" }] : []), + ...(abilityStore.can("read", "configuration", "education") + ? [{ key: "education", title: "Aus-/Fortbildungen" }] + : []), ...(abilityStore.can("read", "configuration", "executive_position") ? [{ key: "executive_position", title: "Vereinsämter" }] : []), diff --git a/src/types/permissionTypes.ts b/src/types/permissionTypes.ts index eff7fd1..013615c 100644 --- a/src/types/permissionTypes.ts +++ b/src/types/permissionTypes.ts @@ -13,6 +13,7 @@ export type PermissionModule = | "communication_type" | "membership_status" | "salutation" + | "education" | "calendar_type" | "user" | "role" @@ -70,6 +71,7 @@ export const permissionModules: Array<PermissionModule> = [ "communication_type", "membership_status", "salutation", + "education", "calendar_type", "user", "role", @@ -91,6 +93,7 @@ export const sectionsAndModules: SectionsAndModulesObject = { "communication_type", "membership_status", "salutation", + "education", "calendar_type", "query_store", "template", diff --git a/src/viewmodels/admin/club/member/member.models.ts b/src/viewmodels/admin/club/member/member.models.ts index 7701876..53b1d50 100644 --- a/src/viewmodels/admin/club/member/member.models.ts +++ b/src/viewmodels/admin/club/member/member.models.ts @@ -15,6 +15,7 @@ export interface MemberViewModel { sendNewsletter?: CommunicationViewModel; smsAlarming?: Array<CommunicationViewModel>; preferredCommunication?: Array<CommunicationViewModel>; + note?: string; } export interface MemberStatisticsViewModel { @@ -36,6 +37,7 @@ export interface CreateMemberViewModel { nameaffix: string; birthdate: Date; internalId?: string; + note?: string; } export interface UpdateMemberViewModel { @@ -46,4 +48,5 @@ export interface UpdateMemberViewModel { nameaffix: string; birthdate: Date; internalId?: string; + note?: string; } diff --git a/src/viewmodels/admin/club/member/memberEducation.models.ts b/src/viewmodels/admin/club/member/memberEducation.models.ts new file mode 100644 index 0000000..4013bce --- /dev/null +++ b/src/viewmodels/admin/club/member/memberEducation.models.ts @@ -0,0 +1,26 @@ +export interface MemberEducationViewModel { + id: number; + start: Date; + end?: Date; + place?: string; + note?: string; + education: string; + educationId: number; +} + +export interface CreateMemberEducationViewModel { + start: Date; + end?: Date; + place?: string; + note?: string; + educationId: number; +} + +export interface UpdateMemberEducationViewModel { + id: number; + start: Date; + end?: Date; + place?: string; + note?: string; + educationId: number; +} diff --git a/src/viewmodels/admin/configuration/education.models.ts b/src/viewmodels/admin/configuration/education.models.ts new file mode 100644 index 0000000..9ccab1f --- /dev/null +++ b/src/viewmodels/admin/configuration/education.models.ts @@ -0,0 +1,16 @@ +export interface EducationViewModel { + id: number; + education: string; + description: string | null; +} + +export interface CreateEducationViewModel { + education: string; + description: string | null; +} + +export interface UpdateEducationViewModel { + id: number; + education: string; + description: string | null; +} diff --git a/src/views/admin/club/members/MemberEdit.vue b/src/views/admin/club/members/MemberEdit.vue index f7ce25f..e233718 100644 --- a/src/views/admin/club/members/MemberEdit.vue +++ b/src/views/admin/club/members/MemberEdit.vue @@ -75,6 +75,10 @@ <label for="internalId">Interne ID (optional)</label> <input type="text" id="internalId" v-model="member.internalId" /> </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"> <button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm"> verwerfen @@ -93,7 +97,6 @@ <script setup lang="ts"> import { defineComponent } from "vue"; import { mapActions, mapState } from "pinia"; -import MainTemplate from "@/templates/Main.vue"; import { useMemberStore } from "@/stores/admin/club/member/member"; import type { MemberViewModel, UpdateMemberViewModel } from "@/viewmodels/admin/club/member/member.models"; import Spinner from "@/components/Spinner.vue"; @@ -163,6 +166,7 @@ export default defineComponent({ nameaffix: formData.nameaffix.value, birthdate: formData.birthdate.value, internalId: formData.internalId.value, + note: formData.note.value, }; this.status = "loading"; this.updateActiveMember(updateMember) diff --git a/src/views/admin/club/members/MemberEducations.vue b/src/views/admin/club/members/MemberEducations.vue new file mode 100644 index 0000000..0fa7951 --- /dev/null +++ b/src/views/admin/club/members/MemberEducations.vue @@ -0,0 +1,51 @@ +<template> + <div class="flex flex-col gap-2 h-full w-full overflow-y-auto"> + <div v-if="memberEducations != null" class="flex flex-col gap-2 w-full"> + <MemberEducationListItem v-for="education in memberEducations" :key="education.id" :education="education" /> + </div> + <Spinner v-if="loading == 'loading'" class="mx-auto" /> + <p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">↺ 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> diff --git a/src/views/admin/club/members/MemberOverview.vue b/src/views/admin/club/members/MemberOverview.vue index f4bb946..d72763f 100644 --- a/src/views/admin/club/members/MemberOverview.vue +++ b/src/views/admin/club/members/MemberOverview.vue @@ -25,13 +25,17 @@ <label for="birthdate">Geburtsdatum</label> <input type="date" id="birthdate" :value="activeMemberObj.birthdate" readonly /> </div> + <div> + <label for="note">Notiz</label> + <textarea type="text" id="note" v-model="activeMemberObj.note" readonly /> + </div> <div v-if="membershipStatistics.length != 0 || totalMembershipStatistics != undefined"> <p>Statistiken zur Mitgliedschaft</p> - <div class="flex flex-col h-fit w-full rounded-md overflow-hidden"> + <div class="flex flex-col h-fit w-full rounded-md overflow-hidden divide-y divide-white"> <div class="bg-primary p-2 text-white flex flex-row justify-between items-center"> <p> - gesamt {{ totalMembershipStatistics.durationInDays }} Tage - <span class="whitespace-nowrap"> ~> {{ totalMembershipStatistics.exactDuration }}</span> + gesamt {{ totalMembershipStatistics?.durationInDays }} Tage + <span class="whitespace-nowrap"> ~> {{ totalMembershipStatistics?.exactDuration }}</span> </p> </div> <div diff --git a/src/views/admin/club/members/MemberRouting.vue b/src/views/admin/club/members/MemberRouting.vue index 866d063..445b689 100644 --- a/src/views/admin/club/members/MemberRouting.vue +++ b/src/views/admin/club/members/MemberRouting.vue @@ -21,7 +21,7 @@ <template #diffMain> <div class="flex flex-col gap-2 grow px-7 overflow-hidden"> <div class="flex flex-col grow gap-2 overflow-hidden"> - <div class="w-full flex flex-row max-lg:flex-wrap justify-center"> + <div class="w-full flex flex-row max-lg:flex-wrap justify-center items-stretch"> <RouterLink v-for="tab in tabs" :key="tab.route" @@ -31,7 +31,7 @@ > <p :class="[ - 'w-full rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden', + 'flex w-full h-full items-center justify-center rounded-lg py-2.5 text-sm text-center font-medium leading-5 focus:ring-0 outline-hidden', isActive ? 'bg-red-200 shadow-sm border-b-2 border-primary rounded-b-none' : ' hover:bg-red-200', ]" > @@ -68,8 +68,9 @@ export default defineComponent({ { route: "admin-club-member-overview", title: "Übersicht" }, { route: "admin-club-member-membership", title: "Mitgliedschaft" }, { route: "admin-club-member-communication", title: "Kommunikation" }, - { route: "admin-club-member-awards", title: "Auszeichnungen" }, - { route: "admin-club-member-qualifications", title: "Qualifikationen" }, + { route: "admin-club-member-awards", title: "Auszeichnungen / Ehrungen" }, + { route: "admin-club-member-educations", title: "Aus- / Fortbildungen" }, + { route: "admin-club-member-qualifications", title: "Qualifikationen / Funktionen" }, { route: "admin-club-member-positions", title: "Vereinsämter" }, ], }; diff --git a/src/views/admin/configuration/education/Education.vue b/src/views/admin/configuration/education/Education.vue new file mode 100644 index 0000000..04681a4 --- /dev/null +++ b/src/views/admin/configuration/education/Education.vue @@ -0,0 +1,49 @@ +<template> + <MainTemplate title="Aus-/Fortbildungen"> + <template #diffMain> + <div class="flex flex-col gap-4 h-full pl-7"> + <div class="flex flex-col gap-2 grow overflow-y-scroll pr-7"> + <EducationListItem v-for="education in educations" :key="education.id" :education="education" /> + </div> + <div class="flex flex-row gap-4"> + <button v-if="can('create', 'configuration', 'education')" primary class="w-fit!" @click="openCreateModal"> + Aus-/Fortbildung erstellen + </button> + </div> + </div> + </template> + </MainTemplate> +</template> + +<script setup lang="ts"> +import { defineComponent, defineAsyncComponent, markRaw } from "vue"; +import { mapState, mapActions } from "pinia"; +import MainTemplate from "@/templates/Main.vue"; +import { useEducationStore } from "@/stores/admin/configuration/education"; +import EducationListItem from "@/components/admin/configuration/education/EducationListItem.vue"; +import { useModalStore } from "@/stores/modal"; +import { useAbilityStore } from "@/stores/ability"; +</script> + +<script lang="ts"> +export default defineComponent({ + computed: { + ...mapState(useEducationStore, ["educations"]), + ...mapState(useAbilityStore, ["can"]), + }, + mounted() { + this.fetchEducations(); + }, + methods: { + ...mapActions(useEducationStore, ["fetchEducations"]), + ...mapActions(useModalStore, ["openModal"]), + openCreateModal() { + this.openModal( + markRaw( + defineAsyncComponent(() => import("@/components/admin/configuration/education/CreateEducationModal.vue")) + ) + ); + }, + }, +}); +</script> diff --git a/src/views/admin/configuration/education/EducationEdit.vue b/src/views/admin/configuration/education/EducationEdit.vue new file mode 100644 index 0000000..c3516b2 --- /dev/null +++ b/src/views/admin/configuration/education/EducationEdit.vue @@ -0,0 +1,120 @@ +<template> + <MainTemplate :title="`Aus-/Fortbildung ${origin?.education} - Daten bearbeiten`"> + <template #headerInsert> + <RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink> + </template> + <template #main> + <Spinner v-if="loading == 'loading'" class="mx-auto" /> + <p v-else-if="loading == 'failed'">laden fehlgeschlagen</p> + <form + v-else-if="education != null" + class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto" + @submit.prevent="triggerUpdate" + > + <div> + <label for="education">Bezeichnung</label> + <input type="text" id="education" required v-model="education.education" /> + </div> + <div> + <label for="description">Beschreibung (optional)</label> + <input type="text" id="description" v-model="education.description" /> + </div> + <div class="flex flex-row justify-end gap-2"> + <button primary-outline type="reset" class="w-fit!" :disabled="canSaveOrReset" @click="resetForm"> + verwerfen + </button> + <button primary type="submit" class="w-fit!" :disabled="status == 'loading' || canSaveOrReset"> + speichern + </button> + <Spinner v-if="status == 'loading'" class="my-auto" /> + <SuccessCheckmark v-else-if="status?.status == 'success'" /> + <FailureXMark v-else-if="status?.status == 'failed'" /> + </div> + </form> + </template> + </MainTemplate> +</template> + +<script setup lang="ts"> +import { defineComponent } from "vue"; +import { mapState, mapActions } from "pinia"; +import MainTemplate from "@/templates/Main.vue"; +import { useEducationStore } from "@/stores/admin/configuration/education"; +import Spinner from "@/components/Spinner.vue"; +import SuccessCheckmark from "@/components/SuccessCheckmark.vue"; +import FailureXMark from "@/components/FailureXMark.vue"; +import { RouterLink } from "vue-router"; +import type { UpdateEducationViewModel, EducationViewModel } from "@/viewmodels/admin/configuration/education.models"; +import cloneDeep from "lodash.clonedeep"; +import isEqual from "lodash.isequal"; +</script> + +<script lang="ts"> +export default defineComponent({ + props: { + id: String, + }, + data() { + return { + loading: "loading" as "loading" | "fetched" | "failed", + status: null as null | "loading" | { status: "success" | "failed"; reason?: string }, + origin: null as null | EducationViewModel, + education: null as null | EducationViewModel, + timeout: null as any, + }; + }, + computed: { + canSaveOrReset(): boolean { + return isEqual(this.origin, this.education); + }, + }, + mounted() { + this.fetchItem(); + }, + beforeUnmount() { + try { + clearTimeout(this.timeout); + } catch (error) {} + }, + methods: { + ...mapActions(useEducationStore, ["fetchEducationById", "updateActiveEducation"]), + resetForm() { + this.education = cloneDeep(this.origin); + }, + fetchItem() { + this.fetchEducationById(parseInt(this.id ?? "")) + .then((result) => { + this.education = result.data; + this.origin = cloneDeep(result.data); + this.loading = "fetched"; + }) + .catch((err) => { + this.loading = "failed"; + }); + }, + triggerUpdate(e: any) { + if (this.education == null) return; + let formData = e.target.elements; + let updateEducation: UpdateEducationViewModel = { + id: this.education.id, + education: formData.education.value, + description: formData.description.value, + }; + this.status = "loading"; + this.updateActiveEducation(updateEducation) + .then(() => { + this.fetchItem(); + this.status = { status: "success" }; + }) + .catch((err) => { + this.status = { status: "failed" }; + }) + .finally(() => { + this.timeout = setTimeout(() => { + this.status = null; + }, 2000); + }); + }, + }, +}); +</script>