create, edit, delete member related Data

This commit is contained in:
Julian Krauser 2024-09-27 14:55:49 +02:00
parent 585ff008b9
commit 27a4d2187d
36 changed files with 2375 additions and 70 deletions

View file

@ -76,7 +76,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>

View file

@ -26,7 +26,9 @@
<div class="flex flex-row justify-end">
<div class="flex flex-row gap-4 py-2">
<button primary-outline @click="closeModal" :disabled="status != null">abbrechen</button>
<button primary-outline @click="closeModal" :disabled="status == 'loading' || status?.status == 'success'">
abbrechen
</button>
</div>
</div>
</div>

View file

@ -0,0 +1,156 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Auzeichnung hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedAward" name="award">
<ListboxLabel>Auszeichnung</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{ awards.length != 0 ? (selectedAward?.award ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="awards.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="award in awards"
:key="award.id"
:value="award"
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']">{{ award.award }}</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="date">Vergeben am</label>
<input type="date" id="date" required />
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="given" />
<label for="given">wurde übergeben / angenommen</label>
</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 { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership";
import { useAwardStore } from "@/stores/admin/award";
import type { AwardViewModel } from "@/viewmodels/admin/award.models";
import type { CreateMemberAwardViewModel } from "@/viewmodels/admin/memberAward.models";
import { useMemberAwardStore } from "@/stores/admin/memberAward";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedAward: undefined as undefined | AwardViewModel,
};
},
computed: {
...mapState(useAwardStore, ["awards"]),
},
mounted() {
this.fetchAwards();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberAwardStore, ["createMemberAward"]),
...mapActions(useAwardStore, ["fetchAwards"]),
triggerCreate(e: any) {
if (this.selectedAward == undefined) return;
let formData = e.target.elements;
let createMemberAward: CreateMemberAwardViewModel = {
date: formData.date.value,
note: formData.note.value,
given: formData.given.checked,
awardId: this.selectedAward.id,
};
this.createMemberAward(createMemberAward)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

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">Mitglied-Auzeichnung löschen</p>
</div>
<br />
<p class="text-center">Auszeichnung {{ memberAward?.award }} 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 { useMemberAwardStore } from "@/stores/admin/memberAward";
</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(useMemberAwardStore, ["memberAwards"]),
memberAward() {
return this.memberAwards.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberAwardStore, ["deleteMemberAward"]),
triggerDelete() {
this.deleteMemberAward(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,186 @@
<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">&#8634; laden fehlgeschlagen</p>
<form v-else-if="memberAward != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="memberAward.awardId" name="award">
<ListboxLabel>Auszeichnung</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{ awards.length != 0 ? (selectedAward ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="awards.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="award in awards"
:key="award.id"
:value="award.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']">{{ award.award }}</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="date">Vergeben am</label>
<input type="date" id="date" required v-model="memberAward.date" />
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="given" v-model="memberAward.given" />
<label for="given">wurde übergeben / angenommen</label>
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" v-model="memberAward.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 != null">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 { useAwardStore } from "@/stores/admin/award";
import type {
CreateMemberAwardViewModel,
MemberAwardViewModel,
UpdateMemberAwardViewModel,
} from "@/viewmodels/admin/memberAward.models";
import { useMemberAwardStore } from "@/stores/admin/memberAward";
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 | MemberAwardViewModel,
memberAward: null as null | MemberAwardViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useAwardStore, ["awards"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.memberAward);
},
selectedAward() {
return this.awards.find((ms) => ms.id == this.memberAward?.awardId)?.award;
},
},
mounted() {
this.fetchAwards();
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberAwardStore, ["updateMemberAward", "fetchMemberAwardById"]),
...mapActions(useAwardStore, ["fetchAwards"]),
resetForm() {
this.memberAward = cloneDeep(this.origin);
},
fetchItem() {
this.fetchMemberAwardById(this.data)
.then((result) => {
this.memberAward = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.memberAward == null) return;
let formData = e.target.elements;
let updateMemberAward: UpdateMemberAwardViewModel = {
id: this.memberAward.id,
date: formData.date.value,
note: formData.note.value,
given: formData.given.checked,
awardId: this.memberAward.awardId,
};
this.updateMemberAward(updateMemberAward)
.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,22 +1,24 @@
<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>{{ award.award }}</p>
<PencilIcon class="w-5 h-5" />
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
<p class="grow">{{ award.award }}</p>
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div class="p-2">
<p>erhalten am: {{ award.date }}</p>
<p v-if="!award.given">annahme abgelehnt/verwehrt</p>
<p v-if="!award.given">Annahme abgelehnt/verwehrt</p>
<p v-if="award.note">Notiz: {{ award.note }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { MemberAwardViewModel } from "@/viewmodels/admin/memberAward.models";
import { PencilIcon } from "@heroicons/vue/24/outline";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
@ -27,5 +29,20 @@ export default defineComponent({
default: {},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberAwardEditModal.vue"))),
this.award.id
);
},
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberAwardDeleteModal.vue"))),
this.award.id
);
},
},
});
</script>

View file

@ -0,0 +1,181 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Kommunikation hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedCommunicationType" name="communication">
<ListboxLabel>Kommunikationsart</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
communicationTypes.length != 0
? (selectedCommunicationType?.type ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="communicationTypes.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="type in communicationTypes"
:key="type.id"
:value="type"
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']">{{ type.type }}</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 v-if="selectedCommunicationType?.fields.includes('mobile')">
<label for="mobile">Telefon</label>
<input type="text" id="mobile" required />
</div>
<div v-if="selectedCommunicationType?.fields.includes('email')">
<label for="email">Mail-Adresse</label>
<input type="text" id="email" required />
</div>
<div v-if="selectedCommunicationType?.fields.includes('city')">
<label for="city">Stadt</label>
<input type="text" id="city" required />
</div>
<div v-if="selectedCommunicationType?.fields.includes('street')">
<label for="street">Straße</label>
<input type="text" id="street" required />
</div>
<div v-if="selectedCommunicationType?.fields.includes('streetNumber')">
<label for="streetNumber">Hausnummer</label>
<input type="number" id="streetNumber" min="0" required />
</div>
<div v-if="selectedCommunicationType?.fields.includes('streetNumberAddition')">
<label for="streetNumberAddition">Hausnummer-Zusatz (optional)</label>
<input type="text" id="streetNumberAddition" />
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="preferred" />
<label for="preferred">bevorzugt?</label>
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="isNewsletterMain" />
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
</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 { useCommunicationStore } from "@/stores/admin/communication";
import type { CreateCommunicationViewModel } from "@/viewmodels/admin/communication.models";
import { useCommunicationTypeStore } from "@/stores/admin/communicationType";
import type { CommunicationTypeViewModel } from "@/viewmodels/admin/communicationType.models";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedCommunicationType: undefined as undefined | CommunicationTypeViewModel,
};
},
computed: {
...mapState(useCommunicationTypeStore, ["communicationTypes"]),
},
mounted() {
this.fetchCommunicationTypes();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationStore, ["createCommunication"]),
...mapActions(useCommunicationTypeStore, ["fetchCommunicationTypes"]),
triggerCreate(e: any) {
if (this.selectedCommunicationType == undefined) return;
let formData = e.target.elements;
let createCommunication: CreateCommunicationViewModel = {
preferred: formData.preferred.checked,
mobile: formData.mobile?.value,
email: formData.email?.value,
city: formData.city?.value,
street: formData.street?.value,
streetNumber: formData.streetNumber?.value,
streetNumberAddition: formData.streetNumberAddition?.value,
isNewsletterMain: formData.isNewsletterMain.checked,
typeId: this.selectedCommunicationType.id,
};
this.createCommunication(createCommunication)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,84 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Kommunikation löschen</p>
</div>
<br />
<p class="text-center">
Kommunikation {{ memberCommunication?.type.type }}
{{ memberCommunication?.preferred ? "(bevorzugt)" : "" }} 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 { useCommunicationStore } from "@/stores/admin/communication";
</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(useCommunicationStore, ["communications"]),
memberCommunication() {
return this.communications.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationStore, ["deleteCommunication"]),
triggerDelete() {
this.deleteCommunication(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,153 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Kommunikation 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="communication != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<p>Type: {{ communication.type.type }}</p>
</div>
<div v-if="communication.type.fields.includes('mobile')">
<label for="mobile">Telefon</label>
<input type="text" id="mobile" required v-model="communication.mobile" />
</div>
<div v-if="communication.type.fields.includes('email')">
<label for="email">Mail-Adresse</label>
<input type="text" id="email" required v-model="communication.email" />
</div>
<div v-if="communication.type.fields.includes('city')">
<label for="city">Stadt</label>
<input type="text" id="city" required v-model="communication.city" />
</div>
<div v-if="communication.type.fields.includes('street')">
<label for="street">Straße</label>
<input type="text" id="street" required v-model="communication.street" />
</div>
<div v-if="communication.type.fields.includes('streetNumber')">
<label for="streetNumber">Hausnummer</label>
<input type="number" id="streetNumber" min="0" required v-model="communication.streetNumber" />
</div>
<div v-if="communication.type.fields.includes('streetNumberAddition')">
<label for="streetNumberAddition">Hausnummer-Zusatz (optional)</label>
<input type="text" id="streetNumberAddition" v-model="communication.streetNumberAddition" />
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="preferred" v-model="communication.preferred" />
<label for="preferred">bevorzugt?</label>
</div>
<div class="flex flex-row items-center gap-2">
<input type="checkbox" id="isNewsletterMain" v-model="communication.isNewsletterMain" />
<label for="isNewsletterMain">Newsletter hier hin versenden?</label>
</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 != null">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 { useCommunicationStore } from "@/stores/admin/communication";
import type {
CreateCommunicationViewModel,
CommunicationViewModel,
UpdateCommunicationViewModel,
} from "@/viewmodels/admin/communication.models";
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 | CommunicationViewModel,
communication: null as null | CommunicationViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useCommunicationStore, ["communications"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.communication);
},
},
mounted() {
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useCommunicationStore, ["updateCommunication", "fetchCommunicationById"]),
resetForm() {
this.communication = cloneDeep(this.origin);
},
fetchItem() {
this.fetchCommunicationById(this.data)
.then((result) => {
this.communication = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.communication == null) return;
let formData = e.target.elements;
let updateCommunication: UpdateCommunicationViewModel = {
id: this.communication.id,
preferred: formData.preferred.checked,
mobile: formData.mobile?.value,
email: formData.email?.value,
city: formData.city?.value,
street: formData.street?.value,
streetNumber: formData.streetNumber?.value,
streetNumberAddition: formData.streetNumberAddition?.value,
isNewsletterMain: formData.isNewsletterMain.checked,
};
this.updateCommunication(updateCommunication)
.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,9 +1,10 @@
<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">
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
<EnvelopeIcon class="h-5 w-5 pr-1 box-content" v-if="communication.isNewsletterMain" />
<p class="grow">{{ communication.type.type }} {{ communication.preferred ? "(bevorzugt)" : "" }}</p>
<PencilIcon class="w-5 h-5" />
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div class="p-2">
<p v-for="field in communication.type.fields" :key="field">{{ field }}: {{ communication[field] || "--" }}</p>
@ -12,10 +13,11 @@
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { CommunicationViewModel } from "@/viewmodels/admin/communication.models";
import { EnvelopeIcon, PencilIcon } from "@heroicons/vue/24/outline";
import { EnvelopeIcon, PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
@ -26,5 +28,22 @@ export default defineComponent({
default: {},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberCommunicationEditModal.vue"))),
this.communication.id
);
},
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/member/MemberCommunicationDeleteModal.vue"))
),
this.communication.id
);
},
},
});
</script>

