WIP: Special view for page printing

This commit is contained in:
Ivymaster
2026-09-15 14:23:15 +02:00
parent 3ec56f492f
commit 8bac1c1ebb
9 changed files with 865 additions and 80 deletions
+5
View File
@@ -55,6 +55,11 @@ class Tempus extends Auth_Controller
$this->_loadView(array('organisationen' => $organisationen, 'studiensemestern' => $studiensemestern));
}
public function preview()
{
$this->_loadView();
}
private function _loadView($extraVariables = [])
{
$this->load->view('Tempus', [
+11 -24
View File
@@ -16,14 +16,14 @@ class CollisionChecker
$this->_ci->load->library('collision/checks/RoomCollisionCheck');
$this->_ci->load->library('collision/checks/LectureCollisionCheck');
$this->_ci->load->library('collision/checks/ResourcesCollisionCheck');
$this->_ci->load->library('collision/checks/VerbandCollisionCheck');
$this->_ci->load->library('collision/checks/StudentCollisionCheck');
$this->_ci->load->library('collision/checks/ResourcesCollisionCheck');
$this->register($this->_ci->roomcollisioncheck);
$this->register($this->_ci->lecturecollisioncheck);
$this->register($this->_ci->resourcescollisioncheck);
$this->register($this->_ci->verbandcollisioncheck);
$this->register($this->_ci->studentcollisioncheck);
$this->register($this->_ci->resourcescollisioncheck);
Events::trigger('collision_register', $this);
}
@@ -50,41 +50,28 @@ class CollisionChecker
}
public function runAll($kalender_ids)
{
$results = array_fill_keys($kalender_ids, []);
foreach ($this->_checks as $check)
{
$batchResult = $check->checkAll($kalender_ids);
foreach ($batchResult as $kalender_id => $errors)
{
$results[$kalender_id] = array_merge($results[$kalender_id], $errors);
}
}
return $results;
}
public function runAny($kalender_ids)
{
$remaining_ids = array_values(array_unique($kalender_ids));
$results = array_fill_keys($remaining_ids, []);
$collision_ids = [];
foreach ($this->_checks as $check)
{
if (empty($remaining_ids)) break;
$batch_result = $check->checkAll($remaining_ids);
foreach ($batch_result as $kalender_id => $errors)
log_message('error', 'CollisionChecker: Running check ' . $check->getName() . ' for ' . count($remaining_ids) . ' remaining kalender_ids');
$batchResult = $check->checkAll($remaining_ids);
foreach ($batchResult as $kalender_id => $errors)
{
if (!empty($errors))
$collision_ids[$kalender_id] = true;
if (empty($errors)) continue;
$results[$kalender_id] = array_merge($results[$kalender_id], $errors);
$collision_ids[$kalender_id] = true;
}
if (!empty($collision_ids))
$remaining_ids = array_values(array_diff($remaining_ids, array_keys($collision_ids)));
}
return $collision_ids;
return $results;
}
}
+1 -1
View File
@@ -845,7 +845,7 @@ class KalenderLib
foreach ($kalender_id_batches as $kalender_ids)
{
$collision_ids = $this->_ci->collisionchecker->runAny($kalender_ids);
$collision_ids = $this->_ci->collisionchecker->runAll($kalender_ids);
foreach ($collision_ids as $kalender_id => $has_collision)
{
+2
View File
@@ -16,6 +16,7 @@
*/
import FhcTempus from "../components/Tempus/Tempus.js";
import FhcTempusCalendarPreview from "../components/Tempus/TempusCalendarPreview.js";
import FhcTempusSync from "../components/Tempus/TempusSync.js";
import Phrasen from "../plugins/Phrasen.js";
@@ -28,6 +29,7 @@ const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(),
routes: [
{ path: `/${ciPath}/Tempus`, component: FhcTempus },
{ path: `/${ciPath}/Tempus/preview`, component: FhcTempusCalendarPreview },
{ path: `/${ciPath}/Tempus/sync`, component: FhcTempusSync },
]
});
+17 -2
View File
@@ -16,6 +16,9 @@ export default {
rangeViewPresets: {
default: {},
},
rangeViewPreviewLink: {
default: null,
},
},
props: {
currentDate: {
@@ -133,12 +136,13 @@ export default {
@cal-click-default.capture="handleClickDefaults"
>
<div
v-if="rangeViewPresets?.presets?.length"
v-if="rangeViewPresets?.presets?.length || rangeViewPreviewLink"
id="rangePresetSelector"
class="w-100 d-flex flex-row gap-2 justify-content-center align-items-center py-2"
>
<span>{{ rangeViewPresets.label }}</span>
<span v-if="rangeViewPresets?.presets?.length">{{ rangeViewPresets.label }}</span>
<form-input
v-if="rangeViewPresets?.presets?.length"
name="rangePresetSelector"
type="select"
v-model="selectedRangePreset"
@@ -151,6 +155,17 @@ export default {
{{ preset.name }}
</option>
</form-input>
<a
v-if="rangeViewPreviewLink"
:href="rangeViewPreviewLink"
target="_blank"
rel="noopener"
class="btn btn-outline-secondary"
aria-label="Vorschau öffnen"
title="Vorschau öffnen"
>
<i class="fa-solid fa-eye"></i>
</a>
</div>
<div class="flex-grow-1">
<base-slider ref="slider" v-slot="slot">
+61 -8
View File
@@ -10,6 +10,17 @@ 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: {
@@ -19,6 +30,7 @@ export default {
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,
@@ -61,10 +73,18 @@ export default {
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', 'range'],
},
getPromiseFunc: {
type: Function,
required: true,
@@ -92,6 +112,22 @@ export default {
showEvents: {
type: Boolean,
default: true
},
isEventDraggingEnabled: {
type: Boolean,
default: true,
},
isEventResizingEnabled: {
type: Boolean,
default: true,
},
isRangeVirtualScrollEnabled: {
type: Boolean,
default: true,
},
rangeViewPreviewLink: {
type: String,
default: null,
}
},
emits: [
@@ -111,7 +147,7 @@ export default {
visibleDates: null,
eventReloadKey: 0,
refreshEventsAfterReload: false,
modes: {
calendarModes: {
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth),
tableList: Vue.markRaw(ModeTable),
@@ -130,7 +166,7 @@ export default {
teachingunits: null,
hoursplan: null,
showRaster: true,
currentRangeLength: this.rangeLength,
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,
@@ -139,6 +175,10 @@ export default {
};
},
watch: {
range(range) {
const rangeLength = getRangeLength(range);
this.currentRangeLength = rangeLength ?? this.rangeLength;
},
events() {
if (this.refreshEventsAfterReload) {
this.eventReloadKey += 1;
@@ -149,6 +189,14 @@ export default {
}
},
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);
@@ -180,7 +228,11 @@ export default {
// that is visible inside the slider viewport, including a one-day buffer
// before and after the visible dates.
console.log('visibleDates', this.visibleDates);
if (this.currentMode === 'range' && Array.isArray(this.visibleDates))
if (
this.isRangeVirtualScrollEnabled
&& this.currentMode === 'range'
&& Array.isArray(this.visibleDates)
)
{
const visibleIntervals = this.visibleDates
.map(date => luxon.Interval.fromDateTimes(
@@ -360,7 +412,7 @@ export default {
ref="calendar"
class="fhc-calendar-lvplan"
:date="date"
:modes="modes"
:modes="availableModes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
@@ -371,10 +423,10 @@ export default {
:time-grid="showRaster ? teachingunits : null"
:hours-plan="hoursplan"
show-btns
:draggable-events="true"
:resizable-events="true"
:on-drop="['week', 'range'].includes(currentMode) ? ondrop : null"
:on-resize="onresize"
:draggable-events="isEventDraggingEnabled"
:resizable-events="isEventResizingEnabled"
:on-drop="isEventDraggingEnabled && ['week', 'range'].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"
@@ -418,6 +470,7 @@ export default {
<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"
+13
View File
@@ -153,6 +153,18 @@ export default {
.filter((lecture) => lecture.showEvents)
.map((lecture) => lecture.uid);
},
rangeViewPreviewLink() {
const previewUrl = `${this.tempusRoot.replace(/\/$/, '')}/preview`;
if (!this.lastRange?.start || !this.lastRange?.end)
return previewUrl;
const params = new URLSearchParams({
startDate: this.lastRange.start.toISODate(),
endDate: this.lastRange.end.toISODate(),
});
return `${previewUrl}?${params}`;
},
keyboardShortcuts() {
return getTempusShortcuts(this);
},
@@ -1221,6 +1233,7 @@ export default {
:visible-status="visibleStatus"
:date="calendarDate"
:mode="currentMode"
:range-view-preview-link="rangeViewPreviewLink"
:parkedEvents="parkedKeys"
:visible-lecturers="visibleLecturerUids"
:show-events="showEvents"
@@ -0,0 +1,663 @@
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import FhcCalendar from '../Calendar/Tempus.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import ApiRenderers from '../../api/factory/renderers.js';
import ApiTempusConfig from '../../api/factory/tempus/config.js';
function getRangeFromQuery(query, timezone) {
const startDate = Array.isArray(query?.startDate)
? query.startDate[0]
: query?.startDate;
const endDate = Array.isArray(query?.endDate)
? query.endDate[0]
: query?.endDate;
const start = luxon.DateTime.fromISO(startDate ?? '', {
zone: timezone,
}).startOf('day');
const end = luxon.DateTime.fromISO(endDate ?? '', {
zone: timezone,
}).endOf('day');
if (!start.isValid || !end.isValid || end < start)
return null;
return luxon.Interval.fromDateTimes(start, end);
}
export default {
name: 'TempusCalendarPreview',
components: {
FhcCalendar,
},
props: {
defaultSemester: String,
config: Object,
permissions: Object,
tempusRoot: String,
cisRoot: String,
activeAddons: String, // semicolon separated list of active addons
viewData: Object,
logoutUrl: String,
avatarUrl: String,
},
provide() {
return {
cisRoot: this.cisRoot,
defaultSemester: this.defaultSemester,
currentSemester: this.defaultSemester,
renderers: Vue.computed(() => this.renderers),
appConfig: Vue.computed(() => this.appconfig),
canToggleGrid: this.permissions.stundenraster,
};
},
data() {
const today = luxon.DateTime.now().setZone(this.config.timezone).toISODate();
return {
appconfig: {},
currentMode: 'range',
configEndpoints: ApiTempusConfig,
hoveredEvent: null,
renderers: {},
ort_kurzbz: null,
parkedKeys: new Set(),
lecturers: [],
studiengaenge: [],
rooms: [],
overlayCache: {},
extraBackgrounds: [],
lastRange: null,
visibleStatus: ['all'],
selectedStudiensemester: this.defaultSemester,
urlRange: null,
calendarDatesByMode: {
week: today,
month: today,
tableList: today,
range: today,
},
historyEntries: [],
previewRole: 'planer',
multiWeekModal: {
show: false,
lehreinheitId: null,
ortKurzbz: null,
startTime: null,
endTime: null,
},
studiengaenge_all: [],
maxDailyEventLimitForMonthView: 10,
raumvorschlagPreview: null,
raumvorschlagLoading: false,
showEvents: true,
reservierungPending: false,
bcc: null,
currentlyUpdatedEvent: null,
};
},
computed: {
calendarDate() {
return (
this.calendarDatesByMode[this.currentMode] ??
this.calendarDatesByMode.week
);
},
visibleLecturerUids() {
if (!this.lecturers.length) return null;
return this.lecturers
.filter((lecture) => lecture.showEvents)
.map((lecture) => lecture.uid);
},
},
methods: {
handleChangeDate(newDate) {
if (!(newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid))
return;
this.calendarDatesByMode = {
...this.calendarDatesByMode,
[this.currentMode]: newDate.toISODate(),
};
},
handleChangeMode(newMode, newDate) {
if (!newMode) return;
if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid) {
this.calendarDatesByMode = {
...this.calendarDatesByMode,
[this.currentMode]: newDate.toISODate(),
};
}
this.currentMode = newMode;
},
getPromiseFunc(start, end) {
const collisionCheck = false;
const hasRooms = this.rooms.length > 0;
const hasLektoren = this.lecturers.length > 0;
const hasStg = this.studiengaenge.length > 0;
const filter = {};
if (hasRooms) filter.ort = this.rooms.map((room) => room.ort_kurzbz);
if (hasStg) {
filter.stg = this.studiengaenge.map(
({ stg_kz, semester, orgform_kurzbz }) => ({
stg_kz,
semester,
orgform_kurzbz,
}),
);
}
if (hasLektoren)
filter.uid = this.lecturers.map((lecture) => lecture.uid);
let response = null;
if (this.previewRole === 'lektor')
response = [
this.$api.call(
ApiKalender.getPlanLecturer(
start.toISODate(),
end.toISODate(),
collisionCheck,
),
),
];
if (this.previewRole === 'student')
response = [
this.$api.call(
ApiKalender.getPlanStudent(
start.toISODate(),
end.toISODate(),
collisionCheck,
),
),
];
response = [
this.$api.call(
ApiKalender.getPlan(
filter,
start.toISODate(),
end.toISODate(),
collisionCheck,
),
),
];
if (response) {
response[0].then((result) => {
this.scrollToAndEmphasizeUpdatedEvent();
});
}
return response;
},
_parseDates(start, end) {
const startDT = luxon.DateTime.fromISO(start);
const endDT = luxon.DateTime.fromISO(end);
if (!startDT.isValid || !endDT.isValid) {
alert('Ungültiges Datum');
return null;
}
return {
startDT,
endDT,
start_time: startDT.toFormat('yyyy-MM-dd HH:mm'),
end_time: endDT.toFormat('yyyy-MM-dd HH:mm'),
};
},
_updateKalenderEvent(obj, startDT, endDT, start_time, end_time, onSuccess) {
const origStart = luxon.DateTime.fromISO(obj.orig.isostart);
const origEnd = luxon.DateTime.fromISO(obj.orig.isoend);
if (
origStart.toMillis() === startDT.toMillis() &&
origEnd.toMillis() === endDT.toMillis()
)
return;
const updatedInfos = {
ort_kurzbz: this.rooms.length
? this.rooms.map((room) => room.ort_kurzbz)
: (obj.orig.ort_kurzbz ?? []),
start_time,
end_time,
};
return this.$api
.call(
ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos),
)
.then(() => {
if (onSuccess) {
onSuccess();
this.$refs.calendar.$refs.calendar.$refs.mode.$refs.view.$refs.grid.disableAutoScroll();
this.currentlyUpdatedEvent = obj.orig;
}
})
.catch((error) => {
this.currentlyUpdatedEvent = null;
this.$refs.calendar.clearOutCalendarEventEmphasis();
this.$nextTick(() => {
this.$refs.calendar.reloadEvents();
});
throw error;
});
},
handleRange(range) {
if (!range?.start || !range?.end) return;
if (this.currentMode === 'week') {
//Workaround because, updateRange is emitting 2 times
const startDay = range.start.startOf('day');
const endDay = range.end.startOf('day');
const days = Math.round(endDay.diff(startDay, 'days').days) + 1;
if (days > 8) return;
}
this.lastRange = range;
const key = `${range.start.toISODate()}_${range.end.toISODate()}_${this.currentMode}`;
for (const lect of this.lecturers) {
this.getOverlays(lect.uid, range, key);
}
this.rebuildExtraBackgrounds();
if (this.raumvorschlagPreview)
this.previewRaumvorschlag({
lehreinheit_id: this.raumvorschlagPreview.lehreinheit_id,
});
},
getOverlays(uid, range, rangeKey) {
if (!this.overlayCache[uid]) this.overlayCache[uid] = {};
let entry = this.overlayCache[uid][rangeKey];
if (entry?.loaded || entry?.loading) return;
entry = this.overlayCache[uid][rangeKey] = {
blocks: [],
wishes: [],
loading: true,
loaded: false,
};
const promises = [];
const lect = this.lecturers.find((lecture) => lecture.uid === uid);
if (lect.overlays.wishes) {
promises.push(
this.$api
.call(
ApiKalender.getLektorZeitwuensche(
uid,
range.start.toISODate(),
range.end.toISODate(),
),
)
.then((result) => {
entry.wishes = (result.data || []).map((zeitwunsch) => ({
class: `bg-lecturer-wish bg-uid-${uid} wish-w-${zeitwunsch.gewicht}`,
start: zeitwunsch.isostart,
end: zeitwunsch.isoend,
label: zeitwunsch.label,
}));
}),
);
}
if (lect.overlays.blocks) {
promises.push(
this.$api
.call(
ApiKalender.getLektorZeitsperren(
uid,
range.start.toISODate(),
range.end.toISODate(),
),
)
.then((result) => {
entry.blocks = (result.data || []).map((zeitsperre) => ({
class: `bg-lecturer-block bg-uid-${uid}`,
start: zeitsperre.isostart,
end: zeitsperre.isoend,
label: zeitsperre.label,
}));
}),
);
}
Promise.allSettled(promises).then(() => {
entry.loading = false;
entry.loaded = true;
this.rebuildExtraBackgrounds();
});
},
rebuildExtraBackgrounds() {
if (!this.lastRange) return;
const key =
`${this.lastRange.start.toISODate()}_` +
`${this.lastRange.end.toISODate()}_` +
`${this.currentMode}`;
let res = [];
for (let lect of this.lecturers) {
const entry = this.overlayCache[lect.uid]?.[key];
if (!entry) continue;
if (lect.overlays.blocks) res.push(...(entry.blocks || []));
if (lect.overlays.wishes) res.push(...(entry.wishes || []));
}
if (this.raumvorschlagPreview)
res.push(...this.raumvorschlagPreview.slots);
this.extraBackgrounds = res;
},
openReservierung() {
this.reservierungPending = false;
this.$refs.reservierung?.show();
},
onEventHover(event) {
this.hoveredEvent = event;
},
onEventUnhover(event) {
if (this.hoveredEvent?.kalender_id === event?.kalender_id) {
this.hoveredEvent = null;
}
},
scrollToAndEmphasizeUpdatedEvent() {
if (!this.currentlyUpdatedEvent) return;
document
.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',
);
});
setTimeout(() => {
const eventEl = document.querySelector(
`[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_kalender_gruppen_id}"]`,
);
if (!eventEl) return;
const calendar = document.querySelector('.fhc-calendar-base-grid');
const eventRect = eventEl.getBoundingClientRect();
const offset = 300;
const isInsideScrolledView =
eventEl.offsetLeft < calendar.scrollLeft + calendar.clientWidth &&
eventEl.offsetLeft + eventEl.offsetWidth > calendar.scrollLeft &&
eventEl.offsetTop < calendar.scrollTop + calendar.clientHeight &&
eventEl.offsetTop + eventEl.offsetHeight > calendar.scrollTop;
const rect = eventEl.getBoundingClientRect();
if (!isInsideScrolledView) {
eventEl.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}
let timeout = 0;
let emphasizeUpdateClassName = isInsideScrolledView
? 'updated-event'
: 'updated-event-long';
if (!isInsideScrolledView) timeout = 300;
setTimeout(() => {
eventEl.classList.add(emphasizeUpdateClassName);
}, timeout);
this.currentlyUpdatedEvent = null;
}, 100);
},
updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) {
if (!calendarGruppenId)
return;
let startOfDay = startDT.startOf('day');
let newPotentialStart = startDT.diff(startOfDay).toMillis() ?? 1;
let newPotentialEnd = endDT.diff(startOfDay).toMillis();
const calendar = this.$refs.calendar?.$el;
let element = calendar?.querySelector(
`[data-group-id="event-group-${calendarGruppenId}"]`,
);
if (!element) return;
const targetGridLine = [...calendar.querySelectorAll(
'.fhc-calendar-base-grid-line',
)].find((gridLine) => {
const [rowStart, columnStart, rowEnd] = getComputedStyle(
gridLine,
).gridArea.split(' / ');
return (
rowStart === '1' &&
columnStart === String(startDT.weekday) &&
rowEnd === '-1'
);
});
const changedDay =
targetGridLine && element.parentElement !== targetGridLine;
setTimeout(() => {
if (!calendar.contains(element)) return;
if (changedDay) {
targetGridLine.insertBefore(element, null);
element.classList.add('tempus-temporary-calendar-event');
}
element.style.gridRowEnd = 't_' + newPotentialEnd;
element.style.gridRowStart = 't_' + newPotentialStart;
}, 100);
const outerDiv = document.createElement('div');
outerDiv.className = 'spinner-overlay';
const innerDiv = document.createElement('div');
innerDiv.className = 'spinner';
outerDiv.appendChild(innerDiv);
element.appendChild(outerDiv);
const eventRect = element.getBoundingClientRect();
const offset = 300;
const isInsideScrolledView =
element.offsetLeft < calendar.scrollLeft + calendar.clientWidth &&
element.offsetLeft + element.offsetWidth > calendar.scrollLeft &&
element.offsetTop < calendar.scrollTop + calendar.clientHeight &&
element.offsetTop + element.offsetHeight > calendar.scrollTop;
const rect = element.getBoundingClientRect();
if (!isInsideScrolledView) {
element.scrollIntoView({
behavior: 'smooth',
inline: 'center',
block: 'nearest',
});
}
},
clearTemporaryEvents() {
const calendar = this.$refs.calendar?.$el;
if (!calendar) return;
const tempEvents = calendar.querySelectorAll(
'.tempus-temporary-calendar-event',
);
tempEvents.forEach((event) => {
event.remove();
});
}
},
watch: {
'$route.query': {
immediate: true,
handler(query) {
this.urlRange = getRangeFromQuery(query, this.config.timezone);
const rangeStart = this.urlRange?.start.toISODate()
?? luxon.DateTime.now().setZone(this.config.timezone).toISODate();
this.calendarDatesByMode = {
...this.calendarDatesByMode,
range: rangeStart,
};
},
},
lecturers: {
deep: true,
handler() {
this.rebuildExtraBackgrounds();
},
},
rooms() {
this.$refs.calendar.resetEventLoader();
},
},
mounted() {
this.reservierungPending = false;
this.bcc = new BroadcastChannel('fhc-dnd');
this.bcc.addEventListener('message', (e) => {
if (e.data === 'dropped' && !this.reservierungPending)
this.$refs.calendar.resetEventLoader();
});
},
beforeUnmount() {
this.bcc?.close();
},
async created() {
await this.$api
.call(ApiRenderers.loadTempusRenderers())
.then((res) => res.data)
.then((data) => {
console.log(Object.keys(data))
for (let rendertype of Object.keys(data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (data[rendertype].modalTitle)
modalTitle = Vue.markRaw(
Vue.defineAsyncComponent(
() => import(data[rendertype].modalTitle),
),
);
if (data[rendertype].modalContent)
modalContent = Vue.markRaw(
Vue.defineAsyncComponent(
() => import(data[rendertype].modalContent),
),
);
if (data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(
Vue.defineAsyncComponent(
() => import(data[rendertype].calendarEvent),
),
);
if (data[rendertype].calendarEventStyles) {
var head = document.head;
if (
!head.querySelector(
`link[href="${data[rendertype].calendarEventStyles}"]`,
)
) {
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {};
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
},
template: /* html */ `
<div
class="tempus"
data-cy="tempus-preview"
>
<fhc-calendar
ref="calendar"
:timezone="config.timezone"
:get-promise-func="getPromiseFunc"
:visible-status="visibleStatus"
:date="calendarDate"
:range="urlRange"
:mode="currentMode"
:modes="['range']"
:parkedEvents="parkedKeys"
:visible-lecturers="visibleLecturerUids"
:show-events="showEvents"
:is-event-dragging-enabled="false"
:is-event-resizing-enabled="false"
:isRangeVirtualScrollEnabled="false"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@event-hover="onEventHover"
@event-unhover="onEventUnhover"
@open-reservierung="openReservierung"
:extra-backgrounds="extraBackgrounds"
@update:range="handleRange"
@events-reloaded="clearTemporaryEvents"
class="responsive-calendar"
:cache-multiplier="currentMode === 'week' ? 1 : 0"
/>
</div>`,
};
@@ -1,7 +1,10 @@
const PLAN_REQUEST_INTERVAL_DAYS = 60;
export function useEventLoader(
rangeInterval,
getPromiseFunc,
cacheMultiplier = 1,
requestIntervalDays = PLAN_REQUEST_INTERVAL_DAYS,
) {
let hasFirstLoadOccurred = false;
@@ -24,6 +27,14 @@ export function useEventLoader(
return Math.round(currentlyDisplayedDateRange.length() * cacheSize) + 1;
};
const getRequestIntervalDays = () => {
const intervalDays = Number(Vue.toValue(requestIntervalDays));
return Number.isFinite(intervalDays) && intervalDays > 0
? Math.max(1, Math.floor(intervalDays))
: PLAN_REQUEST_INTERVAL_DAYS;
};
const reload = (isCacheEnabled = true) => {
if (
currentlyDisplayedDateRange &&
@@ -74,56 +85,66 @@ export function useEventLoader(
addVisualForEventsLoading();
Promise.allSettled(promises).then((results) => {
let newlyLoadedEvents = [];
let hasSuccessfullyLoadedEvents = false;
let newlyLoadedEvents = [];
let pendingPromiseCount = promises.length;
let hasSuccessfullyLoadedEvents = false;
results.forEach((res) => {
if (
!(res.status === "fulfilled" && res.value.meta.status === "success")
)
return;
promises.forEach((promise) => {
Promise.resolve(promise)
.then((res) => {
if (res.meta.status !== "success") return;
hasSuccessfullyLoadedEvents = true;
if (res.value.meta.lv) lv.value = res.value.meta.lv;
newlyLoadedEvents = res.value.data;
});
hasSuccessfullyLoadedEvents = true;
if (res.meta.lv) lv.value = res.meta.lv;
newlyLoadedEvents = newlyLoadedEvents.concat(res.data);
let tempAllEvents = Array.from(
new Map(
[...allEvents.value, ...newlyLoadedEvents].map((event) => [
event.eindeutige_kalender_gruppen_id,
event,
]),
).values(),
);
if (!isCacheEnabled && hasSuccessfullyLoadedEvents) {
const newlyLoadedEventIds = new Set(
newlyLoadedEvents.map(
(event) => event.eindeutige_kalender_gruppen_id,
),
);
tempAllEvents = tempAllEvents.filter((event) => {
const eventStart = getTimestampFromISODate(event.isostart);
const eventEnd = getTimestampFromISODate(event.isoend);
const overlapsDisplayedDateRange =
eventStart < currentlyDisplayedDateRange.end.ts &&
eventEnd > currentlyDisplayedDateRange.start.ts;
return (
!overlapsDisplayedDateRange ||
newlyLoadedEventIds.has(event.eindeutige_kalender_gruppen_id)
let tempAllEvents = Array.from(
new Map(
[...allEvents.value, ...newlyLoadedEvents].map((event) => [
event.eindeutige_kalender_gruppen_id,
event,
]),
).values(),
);
if (!isCacheEnabled) {
const newlyLoadedEventIds = new Set(
newlyLoadedEvents.map(
(event) => event.eindeutige_kalender_gruppen_id,
),
);
tempAllEvents = tempAllEvents.filter((event) => {
const eventStart = getTimestampFromISODate(event.isostart);
const eventEnd = getTimestampFromISODate(event.isoend);
const overlapsDisplayedDateRange =
eventStart < currentlyDisplayedDateRange.end.ts &&
eventEnd > currentlyDisplayedDateRange.start.ts;
return (
!overlapsDisplayedDateRange ||
newlyLoadedEventIds.has(event.eindeutige_kalender_gruppen_id)
);
});
}
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(tempAllEvents),
);
})
.catch(() => {})
.finally(() => {
pendingPromiseCount -= 1;
if (pendingPromiseCount === 0) {
if (!hasSuccessfullyLoadedEvents) {
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(allEvents.value),
);
}
hasFirstLoadOccurred = true;
}
});
}
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(tempAllEvents),
);
hasFirstLoadOccurred = true;
});
};
@@ -188,6 +209,32 @@ export function useEventLoader(
allowedCacheStartTimestamp = startTimestamp - cachePadding;
allowedCacheEndTimestamp = endTimestamp + cachePadding;
const requestEnd = getLuxonDateFromMillis(modifiedRequestEndTimestamp);
let requestStart = getLuxonDateFromMillis(modifiedRequestStartTimestamp);
const intervalDays = getRequestIntervalDays();
const intervalEnd = requestStart
.plus({ days: intervalDays - 1 })
.endOf("day");
if (requestEnd > intervalEnd) {
while (requestStart < requestEnd) {
console.log("requestStart", requestStart.toISO() + " requestEnd", requestEnd.toISO() + " intervalDays", intervalDays);
let intervalRequestEnd = requestStart
.plus({ days: intervalDays - 1 })
.endOf("day");
if (intervalRequestEnd > requestEnd) intervalRequestEnd = requestEnd;
result = mergePromiseElements(
getPromiseFunc(requestStart, intervalRequestEnd),
result,
);
requestStart = intervalRequestEnd.plus({ milliseconds: 1 });
}
return result;
}
return mergePromiseElements(
getPromiseFunc(
getLuxonDateFromMillis(modifiedRequestStartTimestamp),