diff --git a/public/css/Cis4/CoreCalendarEvents.css b/public/css/Cis4/CoreCalendarEvents.css index d2d067b43..a3f132a4d 100644 --- a/public/css/Cis4/CoreCalendarEvents.css +++ b/public/css/Cis4/CoreCalendarEvents.css @@ -108,7 +108,8 @@ display: none; } -.weekPageContainer .calendar-event-collisions +.weekPageContainer .calendar-event-collisions, +.rangePageContainer .calendar-event-collisions { display: grid; grid-template-columns: auto 1fr; diff --git a/public/css/Tempus_print.css b/public/css/Tempus_print.css index 33d4c526e..dc552ec8d 100644 --- a/public/css/Tempus_print.css +++ b/public/css/Tempus_print.css @@ -77,7 +77,31 @@ header, .fhc-calendar-mode-range .main-header:not(.main-header-empty), .fhc-calendar-mode-range .fhc-calendar-base-grid-line { - border-top: solid !important; - border-top-color: black !important; - border-top-width: 1px !important; + border-top: solid 1px black !important; +} + +.fhc-calendar-mode-range .main-header:not(.main-header-empty) { + padding-top: .25rem !important; +} + +.fhc-calendar-mode-range .fhc-calendar-base-grid-line { + grid-auto-rows: max-content !important; + overflow: hidden !important; +} + +.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event { + break-inside: avoid !important; + margin: 1px !important; + page-break-inside: avoid !important; + z-index: auto !important; +} + +.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event > .rangePageContainer { + background-color: var(--event-bg, var(--event-default-bg)) !important; + break-inside: avoid !important; + page-break-inside: avoid !important; +} + +.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event > .rangePageContainer * { + background-color: transparent !important; } diff --git a/public/js/api/factory/tempus/kalender.js b/public/js/api/factory/tempus/kalender.js index 046aec385..c1ba84a6a 100644 --- a/public/js/api/factory/tempus/kalender.js +++ b/public/js/api/factory/tempus/kalender.js @@ -22,7 +22,7 @@ export default { params: { lehreinheit_id, start_date, end_date } }; }, - getPlanLecturer(start_date, end_date) + getPlanLecturer(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null) { return { method: 'get', diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index e53f57e32..610dd090d 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -93,6 +93,10 @@ export default { type: Number, default: 1 }, + waitForAllPromises: { + type: Boolean, + default: true, + }, parkedEvents: { type: Object, default: () => new Set(), @@ -125,6 +129,10 @@ export default { type: Boolean, default: true, }, + canToggleCollisionCheck: { + type: Boolean, + default: true, + }, rangeViewPreviewLink: { type: String, default: null, @@ -224,10 +232,6 @@ export default { visibleEvents() { let list = this.events; - // Start with the first week and then keep events overlapping a grid line - // 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.isRangeVirtualScrollEnabled && this.currentMode === 'range' @@ -374,7 +378,9 @@ export default { const { events, lv, reset } = useEventLoader( rangeInterval, props.getPromiseFunc, - () => props.cacheMultiplier + () => props.cacheMultiplier, + undefined, + props.waitForAllPromises, ); Vue.watch(lv, (newValue) => { @@ -482,7 +488,10 @@ export default { > Reservierung -
+
{{ appConfig.ignore_kollision ? 'Kollisionscheck aus' : 'Kollisionscheck an' }} diff --git a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js index a019aa871..9dd0c55ce 100644 --- a/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Tempus/Renderer/Lehreinheit/calendarEvent.js @@ -154,7 +154,11 @@ export default { class="position-relative" @wheel.stop > -
+
{{event.verplante_stunden}}
{{ topicString }} diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index ab7798ea8..a7bac7d5b 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -155,13 +155,16 @@ export default { }, 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(), + previewRole: this.previewRole, }); + if (!this.lastRange?.start || !this.lastRange?.end) + return `${previewUrl}?${params}`; + + params.set('startDate', this.lastRange.start.toISODate()); + params.set('endDate', this.lastRange.end.toISODate()); + for (const [key, value] of Object.entries(this.getPlanFilter())) + params.set(key, JSON.stringify(value)); return `${previewUrl}?${params}`; }, @@ -450,18 +453,12 @@ export default { searchfunction(params) { return this.$api.call(ApiSearchbar.search(params)); }, - getPromiseFunc(start, end) { - const isMonth = this.currentMode?.toLowerCase() === "month"; - const collisionCheck = !isMonth; - const maxDailyEventLimit = isMonth ? this.maxDailyEventLimitForMonthView : null; - const hasRooms = this.rooms.length > 0; - const hasLektoren = this.lecturers.length > 0; - const hasStg = this.studiengaenge.length > 0; - + getPlanFilter() { const filter = {}; - if (hasRooms) filter.ort = this.rooms.map((room) => room.ort_kurzbz); - if (hasStg) { + if (this.rooms.length) + filter.ort = this.rooms.map((room) => room.ort_kurzbz); + if (this.studiengaenge.length) { filter.stg = this.studiengaenge.map( ({ stg_kz, semester, orgform_kurzbz }) => ({ stg_kz, @@ -470,11 +467,19 @@ export default { }), ); } - if (hasLektoren) + if (this.lecturers.length) filter.uid = this.lecturers.map((lecture) => lecture.uid); + return filter; + }, + getPromiseFunc(start, end) { + const isMonth = this.currentMode?.toLowerCase() === "month"; + const collisionCheck = !isMonth; + const maxDailyEventLimit = isMonth ? this.maxDailyEventLimitForMonthView : null; + const filter = this.getPlanFilter(); + let response = null; - if (this.previewRole === 'lektor') + if (this.previewRole === 'lektor') { response = [ this.$api.call( ApiKalender.getPlanLecturer( @@ -485,8 +490,7 @@ export default { ), ), ]; - - if (this.previewRole === 'student') + } else if (this.previewRole === 'student') { response = [ this.$api.call( ApiKalender.getPlanStudent( @@ -497,18 +501,19 @@ export default { ), ), ]; - - response = [ - this.$api.call( - ApiKalender.getPlan( - filter, - start.toISODate(), - end.toISODate(), - collisionCheck, - maxDailyEventLimit, + } else { + response = [ + this.$api.call( + ApiKalender.getPlan( + filter, + start.toISODate(), + end.toISODate(), + collisionCheck, + maxDailyEventLimit, + ), ), - ), - ]; + ]; + } if (response) { response[0].then((result) => { @@ -957,22 +962,22 @@ export default { 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(); - } + 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', - ); - }); + el.classList.remove( + 'updating-event', + 'updated-event', + 'updated-event-long', + ); + }); setTimeout(() => { const eventEl = document.querySelector( @@ -1000,18 +1005,18 @@ export default { }); } - let timeout = 0; - let emphasizeUpdateClassName = isInsideScrolledView - ? 'updated-event' - : 'updated-event-long'; + let timeout = 0; + let emphasizeUpdateClassName = isInsideScrolledView + ? 'updated-event' + : 'updated-event-long'; - if (!isInsideScrolledView) timeout = 300; + if (!isInsideScrolledView) timeout = 300; - setTimeout(() => { - eventEl.classList.add(emphasizeUpdateClassName); - }, timeout); + setTimeout(() => { + eventEl.classList.add(emphasizeUpdateClassName); + }, timeout); - this.currentlyUpdatedEvent = null; + this.currentlyUpdatedEvent = null; }, 100); }, updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) { @@ -1052,8 +1057,19 @@ export default { targetGridLine.insertBefore(element, null); element.classList.add('tempus-temporary-calendar-event'); } - element.style.gridRowEnd = 't_' + newPotentialEnd; - element.style.gridRowStart = 't_' + newPotentialStart; + if (this.currentMode === 'range') { + element.style.gridColumnStart = 't_' + newPotentialStart; + element.style.gridColumnEnd = 't_' + newPotentialEnd; + } else { + element.style.gridRowStart = 't_' + newPotentialStart; + element.style.gridRowEnd = 't_' + newPotentialEnd; + } + + element.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest', + }); }, 100); const outerDiv = document.createElement('div'); @@ -1066,24 +1082,6 @@ export default { 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; @@ -1249,6 +1247,7 @@ export default { @events-reloaded="clearTemporaryEvents" class="responsive-calendar" :cache-multiplier="currentMode === 'week' ? 1 : 0" + :wait-for-all-promises="false" /> diff --git a/public/js/components/Tempus/TempusCalendarPreview.js b/public/js/components/Tempus/TempusCalendarPreview.js index c65a91286..6fd96de6a 100644 --- a/public/js/components/Tempus/TempusCalendarPreview.js +++ b/public/js/components/Tempus/TempusCalendarPreview.js @@ -39,6 +39,22 @@ function getRangeFromQuery(query, timezone) { return luxon.Interval.fromDateTimes(start, end); } +function getPlanFilterFromQuery(query) { + const filter = {}; + for (const key of ['ort', 'stg', 'uid']) { + const value = Array.isArray(query?.[key]) ? query[key][0] : query?.[key]; + if (typeof value !== 'string') continue; + + try { + filter[key] = JSON.parse(value); + } catch { + // Ignore malformed filter parameters. + } + } + + return filter; +} + export default { name: 'TempusCalendarPreview', components: { @@ -85,6 +101,7 @@ export default { visibleStatus: ['all'], selectedStudiensemester: this.defaultSemester, urlRange: null, + planFilter: {}, calendarDatesByMode: { week: today, month: today, @@ -147,28 +164,11 @@ export default { 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); + const collisionCheck = true; + const filter = this.planFilter; let response = null; - if (this.previewRole === 'lektor') + if (this.previewRole === 'lektor') { response = [ this.$api.call( ApiKalender.getPlanLecturer( @@ -179,8 +179,7 @@ export default { ), ), ]; - - if (this.previewRole === 'student') + } else if (this.previewRole === 'student') { response = [ this.$api.call( ApiKalender.getPlanStudent( @@ -191,8 +190,8 @@ export default { ), ), ]; - - response = [ + } else { + response = [ this.$api.call( ApiKalender.getPlan( filter, @@ -203,6 +202,7 @@ export default { ), ), ]; + } if (response) { response[0].then((result) => { @@ -397,25 +397,27 @@ export default { this.hoveredEvent = null; } }, - scrollToAndEmphasizeUpdatedEvent() { + scrollToAndEmphasizeUpdatedEvent(shouldEmphasize = true) { 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(); - } + if (shouldEmphasize) { + 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', - ); - }); + el.classList.remove( + 'updating-event', + 'updated-event', + 'updated-event-long', + ); + }); + } setTimeout(() => { const eventEl = document.querySelector( @@ -443,18 +445,20 @@ export default { }); } - let timeout = 0; - let emphasizeUpdateClassName = isInsideScrolledView - ? 'updated-event' - : 'updated-event-long'; + if (shouldEmphasize) { + let timeout = 0; + let emphasizeUpdateClassName = isInsideScrolledView + ? 'updated-event' + : 'updated-event-long'; - if (!isInsideScrolledView) timeout = 300; + if (!isInsideScrolledView) timeout = 300; - setTimeout(() => { - eventEl.classList.add(emphasizeUpdateClassName); - }, timeout); + setTimeout(() => { + eventEl.classList.add(emphasizeUpdateClassName); + }, timeout); - this.currentlyUpdatedEvent = null; + this.currentlyUpdatedEvent = null; + } }, 100); }, updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) { @@ -495,8 +499,19 @@ export default { targetGridLine.insertBefore(element, null); element.classList.add('tempus-temporary-calendar-event'); } - element.style.gridRowEnd = 't_' + newPotentialEnd; - element.style.gridRowStart = 't_' + newPotentialStart; + if (this.currentMode === 'range') { + element.style.gridColumnStart = 't_' + newPotentialStart; + element.style.gridColumnEnd = 't_' + newPotentialEnd; + } else { + element.style.gridRowStart = 't_' + newPotentialStart; + element.style.gridRowEnd = 't_' + newPotentialEnd; + } + + element.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest', + }); }, 100); const outerDiv = document.createElement('div'); @@ -509,24 +524,6 @@ export default { 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; @@ -544,6 +541,10 @@ export default { '$route.query': { immediate: true, handler(query) { + this.planFilter = getPlanFilterFromQuery(query); + this.previewRole = Array.isArray(query?.previewRole) + ? query.previewRole[0] + : query?.previewRole ?? 'planer'; this.urlRange = getRangeFromQuery(query, this.config.timezone); const rangeStart = this.urlRange?.start.toISODate() ?? luxon.DateTime.now().setZone(this.config.timezone).toISODate(); @@ -580,7 +581,6 @@ export default { .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; @@ -648,6 +648,7 @@ export default { :is-event-dragging-enabled="false" :is-event-resizing-enabled="false" :isRangeVirtualScrollEnabled="false" + :can-toggle-collision-check="false" @update:date="handleChangeDate" @update:mode="handleChangeMode" @event-hover="onEventHover" diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js index 6d33e70fc..a527474c7 100644 --- a/public/js/composables/Tempus/TempusEventLoader.js +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -5,6 +5,7 @@ export function useEventLoader( getPromiseFunc, cacheMultiplier = 1, requestIntervalDays = PLAN_REQUEST_INTERVAL_DAYS, + waitForAllPromises = true, ) { let hasFirstLoadOccurred = false; @@ -85,6 +86,62 @@ export function useEventLoader( addVisualForEventsLoading(); + if (waitForAllPromises) { + Promise.allSettled(promises).then((results) => { + let newlyLoadedEvents = []; + let hasSuccessfullyLoadedEvents = false; + + results.forEach((res) => { + if ( + !(res.status === "fulfilled" && res.value.meta.status === "success") + ) + return; + + hasSuccessfullyLoadedEvents = true; + if (res.value.meta.lv) lv.value = res.value.meta.lv; + newlyLoadedEvents = newlyLoadedEvents.concat(res.value.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) + ); + }); + } + + allEvents.value = removeVisualForEventsLoading( + ensureEventsAreInValidCacheRange(tempAllEvents), + ); + + hasFirstLoadOccurred = true; + }); + + return; + } + let newlyLoadedEvents = []; let pendingPromiseCount = promises.length; let hasSuccessfullyLoadedEvents = false;