View file

@ -0,0 +1,157 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Vereinsamt hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedExecutivePosition" name="executivePosition">
<ListboxLabel>Qualifikation</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
executivePositions.length != 0
? (selectedExecutivePosition?.position ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="executivePositions.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="executivePosition in executivePositions"
:key="executivePosition.id"
:value="executivePosition"
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']">{{
executivePosition.position
}}</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">Startdatum</label>
<input type="date" id="start" required />
</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 { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership";
import { useExecutivePositionStore } from "@/stores/admin/executivePosition";
import type { ExecutivePositionViewModel } from "@/viewmodels/admin/executivePosition.models";
import type { CreateMemberExecutivePositionViewModel } from "@/viewmodels/admin/memberExecutivePosition.models";
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedExecutivePosition: undefined as undefined | ExecutivePositionViewModel,
};
},
computed: {
...mapState(useExecutivePositionStore, ["executivePositions"]),
},
mounted() {
this.fetchExecutivePositions();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberExecutivePositionStore, ["createMemberExecutivePosition"]),
...mapActions(useExecutivePositionStore, ["fetchExecutivePositions"]),
triggerCreate(e: any) {
if (this.selectedExecutivePosition == undefined) return;
let formData = e.target.elements;
let createMemberExecutivePosition: CreateMemberExecutivePositionViewModel = {
start: formData.start.value,
note: formData.note.value,
executivePositionId: this.selectedExecutivePosition.id,
};
this.createMemberExecutivePosition(createMemberExecutivePosition)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

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">Mitglied-Vereinsamt löschen</p>
</div>
<br />
<p class="text-center">Auszeichnung {{ memberExecutivePosition?.executivePosition }} 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 { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
</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(useMemberExecutivePositionStore, ["memberExecutivePositions"]),
memberExecutivePosition() {
return this.memberExecutivePositions.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberExecutivePositionStore, ["deleteMemberExecutivePosition"]),
triggerDelete() {
this.deleteMemberExecutivePosition(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,195 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Vereinsamt 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="memberExecutivePosition != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="memberExecutivePosition.executivePositionId" name="executivePosition">
<ListboxLabel>Auszeichnung</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
executivePositions.length != 0
? (selectedExecutivePosition ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="executivePositions.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="executivePosition in executivePositions"
:key="executivePosition.id"
:value="executivePosition.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']">{{
executivePosition.position
}}</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">Startdatum</label>
<input type="date" id="start" required v-model="memberExecutivePosition.start" />
</div>
<div>
<label for="end">Enddatum</label>
<input type="date" id="end" required v-model="memberExecutivePosition.end" />
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" v-model="memberExecutivePosition.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 != null">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 { useExecutivePositionStore } from "@/stores/admin/executivePosition";
import type {
CreateMemberExecutivePositionViewModel,
MemberExecutivePositionViewModel,
UpdateMemberExecutivePositionViewModel,
} from "@/viewmodels/admin/memberExecutivePosition.models";
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
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 | MemberExecutivePositionViewModel,
memberExecutivePosition: null as null | MemberExecutivePositionViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useExecutivePositionStore, ["executivePositions"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.memberExecutivePosition);
},
selectedExecutivePosition() {
return this.executivePositions.find((ms) => ms.id == this.memberExecutivePosition?.executivePositionId)?.position;
},
},
mounted() {
this.fetchExecutivePositions();
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberExecutivePositionStore, [
"updateMemberExecutivePosition",
"fetchMemberExecutivePositionById",
]),
...mapActions(useExecutivePositionStore, ["fetchExecutivePositions"]),
resetForm() {
this.memberExecutivePosition = cloneDeep(this.origin);
},
fetchItem() {
this.fetchMemberExecutivePositionById(this.data)
.then((result) => {
this.memberExecutivePosition = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.memberExecutivePosition == null) return;
let formData = e.target.elements;
let updateMemberExecutivePosition: UpdateMemberExecutivePositionViewModel = {
id: this.memberExecutivePosition.id,
start: formData.start.value,
end: formData.end.value,
note: formData.note.value,
executivePositionId: this.memberExecutivePosition.executivePositionId,
};
this.updateMemberExecutivePosition(updateMemberExecutivePosition)
.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,20 +1,22 @@
<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>{{ position.executivePosition }} von {{ position.start }} bis {{ position.end ?? "heute" }}</p>
<PencilIcon class="w-5 h-5" />
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
<p class="grow">{{ position.executivePosition }} von {{ position.start }} bis {{ position.end ?? "heute" }}</p>
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div class="p-2">
<div v-if="position.note" class="p-2">
<p v-if="position.note">Notiz: {{ position.note }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { MemberExecutivePositionViewModel } from "@/viewmodels/admin/memberExecutivePosition.models";
import { PencilIcon } from "@heroicons/vue/24/outline";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
@ -25,5 +27,24 @@ export default defineComponent({
default: {},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/member/MemberExecutivePositionEditModal.vue"))
),
this.position.id
);
},
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/member/MemberExecutivePositionDeleteModal.vue"))
),
this.position.id
);
},
},
});
</script>

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-Qualifikation hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedQualification" name="qualification">
<ListboxLabel>Qualifikation</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
qualifications.length != 0
? (selectedQualification?.qualification ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="qualifications.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="qualification in qualifications"
:key="qualification.id"
:value="qualification"
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']">{{
qualification.qualification
}}</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">Startdatum</label>
<input type="date" id="start" required />
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" />
</div>
<div>
<label for="end">Enddatum (optional)</label>
<input type="date" id="end" />
</div>
<div>
<label for="terminationReason">beendet, weil (optional)</label>
<input type="text" id="terminationReason" />
</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 { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership";
import { useQualificationStore } from "@/stores/admin/qualification";
import type { QualificationViewModel } from "@/viewmodels/admin/qualification.models";
import type { CreateMemberQualificationViewModel } from "@/viewmodels/admin/memberQualification.models";
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedQualification: undefined as undefined | QualificationViewModel,
};
},
computed: {
...mapState(useQualificationStore, ["qualifications"]),
},
mounted() {
this.fetchQualifications();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberQualificationStore, ["createMemberQualification"]),
...mapActions(useQualificationStore, ["fetchQualifications"]),
triggerCreate(e: any) {
if (this.selectedQualification == undefined) return;
let formData = e.target.elements;
let createMemberQualification: CreateMemberQualificationViewModel = {
start: formData.start.value,
note: formData.note.value,
qualificationId: this.selectedQualification.id,
};
this.createMemberQualification(createMemberQualification)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,85 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Qualifikation löschen</p>
</div>
<br />
<p class="text-center">
Qualifikation {{ memberQualification?.qualification }} von {{ memberQualification?.start }} bis
{{ memberQualification?.end ?? "heute" }} 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 { useQualificationStore } from "@/stores/admin/qualification";
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
</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(useMemberQualificationStore, ["memberQualifications"]),
memberQualification() {
return this.memberQualifications.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberQualificationStore, ["deleteMemberQualification"]),
triggerDelete() {
this.deleteMemberQualification(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,194 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitglied-Qualifikation 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="memberQualification != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="memberQualification.qualificationId" name="qualification">
<ListboxLabel>Qualifikation</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
qualifications.length != 0 ? (selectedQualification ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="qualifications.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="qualification in qualifications"
:key="qualification.id"
:value="qualification.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']">{{
qualification.qualification
}}</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">Startdatum</label>
<input type="date" id="start" required v-model="memberQualification.start" />
</div>
<div>
<label for="note">Notiz (optional)</label>
<input type="text" id="note" v-model="memberQualification.note" />
</div>
<div>
<label for="end">Enddatum (optional)</label>
<input type="date" id="end" v-model="memberQualification.end" />
</div>
<div>
<label for="terminationReason">beendet, weil (optional)</label>
<input type="text" id="terminationReason" v-model="memberQualification.terminationReason" />
</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 != null">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 { useQualificationStore } from "@/stores/admin/qualification";
import type {
CreateMemberQualificationViewModel,
MemberQualificationViewModel,
UpdateMemberQualificationViewModel,
} from "@/viewmodels/admin/memberQualification.models";
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
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 | MemberQualificationViewModel,
memberQualification: null as null | MemberQualificationViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useQualificationStore, ["qualifications"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.memberQualification);
},
selectedQualification() {
return this.qualifications.find((ms) => ms.id == this.memberQualification?.qualificationId)?.qualification;
},
},
mounted() {
this.fetchQualifications();
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMemberQualificationStore, ["updateMemberQualification", "fetchMemberQualificationById"]),
...mapActions(useQualificationStore, ["fetchQualifications"]),
resetForm() {
this.memberQualification = cloneDeep(this.origin);
},
fetchItem() {
this.fetchMemberQualificationById(this.data)
.then((result) => {
this.memberQualification = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.memberQualification == null) return;
let formData = e.target.elements;
let updateMemberQualification: UpdateMemberQualificationViewModel = {
id: this.memberQualification.id,
start: formData.start.value,
note: formData.note.value,
end: formData.end.value,
terminationReason: formData.terminationReason.value,
qualificationId: this.memberQualification.qualificationId,
};
this.updateMemberQualification(updateMemberQualification)
.then(() => {
this.fetchItem();
this.status = { status: "success" };
})
.catch((err) => {
this.status = { status: "failed" };
})
.finally(() => {
this.timeout = setTimeout(() => {
this.status = null;
}, 2000);
});
},
},
});
</script>

