Files
FHC-Core/public/js/components/Calendar/Tempus.js
T

510 lines
13 KiB
JavaScript

import FhcCalendar from './Base.js';
import { useEventLoader } from '../../composables/Tempus/TempusEventLoader.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeTable from './Mode/Table.js';
import ModeMultipleWeeks from './Mode/MultipleWeeks.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
import ApiStudiensemester from '../../api/factory/studiensemester.js';
function getRangeLength(range) {
if (!(range instanceof luxon.Interval) || !range.isValid)
return null;
const start = range.start.startOf('day');
const end = range.end.startOf('day');
if (end < start) return null;
return Math.floor(end.diff(start, 'days').days) + 1;
}
export default {
name: 'CalendarTempus',
components: {
FhcCalendar,
},
provide() {
return {
rangeLength: Vue.computed(() => this.currentRangeLength),
rangeViewPresets: Vue.computed(() => this.semesterRangePresets),
rangeViewPreviewLink: Vue.computed(() => this.rangeViewPreviewLink),
// rangeViewSelectedPreset: Vue.computed({
// get: () => this.selectedRangePreset,
// set: value => this.selectedRangePreset = value,
// }),
};
},
inject: {
renderers: {from: 'renderers'},
currentSemester: {
from: 'currentSemester',
default: null,
},
canToggleGrid: {
from: 'canToggleGrid',
default: false
},
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
},
rangeLength: {
default: 30,
},
shouldIncludeRangeMode: {
type: Boolean,
default: false,
},
},
directives: {
draggable,
},
props: {
timezone: {
type: String,
required: true,
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local(),
},
range: {
type: luxon.Interval,
default: null,
},
mode: {
type: String,
default: 'Week',
},
modes: {
type: Array,
default: () => ['week', 'month', 'tableList', 'multipleWeeks'],
},
getPromiseFunc: {
type: Function,
required: true,
},
cacheMultiplier: {
type: Number,
default: 1
},
waitForAllPromises: {
type: Boolean,
default: true,
},
parkedEvents: {
type: Object,
default: () => new Set(),
},
visibleLecturers: {
type: Array,
default: null,
},
extraBackgrounds: {
type: Array,
default: () => [],
},
visibleStatus: {
type: Array,
default: () => ['all'],
},
showEvents: {
type: Boolean,
default: true
},
isEventDraggingEnabled: {
type: Boolean,
default: true,
},
isEventResizingEnabled: {
type: Boolean,
default: true,
},
isRangeVirtualScrollEnabled: {
type: Boolean,
default: true,
},
canToggleCollisionCheck: {
type: Boolean,
default: true,
},
rangeViewPreviewLink: {
type: String,
default: null,
}
},
emits: [
"update:date",
"update:mode",
"update:range",
"update:date-range",
"drop",
"resize",
"event-hover",
"event-unhover",
"open-reservierung",
"events-reloaded",
],
data() {
return {
visibleDates: null,
eventReloadKey: 0,
refreshEventsAfterReload: false,
calendarModes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
tableList: Vue.markRaw(ModeTable),
multipleWeeks: Vue.markRaw(ModeMultipleWeeks),
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
},
week: {
collapseEmptyDays: false,
},
},
currentMode: this.mode,
teachingunits: null,
hoursplan: null,
showRaster: true,
currentRangeLength: getRangeLength(this.range) ?? this.rangeLength,
selectedRangePreset: null, //this.currentSemester, TODO: This is a hack to make the range mode work, but it should be fixed in the future
semesterRangePresets: {
label: null,
presets: [],
},
};
},
watch: {
range(range) {
const rangeLength = getRangeLength(range);
this.currentRangeLength = rangeLength ?? this.rangeLength;
},
events() {
if (this.refreshEventsAfterReload) {
this.eventReloadKey += 1;
this.refreshEventsAfterReload = false;
}
this.$emit('events-reloaded');
}
},
computed: {
availableModes() {
return this.modes.reduce((availableModes, mode) => {
if (this.calendarModes[mode])
availableModes[mode] = this.calendarModes[mode];
return availableModes;
}, {});
},
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}];
}
return [...past, ...(this.extraBackgrounds || [])];
},
visibleEvents() {
let list = this.events;
if (
this.isRangeVirtualScrollEnabled
&& this.currentMode === 'multipleWeeks'
&& Array.isArray(this.visibleDates)
)
{
const visibleIntervals = this.visibleDates
.map(date => luxon.Interval.fromDateTimes(
luxon.DateTime.fromISO(date.start).setZone(this.timezone).minus({ days: 1 }),
luxon.DateTime.fromISO(date.end).setZone(this.timezone).plus({ days: 1 })
))
.filter(interval => interval.isValid);
list = list.filter(event => {
const eventStart = luxon.DateTime.fromISO(event.isostart).setZone(this.timezone);
const eventEnd = luxon.DateTime.fromISO(event.isoend).setZone(this.timezone);
if (!eventStart.isValid || !eventEnd.isValid)
return true;
return visibleIntervals.some(interval =>
eventStart < interval.end && eventEnd > interval.start
);
});
}
if (Array.isArray(this.visibleLecturers))
{
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter((event) => {
if (!event.lektor?.length) return true;
return event.lektor.some((lektor) =>
visibleLectures.has(lektor.mitarbeiter_uid),
);
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter((event) =>
this.visibleStatus.includes(event.status_kurzbz),
);
},
},
methods: {
eventStyle(event) {
if (!event.farbe) return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
if (this.currentMode === 'tableList')
return;
this.rangeInterval = rangeInterval;
if (
this.visibleDates === null
&& rangeInterval instanceof luxon.Interval
&& rangeInterval.isValid
)
{
const start = rangeInterval.start.startOf('day');
this.visibleDates = [{
start: start.toISO(),
end: start.plus({ days: 7 }).toISO()
}];
}
this.$emit('update:range', rangeInterval);
},
updateVisibleDates(visibleDates) {
this.visibleDates = visibleDates;
},
handleDateRange({ start, end }) {
this.rangeInterval = luxon.Interval.fromDateTimes(start.startOf('day'), end.endOf('day'));
this.reset(false, true);
this.$emit('update:range', this.rangeInterval);
this.$emit('update:date-range', { start, end });
},
handleDateUpdate(newDate, newMode, newRangeLength) {
if (Number.isFinite(newRangeLength))
this.currentRangeLength = newRangeLength;
this.$emit('update:date', newDate, newMode);
},
ondrop(payload){
this.$emit('drop', payload);
},
onresize(payload) {
this.$emit('resize', payload);
},
resetEventLoader(arePreviousEventsCleared = true, isLoaderEventVisualReset = true) {
this.reset(arePreviousEventsCleared, isLoaderEventVisualReset);
if (arguments.length === 0) {
this.$nextTick(() => {
const view = this.$refs.calendar?.$refs.mode?.$refs.view;
view?.resetColumnWidthsForEventReload?.();
view?.resetGridHeightsForEventReload?.();
});
}
},
reloadEvents() {
this.refreshEventsAfterReload = true;
this.resetEventLoader(false);
},
navigatePrev() {
this.$refs.calendar.clickPrev();
},
navigateNext() {
this.$refs.calendar.clickNext();
},
async fetchSemesters() {
const semestersResponse = await this.$api.call(ApiStudiensemester.getAll());
if (semestersResponse.meta.status === "success") {
this.semesterRangePresets = {
label: "View specific semester",
presets: semestersResponse.data.map((semester) => {
let startDate = luxon.DateTime.fromISO(semester.start);
let endDate = luxon.DateTime.fromISO(semester.ende);
return {
startDate,
endDate,
name: semester.studiensemester_kurzbz,
description: semester.bezeichnung,
};
})
};
}
},
clearOutCalendarEventEmphasis() {
this.$refs.calendar.$el
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
() => props.cacheMultiplier,
undefined,
props.waitForAllPromises,
);
Vue.watch(lv, (newValue) => {
context.emit('update:lv', newValue);
});
return {
rangeInterval,
events,
lv,
reset,
};
},
created() {
this.$api.call(ApiKalender.getStunden()).then((res) => {
return (this.teachingunits = res.data.map((el) => ({
id: el.stunde,
start: el.beginn,
end: el.ende,
})));
});
this.$api.call(ApiKalender.getCalendarHours()).then((res) => {
this.hoursplan = {
start: res.data.start,
end: res.data.end,
};
});
this.fetchSemesters();
},
template: /* html */ `
<fhc-calendar
ref="calendar"
class="fhc-calendar-tempus"
:date="date"
:modes="availableModes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="visibleEvents || []"
:event-reload-key="eventReloadKey"
:backgrounds="backgrounds"
:time-grid="showRaster ? teachingunits : null"
:hours-plan="hoursplan"
show-btns
:draggable-events="isEventDraggingEnabled"
:resizable-events="isEventResizingEnabled"
:on-drop="isEventDraggingEnabled && ['week', 'multipleWeeks'].includes(currentMode) ? ondrop : null"
:on-resize="isEventResizingEnabled ? onresize : null"
@update:date="handleDateUpdate"
@update:mode="(newMode, newDate) => { currentMode = newMode; $emit('update:mode', newMode, newDate) }"
@update:range="updateRange"
@update:visible-dates="updateVisibleDates"
@update:date-range="handleDateRange"
>
<template v-slot="{ event, mode }">
<div
:class="['event-type-' + event.type + ' ' + mode + 'PageContainer', { 'event--parked': parkedEvents.has(String(event.kalender_id)) }, {'event--opacity': !showEvents}]"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
@mouseenter="$emit('event-hover', event)"
@mouseleave="$emit('event-unhover', event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<div class="d-flex align-items-center gap-2">
<div
class="d-flex align-items-center gap-2"
style="cursor:pointer"
@click="showRaster = !showRaster"
v-if="canToggleGrid"
>
<i :class="showRaster ? 'fa-solid fa-toggle-on text-primary' : 'fa-solid fa-toggle-off text-muted'"></i>
<span class="form-check-label">Stundenraster</span>
</div>
<div
v-if="isEventDraggingEnabled"
class="d-flex align-items-center gap-2 "
v-draggable:move.noimage="{ type: 'reservierung', id: null, orig: {} }"
data-cy="reservationDragHandle"
>
<i
class="fa-solid fa-calendar-plus text-primary"
style="cursor:pointer"
@click.stop="$emit('open-reservierung')"
></i>
<span>Reservierung</span>
</div>
<div
v-if="canToggleCollisionCheck"
class="d-flex align-items-center gap-2"
>
<i :class="appConfig.ignore_kollision ? 'fa-solid fa-triangle-exclamation text-danger' : 'fa-solid fa-circle-check text-success'"></i>
<span class="form-check-label">
{{ appConfig.ignore_kollision ? 'Kollisionscheck aus' : 'Kollisionscheck an' }}
</span>
</div>
</div>
</template>
</fhc-calendar>`
}