<template> <div class="relative w-full md:max-w-md"> <TrashIcon v-if="can('delete', 'club', 'calendar')" 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 as HTMLInputElement).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 as HTMLInputElement).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 == 'loading' || status?.status == 'success'"> 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/club/calendar"; import type { CalendarViewModel, CreateCalendarViewModel, UpdateCalendarViewModel, } from "@/viewmodels/admin/club/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/settings/calendarType"; import type { CalendarTypeViewModel } from "@/viewmodels/admin/settings/calendarType.models"; import cloneDeep from "lodash.clonedeep"; import isEqual from "lodash.isequal"; import { useAbilityStore } from "@/stores/ability"; </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"]), ...mapState(useAbilityStore, ["can"]), 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.status = "loading"; 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>