View file

@ -1,10 +1,13 @@
<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>{{ qualification.qualification }} von {{ qualification.start }} bis {{ qualification.end ?? "heute" }}</p>
<PencilIcon class="w-5 h-5" />
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
<p class="grow">
{{ qualification.qualification }} von {{ qualification.start }} bis {{ qualification.end ?? "heute" }}
</p>
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div class="p-2">
<div v-if="qualification.note || qualification.terminationReason" class="p-2">
<p v-if="qualification.note">Notiz: {{ qualification.note }}</p>
<p v-if="qualification.terminationReason">beendet, weil: {{ qualification.terminationReason }}</p>
</div>
@ -12,10 +15,11 @@
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { MemberQualificationViewModel } from "@/viewmodels/admin/memberQualification.models";
import { PencilIcon } from "@heroicons/vue/24/outline";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
@ -26,5 +30,22 @@ export default defineComponent({
default: {},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberQualificationEditModal.vue"))),
this.qualification.id
);
},
openDeleteModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/member/MemberQualificationDeleteModal.vue"))
),
this.qualification.id
);
},
},
});
</script>

View file

@ -0,0 +1,152 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitgliedschaft hinzufügen</p>
</div>
<br />
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="selectedStatus" name="status">
<ListboxLabel>Status</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
membershipStatus.length != 0
? (selectedStatus?.status ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="membershipStatus.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="status in membershipStatus"
:key="status.id"
:value="status"
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']">{{
status.status
}}</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="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" />
</div>
<div>
<label for="start">Startdatum</label>
<input type="date" id="start" required />
</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 { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
import type { MembershipStatusViewModel } from "@/viewmodels/admin/membershipStatus.models";
import type { CreateMembershipViewModel } from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership";
</script>
<script lang="ts">
export default defineComponent({
data() {
return {
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
timeout: undefined as any,
selectedStatus: undefined as undefined | MembershipStatusViewModel,
};
},
computed: {
...mapState(useMembershipStatusStore, ["membershipStatus"]),
},
mounted() {
this.fetchMembershipStatus();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStore, ["createMembership"]),
...mapActions(useMembershipStatusStore, ["fetchMembershipStatus"]),
triggerCreate(e: any) {
if (this.selectedStatus == undefined) return;
let formData = e.target.elements;
let createMember: CreateMembershipViewModel = {
internalId: formData.internalId.value,
start: formData.start.value,
statusId: this.selectedStatus.id,
};
this.createMembership(createMember)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,83 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitgliedschaft löschen</p>
</div>
<br />
<p class="text-center">
Mitgliedschaft {{ membership?.start }} bis {{ membership?.end ?? "heute" }}: {{ membership?.status }} 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 { useMembershipStore } from "@/stores/admin/membership";
</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(useMembershipStore, ["memberships"]),
membership() {
return this.memberships.find((m) => m.id == this.data);
},
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStore, ["deleteMembership"]),
triggerDelete() {
this.deleteMembership(this.data)
.then(() => {
this.status = { status: "success" };
this.timeout = setTimeout(() => {
this.closeModal();
}, 1500);
})
.catch(() => {
this.status = { status: "failed" };
});
},
},
});
</script>

