#3-calendar #7
25 changed files with 1478 additions and 27 deletions
64
package-lock.json
generated
64
package-lock.json
generated
|
@ -9,6 +9,11 @@
|
|||
"version": "0.0.3",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
|
@ -2414,6 +2419,55 @@
|
|||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/core": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
|
||||
"integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"preact": "~10.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/daygrid": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
|
||||
"integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/interaction": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz",
|
||||
"integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/timegrid": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz",
|
||||
"integrity": "sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fullcalendar/daygrid": "~6.1.15"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15"
|
||||
}
|
||||
},
|
||||
"node_modules/@fullcalendar/vue3": {
|
||||
"version": "6.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@fullcalendar/vue3/-/vue3-6.1.15.tgz",
|
||||
"integrity": "sha512-ctfTICGrNEIj7gmLHQcUYe0WzDTSW5Vd9hyOnVChxPU75AZU9WqdDMkHwJYnfNxNhT6QQuiMHq/qsRRd5zQwOw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@fullcalendar/core": "~6.1.15",
|
||||
"vue": "^3.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@headlessui/vue": {
|
||||
"version": "1.7.22",
|
||||
"resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.22.tgz",
|
||||
|
@ -7617,6 +7671,16 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.12.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
|
||||
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz",
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
"author": "JK Effects",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.15",
|
||||
"@fullcalendar/daygrid": "^6.1.15",
|
||||
"@fullcalendar/interaction": "^6.1.15",
|
||||
"@fullcalendar/timegrid": "^6.1.15",
|
||||
"@fullcalendar/vue3": "^6.1.15",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@heroicons/vue": "^2.1.5",
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<footer
|
||||
v-if="authCheck && routeName.includes('admin')"
|
||||
class="md:hidden flex flex-row h-16 justify-center md:justify-normal p-1 bg-white"
|
||||
class="md:hidden flex flex-row h-16 min-h-16 justify-center md:justify-normal p-1 bg-white"
|
||||
>
|
||||
<div class="w-full flex flex-row gap-2 h-full align-middle">
|
||||
<TopLevelLink v-for="item in topLevel" :key="item.key" :link="item" :disableSubLink="true" />
|
||||
|
|
219
src/components/admin/club/calendar/CreateCalendarModal.vue
Normal file
219
src/components/admin/club/calendar/CreateCalendarModal.vue
Normal file
|
@ -0,0 +1,219 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Termintyp erstellen</p>
|
||||
</div>
|
||||
<br />
|
||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<Listbox v-model="selectedType" name="type">
|
||||
<ListboxLabel>Termintyp</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">
|
||||
{{
|
||||
calendarTypes.length != 0 ? (selectedType?.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="calendarTypes.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 calendarTypes"
|
||||
: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>
|
||||
<label for="title">Titel</label>
|
||||
<input type="text" id="title" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="content">Beschreibung (optional)</label>
|
||||
<textarea id="content" class="h-18"></textarea>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allDay"
|
||||
:checked="data.allDay"
|
||||
@change="(e) => (allDay = (e.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<label for="allDay">ganztägig</label>
|
||||
</div>
|
||||
<div v-if="!allDay" class="flex flex-row gap-2">
|
||||
<div class="w-full">
|
||||
<label for="starttime">Startzeit</label>
|
||||
<input type="datetime-local" id="starttime" required :value="data.start.split(':00+')[0]" />
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="endtime">Endzeit</label>
|
||||
<input type="datetime-local" id="endtime" required :value="data.end.split(':00+')[0]" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-row gap-2">
|
||||
<div class="w-full">
|
||||
<label for="startdate">Startdatum</label>
|
||||
<input
|
||||
type="date"
|
||||
id="startdate"
|
||||
required
|
||||
:value="data.start"
|
||||
@change="($event) => ($refs.enddate.max = ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="enddate">Enddatum</label>
|
||||
<input
|
||||
ref="enddate"
|
||||
type="date"
|
||||
id="enddate"
|
||||
required
|
||||
:value="decrementEndDate(data.end)"
|
||||
:min="data.start"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="location">Ort (optional)</label>
|
||||
<input type="text" id="location" />
|
||||
</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 != null && status != 'loading' && status?.status != 'failed'"
|
||||
>
|
||||
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 { useCalendarStore } from "@/stores/admin/calendar";
|
||||
import type { CreateCalendarViewModel } from "@/viewmodels/admin/calendar.models";
|
||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
allDay: false,
|
||||
selectedType: undefined as undefined | CalendarTypeViewModel,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useModalStore, ["data"]),
|
||||
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||
},
|
||||
mounted() {
|
||||
this.allDay = this.data.allDay;
|
||||
this.fetchCalendarTypes();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useCalendarStore, ["createCalendar"]),
|
||||
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||
triggerCreate(e: any) {
|
||||
if (!this.selectedType) return;
|
||||
let formData = e.target.elements;
|
||||
let createCalendar: CreateCalendarViewModel = {
|
||||
typeId: this.selectedType.id,
|
||||
starttime: this.allDay
|
||||
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0))
|
||||
: formData.starttime.value,
|
||||
endtime: this.allDay
|
||||
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999))
|
||||
: formData.endtime.value,
|
||||
title: formData.title.value,
|
||||
content: formData.content.value,
|
||||
location: formData.location.value,
|
||||
allDay: this.allDay,
|
||||
};
|
||||
this.createCalendar(createCalendar)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
decrementEndDate(utcDateString: string) {
|
||||
const localDate = new Date(utcDateString);
|
||||
|
||||
const year = localDate.getFullYear();
|
||||
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(localDate.getDate() - 1).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
94
src/components/admin/club/calendar/DeleteCalendarModal.vue
Normal file
94
src/components/admin/club/calendar/DeleteCalendarModal.vue
Normal file
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Termin {{ calendar?.title }} löschen?</p>
|
||||
</div>
|
||||
<br />
|
||||
<p class="text-center">
|
||||
{{
|
||||
new Date(calendar?.starttime ?? "").toLocaleString("de-De", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}}
|
||||
bis
|
||||
{{
|
||||
new Date(calendar?.endtime ?? "").toLocaleString("de-De", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<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 != null">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 { useCalendarStore } from "@/stores/admin/calendar";
|
||||
</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(useCalendarStore, ["calendars"]),
|
||||
calendar() {
|
||||
return this.calendars.find((r) => r.id == this.data);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useCalendarStore, ["deleteCalendar"]),
|
||||
triggerDelete() {
|
||||
this.deleteCalendar(this.data)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
302
src/components/admin/club/calendar/UpdateCalendarModal.vue
Normal file
302
src/components/admin/club/calendar/UpdateCalendarModal.vue
Normal file
|
@ -0,0 +1,302 @@
|
|||
<template>
|
||||
<div class="relative w-full md:max-w-md">
|
||||
<TrashIcon class="absolute top-3 right-3 w-5 h-5 cursor-pointer" @click="deleteCalendar" />
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Termintyp erstellen</p>
|
||||
</div>
|
||||
<br />
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||
<form v-else-if="calendar != null" class="flex flex-col gap-4 py-2" @submit.prevent="triggerUpdate">
|
||||
<div>
|
||||
<Listbox v-model="calendar.type" name="type">
|
||||
<ListboxLabel>Termintyp</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">
|
||||
{{
|
||||
calendarTypes.length != 0 ? (calendar.type?.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="calendarTypes.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 calendarTypes"
|
||||
: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>
|
||||
<label for="title">Titel</label>
|
||||
<input type="text" id="title" required v-model="calendar.title" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="content">Beschreibung (optional)</label>
|
||||
<textarea id="content" class="h-18" v-model="calendar.content"></textarea>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input type="checkbox" id="allDay" v-model="calendar.allDay" />
|
||||
<label for="allDay">ganztägig</label>
|
||||
</div>
|
||||
<div v-if="calendar.allDay == false" class="flex flex-row gap-2">
|
||||
<div class="w-full">
|
||||
<label for="starttime">Startzeit</label>
|
||||
<input
|
||||
type="datetime-local"
|
||||
id="starttime"
|
||||
required
|
||||
:value="formatForDateTimeLocalInput(calendar.starttime)"
|
||||
@change="
|
||||
($event) => {
|
||||
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||
$refs.endtime.min = formatForDateTimeLocalInput(($event.target as HTMLInputElement).value);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="endtime">Endzeit</label>
|
||||
<input
|
||||
ref="endtime"
|
||||
type="datetime-local"
|
||||
id="endtime"
|
||||
required
|
||||
:value="formatForDateTimeLocalInput(calendar.endtime)"
|
||||
:min="formatForDateTimeLocalInput(calendar.starttime)"
|
||||
@change="
|
||||
($event) => {
|
||||
calendar!.endtime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-row gap-2">
|
||||
<div class="w-full">
|
||||
<label for="startdate">Startdatum</label>
|
||||
<input
|
||||
type="date"
|
||||
id="startdate"
|
||||
required
|
||||
:value="formatForDateInput(calendar.starttime)"
|
||||
@change="
|
||||
($event) => {
|
||||
calendar!.starttime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||
$refs.enddate.min = formatForDateInput(($event.target as HTMLInputElement).value);
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<label for="enddate">Enddatum</label>
|
||||
<input
|
||||
ref="enddate"
|
||||
type="date"
|
||||
id="enddate"
|
||||
required
|
||||
:min="formatForDateInput(calendar.starttime)"
|
||||
:value="formatForDateInput(calendar.endtime)"
|
||||
@change="
|
||||
($event) => {
|
||||
calendar!.endtime = new Date(($event.target as HTMLInputElement).value).toISOString();
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="location">Ort (optional)</label>
|
||||
<input type="text" id="location" v-model="calendar.location" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row 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>
|
||||
|
||||
<div class="flex flex-row justify-end">
|
||||
<div class="flex flex-row gap-4 py-2">
|
||||
<button
|
||||
primary-outline
|
||||
@click="closeModal"
|
||||
:disabled="status != null && status != 'loading' && status?.status != 'failed'"
|
||||
>
|
||||
abbrechen / schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineAsyncComponent, defineComponent, markRaw } 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 { useCalendarStore } from "@/stores/admin/calendar";
|
||||
import type {
|
||||
CalendarViewModel,
|
||||
CreateCalendarViewModel,
|
||||
UpdateCalendarViewModel,
|
||||
} from "@/viewmodels/admin/calendar.models";
|
||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon, TrashIcon } from "@heroicons/vue/20/solid";
|
||||
import { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import isEqual from "lodash.isEqual";
|
||||
</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 | CalendarViewModel,
|
||||
calendar: null as null | CalendarViewModel,
|
||||
timeout: undefined as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useModalStore, ["data"]),
|
||||
...mapState(useCalendarStore, ["calendars"]),
|
||||
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.origin, this.calendar);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendarTypes();
|
||||
this.fetchItem();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal", "openModal"]),
|
||||
...mapActions(useCalendarStore, ["updateCalendar", "fetchCalendarById"]),
|
||||
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||
resetForm() {
|
||||
this.calendar = cloneDeep(this.origin);
|
||||
},
|
||||
fetchItem() {
|
||||
this.fetchCalendarById(this.data ?? "")
|
||||
.then((result) => {
|
||||
this.calendar = result.data;
|
||||
this.origin = cloneDeep(result.data);
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
triggerUpdate(e: any) {
|
||||
if (this.calendar == null) return;
|
||||
let formData = e.target.elements;
|
||||
let updateCalendar: UpdateCalendarViewModel = {
|
||||
id: this.calendar.id,
|
||||
typeId: this.calendar.type.id,
|
||||
starttime: this.calendar.allDay
|
||||
? new Date(new Date(formData.startdate.value).setHours(0, 0, 0, 0)).toISOString()
|
||||
: new Date(formData.starttime.value).toISOString(),
|
||||
endtime: this.calendar.allDay
|
||||
? new Date(new Date(formData.enddate.value).setHours(23, 59, 59, 999)).toISOString()
|
||||
: new Date(formData.endtime.value).toISOString(),
|
||||
title: formData.title.value,
|
||||
content: formData.content.value,
|
||||
location: formData.location.value,
|
||||
allDay: this.calendar.allDay,
|
||||
};
|
||||
this.updateCalendar(updateCalendar)
|
||||
.then(() => {
|
||||
this.fetchItem();
|
||||
this.status = { status: "success" };
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
})
|
||||
.finally(() => {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
deleteCalendar() {
|
||||
if (this.origin == null) return;
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/DeleteCalendarModal.vue"))),
|
||||
this.origin.id
|
||||
);
|
||||
},
|
||||
formatForDateTimeLocalInput(utcDateString: string) {
|
||||
const localDate = new Date(utcDateString);
|
||||
|
||||
const year = localDate.getFullYear();
|
||||
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(localDate.getDate()).padStart(2, "0");
|
||||
const hours = String(localDate.getHours()).padStart(2, "0");
|
||||
const minutes = String(localDate.getMinutes()).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
},
|
||||
formatForDateInput(utcDateString: string) {
|
||||
const localDate = new Date(utcDateString);
|
||||
|
||||
const year = localDate.getFullYear();
|
||||
const month = String(localDate.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(localDate.getDate()).padStart(2, "0");
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-fit w-full border border-primary rounded-md overflow-hidden">
|
||||
<div class="bg-primary p-2 text-white flex flex-row justify-between items-center">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<div class="rounded-md h-5 w-5" :style="'background-color:' + calendarType.color">
|
||||
<EyeIcon v-if="calendarType.nscdr" class="w-5 h-5" />
|
||||
</div>
|
||||
<p>{{ calendarType.type }}</p>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<RouterLink
|
||||
v-if="can('update', 'settings', 'calendar_type')"
|
||||
:to="{ name: 'admin-settings-calendar_type-edit', params: { id: calendarType.id } }"
|
||||
>
|
||||
<PencilIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</RouterLink>
|
||||
<div v-if="can('delete', 'settings', 'calendar_type')" @click="openDeleteModal">
|
||||
<TrashIcon class="w-5 h-5 p-1 box-content cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineAsyncComponent, markRaw, type PropType } from "vue";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import { PencilIcon, TrashIcon, EyeIcon } from "@heroicons/vue/24/outline";
|
||||
import { useAbilityStore } from "@/stores/ability";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import type { CalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
props: {
|
||||
calendarType: { type: Object as PropType<CalendarTypeViewModel>, default: {} },
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openDeleteModal() {
|
||||
this.openModal(
|
||||
markRaw(
|
||||
defineAsyncComponent(() => import("@/components/admin/settings/calendarType/DeleteCalendarTypeModal.vue"))
|
||||
),
|
||||
this.calendarType.id
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Termintyp erstellen</p>
|
||||
</div>
|
||||
<br />
|
||||
<form class="flex flex-col gap-4 py-2" @submit.prevent="triggerCreate">
|
||||
<div>
|
||||
<label for="type">Bezeichnung</label>
|
||||
<input type="text" id="type" required />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input type="color" id="color" required class="!px-1 !py-0 !w-10" />
|
||||
<label for="color">Farbe</label>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input type="checkbox" id="nscdr" />
|
||||
<label for="nscdr">Standard Kalender Auslieferung (optional)</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 != null && status?.status != 'failed'">
|
||||
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 { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
import type { CreateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
|
||||
import { Listbox, ListboxButton, ListboxOptions, ListboxOption, ListboxLabel } from "@headlessui/vue";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import type { CalendarFieldType } from "@/types/fieldTypes";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
status: null as null | "loading" | { status: "success" | "failed"; reason?: string },
|
||||
timeout: undefined as any,
|
||||
selectedFields: [] as Array<CalendarFieldType>,
|
||||
};
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useCalendarTypeStore, ["createCalendarType"]),
|
||||
triggerCreate(e: any) {
|
||||
let formData = e.target.elements;
|
||||
let createCalendarType: CreateCalendarTypeViewModel = {
|
||||
type: formData.type.value,
|
||||
color: formData.color.value,
|
||||
nscdr: formData.nscdr.checked,
|
||||
};
|
||||
this.createCalendarType(createCalendarType)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<div class="w-full md:max-w-md">
|
||||
<div class="flex flex-col items-center">
|
||||
<p class="text-xl font-medium">Termintyp {{ calendarType?.type }} 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 != null">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 { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
</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(useCalendarTypeStore, ["calendarTypes"]),
|
||||
calendarType() {
|
||||
return this.calendarTypes.find((r) => r.id == this.data);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["closeModal"]),
|
||||
...mapActions(useCalendarTypeStore, ["deleteCalendarType"]),
|
||||
triggerDelete() {
|
||||
this.deleteCalendarType(this.data)
|
||||
.then(() => {
|
||||
this.status = { status: "success" };
|
||||
this.timeout = setTimeout(() => {
|
||||
this.closeModal();
|
||||
}, 1500);
|
||||
})
|
||||
.catch(() => {
|
||||
this.status = { status: "failed" };
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -1,7 +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">
|
||||
<p>{{ user.firstname }} {{ user.lastname }} <small v-if="user.permissions_total.admin">(Admin)</small></p>
|
||||
<p>
|
||||
{{ user.firstname }} {{ user.lastname }} <small v-if="user.permissions_total.admin">(Admin)</small
|
||||
><small v-if="isOwner"> (Owner)</small>
|
||||
</p>
|
||||
<div class="flex flex-row">
|
||||
<RouterLink
|
||||
v-if="can('update', 'user', 'user')"
|
||||
|
@ -62,8 +65,7 @@ export default defineComponent({
|
|||
user: { type: Object as PropType<UserViewModel>, default: {} },
|
||||
},
|
||||
computed: {
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
...mapState(useAbilityStore, ["can"]),
|
||||
...mapState(useAbilityStore, ["can", "isOwner"]),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
|
|
12
src/main.css
12
src/main.css
|
@ -56,7 +56,7 @@ body {
|
|||
@apply w-full h-full overflow-hidden flex flex-col;
|
||||
}
|
||||
|
||||
button:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"]):not([class*="ql"] *),
|
||||
button:not([headlessui]):not([id*="headlessui"]):not([class*="headlessui"]):not([class*="ql"] *):not([class*="fc"]),
|
||||
a[button] {
|
||||
@apply relative box-border h-10 w-full flex justify-center py-2 px-4 text-sm font-medium rounded-md focus:outline-none focus:ring-0;
|
||||
}
|
||||
|
@ -118,3 +118,13 @@ summary > svg {
|
|||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.fc-button-primary {
|
||||
@apply !bg-primary !border-primary !outline-none !ring-0 hover:!bg-red-700 hover:!border-red-700;
|
||||
}
|
||||
.fc-button-active {
|
||||
@apply !bg-red-500 !border-red-500;
|
||||
}
|
||||
.fc-toolbar {
|
||||
@apply flex-wrap;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ export type Payload = JwtPayload & {
|
|||
firstname: string;
|
||||
lastname: string;
|
||||
mail: string;
|
||||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
};
|
||||
|
||||
|
@ -66,16 +67,16 @@ export async function isAuthenticatedPromise(): Promise<Payload> {
|
|||
});
|
||||
}
|
||||
|
||||
var { firstname, lastname, mail, username, permissions } = decoded;
|
||||
var { firstname, lastname, mail, username, permissions, isOwner } = decoded;
|
||||
|
||||
if (Object.keys(permissions ?? {}).length === 0) {
|
||||
if (Object.keys(permissions ?? {}).length === 0 && !isOwner) {
|
||||
auth.setFailed();
|
||||
reject("nopermissions");
|
||||
}
|
||||
|
||||
auth.setSuccess();
|
||||
account.setAccountData(firstname, lastname, mail, username);
|
||||
ability.setAbility(permissions);
|
||||
ability.setAbility(permissions, isOwner);
|
||||
resolve(decoded);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -136,7 +136,7 @@ const router = createRouter({
|
|||
{
|
||||
path: "calendar",
|
||||
name: "admin-club-calendar",
|
||||
component: () => import("@/views/admin/members/Overview.vue"),
|
||||
component: () => import("@/views/admin/club/calendar/Calendar.vue"),
|
||||
meta: { type: "read", section: "club", module: "calendar" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
|
@ -267,13 +267,6 @@ const router = createRouter({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "executive-position",
|
||||
name: "admin-settings-executive_position",
|
||||
component: () => import("@/views/admin/settings/ExecutivePosition.vue"),
|
||||
meta: { type: "read", section: "settings", module: "executive_position" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: "executive-position",
|
||||
name: "admin-settings-executive_position-route",
|
||||
|
@ -285,6 +278,8 @@ const router = createRouter({
|
|||
path: "",
|
||||
name: "admin-settings-executive_position",
|
||||
component: () => import("@/views/admin/settings/ExecutivePosition.vue"),
|
||||
meta: { type: "read", section: "settings", module: "executive_position" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
|
@ -340,6 +335,28 @@ const router = createRouter({
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "calendar-type",
|
||||
name: "admin-settings-calendar_type-route",
|
||||
component: () => import("@/views/RouterView.vue"),
|
||||
meta: { type: "read", section: "settings", module: "calendar_type" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "admin-settings-calendar_type",
|
||||
component: () => import("@/views/admin/settings/CalendarType.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id/edit",
|
||||
name: "admin-settings-calendar_type-edit",
|
||||
component: () => import("@/views/admin/settings/CalendarTypeEdit.vue"),
|
||||
meta: { type: "update", section: "settings", module: "calendar_type" },
|
||||
beforeEnter: [abilityAndNavUpdate],
|
||||
props: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -5,6 +5,7 @@ export const useAbilityStore = defineStore("ability", {
|
|||
state: () => {
|
||||
return {
|
||||
permissions: {} as PermissionObject,
|
||||
isOwner: false as boolean,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
|
@ -12,6 +13,7 @@ export const useAbilityStore = defineStore("ability", {
|
|||
(state) =>
|
||||
(type: PermissionType | "admin", section: PermissionSection, module?: PermissionModule): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (
|
||||
|
@ -30,6 +32,7 @@ export const useAbilityStore = defineStore("ability", {
|
|||
(state) =>
|
||||
(type: PermissionType | "admin", section: PermissionSection): boolean => {
|
||||
const permissions = state.permissions;
|
||||
if (state.isOwner) return true;
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (
|
||||
|
@ -48,6 +51,7 @@ export const useAbilityStore = defineStore("ability", {
|
|||
section: PermissionSection,
|
||||
module?: PermissionModule
|
||||
): boolean => {
|
||||
// ignores ownership
|
||||
if (type == "admin") return permissions?.admin ?? false;
|
||||
if (permissions?.admin) return true;
|
||||
if (
|
||||
|
@ -64,8 +68,9 @@ export const useAbilityStore = defineStore("ability", {
|
|||
},
|
||||
},
|
||||
actions: {
|
||||
setAbility(permissions: PermissionObject) {
|
||||
setAbility(permissions: PermissionObject, isOwner: boolean) {
|
||||
this.permissions = permissions;
|
||||
this.isOwner = isOwner;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
77
src/stores/admin/calendar.ts
Normal file
77
src/stores/admin/calendar.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type {
|
||||
CreateCalendarViewModel,
|
||||
UpdateCalendarViewModel,
|
||||
CalendarViewModel,
|
||||
} from "@/viewmodels/admin/calendar.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const useCalendarStore = defineStore("calendar", {
|
||||
state: () => {
|
||||
return {
|
||||
calendars: [] as Array<CalendarViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
loadingFields: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
formattedItems: (state) => {
|
||||
return state.calendars.map((c) => ({
|
||||
id: c.id,
|
||||
title: c.title,
|
||||
start: c.starttime,
|
||||
end: c.endtime,
|
||||
backgroundColor: c.type.color,
|
||||
}));
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchCalendars() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/calendar/items")
|
||||
.then((result) => {
|
||||
this.calendars = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchCalendarById(id: string): Promise<AxiosResponse<any, any>> {
|
||||
return http.get(`/admin/calendar/item/${id}`);
|
||||
},
|
||||
async createCalendar(calendar: CreateCalendarViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/calendar/item`, {
|
||||
starttime: calendar.starttime,
|
||||
endtime: calendar.endtime,
|
||||
title: calendar.title,
|
||||
content: calendar.content,
|
||||
allDay: calendar.allDay,
|
||||
location: calendar.location,
|
||||
typeId: calendar.typeId,
|
||||
});
|
||||
this.fetchCalendars();
|
||||
return result;
|
||||
},
|
||||
async updateCalendar(calendar: UpdateCalendarViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.patch(`/admin/calendar/item/${calendar.id}`, {
|
||||
starttime: calendar.starttime,
|
||||
endtime: calendar.endtime,
|
||||
title: calendar.title,
|
||||
content: calendar.content,
|
||||
allDay: calendar.allDay,
|
||||
location: calendar.location,
|
||||
typeId: calendar.typeId,
|
||||
});
|
||||
this.fetchCalendars();
|
||||
return result;
|
||||
},
|
||||
async deleteCalendar(calendar: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.delete(`/admin/calendar/item/${calendar}`);
|
||||
this.fetchCalendars();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
58
src/stores/admin/calendarType.ts
Normal file
58
src/stores/admin/calendarType.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { defineStore } from "pinia";
|
||||
import type {
|
||||
CreateCalendarTypeViewModel,
|
||||
UpdateCalendarTypeViewModel,
|
||||
CalendarTypeViewModel,
|
||||
} from "@/viewmodels/admin/calendarType.models";
|
||||
import { http } from "@/serverCom";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const useCalendarTypeStore = defineStore("calendarType", {
|
||||
state: () => {
|
||||
return {
|
||||
calendarTypes: [] as Array<CalendarTypeViewModel>,
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
loadingFields: "loading" as "loading" | "fetched" | "failed",
|
||||
};
|
||||
},
|
||||
actions: {
|
||||
fetchCalendarTypes() {
|
||||
this.loading = "loading";
|
||||
http
|
||||
.get("/admin/calendar/types")
|
||||
.then((result) => {
|
||||
this.calendarTypes = result.data;
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
fetchCalendarTypeById(id: number): Promise<AxiosResponse<any, any>> {
|
||||
return http.get(`/admin/calendar/type/${id}`);
|
||||
},
|
||||
async createCalendarType(calendarType: CreateCalendarTypeViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.post(`/admin/calendar/type`, {
|
||||
type: calendarType.type,
|
||||
nscdr: calendarType.nscdr,
|
||||
color: calendarType.color,
|
||||
});
|
||||
this.fetchCalendarTypes();
|
||||
return result;
|
||||
},
|
||||
async updateActiveCalendarType(calendarType: UpdateCalendarTypeViewModel): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.patch(`/admin/calendar/type/${calendarType.id}`, {
|
||||
type: calendarType.type,
|
||||
nscdr: calendarType.nscdr,
|
||||
color: calendarType.color,
|
||||
});
|
||||
this.fetchCalendarTypes();
|
||||
return result;
|
||||
},
|
||||
async deleteCalendarType(calendarType: number): Promise<AxiosResponse<any, any>> {
|
||||
const result = await http.delete(`/admin/calendar/type/${calendarType}`);
|
||||
this.fetchCalendarTypes();
|
||||
return result;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -87,9 +87,9 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
mainTitle: "Verein",
|
||||
main: [
|
||||
...(abilityStore.can("read", "club", "member") ? [{ key: "member", title: "Mitglieder" }] : []),
|
||||
...(abilityStore.can("read", "club", "calendar") ? [{ key: "calendar", title: "Termine" }] : []),
|
||||
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
|
||||
...(abilityStore.can("read", "club", "calendar") ? [{ key: "calendar", title: "Kalender" }] : []),
|
||||
...(abilityStore.can("read", "club", "protocoll") ? [{ key: "protocol", title: "Protokolle" }] : []),
|
||||
...(abilityStore.can("read", "club", "newsletter") ? [{ key: "newsletter", title: "Newsletter" }] : []),
|
||||
],
|
||||
},
|
||||
settings: {
|
||||
|
@ -108,6 +108,9 @@ export const useNavigationStore = defineStore("navigation", {
|
|||
...(abilityStore.can("read", "settings", "membership_status")
|
||||
? [{ key: "membership_status", title: "Mitgliedsstatus" }]
|
||||
: []),
|
||||
...(abilityStore.can("read", "settings", "calendar_type")
|
||||
? [{ key: "calendar_type", title: "Terminarten" }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
user: {
|
||||
|
|
|
@ -10,6 +10,7 @@ export type PermissionModule =
|
|||
| "executive_position"
|
||||
| "communication"
|
||||
| "membership_status"
|
||||
| "calendar_type"
|
||||
| "user"
|
||||
| "role";
|
||||
|
||||
|
@ -45,12 +46,13 @@ export const permissionModules: Array<PermissionModule> = [
|
|||
"executive_position",
|
||||
"communication",
|
||||
"membership_status",
|
||||
"calendar_type",
|
||||
"user",
|
||||
"role",
|
||||
];
|
||||
export const permissionTypes: Array<PermissionType> = ["read", "create", "update", "delete"];
|
||||
export const sectionsAndModules: SectionsAndModulesObject = {
|
||||
club: ["member", "calendar", "newsletter", "protocol"],
|
||||
settings: ["qualification", "award", "executive_position", "communication", "membership_status"],
|
||||
settings: ["qualification", "award", "executive_position", "communication", "membership_status", "calendar_type"],
|
||||
user: ["user", "role"],
|
||||
};
|
||||
|
|
35
src/viewmodels/admin/calendar.models.ts
Normal file
35
src/viewmodels/admin/calendar.models.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import type { CalendarTypeViewModel } from "./calendarType.models";
|
||||
|
||||
export interface CalendarViewModel {
|
||||
id: string;
|
||||
starttime: string;
|
||||
endtime: string;
|
||||
title: string;
|
||||
content: string;
|
||||
location: string;
|
||||
allDay: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
type: CalendarTypeViewModel;
|
||||
}
|
||||
|
||||
export interface CreateCalendarViewModel {
|
||||
starttime: string;
|
||||
endtime: string;
|
||||
title: string;
|
||||
content: string;
|
||||
location: string;
|
||||
allDay: boolean;
|
||||
typeId: string;
|
||||
}
|
||||
|
||||
export interface UpdateCalendarViewModel {
|
||||
id: string;
|
||||
starttime: string;
|
||||
endtime: string;
|
||||
title: string;
|
||||
content: string;
|
||||
location: string;
|
||||
allDay: boolean;
|
||||
typeId: string;
|
||||
}
|
19
src/viewmodels/admin/calendarType.models.ts
Normal file
19
src/viewmodels/admin/calendarType.models.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export interface CalendarTypeViewModel {
|
||||
id: string;
|
||||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface CreateCalendarTypeViewModel {
|
||||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface UpdateCalendarTypeViewModel {
|
||||
id: string;
|
||||
type: string;
|
||||
nscdr: boolean;
|
||||
color: string;
|
||||
}
|
|
@ -7,6 +7,7 @@ export interface UserViewModel {
|
|||
mail: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
isOwner: boolean;
|
||||
permissions: PermissionObject;
|
||||
roles: Array<RoleViewModel>;
|
||||
permissions_total: PermissionObject;
|
||||
|
|
140
src/views/admin/club/calendar/Calendar.vue
Normal file
140
src/views/admin/club/calendar/Calendar.vue
Normal file
|
@ -0,0 +1,140 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Kalender</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col w-full h-full gap-2 justify-between px-7 overflow-hidden">
|
||||
<FullCalendar :options="calendarOptions" class="max-h-full h-full" />
|
||||
</div>
|
||||
</template>
|
||||
</MainTemplate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, markRaw, defineAsyncComponent } from "vue";
|
||||
import { mapActions, mapState } from "pinia";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
import MainTemplate from "@/templates/Main.vue";
|
||||
import FullCalendar from "@fullcalendar/vue3";
|
||||
import deLocale from "@fullcalendar/core/locales/de";
|
||||
import dayGridPlugin from "@fullcalendar/daygrid";
|
||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||
import interactionPlugin from "@fullcalendar/interaction";
|
||||
import { useCalendarStore } from "../../../../stores/admin/calendar";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useCalendarStore, ["formattedItems"]),
|
||||
calendarOptions() {
|
||||
return {
|
||||
timeZone: "local",
|
||||
locale: deLocale,
|
||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin],
|
||||
initialView: "dayGridMonth",
|
||||
headerToolbar: {
|
||||
left: "dayGridMonth,timeGridWeek",
|
||||
center: "title",
|
||||
right: "prev,today,next",
|
||||
},
|
||||
eventDisplay: "block",
|
||||
weekends: true,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectMirror: false,
|
||||
dayMaxEvents: true,
|
||||
weekNumbers: true,
|
||||
displayEventTime: true,
|
||||
nowIndicator: true,
|
||||
weekText: "KW",
|
||||
allDaySlot: false,
|
||||
events: this.formattedItems,
|
||||
select: this.select,
|
||||
eventClick: this.eventClick,
|
||||
};
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendars();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
...mapActions(useCalendarStore, ["fetchCalendars"]),
|
||||
select(e: any) {
|
||||
console.log(e);
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/CreateCalendarModal.vue"))),
|
||||
{
|
||||
start: e.startStr,
|
||||
end: e.endStr,
|
||||
allDay: e.allDay,
|
||||
}
|
||||
);
|
||||
},
|
||||
eventClick(e: any) {
|
||||
this.openModal(
|
||||
markRaw(defineAsyncComponent(() => import("@/components/admin/club/calendar/UpdateCalendarModal.vue"))),
|
||||
e.event.id
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
locale: deLocale,
|
||||
events: this.absencesList.map((x) => ({
|
||||
id: x.absenceId,
|
||||
start: x.startDate,
|
||||
end: x.endDate,
|
||||
allday: true,
|
||||
backgroundColor: this.getColorForAbsenceType(x.absenceType),
|
||||
borderColor: '#ffffff',
|
||||
title: this.getAbsenceType(x.absenceType) + ' ' + x.fullName,
|
||||
})),
|
||||
plugins: [
|
||||
interactionPlugin,
|
||||
dayGridPlugin,
|
||||
timeGridPlugin,
|
||||
listPlugin,
|
||||
multiMonthPlugin,
|
||||
],
|
||||
initialView: 'dayGridMonth',
|
||||
eventDisplay: 'block',
|
||||
weekends: false,
|
||||
editable: true,
|
||||
selectable: true,
|
||||
selectMirror: true,
|
||||
dayMaxEvents: true,
|
||||
weekNumbers: true,
|
||||
displayEventTime: false,
|
||||
weekText: 'KW',
|
||||
validRange: { start: '2023-01-01', end: '' },
|
||||
headerToolbar: {
|
||||
left: 'today prev,next',
|
||||
center: 'title',
|
||||
right: 'listMonth,dayGridMonth,multiMonthYear,customview',
|
||||
},
|
||||
views: {
|
||||
customview: {
|
||||
type: 'multiMonth',
|
||||
multiMonthMaxColumns: 1,
|
||||
duration: { month: 12 },
|
||||
buttonText: 'grid',
|
||||
},
|
||||
},
|
||||
dateClick: this.handleDateSelect.bind(this),
|
||||
datesSet: this.handleMonthChange.bind(this),
|
||||
select: this.handleDateSelect.bind(this),
|
||||
eventClick: this.handleEventClick.bind(this),
|
||||
eventsSet: this.handleEvents.bind(this),
|
||||
};
|
||||
|
||||
*/
|
||||
</script>
|
54
src/views/admin/settings/CalendarType.vue
Normal file
54
src/views/admin/settings/CalendarType.vue
Normal file
|
@ -0,0 +1,54 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Termintyp</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #diffMain>
|
||||
<div class="flex flex-col gap-4 grow pl-7">
|
||||
<div class="flex flex-col gap-2 grow overflow-y-scroll pr-7">
|
||||
<CalendarTypeListItem
|
||||
v-for="calendarType in calendarTypes"
|
||||
:key="calendarType.id"
|
||||
:calendarType="calendarType"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row gap-4">
|
||||
<button primary class="!w-fit" @click="openCreateModal">Termintyp 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 { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
import CalendarTypeListItem from "@/components/admin/settings/calendarType/CalendarTypeListItem.vue";
|
||||
import { useModalStore } from "@/stores/modal";
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default defineComponent({
|
||||
computed: {
|
||||
...mapState(useCalendarTypeStore, ["calendarTypes"]),
|
||||
},
|
||||
mounted() {
|
||||
this.fetchCalendarTypes();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCalendarTypeStore, ["fetchCalendarTypes"]),
|
||||
...mapActions(useModalStore, ["openModal"]),
|
||||
openCreateModal() {
|
||||
this.openModal(
|
||||
markRaw(
|
||||
defineAsyncComponent(() => import("@/components/admin/settings/calendarType/CreateCalendarTypeModal.vue"))
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
132
src/views/admin/settings/CalendarTypeEdit.vue
Normal file
132
src/views/admin/settings/CalendarTypeEdit.vue
Normal file
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<MainTemplate>
|
||||
<template #headerInsert>
|
||||
<RouterLink to="../" class="text-primary">zurück zur Liste (abbrechen)</RouterLink>
|
||||
</template>
|
||||
<template #topBar>
|
||||
<div class="flex flex-row items-center justify-between pt-5 pb-3 px-7">
|
||||
<h1 class="font-bold text-xl h-8">Termintyp {{ origin?.type }} - Daten bearbeiten</h1>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<Spinner v-if="loading == 'loading'" class="mx-auto" />
|
||||
<p v-else-if="loading == 'failed'">laden fehlgeschlagen</p>
|
||||
<form
|
||||
v-else-if="calendarType != null"
|
||||
class="flex flex-col gap-4 py-2 w-full max-w-xl mx-auto"
|
||||
@submit.prevent="triggerUpdate"
|
||||
>
|
||||
<div>
|
||||
<label for="type">Bezeichnung</label>
|
||||
<input type="text" id="type" required v-model="calendarType.type" />
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input type="color" id="color" required v-model="calendarType.color" class="!px-1 !py-0 !w-10" />
|
||||
<label for="color">Farbe</label>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<input type="checkbox" id="nscdr" v-model="calendarType.nscdr" />
|
||||
<label for="nscdr">Standard Kalender Auslieferung (optional)</label>
|
||||
</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 { useCalendarTypeStore } from "@/stores/admin/calendarType";
|
||||
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 { CalendarTypeViewModel, UpdateCalendarTypeViewModel } from "@/viewmodels/admin/calendarType.models";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
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 | CalendarTypeViewModel,
|
||||
calendarType: null as null | CalendarTypeViewModel,
|
||||
timeout: null as any,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.origin, this.calendarType);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchItem();
|
||||
},
|
||||
beforeUnmount() {
|
||||
try {
|
||||
clearTimeout(this.timeout);
|
||||
} catch (error) {}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useCalendarTypeStore, ["fetchCalendarTypeById", "updateActiveCalendarType"]),
|
||||
resetForm() {
|
||||
this.calendarType = cloneDeep(this.origin);
|
||||
},
|
||||
fetchItem() {
|
||||
this.fetchCalendarTypeById(parseInt(this.id ?? ""))
|
||||
.then((result) => {
|
||||
this.calendarType = result.data;
|
||||
this.origin = cloneDeep(result.data);
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loading = "failed";
|
||||
});
|
||||
},
|
||||
triggerUpdate(e: any) {
|
||||
if (this.calendarType == null) return;
|
||||
let formData = e.target.elements;
|
||||
let updateCalendarType: UpdateCalendarTypeViewModel = {
|
||||
id: this.calendarType.id,
|
||||
type: formData.type.value,
|
||||
color: formData.color.value,
|
||||
nscdr: formData.nscdr.checked,
|
||||
};
|
||||
this.status = "loading";
|
||||
this.updateActiveCalendarType(updateCalendarType)
|
||||
.then(() => {
|
||||
this.fetchItem();
|
||||
this.status = { status: "success" };
|
||||
})
|
||||
.catch((err) => {
|
||||
this.status = { status: "failed" };
|
||||
})
|
||||
.finally(() => {
|
||||
this.timeout = setTimeout(() => {
|
||||
this.status = null;
|
||||
}, 2000);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
|
@ -74,11 +74,6 @@ export default defineComponent({
|
|||
props: {
|
||||
id: String,
|
||||
},
|
||||
watch: {
|
||||
origin() {
|
||||
this.assigned = this.origin?.roles.map((r) => r.id) ?? [];
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: "loading" as "loading" | "fetched" | "failed",
|
||||
|
@ -97,7 +92,7 @@ export default defineComponent({
|
|||
return this.roles.filter((r) => !this.assigned.includes(r.id));
|
||||
},
|
||||
canSaveOrReset(): boolean {
|
||||
return isEqual(this.origin?.roles, this.assigned);
|
||||
return isEqual(this.origin?.roles.map((r) => r.id) ?? [], this.assigned);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
@ -118,8 +113,8 @@ export default defineComponent({
|
|||
fetchItem() {
|
||||
this.fetchUserById(parseInt(this.id ?? ""))
|
||||
.then((result) => {
|
||||
this.assigned = this.origin?.roles.map((r) => r.id) ?? [];
|
||||
this.origin = cloneDeep(result.data);
|
||||
this.assigned = this.origin?.roles.map((r) => r.id) ?? [];
|
||||
this.loading = "fetched";
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
Loading…
Reference in a new issue