mirror of
https://gitlab.dit.htwk-leipzig.de/htwk-software/htwkalender-pwa.git
synced 2025-07-16 09:38:51 +02:00
219 lines
6.0 KiB
Vue
219 lines
6.0 KiB
Vue
<!--
|
|
Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
|
|
Copyright (C) 2024 HTWKalender support@htwkalender.de
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
-->
|
|
|
|
<script setup lang="ts">
|
|
import FullCalendar from "@fullcalendar/vue3";
|
|
import { computed, ComputedRef, inject, Ref, ref, watch } from "vue";
|
|
import {
|
|
CalendarOptions,
|
|
DatesSetArg,
|
|
EventClickArg,
|
|
} from "@fullcalendar/core";
|
|
import allLocales from "@fullcalendar/core/locales-all";
|
|
import dayGridPlugin from "@fullcalendar/daygrid";
|
|
import interactionPlugin from "@fullcalendar/interaction";
|
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
|
import iCalenderPlugin from "@fullcalendar/icalendar";
|
|
import router from "@/router";
|
|
import { formatYearMonthDay } from "@/helpers/dates.ts";
|
|
import { useI18n } from "vue-i18n";
|
|
import { useQuery, useQueryClient } from "@tanstack/vue-query";
|
|
import tokenStore from "@/store/tokenStore.ts";
|
|
import { parseICalData } from "@/helpers/ical.ts";
|
|
import { fetchICalendarEvents } from "@/api/loadICal.ts";
|
|
|
|
const { t } = useI18n({ useScope: "global" });
|
|
|
|
const props = defineProps({
|
|
token: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
});
|
|
|
|
const op = ref();
|
|
const clickedEvent = ref();
|
|
|
|
const toggle = (info: EventClickArg) => {
|
|
const start = !info.event.start ? "" : info.event.start;
|
|
const end = !info.event.end ? "" : info.event.end;
|
|
|
|
if (op.value.visible) {
|
|
clickedEvent.value = null;
|
|
op.value.hide();
|
|
return;
|
|
} else {
|
|
clickedEvent.value = {
|
|
title: info.event._def.title,
|
|
start: start,
|
|
end: end,
|
|
notes: info.event._def.extendedProps.notes,
|
|
allDay: info.event._def.allDay,
|
|
location: info.event._def.extendedProps.location,
|
|
};
|
|
op.value.show(info.jsEvent);
|
|
op.value.target = info.el;
|
|
}
|
|
};
|
|
|
|
const selectedToken = computed(() => {
|
|
return props.token;
|
|
});
|
|
|
|
const mobilePage = inject("mobilePage") as Ref<boolean>;
|
|
const date: Ref<Date> = ref(new Date());
|
|
|
|
const { data: calendar } = useQuery({
|
|
queryKey: ["userCalendar", selectedToken],
|
|
queryFn: () => fetchICalendarEvents(selectedToken.value),
|
|
select: (data) => {
|
|
return data;
|
|
},
|
|
staleTime: 12 * 60 * 60 * 1000, // 12 hours
|
|
refetchOnWindowFocus: "always",
|
|
refetchOnReconnect: "always",
|
|
networkMode: "offlineFirst",
|
|
enabled: () => tokenStore().token !== "",
|
|
});
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
const invalidateAndRefetchCalendar = () => {
|
|
console.debug("invalidateAndRefetchCalendar", selectedToken);
|
|
const queryKey = ["userCalendar", selectedToken];
|
|
queryClient.invalidateQueries({ queryKey: queryKey }).then(() => {
|
|
queryClient.refetchQueries({ queryKey: queryKey });
|
|
});
|
|
};
|
|
|
|
defineExpose({
|
|
invalidateAndRefetchCalendar,
|
|
});
|
|
|
|
const events = computed(() => {
|
|
return parseICalData(calendar.value);
|
|
});
|
|
|
|
const fullCalendar = ref<InstanceType<typeof FullCalendar>>();
|
|
|
|
const calendarOptions: ComputedRef<CalendarOptions> = computed(() => ({
|
|
locales: allLocales,
|
|
locale: t("languageCode"),
|
|
plugins: [dayGridPlugin, interactionPlugin, timeGridPlugin, iCalenderPlugin],
|
|
// local debugging of mobilePage variable in object creation on ternary expression
|
|
initialView: mobilePage.value ? "Day" : "week",
|
|
initialDate: date.value,
|
|
dayHeaderFormat: { weekday: "short", omitCommas: true },
|
|
slotDuration: "00:15:00",
|
|
eventTimeFormat: {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
hour12: false,
|
|
},
|
|
eventClick(info) {
|
|
toggle(info);
|
|
},
|
|
height: "auto",
|
|
views: {
|
|
week: {
|
|
type: "timeGrid",
|
|
slotLabelFormat: {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
omitZeroMinute: false,
|
|
meridiem: false,
|
|
hour12: false,
|
|
},
|
|
dateAlignment: "week",
|
|
titleFormat: { month: "short", day: "numeric" },
|
|
slotMinTime: "06:00:00",
|
|
slotMaxTime: "22:00:00",
|
|
duration: { days: 7 },
|
|
firstDay: 1,
|
|
allDaySlot: false,
|
|
hiddenDays: [0],
|
|
},
|
|
Day: {
|
|
type: "timeGrid",
|
|
slotLabelFormat: {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
omitZeroMinute: false,
|
|
meridiem: false,
|
|
hour12: false,
|
|
},
|
|
titleFormat: { month: "short", day: "numeric" },
|
|
slotMinTime: "06:00:00",
|
|
slotMaxTime: "22:00:00",
|
|
duration: { days: 1 },
|
|
allDaySlot: false,
|
|
hiddenDays: [0],
|
|
},
|
|
},
|
|
headerToolbar: {
|
|
end: "prev,next today",
|
|
center: "title",
|
|
start: "week,Day",
|
|
},
|
|
|
|
datesSet: function (dateInfo: DatesSetArg) {
|
|
const view = dateInfo.view;
|
|
const offset = new Date().getTimezoneOffset();
|
|
const endDate = new Date(view.activeEnd.getTime() - offset * 60 * 1000);
|
|
router.replace({
|
|
query: {
|
|
...router.currentRoute.value.query,
|
|
date: formatYearMonthDay(endDate),
|
|
},
|
|
});
|
|
},
|
|
events: events.value,
|
|
}));
|
|
|
|
watch(mobilePage, () => {
|
|
fullCalendar.value?.getApi().changeView(mobilePage.value ? "Day" : "week");
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<FullCalendar
|
|
id="overlay-mount-point"
|
|
ref="fullCalendar"
|
|
:options="calendarOptions"
|
|
>
|
|
</FullCalendar>
|
|
|
|
<OverlayPanel ref="op">
|
|
<div>
|
|
<h3>{{ clickedEvent.title }}</h3>
|
|
<p>Location: {{ clickedEvent.location }}</p>
|
|
<p>Start: {{ clickedEvent.start?.toLocaleString() }}</p>
|
|
<p>End: {{ clickedEvent.end?.toLocaleString() }}</p>
|
|
<p>Notes: {{ clickedEvent.notes }}</p>
|
|
</div>
|
|
</OverlayPanel>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.fc-toolbar.fc-header-toolbar) {
|
|
flex-wrap: wrap;
|
|
justify-content: space-between;
|
|
gap: 0.5rem;
|
|
}
|
|
</style>
|