View file

@ -0,0 +1,194 @@
<template>
<div class="w-full md:max-w-md">
<div class="flex flex-col items-center">
<p class="text-xl font-medium">Mitgliedschaft 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="membership != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
<div>
<Listbox v-model="membership.statusId" name="status">
<ListboxLabel>Status</ListboxLabel>
<div class="relative mt-1">
<ListboxButton
class="rounded-md shadow-sm relative block w-full px-3 py-2 border border-gray-300 focus:border-primary placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-0 focus:z-10 sm:text-sm resize-none"
>
<span class="block truncate w-full text-start">
{{
membershipStatus.length != 0 ? (selectedStatus ?? "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-none sm:text-sm h-32 overflow-y-auto"
>
<ListboxOption v-if="membershipStatus.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="status in membershipStatus"
:key="status.id"
:value="status.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']">{{
status.status
}}</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="internalId">Interne ID (optional)</label>
<input type="text" id="internalId" v-model="membership.internalId" />
</div>
<div>
<label for="start">Startdatum</label>
<input type="date" id="start" required v-model="membership.start" />
</div>
<div>
<label for="end">Enddatum (optional)</label>
<input type="date" id="end" v-model="membership.end" />
</div>
<div>
<label for="terminationReason">beendet, weil (optional)</label>
<input type="text" id="terminationReason" v-model="membership.terminationReason" />
</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 != null">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 { useMembershipStatusStore } from "@/stores/admin/membershipStatus";
import type {
CreateMembershipViewModel,
MembershipViewModel,
UpdateMembershipViewModel,
} from "@/viewmodels/admin/membership.models";
import { useMembershipStore } from "@/stores/admin/membership";
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 | MembershipViewModel,
membership: null as null | MembershipViewModel,
timeout: undefined as any,
};
},
computed: {
...mapState(useMembershipStatusStore, ["membershipStatus"]),
...mapState(useModalStore, ["data"]),
canSaveOrReset(): boolean {
return isEqual(this.origin, this.membership);
},
selectedStatus() {
return this.membershipStatus.find((ms) => ms.id == this.membership?.statusId)?.status;
},
},
mounted() {
this.fetchMembershipStatus();
this.fetchItem();
},
beforeUnmount() {
try {
clearTimeout(this.timeout);
} catch (error) {}
},
methods: {
...mapActions(useModalStore, ["closeModal"]),
...mapActions(useMembershipStore, ["updateMembership", "fetchMembershipById"]),
...mapActions(useMembershipStatusStore, ["fetchMembershipStatus"]),
resetForm() {
this.membership = cloneDeep(this.origin);
},
fetchItem() {
this.fetchMembershipById(this.data)
.then((result) => {
this.membership = result.data;
this.origin = cloneDeep(result.data);
this.loading = "fetched";
})
.catch((err) => {
this.loading = "failed";
});
},
triggerCreate(e: any) {
if (this.membership == null) return;
let formData = e.target.elements;
let updateMembership: UpdateMembershipViewModel = {
id: this.membership.id,
internalId: formData.internalId.value,
start: formData.start.value,
end: formData.end.value,
terminationReason: formData.terminationReason.value,
statusId: this.membership.statusId,
};
this.updateMembership(updateMembership)
.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,23 +1,26 @@
<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>
<div class="bg-primary p-2 text-white flex flex-row gap-2 justify-between items-center">
<p class="grow">
{{ membership.start }} bis {{ membership.end ?? "heute" }}:
{{ membership.status }}
</p>
<PencilIcon class="w-5 h-5" />
<PencilIcon class="w-5 h-5 cursor-pointer" @click="openEditModal" />
<TrashIcon class="w-5 h-5 cursor-pointer" @click="openDeleteModal" />
</div>
<div v-if="membership.terminationReason" class="p-2">
<p>Grund: {{ membership.terminationReason }}</p>
<div v-if="membership.terminationReason || membership.internalId" class="p-2">
<p v-if="membership.internalId">Interne ID: {{ membership.internalId }}</p>
<p v-if="membership.terminationReason">beendet, weil: {{ membership.terminationReason }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent, type PropType } from "vue";
import { defineAsyncComponent, defineComponent, markRaw, type PropType } from "vue";
import { mapState, mapActions } from "pinia";
import type { MembershipViewModel } from "@/viewmodels/admin/membership.models";
import { PencilIcon } from "@heroicons/vue/24/outline";
import { PencilIcon, TrashIcon } from "@heroicons/vue/24/outline";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
@ -28,5 +31,20 @@ export default defineComponent({
default: {},
},
},
methods: {
...mapActions(useModalStore, ["openModal"]),
openEditModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MembershipEditModal.vue"))),
this.membership.id
);
},
openDeleteModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MembershipDeleteModal.vue"))),
this.membership.id
);
},
},
});
</script>

View file

@ -46,6 +46,7 @@ export const useCommunicationStore = defineStore("communication", {
streetNumber: communication.streetNumber,
streetNumberAddition: communication.streetNumberAddition,
typeId: communication.typeId,
isNewsletterMain: communication.isNewsletterMain,
});
this.fetchCommunicationsForMember();
return result;
@ -60,6 +61,7 @@ export const useCommunicationStore = defineStore("communication", {
street: communication.street,
streetNumber: communication.streetNumber,
streetNumberAddition: communication.streetNumberAddition,
isNewsletterMain: communication.isNewsletterMain,
});
this.fetchCommunicationsForMember();
return result;

View file

@ -42,8 +42,6 @@ export const useMemberQualificationStore = defineStore("memberQualification", {
const result = await http.post(`/admin/member/${memberId}/qualification`, {
note: memberQualification.note,
start: memberQualification.start,
end: memberQualification.end,
terminationReason: memberQualification.terminationReason,
qualificationId: memberQualification.qualificationId,
});
this.fetchMemberQualificationsForMember();

View file

@ -38,10 +38,8 @@ export const useMembershipStore = defineStore("membership", {
async createMembership(membership: CreateMembershipViewModel): Promise<AxiosResponse<any, any>> {
const memberId = useMemberStore().activeMember;
const result = await http.post(`/admin/member/${memberId}/membership`, {
interalId: membership.internalId,
internalId: membership.internalId,
start: membership.start,
end: membership.end,
terminationReason: membership.terminationReason,
statusId: membership.statusId,
});
this.fetchMembershipsForMember();
@ -50,7 +48,7 @@ export const useMembershipStore = defineStore("membership", {
async updateMembership(membership: UpdateMembershipViewModel): Promise<AxiosResponse<any, any>> {
const memberId = useMemberStore().activeMember;
const result = await http.patch(`/admin/member/${memberId}/membership/${membership.id}`, {
interalId: membership.internalId,
internalId: membership.internalId,
start: membership.start,
end: membership.end,
terminationReason: membership.terminationReason,

View file

@ -22,6 +22,7 @@ export interface CreateCommunicationViewModel {
streetNumber: number;
streetNumberAddition: string;
typeId: number;
isNewsletterMain: boolean;
}
export interface UpdateCommunicationViewModel {
@ -33,4 +34,5 @@ export interface UpdateCommunicationViewModel {
street: string;
streetNumber: number;
streetNumberAddition: string;
isNewsletterMain: boolean;
}

View file

@ -4,6 +4,7 @@ export interface MemberAwardViewModel {
note?: string;
date: Date;
award: string;
awardId: number;
}
export interface CreateMemberAwardViewModel {

View file

@ -4,6 +4,7 @@ export interface MemberExecutivePositionViewModel {
start: Date;
end?: Date;
executivePosition: string;
executivePositionId: number;
}
export interface CreateMemberExecutivePositionViewModel {

View file

@ -5,14 +5,13 @@ export interface MemberQualificationViewModel {
end?: Date;
terminationReason?: string;
qualification: string;
qualificationId: number;
}
export interface CreateMemberQualificationViewModel {
note?: string;
start: Date;
end?: Date;
terminationReason?: string;
qualificationId: string;
qualificationId: number;
}
export interface UpdateMemberQualificationViewModel {
@ -21,5 +20,5 @@ export interface UpdateMemberQualificationViewModel {
start: Date;
end?: Date;
terminationReason?: string;
qualificationId: string;
qualificationId: number;
}

View file

@ -5,13 +5,12 @@ export interface MembershipViewModel {
end?: Date;
terminationReason?: string;
status: string;
statusId: number;
}
export interface CreateMembershipViewModel {
internalId?: string;
start: Date;
end?: Date;
terminationReason?: string;
statusId: number;
}

View file

@ -4,23 +4,27 @@
<MemberAwardListItem v-for="award in memberAwards" :key="award.id" :award="award" />
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="">Auszeichnung hinzufügen</button>
<button primary class="!w-fit" @click="openCreateModal">Auszeichnung hinzufügen</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useMemberAwardStore } from "@/stores/admin/memberAward";
import MemberAwardListItem from "@/components/admin/club/member/MemberAwardListItem.vue";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMemberAwardStore, ["memberAwards", "loading"]),
},
@ -29,9 +33,15 @@ export default defineComponent({
},
methods: {
...mapActions(useMemberAwardStore, ["fetchMemberAwardsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchMemberAwardsForMember();
},
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberAwardCreateModal.vue")))
);
},
},
});
</script>

View file

@ -8,15 +8,15 @@
/>
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="">Kommunikation hinzufügen</button>
<button primary class="!w-fit" @click="openCreateModal">Kommunikation hinzufügen</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import { useMemberStore } from "@/stores/admin/member";
import type { MemberViewModel, UpdateMemberViewModel } from "@/viewmodels/admin/member.models";
@ -24,10 +24,14 @@ import Spinner from "@/components/Spinner.vue";
import type { CommunicationViewModel } from "@/viewmodels/admin/communication.models";
import { useCommunicationStore } from "@/stores/admin/communication";
import MemberCommunicationListItem from "@/components/admin/club/member/MemberCommunicationListItem.vue";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useCommunicationStore, ["communications", "loading"]),
},
@ -36,9 +40,15 @@ export default defineComponent({
},
methods: {
...mapActions(useCommunicationStore, ["fetchCommunicationsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchCommunicationsForMember();
},
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberCommunicationCreateModal.vue")))
);
},
},
});
</script>

View file

@ -8,23 +8,27 @@
/>
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="">Vereinsamt hinzufügen</button>
<button primary class="!w-fit" @click="openCreateModal">Vereinsamt hinzufügen</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useMemberExecutivePositionStore } from "@/stores/admin/memberExecutivePosition";
import MemberExecutivePositionListItem from "@/components/admin/club/member/MemberExecutivePositionListItem.vue";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMemberExecutivePositionStore, ["memberExecutivePositions", "loading"]),
},
@ -33,9 +37,17 @@ export default defineComponent({
},
methods: {
...mapActions(useMemberExecutivePositionStore, ["fetchMemberExecutivePositionsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchMemberExecutivePositionsForMember();
},
openCreateModal() {
this.openModal(
markRaw(
defineAsyncComponent(() => import("@/components/admin/club/member/MemberExecutivePositionCreateModal.vue"))
)
);
},
},
});
</script>

View file

@ -58,6 +58,7 @@
</div>
<div v-if="activeMemberObj.preferredCommunication?.length != 0">
<p>bevorzugte Kommunikationswege</p>
<div class="flex flex-col gap-2">
<div
v-for="com in activeMemberObj.preferredCommunication"
class="flex flex-col h-fit w-full border border-primary rounded-md"
@ -72,6 +73,7 @@
</div>
</div>
</div>
</div>
<div v-if="activeMemberObj.sendNewsletter">
<p>Newsletter Kommunikationswege</p>
<div class="flex flex-col h-fit w-full border border-primary rounded-md">
@ -90,7 +92,9 @@
</div>
<Spinner v-if="loadingActive == 'loading'" class="mx-auto" />
<p v-else-if="loadingActive == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loadingActive == 'failed'" @click="fetchMemberByActiveId" class="cursor-pointer">
&#8634; laden fehlgeschlagen
</p>
</div>
</template>
@ -103,6 +107,9 @@ import { useMemberStore } from "@/stores/admin/member";
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMemberStore, ["activeMemberObj", "loadingActive"]),
},

View file

@ -8,23 +8,27 @@
/>
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="">Qualifikation hinzufügen</button>
<button primary class="!w-fit" @click="openCreateModal">Qualifikation hinzufügen</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineAsyncComponent, defineComponent, markRaw } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useMemberQualificationStore } from "@/stores/admin/memberQualification";
import MemberQualificationListItem from "@/components/admin/club/member/MemberQualificationListItem.vue";
import { useModalStore } from "@/stores/modal";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMemberQualificationStore, ["memberQualifications", "loading"]),
},
@ -33,9 +37,15 @@ export default defineComponent({
},
methods: {
...mapActions(useMemberQualificationStore, ["fetchMemberQualificationsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchMemberQualificationsForMember();
},
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MemberQualificationCreateModal.vue")))
);
},
},
});
</script>

View file

@ -4,23 +4,27 @@
<MembershipListItem v-for="membership in memberships" :key="membership.id" :membership="membership" />
</div>
<Spinner v-if="loading == 'loading'" class="mx-auto" />
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
<p v-else-if="loading == 'failed'" @click="fetchItem" class="cursor-pointer">&#8634; laden fehlgeschlagen</p>
</div>
<div class="flex flex-row gap-4">
<button primary class="!w-fit" @click="">Mitgliedschaft hinzufügen</button>
<button primary class="!w-fit" @click="openCreateModal">Mitgliedschaft hinzufügen</button>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from "vue";
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
import { mapActions, mapState } from "pinia";
import Spinner from "@/components/Spinner.vue";
import { useMembershipStore } from "@/stores/admin/membership";
import { useModalStore } from "@/stores/modal";
import MembershipListItem from "@/components/admin/club/member/MembershipListItem.vue";
</script>
<script lang="ts">
export default defineComponent({
props: {
memberId: String,
},
computed: {
...mapState(useMembershipStore, ["memberships", "loading"]),
},
@ -29,9 +33,15 @@ export default defineComponent({
},
methods: {
...mapActions(useMembershipStore, ["fetchMembershipsForMember"]),
...mapActions(useModalStore, ["openModal"]),
fetchItem() {
this.fetchMembershipsForMember();
},
openCreateModal() {
this.openModal(
markRaw(defineAsyncComponent(() => import("@/components/admin/club/member/MembershipCreateModal.vue")))
);
},
},
});
</script>