From 4bc858dc48a68a7638bf6e861fb06563fda06796 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Thu, 30 Jul 2026 17:18:55 +0200 Subject: [PATCH 01/18] BUG: Fix for concatinating date range in calendar week mode --- public/js/components/Calendar/Mode/Week.js | 15 ++++----------- public/js/components/Calendar/Tempus.js | 9 +++++++-- public/js/composables/EventLoader.js | 21 +++++++++++++++++++-- 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/public/js/components/Calendar/Mode/Week.js b/public/js/components/Calendar/Mode/Week.js index f8653b9e9..c6f776f73 100644 --- a/public/js/components/Calendar/Mode/Week.js +++ b/public/js/components/Calendar/Mode/Week.js @@ -27,16 +27,9 @@ export default { }, computed: { range() { - let first = this.focusDate.startOf('week', { useLocaleWeeks: true }); - let last = this.focusDate.endOf('week', { useLocaleWeeks: true }); - - if (this.rangeOffset != 0) { - if (this.rangeOffset < 0) { - first = first.plus({ weeks: this.rangeOffset }); - } else { - last = last.plus({ weeks: this.rangeOffset }); - } - } + const targetDate = this.focusDate.plus({ weeks: this.rangeOffset }); + const first = targetDate.startOf('week', { useLocaleWeeks: true }); + const last = targetDate.endOf('week', { useLocaleWeeks: true }); return luxon.Interval.fromDateTimes(first, last); } @@ -98,7 +91,7 @@ export default { this.$emit('update:range', this.range); this.$refs.view.$refs.grid.enableAutoScroll(); }, - template: ` + template: /* html */`
props.mode?.toLowerCase() === 'week'); - const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc); + const { events, lv, reset } = useEventLoader( + rangeInterval, + props.getPromiseFunc, + preserveRequestedRange + ); Vue.watch(lv, newValue => { context.emit('update:lv', newValue); @@ -281,4 +286,4 @@ export default {
` -} \ No newline at end of file +} diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index ed1c01ee0..d08e92a97 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,6 +1,6 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc) { +export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedRange = false) { let loading_id = 0; const events = Vue.ref([]); const loadingEvents = Vue.ref([]); @@ -83,6 +83,23 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { // load the rest let rStart = eventsLoaded[index + 1] + 1; result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); + } else if ( + Vue.toValue(preserveRequestedRange) + && end.ts < eventsLoaded[index] + ) { + // requested range is entirely inside the gap + // between the previous and next loaded chunks + const touchesPrevious = eventsLoaded[index - 1] + 1 == start.ts; + const touchesNext = end.ts + 1 == eventsLoaded[index]; + + if (touchesPrevious && touchesNext) + eventsLoaded.splice(index - 1, 2); + else if (touchesPrevious) + eventsLoaded[index - 1] = end.ts; + else if (touchesNext) + eventsLoaded[index] = start.ts; + else + eventsLoaded.splice(index, 0, start.ts, end.ts); } else { // extend an existing chunk // and load the rest if necessary @@ -146,4 +163,4 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { } return { events: allEvents, lv, reset } -} \ No newline at end of file +} From bb6dfd4d642d9a233ece66761663f3903481254c Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Thu, 30 Jul 2026 17:24:44 +0200 Subject: [PATCH 02/18] BUG: Fix for concatinating date range in calendar month mode --- public/js/components/Calendar/Mode/Month.js | 15 +++------------ public/js/components/Calendar/Tempus.js | 4 +++- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/public/js/components/Calendar/Mode/Month.js b/public/js/components/Calendar/Mode/Month.js index a352ccc98..2bb0d4424 100644 --- a/public/js/components/Calendar/Mode/Month.js +++ b/public/js/components/Calendar/Mode/Month.js @@ -27,18 +27,9 @@ export default { }, computed: { range() { - let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true }); - let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day - - if (this.rangeOffset != 0) { - const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset}); - const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true }); - if (this.rangeOffset < 0) { - first = nextRangeStart; - } else { - last = nextRangeStart.plus({ days: 41 }).endOf('day'); - } - } + const targetDate = this.focusDate.plus({ months: this.rangeOffset }); + const first = targetDate.startOf('month').startOf('week', { useLocaleWeeks: true }); + const last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day return luxon.Interval.fromDateTimes(first, last); } diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 0e096e779..c8923681f 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -167,7 +167,9 @@ export default { }, setup(props, context) { const rangeInterval = Vue.ref(null); - const preserveRequestedRange = Vue.computed(() => props.mode?.toLowerCase() === 'week'); + const preserveRequestedRange = Vue.computed(() => { + return ['week', 'month'].includes(props.mode?.toLowerCase()); + }); const { events, lv, reset } = useEventLoader( rangeInterval, From cc8e7493e28089414ea3e3d8d6f22608d5335930 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Fri, 31 Jul 2026 13:29:04 +0200 Subject: [PATCH 03/18] Added removal of mounted click listeners for detached divs --- public/js/directives/Calendar/Click.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/public/js/directives/Calendar/Click.js b/public/js/directives/Calendar/Click.js index e5de31f1a..c4c0c725b 100644 --- a/public/js/directives/Calendar/Click.js +++ b/public/js/directives/Calendar/Click.js @@ -1,11 +1,17 @@ const clickListeners = []; -function saveAddClickListener(el, source, value) { +function removeClickListener(el) { const index = clickListeners.findIndex(data => data.el == el); - if (index >= 0) { - el.removeEventListener('click', clickListeners[index].listener); - clickListeners.splice(index, 1); - } + if (index < 0) + return; + + el.removeEventListener('click', clickListeners[index].listener); + clickListeners.splice(index, 1); +} + +function saveAddClickListener(el, source, value) { + removeClickListener(el); + const listener = evt => { evt.preventDefault(); evt.stopPropagation(); @@ -45,5 +51,9 @@ export default { if (binding.arg != 'container') { saveAddClickListener(el, binding.arg, binding.value); } + }, + beforeUnmount(el, binding) { + if (binding.arg != 'container') + removeClickListener(el); } -} \ No newline at end of file +} From 9352d5cd46dd7210092e2d9868452bd95c8a4a66 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 3 Aug 2026 09:40:10 +0200 Subject: [PATCH 04/18] Add disabling of collision check for month view --- .../api/frontend/v1/tempus/Kalender.php | 5 +++- application/libraries/KalenderLib.php | 4 +-- public/js/api/factory/tempus/kalender.js | 12 ++++---- public/js/components/Calendar/Tempus.js | 7 ++++- public/js/components/Tempus/Tempus.js | 21 ++++++++++++-- public/js/composables/EventLoader.js | 28 ++++++++++++++++++- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/application/controllers/api/frontend/v1/tempus/Kalender.php b/application/controllers/api/frontend/v1/tempus/Kalender.php index 7a65a5211..9abd22ab3 100644 --- a/application/controllers/api/frontend/v1/tempus/Kalender.php +++ b/application/controllers/api/frontend/v1/tempus/Kalender.php @@ -99,6 +99,8 @@ class Kalender extends FHCAPI_Controller $start_date = $this->_ci->input->post('start_date', TRUE); $end_date = $this->_ci->input->post('end_date', TRUE); + $isCollisionCheckEnabled = $this->_ci->input->post('collisionCheck', TRUE) ?? false; + $filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER); $stundenplan_data = $this->_ci->kalenderlib->getPlanForPlanner( @@ -106,7 +108,8 @@ class Kalender extends FHCAPI_Controller $end_date, isset($filter->ort) ? $filter->ort : null, isset($filter->uid) ? $filter->uid : null, - isset($filter->stg) ? $filter->stg : null + isset($filter->stg) ? $filter->stg : null, + $isCollisionCheckEnabled ); $this->terminateWithSuccess($stundenplan_data); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 3a531102f..93f169db4 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -345,7 +345,7 @@ class KalenderLib $data = $this->_ci->KalenderModel->load(); return $this->_mapEvents($data); } - public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null) + public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null, $collisionCheck = true) { $this->_getBasePlan($start_date, $end_date); @@ -452,7 +452,7 @@ class KalenderLib $data = $this->_ci->KalenderModel->load(); - return $this->_mapEvents($data); + return $this->_mapEvents($data, $collisionCheck); } public function getPlanForStudent($start_date, $end_date) diff --git a/public/js/api/factory/tempus/kalender.js b/public/js/api/factory/tempus/kalender.js index 9d2746434..f6948c7fe 100644 --- a/public/js/api/factory/tempus/kalender.js +++ b/public/js/api/factory/tempus/kalender.js @@ -1,27 +1,27 @@ export default { - getPlan(filter, start_date, end_date) + getPlan(filter, start_date, end_date, collisionCheck = true) { return { method: 'post', url: '/api/frontend/v1/tempus/Kalender/getPlan', - params: { ...filter, start_date, end_date } + params: { ...filter, start_date, end_date, collisionCheck } }; }, - getPlanLecturer(start_date, end_date) + getPlanLecturer(start_date, end_date, collisionCheck = true) { return { method: 'get', url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer', - params: { start_date, end_date } + params: { start_date, end_date, collisionCheck } }; }, - getPlanStudent(start_date, end_date) + getPlanStudent(start_date, end_date, collisionCheck = true) { return { method: 'get', url: '/api/frontend/v1/tempus/Kalender/getPlanStudent', - params: { start_date, end_date } + params: { start_date, end_date, collisionCheck } }; }, diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index c8923681f..7f69e9e9a 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -43,6 +43,10 @@ export default { type: Function, required: true }, + cacheMultiplier: { + type: Number, + default: 1 + }, parkedEvents: { type: Object, default: () => new Set() @@ -174,7 +178,8 @@ export default { const { events, lv, reset } = useEventLoader( rangeInterval, props.getPromiseFunc, - preserveRequestedRange + preserveRequestedRange, + () => props.cacheMultiplier ); Vue.watch(lv, newValue => { diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index cb9d5b499..10db02222 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -424,6 +424,7 @@ export default { return this.$api.call(ApiSearchbar.search(params)); }, getPromiseFunc(start, end) { + const collisionCheck = this.currentMode?.toLowerCase() !== "month"; const hasRooms = this.rooms.length > 0; const hasLektoren = this.lecturers.length > 0; const hasStg = this.studiengaenge.length > 0; @@ -446,20 +447,33 @@ export default { if (this.previewRole === "lektor") return [ this.$api.call( - ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()), + ApiKalender.getPlanLecturer( + start.toISODate(), + end.toISODate(), + collisionCheck, + ), ), ]; if (this.previewRole === "student") return [ this.$api.call( - ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()), + ApiKalender.getPlanStudent( + start.toISODate(), + end.toISODate(), + collisionCheck, + ), ), ]; return [ this.$api.call( - ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()), + ApiKalender.getPlan( + filter, + start.toISODate(), + end.toISODate(), + collisionCheck, + ), ), ]; }, @@ -1275,6 +1289,7 @@ export default { ref="calendar" :timezone="config.timezone" :get-promise-func="getPromiseFunc" + :cache-multiplier="currentMode === 'week' ? 1 : 0" :visible-status="visibleStatus" :date="calendarDate" :mode="currentMode" diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index d08e92a97..2946fc6ad 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,6 +1,6 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedRange = false) { +export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedRange = false, cacheMultiplier = 1) { let loading_id = 0; const events = Vue.ref([]); const loadingEvents = Vue.ref([]); @@ -14,6 +14,23 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR return o.push(n), o; }; + const filterEventsOutsideRange = validRange => { + events.value = events.value.filter(event => { + const start = luxon.DateTime.fromISO(event.isostart).ts; + const end = luxon.DateTime.fromISO(event.isoend).ts; + return start <= validRange.end.ts && end >= validRange.start.ts; + }); + + const loadedRanges = []; + for (let i = 0; i < eventsLoaded.length; i += 2) { + const start = Math.max(eventsLoaded[i], validRange.start.ts); + const end = Math.min(eventsLoaded[i + 1], validRange.end.ts); + if (start < end) + loadedRanges.push(start, end); + } + eventsLoaded.splice(0, eventsLoaded.length, ...loadedRanges); + }; + const markEventsLoaded = (start, end) => { let result = []; if (!eventsLoaded.length) { @@ -132,6 +149,13 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR const range = Vue.toValue(rangeInterval); if (!(range instanceof luxon.Interval)) return; + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + const cachePadding = Math.round(range.length() * cacheSize); + const validRange = luxon.Interval.fromDateTimes( + range.start.minus(cachePadding ?? 0), + range.end.plus(cachePadding ?? 0) + ); + const promises = markEventsLoaded(range.start, range.end); Promise .allSettled(promises) @@ -145,6 +169,8 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR lv.value = res.value.meta.lv; events.value = events.value.concat(res.value.data); + + filterEventsOutsideRange(validRange); loadingEvents.value = []; } }) From 7759884d8d5a1d5ce78aaf7e8cc7cfdf4cbd4d6e Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 3 Aug 2026 09:42:23 +0200 Subject: [PATCH 05/18] Add max daily event amount limit for month view --- .../api/frontend/v1/tempus/Kalender.php | 4 ++- application/libraries/KalenderLib.php | 21 ++++++++++++-- public/js/api/factory/tempus/kalender.js | 28 +++++++++++++++---- public/js/components/Tempus/Tempus.js | 8 +++++- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/application/controllers/api/frontend/v1/tempus/Kalender.php b/application/controllers/api/frontend/v1/tempus/Kalender.php index 9abd22ab3..372947cf5 100644 --- a/application/controllers/api/frontend/v1/tempus/Kalender.php +++ b/application/controllers/api/frontend/v1/tempus/Kalender.php @@ -100,6 +100,7 @@ class Kalender extends FHCAPI_Controller $end_date = $this->_ci->input->post('end_date', TRUE); $isCollisionCheckEnabled = $this->_ci->input->post('collisionCheck', TRUE) ?? false; + $maxDailyEventLimit = $this->_ci->input->post('maxDailyEventLimit', TRUE) ?? false; $filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER); @@ -109,7 +110,8 @@ class Kalender extends FHCAPI_Controller isset($filter->ort) ? $filter->ort : null, isset($filter->uid) ? $filter->uid : null, isset($filter->stg) ? $filter->stg : null, - $isCollisionCheckEnabled + $isCollisionCheckEnabled, + $maxDailyEventLimit ); $this->terminateWithSuccess($stundenplan_data); diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 93f169db4..595a38229 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -140,7 +140,7 @@ class KalenderLib $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC'); } - private function _mapEvents($data, $collisionCheck = true) + private function _mapEvents($data, $collisionCheck = true, $maxDailyEventLimit = null) { $stundenplan_data = []; @@ -274,6 +274,21 @@ class KalenderLib } } + if (!is_null($maxDailyEventLimit) && is_numeric($maxDailyEventLimit) && $maxDailyEventLimit > 0) + { + $eventsPerDay = []; + $events = array_filter($events, function ($event) use (&$eventsPerDay, $maxDailyEventLimit) { + $date = $event->datum; + $eventsPerDay[$date] = $eventsPerDay[$date] ?? 0; + + if ($eventsPerDay[$date] >= $maxDailyEventLimit) + return false; + + $eventsPerDay[$date]++; + return true; + }); + } + return array_values($events); } public function getPlanByOrt($start_date, $end_date, $ort) @@ -345,7 +360,7 @@ class KalenderLib $data = $this->_ci->KalenderModel->load(); return $this->_mapEvents($data); } - public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null, $collisionCheck = true) + public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null, $collisionCheck = true, $maxDailyEventLimit = null) { $this->_getBasePlan($start_date, $end_date); @@ -452,7 +467,7 @@ class KalenderLib $data = $this->_ci->KalenderModel->load(); - return $this->_mapEvents($data, $collisionCheck); + return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit); } public function getPlanForStudent($start_date, $end_date) diff --git a/public/js/api/factory/tempus/kalender.js b/public/js/api/factory/tempus/kalender.js index f6948c7fe..fbc99cb68 100644 --- a/public/js/api/factory/tempus/kalender.js +++ b/public/js/api/factory/tempus/kalender.js @@ -1,27 +1,43 @@ export default { - getPlan(filter, start_date, end_date, collisionCheck = true) + getPlan(filter, start_date, end_date, collisionCheck = true, maxDailyEventLimit = null) { return { method: 'post', url: '/api/frontend/v1/tempus/Kalender/getPlan', - params: { ...filter, start_date, end_date, collisionCheck } + params: { + ...filter, + start_date, + end_date, + collisionCheck, + ...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit }) + } }; }, - getPlanLecturer(start_date, end_date, collisionCheck = true) + getPlanLecturer(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null) { return { method: 'get', url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer', - params: { start_date, end_date, collisionCheck } + params: { + start_date, + end_date, + collisionCheck, + ...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit }) + } }; }, - getPlanStudent(start_date, end_date, collisionCheck = true) + getPlanStudent(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null) { return { method: 'get', url: '/api/frontend/v1/tempus/Kalender/getPlanStudent', - params: { start_date, end_date, collisionCheck } + params: { + start_date, + end_date, + collisionCheck, + ...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit }) + } }; }, diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 10db02222..f75b4ea7a 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -170,6 +170,7 @@ export default { assignedResources: [], areFormButtonsDisplayed: false, }, + maxDailyEventLimitForMonthView: 10, }; }, computed: { @@ -424,7 +425,9 @@ export default { return this.$api.call(ApiSearchbar.search(params)); }, getPromiseFunc(start, end) { - const collisionCheck = this.currentMode?.toLowerCase() !== "month"; + 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; @@ -451,6 +454,7 @@ export default { start.toISODate(), end.toISODate(), collisionCheck, + maxDailyEventLimit, ), ), ]; @@ -462,6 +466,7 @@ export default { start.toISODate(), end.toISODate(), collisionCheck, + maxDailyEventLimit, ), ), ]; @@ -473,6 +478,7 @@ export default { start.toISODate(), end.toISODate(), collisionCheck, + maxDailyEventLimit, ), ), ]; From 36a11ec2fdb62ade0ca32f0cb7e7c2b1179bb259 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Tue, 4 Aug 2026 17:03:47 +0200 Subject: [PATCH 06/18] WIP: Adding custom tempus event loader --- public/js/components/Calendar/Tempus.js | 6 +- public/js/components/Tempus/Tempus.js | 1 + .../composables/Tempus/TempusEventLoader.js | 233 ++++++++++++++++++ 3 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 public/js/composables/Tempus/TempusEventLoader.js diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 7f69e9e9a..7f191569a 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -1,6 +1,6 @@ import FhcCalendar from "./Base.js"; -import { useEventLoader } from '../../composables/EventLoader.js'; +import { useEventLoader } from '../../composables/Tempus/TempusEventLoader.js'; import ModeWeek from './Mode/Week.js'; import ModeMonth from './Mode/Month.js'; @@ -171,14 +171,10 @@ export default { }, setup(props, context) { const rangeInterval = Vue.ref(null); - const preserveRequestedRange = Vue.computed(() => { - return ['week', 'month'].includes(props.mode?.toLowerCase()); - }); const { events, lv, reset } = useEventLoader( rangeInterval, props.getPromiseFunc, - preserveRequestedRange, () => props.cacheMultiplier ); diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index f75b4ea7a..168e89058 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -1301,6 +1301,7 @@ export default { :mode="currentMode" :parkedEvents="parkedKeys" :visible-lecturers="visibleLecturerUids" + :cacheMultiplier="currentMode === 'week' ? 0 : 0" @drop="dropHandler" @resize="resizeHandler" @update:date="handleChangeDate" diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js new file mode 100644 index 000000000..75d87985c --- /dev/null +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -0,0 +1,233 @@ +export function useEventLoader( + rangeInterval, + getPromiseFunc, + cacheMultiplier = 1, +) { + let allEvents = Vue.ref([]); + const events = []; + + const lv = Vue.ref(null); + + let newlyLoadedEvents = []; + + let cachedEventsStartTimestamp = null; + let cachedEventsEndTimestamp = null; + + let allowedCacheStartTimestamp = null; + let allowedCacheEndTimestamp = null; + + let currentlyDisplayedDateRange; + + const reload = () => { + if ( + currentlyDisplayedDateRange && + currentlyDisplayedDateRange?.start?.ts === + rangeInterval?.value?.start?.ts && + currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts + ) { + console.log( + "cached range is the same as requested range, skipping reload", + ); + return; + } + + currentlyDisplayedDateRange = Vue.toValue(rangeInterval); + + if ( + !currentlyDisplayedDateRange?.start || + !currentlyDisplayedDateRange?.end + ) + return; + + if (!(currentlyDisplayedDateRange instanceof luxon.Interval)) return; + + if ( + cachedEventsStartTimestamp === currentlyDisplayedDateRange.start.ts && + cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts + ) { + console.log( + "cached range is the same as requested range, skipping reload", + ); + return; + } + + const promises = requestEvents( + currentlyDisplayedDateRange.start.ts, + currentlyDisplayedDateRange.end.ts, + ); + + ensureCacheRangeIsAllowedRange( + allowedCacheStartTimestamp, + allowedCacheEndTimestamp, + ); + + if (promises.length === 0) { + allEvents.value = removeVisualForEventsLoading( + ensureEventsAreInValidCacheRange(allEvents.value), + ); + + return; + } + + addVisualForEventsLoading(); + + Promise.allSettled(promises).then((results) => { + let newlyLoadedEvents = []; + + results.forEach((res) => { + if ( + !(res.status === "fulfilled" && res.value.meta.status === "success") + ) + return; + if (res.value.meta.lv) lv.value = res.value.meta.lv; + newlyLoadedEvents = res.value.data; + }); + + let tempAllEvents = Array.from( + new Map( + [...allEvents.value, ...newlyLoadedEvents].map((event) => [ + event.eindeutige_gruppen_id, + event, + ]), + ).values(), + ); + + allEvents.value = removeVisualForEventsLoading( + ensureEventsAreInValidCacheRange(tempAllEvents), + ); + }); + }; + + Vue.watchEffect(reload); + + const reset = () => { + reload(); + }; + + const requestEvents = (startTimestamp, endTimestamp) => { + let result = []; + + if (!startTimestamp || !endTimestamp) return result; + if (startTimestamp >= endTimestamp) return result; + + let modifiedRequestStartTimestamp = startTimestamp; + let modifiedRequestEndTimestamp = endTimestamp; + + if (cachedEventsStartTimestamp && cachedEventsEndTimestamp) { + if ( + startTimestamp < cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = startTimestamp; + modifiedRequestEndTimestamp = endTimestamp; + cachedEventsStartTimestamp = startTimestamp; + cachedEventsEndTimestamp = endTimestamp; + } else if ( + startTimestamp < cachedEventsStartTimestamp && + endTimestamp <= cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = startTimestamp; + modifiedRequestEndTimestamp = cachedEventsStartTimestamp; + cachedEventsStartTimestamp = startTimestamp; + } else if ( + startTimestamp >= cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp + ) { + modifiedRequestStartTimestamp = cachedEventsEndTimestamp; + modifiedRequestEndTimestamp = endTimestamp; + cachedEventsEndTimestamp = endTimestamp; + } else { + return result; + } + } else { + cachedEventsStartTimestamp = modifiedRequestStartTimestamp; + cachedEventsEndTimestamp = modifiedRequestEndTimestamp; + } + + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + const cachePadding = Math.round( + currentlyDisplayedDateRange.length() * cacheSize, + ); + + allowedCacheStartTimestamp = startTimestamp - cachePadding; + allowedCacheEndTimestamp = endTimestamp + cachePadding; + + return mergePromiseElements( + getPromiseFunc( + getLuxonDateFromMillis(modifiedRequestStartTimestamp), + getLuxonDateFromMillis(modifiedRequestEndTimestamp), + ), + result, + ); + }; + + const addVisualForEventsLoading = (startTimestamp, endTimestamp) => { + allEvents.value.push({ + loading_id: 1, + type: "loading", + isostart: getISODateFromTimestamp(currentlyDisplayedDateRange.start.ts), + isoend: getISODateFromTimestamp(currentlyDisplayedDateRange.end.ts), + }); + }; + + const removeVisualForEventsLoading = (events) => { + return events.filter((event) => event.type !== "loading"); + }; + + const ensureEventsAreInValidCacheRange = (events) => { + events = events.filter((event) => { + const start = getTimestampFromISODate(event.isostart); + const end = getTimestampFromISODate(event.isoend); + return ( + allowedCacheStartTimestamp <= start && allowedCacheEndTimestamp >= end + ); + }); + + return events; + }; + + const ensureCacheRangeIsAllowedRange = ( + allowedCacheStartTimestamp, + allowedCacheEndTimestamp, + ) => { + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + const cachePadding = Math.round( + currentlyDisplayedDateRange.length() * cacheSize, + ); + + if ( + cachedEventsStartTimestamp < allowedCacheEndTimestamp && + cachedEventsEndTimestamp > allowedCacheStartTimestamp + ) { + if (cachedEventsStartTimestamp < allowedCacheStartTimestamp) { + cachedEventsStartTimestamp = allowedCacheStartTimestamp; + } + if (cachedEventsEndTimestamp > allowedCacheEndTimestamp) { + cachedEventsEndTimestamp = allowedCacheEndTimestamp; + } + } + }; + + return { events: allEvents, lv, reset }; +} + +const mergePromiseElements = (n, o) => { + if (Array.isArray(n)) return o.concat(n); + return (o.push(n), o); +}; + +const getDateFromTimestamp = (timestamp) => { + return luxon.DateTime.fromMillis(timestamp).toFormat("yyyy-MM-dd"); +}; + +const getISODateFromTimestamp = (timestamp) => { + return luxon.DateTime.fromMillis(timestamp).toISO(); +}; + +const getTimestampFromISODate = (isoDate) => { + return luxon.DateTime.fromISO(isoDate).ts; +}; + +const getLuxonDateFromMillis = (millis) => { + return luxon.DateTime.fromMillis(millis); +}; From 0df66047fed20017dcaf6c01ced715872997e8df Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Tue, 4 Aug 2026 17:38:24 +0200 Subject: [PATCH 07/18] BUG: Fix for irelavant req that falls in cache range --- public/js/components/Tempus/Tempus.js | 2 +- .../js/composables/Tempus/TempusEventLoader.js | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 168e89058..935210c72 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -1301,7 +1301,7 @@ export default { :mode="currentMode" :parkedEvents="parkedKeys" :visible-lecturers="visibleLecturerUids" - :cacheMultiplier="currentMode === 'week' ? 0 : 0" + :cacheMultiplier="currentMode === 'week' ? 1 : 0" @drop="dropHandler" @resize="resizeHandler" @update:date="handleChangeDate" diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js index 75d87985c..ea0d1a1c6 100644 --- a/public/js/composables/Tempus/TempusEventLoader.js +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -18,6 +18,12 @@ export function useEventLoader( let currentlyDisplayedDateRange; + const getCachePadding = () => { + const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); + + return Math.round(currentlyDisplayedDateRange.length() * cacheSize) + 1; + }; + const reload = () => { if ( currentlyDisplayedDateRange && @@ -144,10 +150,7 @@ export function useEventLoader( cachedEventsEndTimestamp = modifiedRequestEndTimestamp; } - const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); - const cachePadding = Math.round( - currentlyDisplayedDateRange.length() * cacheSize, - ); + const cachePadding = getCachePadding(); allowedCacheStartTimestamp = startTimestamp - cachePadding; allowedCacheEndTimestamp = endTimestamp + cachePadding; @@ -190,11 +193,6 @@ export function useEventLoader( allowedCacheStartTimestamp, allowedCacheEndTimestamp, ) => { - const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); - const cachePadding = Math.round( - currentlyDisplayedDateRange.length() * cacheSize, - ); - if ( cachedEventsStartTimestamp < allowedCacheEndTimestamp && cachedEventsEndTimestamp > allowedCacheStartTimestamp From a5715571e8297cc26b039282dbe7be931d82a63a Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 5 Aug 2026 08:56:26 +0200 Subject: [PATCH 08/18] BUG: Fix for issue with reload of modified events --- .../composables/Tempus/TempusEventLoader.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js index ea0d1a1c6..7e903e51e 100644 --- a/public/js/composables/Tempus/TempusEventLoader.js +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -4,8 +4,6 @@ export function useEventLoader( cacheMultiplier = 1, ) { let allEvents = Vue.ref([]); - const events = []; - const lv = Vue.ref(null); let newlyLoadedEvents = []; @@ -24,15 +22,16 @@ export function useEventLoader( return Math.round(currentlyDisplayedDateRange.length() * cacheSize) + 1; }; - const reload = () => { + const reload = (isCacheEnabled = true) => { if ( currentlyDisplayedDateRange && currentlyDisplayedDateRange?.start?.ts === rangeInterval?.value?.start?.ts && - currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts + currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts && + isCacheEnabled ) { console.log( - "cached range is the same as requested range, skipping reload", + "cached range is the same as requested range, skipping reload 2", ); return; } @@ -49,7 +48,8 @@ export function useEventLoader( if ( cachedEventsStartTimestamp === currentlyDisplayedDateRange.start.ts && - cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts + cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts && + isCacheEnabled ) { console.log( "cached range is the same as requested range, skipping reload", @@ -60,6 +60,7 @@ export function useEventLoader( const promises = requestEvents( currentlyDisplayedDateRange.start.ts, currentlyDisplayedDateRange.end.ts, + isCacheEnabled ); ensureCacheRangeIsAllowedRange( @@ -107,10 +108,11 @@ export function useEventLoader( Vue.watchEffect(reload); const reset = () => { - reload(); + allEvents.value = []; + reload(false); }; - const requestEvents = (startTimestamp, endTimestamp) => { + const requestEvents = (startTimestamp, endTimestamp, isCacheEnabled = true) => { let result = []; if (!startTimestamp || !endTimestamp) return result; @@ -119,7 +121,7 @@ export function useEventLoader( let modifiedRequestStartTimestamp = startTimestamp; let modifiedRequestEndTimestamp = endTimestamp; - if (cachedEventsStartTimestamp && cachedEventsEndTimestamp) { + if (cachedEventsStartTimestamp && cachedEventsEndTimestamp && isCacheEnabled) { if ( startTimestamp < cachedEventsStartTimestamp && endTimestamp > cachedEventsEndTimestamp @@ -145,7 +147,7 @@ export function useEventLoader( } else { return result; } - } else { + } else if (isCacheEnabled) { cachedEventsStartTimestamp = modifiedRequestStartTimestamp; cachedEventsEndTimestamp = modifiedRequestEndTimestamp; } From 1b23662a54c08e10cdde96c55d325a74f1bdb28f Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Thu, 6 Aug 2026 13:46:11 +0200 Subject: [PATCH 09/18] Add kalender column rename from eindeutige_gruppen_id to eindeutige_kalender_gruppen_id --- .../OperationalResourceToCalenderAPI.php | 4 ++-- .../controllers/system/MigrateKalender.php | 20 +++++++++---------- application/libraries/KalenderLib.php | 20 +++++++++---------- .../checks/ResourcesCollisionCheck.php | 6 +++--- .../models/ressource/Betriebsmittel_model.php | 2 +- .../models/ressource/Kalender_model.php | 2 +- .../Calendar/Base/Grid/Line/Event.js | 2 +- .../composables/Tempus/TempusEventLoader.js | 2 +- system/dbupdate_3.4.php | 1 + ...ge_kalender_gruppen_id_in_tbl_kalender.php | 16 +++++++++++++++ system/seeders/015_kalender.sql | 2 +- 11 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 system/dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php diff --git a/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php b/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php index 376a0fedc..f5a09deff 100644 --- a/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php +++ b/application/controllers/api/frontend/v1/tempus/OperationalResourceToCalenderAPI.php @@ -45,7 +45,7 @@ class OperationalResourceToCalenderAPI extends FHCAPI_Controller $result = $this->KalenderModel->loadWhere(['kalender_id' => $calenderID]); if (empty($result)) $this->terminateWithError("Calendar with id '$calenderID' not found"); - $calenderGroupID = $this->getDataOrTerminateWithError($result)[0]->eindeutige_gruppen_id; + $calenderGroupID = $this->getDataOrTerminateWithError($result)[0]->eindeutige_kalender_gruppen_id; if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id"); @@ -72,7 +72,7 @@ class OperationalResourceToCalenderAPI extends FHCAPI_Controller $calendar = $this->getDataOrTerminateWithError($result)[0]; - $calenderGroupID = $calendar->eindeutige_gruppen_id; + $calenderGroupID = $calendar->eindeutige_kalender_gruppen_id; if (!isset($calenderGroupID)) $this->terminateWithError("Calendar with id '$calenderID' does not have a valid group id"); $result = $this->kalenderlib->addOperationalResourcesToKalenderEvent($calendar, $assignedResources); diff --git a/application/controllers/system/MigrateKalender.php b/application/controllers/system/MigrateKalender.php index 49e895dbc..15b6d28ce 100644 --- a/application/controllers/system/MigrateKalender.php +++ b/application/controllers/system/MigrateKalender.php @@ -290,7 +290,7 @@ class MigrateKalender extends CLI_Controller $createHelperTypeQuery . "WITH test AS ( SELECT - tk.eindeutige_gruppen_id AS eindeutige_gruppen_id, + tk.eindeutige_kalender_gruppen_id AS eindeutige_kalender_gruppen_id, array_agg(tk.kalender_id), array_agg( ROW(tsb.betriebsmittel_id, tsb.insertamum, tsb.insertvon)::betriebsmittel_info @@ -298,7 +298,7 @@ class MigrateKalender extends CLI_Controller FROM sync.tbl_stundenplandev_kalender AS sk JOIN lehre.tbl_kalender tk ON tk.kalender_id = sk.kalender_id JOIN lehre.tbl_stundenplan_betriebsmittel tsb ON tsb.stundenplandev_id = sk.stundenplandev_id - GROUP BY tk.eindeutige_gruppen_id + GROUP BY tk.eindeutige_kalender_gruppen_id ) INSERT INTO lehre.tbl_betriebsmittel_kalender ( eindeutige_kalender_gruppen_id, @@ -307,7 +307,7 @@ class MigrateKalender extends CLI_Controller insertvon ) SELECT - t.eindeutige_gruppen_id, + t.eindeutige_kalender_gruppen_id, bm.betriebsmittel_id, bm.insertamum, bm.insertvon @@ -323,9 +323,9 @@ class MigrateKalender extends CLI_Controller $dbModel = new DB_Model(); $query = "UPDATE lehre.tbl_kalender - SET eindeutige_gruppen_id = gen_random_uuid() + SET eindeutige_kalender_gruppen_id = gen_random_uuid() WHERE vorgaenger_kalender_id IS NULL - AND eindeutige_gruppen_id IS NULL;"; + AND eindeutige_kalender_gruppen_id IS NULL;"; $dbModel->db->query($query); @@ -340,7 +340,7 @@ class MigrateKalender extends CLI_Controller kalender_id, vorgaenger_kalender_id, kalender_id AS root_id, - eindeutige_gruppen_id as root_eindeutige_gruppen_id + eindeutige_kalender_gruppen_id as root_eindeutige_kalender_gruppen_id FROM lehre.tbl_kalender WHERE vorgaenger_kalender_id IS NULL @@ -350,18 +350,18 @@ class MigrateKalender extends CLI_Controller i.kalender_id, i.vorgaenger_kalender_id, t.root_id, - t.root_eindeutige_gruppen_id + t.root_eindeutige_kalender_gruppen_id FROM lehre.tbl_kalender i JOIN tree t ON i.vorgaenger_kalender_id = t.kalender_id - where i.eindeutige_gruppen_id is NULL + where i.eindeutige_kalender_gruppen_id is NULL ) UPDATE lehre.tbl_kalender k - SET eindeutige_gruppen_id = t.root_eindeutige_gruppen_id + SET eindeutige_kalender_gruppen_id = t.root_eindeutige_kalender_gruppen_id FROM tree t WHERE k.kalender_id = t.kalender_id - AND k.eindeutige_gruppen_id IS NULL;"; + AND k.eindeutige_kalender_gruppen_id IS NULL;"; $dbModel->db->query($query); } diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 595a38229..756a59f4a 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -39,7 +39,7 @@ class KalenderLib $end_date = date('Y-m-d', strtotime($end_date . ' +1 day')); $this->_ci->KalenderModel->addSelect('tbl_kalender.kalender_id, - tbl_kalender.eindeutige_gruppen_id, + tbl_kalender.eindeutige_kalender_gruppen_id, tbl_kalender.status_kurzbz, tbl_kalender.typ, tbl_kalender.von, @@ -137,7 +137,7 @@ class KalenderLib $this->_ci->KalenderModel->db->where('tbl_kalender.von >=', $start_date); $this->_ci->KalenderModel->db->where('tbl_kalender.bis <', $end_date); - $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_gruppen_id', 'DESC'); + $this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_kalender_gruppen_id', 'DESC'); } private function _mapEvents($data, $collisionCheck = true, $maxDailyEventLimit = null) @@ -159,7 +159,7 @@ class KalenderLib $bis = new DateTime($row->bis); $events[$id] = (object) [ - 'eindeutige_gruppen_id' => $row->eindeutige_gruppen_id, + 'eindeutige_kalender_gruppen_id' => $row->eindeutige_kalender_gruppen_id, 'type' => $row->typ, 'beginn' => $von->format('H:i:s'), 'ende' => $bis->format('H:i:s'), @@ -461,7 +461,7 @@ class KalenderLib $this->_ci->KalenderModel->addSelect([ "( SELECT COUNT(*) FROM lehre.tbl_betriebsmittel_kalender - WHERE tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id + WHERE tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id ) AS has_assigned_resources" ]); @@ -662,7 +662,7 @@ class KalenderLib 'bis' => $end_date, 'typ' => 'lehreinheit', 'status_kurzbz' => 'planning', - 'eindeutige_gruppen_id' => $this->_ci->KalenderModel->generateUniqueGroupId(), + 'eindeutige_kalender_gruppen_id' => $this->_ci->KalenderModel->generateUniqueGroupId(), 'insertvon' => getAuthUID(), 'insertamum' => date('Y-m-d H:i:s') ) @@ -1062,19 +1062,19 @@ class KalenderLib "; $params = array_merge( - [$calendar->eindeutige_gruppen_id], + [$calendar->eindeutige_kalender_gruppen_id], $allowedResourceIDs ); $this->_ci->db->query($query, $params); } else { - $this->_ci->db->where('eindeutige_kalender_gruppen_id', $calendar->eindeutige_gruppen_id); + $this->_ci->db->where('eindeutige_kalender_gruppen_id', $calendar->eindeutige_kalender_gruppen_id); $this->_ci->db->delete('lehre.tbl_betriebsmittel_kalender'); } foreach ($assignedResources as $assignedResource) { $data = [ - 'eindeutige_kalender_gruppen_id' => $calendar->eindeutige_gruppen_id, + 'eindeutige_kalender_gruppen_id' => $calendar->eindeutige_kalender_gruppen_id, 'betriebsmittel_id' => $assignedResource['betriebsmittel_id'], 'anmerkung' => $assignedResource['anmerkung'] ?? null, 'quelle' => 'tempus_neu', @@ -1195,7 +1195,7 @@ class KalenderLib $kalender_entry = getData($entryResult); - $calendarResources = $this->_ci->BetriebsmittelKalenderModel->loadWhere(['eindeutige_kalender_gruppen_id' => $kalender_entry->eindeutige_gruppen_id]); + $calendarResources = $this->_ci->BetriebsmittelKalenderModel->loadWhere(['eindeutige_kalender_gruppen_id' => $kalender_entry->eindeutige_kalender_gruppen_id]); if (isError($calendarResources)) return $calendarResources; $calendarResourcesItems = getData($calendarResources); @@ -1354,7 +1354,7 @@ class KalenderLib 'typ' => 'lehreinheit', 'status_kurzbz' => 'planning', 'vorgaenger_kalender_id' => $old_id, - 'eindeutige_gruppen_id' => $kalender_entry->eindeutige_gruppen_id, + 'eindeutige_kalender_gruppen_id' => $kalender_entry->eindeutige_kalender_gruppen_id, 'insertvon' => getAuthUID(), 'insertamum' => date('Y-m-d H:i:s') ) diff --git a/application/libraries/collision/checks/ResourcesCollisionCheck.php b/application/libraries/collision/checks/ResourcesCollisionCheck.php index 0be789b7f..0c957965d 100644 --- a/application/libraries/collision/checks/ResourcesCollisionCheck.php +++ b/application/libraries/collision/checks/ResourcesCollisionCheck.php @@ -28,7 +28,7 @@ class ResourcesCollisionCheck implements ICollisionCheck if ($this->_ci->variablelib->getVar('ignore_resources_collisions') === 'true') return []; $this->_ci->KalenderModel->addSelect('kalender_id, von, bis, bm.beschreibung AS betriebsmittel_beschreibung'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender bk', 'bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender bk', 'bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id'); $this->_ci->KalenderModel->addJoin('wawi.tbl_betriebsmittel bm', 'bm.betriebsmittel_id = bk.betriebsmittel_id'); $this->_ci->KalenderModel->db->where('tbl_kalender.kalender_id !=', $data->kalender_id); @@ -59,9 +59,9 @@ class ResourcesCollisionCheck implements ICollisionCheck if (empty($kalender_ids)) return []; $this->_ci->KalenderModel->addSelect('DISTINCT ON (tbl_kalender.kalender_id) tbl_kalender.kalender_id'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender current_bk', 'current_bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_gruppen_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender current_bk', 'current_bk.eindeutige_kalender_gruppen_id = tbl_kalender.eindeutige_kalender_gruppen_id'); $this->_ci->KalenderModel->addJoin('lehre.tbl_betriebsmittel_kalender other_bk', 'other_bk.betriebsmittel_id = current_bk.betriebsmittel_id'); - $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.eindeutige_gruppen_id = other_bk.eindeutige_kalender_gruppen_id'); + $this->_ci->KalenderModel->addJoin('lehre.tbl_kalender other_kalender', 'other_kalender.eindeutige_kalender_gruppen_id = other_bk.eindeutige_kalender_gruppen_id'); $this->_ci->KalenderModel->db->where('other_kalender.kalender_id != tbl_kalender.kalender_id', null, false); $this->_ci->KalenderModel->db->where('other_kalender.von < tbl_kalender.bis', null, false); diff --git a/application/models/ressource/Betriebsmittel_model.php b/application/models/ressource/Betriebsmittel_model.php index cf974086d..2d41754a7 100644 --- a/application/models/ressource/Betriebsmittel_model.php +++ b/application/models/ressource/Betriebsmittel_model.php @@ -43,7 +43,7 @@ class Betriebsmittel_model extends DB_Model AND NOT EXISTS( SELECT 1 FROM lehre.tbl_betriebsmittel_kalender - JOIN lehre.tbl_kalender ON tbl_kalender.eindeutige_gruppen_id = tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id + JOIN lehre.tbl_kalender ON tbl_kalender.eindeutige_kalender_gruppen_id = tbl_betriebsmittel_kalender.eindeutige_kalender_gruppen_id WHERE betriebsmittel_id=tbl_betriebsmittel.betriebsmittel_id AND tbl_kalender.von < ? diff --git a/application/models/ressource/Kalender_model.php b/application/models/ressource/Kalender_model.php index d538b7313..e460beef0 100644 --- a/application/models/ressource/Kalender_model.php +++ b/application/models/ressource/Kalender_model.php @@ -16,7 +16,7 @@ class Kalender_model extends DB_Model { while (true) { $uniqueGroupId = generateUUID(); - $result = $this->loadWhere(['eindeutige_gruppen_id' => $uniqueGroupId]); + $result = $this->loadWhere(['eindeutige_kalender_gruppen_id' => $uniqueGroupId]); if (!hasData($result)) { return $uniqueGroupId; } diff --git a/public/js/components/Calendar/Base/Grid/Line/Event.js b/public/js/components/Calendar/Base/Grid/Line/Event.js index 583d0f9f0..bd2883b49 100644 --- a/public/js/components/Calendar/Base/Grid/Line/Event.js +++ b/public/js/components/Calendar/Base/Grid/Line/Event.js @@ -148,7 +148,7 @@ export default { v-cal-click:event="isHeaderOrFooter ? event : event.orig" @contextmenu.prevent="onRightClick" :data-id="'event-' + event.orig.kalender_id" - :data-group-id="'event-group-' + event.orig.eindeutige_gruppen_id" + :data-group-id="'event-group-' + event.orig.eindeutige_kalender_gruppen_id" data-cy="calendar-event" >
[ - event.eindeutige_gruppen_id, + event.eindeutige_kalender_gruppen_id, event, ]), ).values(), diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php index 66e3f8723..8611d9ed5 100644 --- a/system/dbupdate_3.4.php +++ b/system/dbupdate_3.4.php @@ -100,6 +100,7 @@ require_once('dbupdate_3.4/76150_perm_other_lv_plan.php'); require_once('dbupdate_3.4/68957_dashboard_bookmark_neue_Spalte_sort.php'); require_once('dbupdate_3.4/68530_Dashboard_Cleanup.php'); require_once('dbupdate_3.4/77375_ressourcen.php'); +require_once('dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php'); // *** Pruefung und hinzufuegen der neuen Attribute und Tabellen echo '

Pruefe Tabellen und Attribute!

'; diff --git a/system/dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php b/system/dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php new file mode 100644 index 000000000..b45460f4e --- /dev/null +++ b/system/dbupdate_3.4/78318_rename_column_of_eindeutige_gruppen_id_to_eindeutige_kalender_gruppen_id_in_tbl_kalender.php @@ -0,0 +1,16 @@ +db_query("SELECT 1 FROM information_schema.columns WHERE table_schema = 'lehre' AND table_name = 'tbl_kalender' AND column_name = 'eindeutige_gruppen_id'"); +if($db->db_num_rows($result) === 1) +{ + + $qry = "ALTER TABLE lehre.tbl_kalender + RENAME COLUMN eindeutige_gruppen_id TO eindeutige_kalender_gruppen_id"; + + if(!$db->db_query($qry)) + echo 'lehre.tbl_kalender: '.$db->db_last_error().'
'; + else + echo '
lehre.tbl_kalender column eindeutige_gruppen_id renamed to eindeutige_kalender_gruppen_id'; + +} \ No newline at end of file diff --git a/system/seeders/015_kalender.sql b/system/seeders/015_kalender.sql index 345e003a3..2ba23c754 100644 --- a/system/seeders/015_kalender.sql +++ b/system/seeders/015_kalender.sql @@ -161,7 +161,7 @@ VALUES -- Calendar setup for resources tests INSERT INTO lehre.tbl_kalender -(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_gruppen_id, insertamum, insertvon, updateamum, updatevon) +(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_kalender_gruppen_id, insertamum, insertvon, updateamum, updatevon) VALUES (date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '19:30:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '20:15:00', 'lehreinheit', 'live', NULL, 'ec1bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'), (date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '20:15:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '21:00:00', 'lehreinheit', 'live', NULL, 'ec2bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'), From 1bbe4368bb783845ee7cca897bb6d1c04e9333fe Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 22 Jun 2026 15:06:53 +0200 Subject: [PATCH 10/18] First cherry pick of modifing event loading --- application/libraries/KalenderLib.php | 2 +- public/css/Tempus.css | 66 ++++ .../js/components/Calendar/Base/Grid/Line.js | 36 +-- .../Calendar/Base/Grid/Line/Event.js | 86 ++--- public/js/components/Tempus/Tempus.js | 300 +++++++++++------- public/js/composables/EventLoader.js | 39 ++- 6 files changed, 343 insertions(+), 186 deletions(-) diff --git a/application/libraries/KalenderLib.php b/application/libraries/KalenderLib.php index 8d5aa1a9a..02f5cf4a9 100644 --- a/application/libraries/KalenderLib.php +++ b/application/libraries/KalenderLib.php @@ -159,6 +159,7 @@ class KalenderLib $bis = new DateTime($row->bis); $events[$id] = (object) [ + 'kalender_id' => $id, 'eindeutige_kalender_gruppen_id' => $row->eindeutige_kalender_gruppen_id, 'type' => $row->typ, 'beginn' => $von->format('H:i:s'), @@ -175,7 +176,6 @@ class KalenderLib 'farbe' => isset($row->farbe) ? $row->farbe : '', 'lehrveranstaltung_id' => $row->lehrveranstaltung_id, 'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '', - 'kalender_id' => $id, 'lehreinheit_id' => [], 'lektor' => [], 'teilnehmer_gruppe' => [], diff --git a/public/css/Tempus.css b/public/css/Tempus.css index 3e32789e1..72384c9c7 100644 --- a/public/css/Tempus.css +++ b/public/css/Tempus.css @@ -185,3 +185,69 @@ body { } +.updated-event { + position: relative; + z-index: 10; + animation: modernFocus 1s ease-out; +} + +.updated-event-long { + position: relative; + z-index: 10; + animation: modernFocus 1.8s ease-out; +} + +@keyframes modernFocus { + 0% { + background-color: #cfd4d8; + box-shadow: + 0 0 0 0 rgba(120, 120, 120, 0.8), + 0 0 0 0 rgba(255, 255, 255, 0.6); + } + + 30% { + background-color: #eef1f3; + box-shadow: + 0 0 0 8px rgba(120, 120, 120, 0.25), + 0 0 30px 8px rgba(255, 255, 255, 0.7); + } + + 60% { + background-color: #f7f8f9; + box-shadow: + 0 0 0 14px rgba(120, 120, 120, 0.15), + 0 0 45px 12px rgba(255, 255, 255, 0.5); + } + + 100% { + background-color: inherit; + box-shadow: + 0 0 0 24px rgba(120, 120, 120, 0), + 0 0 60px 20px rgba(255, 255, 255, 0); + } +} + +.deemphasized-event { + animation: deEmphasize 1s ease-out; +} + +.deemphasized-event-long { + animation: deEmphasize 1.8s ease-out; +} + +@keyframes deEmphasize { + 0% { + opacity: 1; + filter: saturate(1) brightness(1); + } + + 30% { + opacity: 0.55; + filter: saturate(0.4) brightness(0.4); + } + + 100% { + opacity: 1; + filter: saturate(1) brightness(1); + } +} \ No newline at end of file diff --git a/public/js/components/Calendar/Base/Grid/Line.js b/public/js/components/Calendar/Base/Grid/Line.js index 4b0f6525e..7f70fd096 100644 --- a/public/js/components/Calendar/Base/Grid/Line.js +++ b/public/js/components/Calendar/Base/Grid/Line.js @@ -1,5 +1,5 @@ -import LineEvent from "./Line/Event.js"; -import LineBackground from "./Line/Background.js"; +import LineEvent from './Line/Event.js'; +import LineBackground from './Line/Background.js'; /** * TODO(chris): @@ -7,19 +7,19 @@ import LineBackground from "./Line/Background.js"; */ export default { - name: "GridLine", + name: 'GridLine', components: { LineEvent, LineBackground, }, inject: { - axisRow: "axisRow", + axisRow: 'axisRow', shouldCompactEvents: { - from: "shouldCompactEvents", + from: 'shouldCompactEvents', default: undefined, }, compactibleEventTypes: { - from: "compactibleEventTypes", + from: 'compactibleEventTypes', default: undefined, }, }, @@ -50,19 +50,20 @@ export default { let formattedEvents = this.events.map((event) => { event.rows = [1, -1]; if (event.startsHere) { - event.rows[0] = - "t_" + event.start.diff(this.date).toMillis(); + event.rows[0] = 't_' + event.start.diff(this.date).toMillis(); } if (event.endsHere) { - event.rows[1] = "t_" + event.end.diff(this.date).toMillis(); + event.rows[1] = 't_' + event.end.diff(this.date).toMillis(); } return event; }); if (this.shouldCompactEvents && this.compactibleEventTypes?.length) { - formattedEvents = - this.compactEvents(formattedEvents, this.compactibleEventTypes); + formattedEvents = this.compactEvents( + formattedEvents, + this.compactibleEventTypes, + ); } return formattedEvents; @@ -71,12 +72,9 @@ export default { methods: { compactEvents(events, compactibleEventTypes) { let formattedEvents = events - .filter( - (event) => - !compactibleEventTypes.includes(event.type), - ) + .filter((event) => !compactibleEventTypes.includes(event.type)) .map((event) => { - event.display = "default"; + event.display = 'default'; return event; }); let eventsToBeCompacted = events.filter((event) => @@ -110,17 +108,17 @@ export default { compactedEvents.forEach((compactedEvent) => { if (compactedEvent.events.length < 4) { formattedEvents.push({ - display: "compacted", + display: 'compacted', ...compactedEvent, }); } else { formattedEvents.push({ - display: "compacted", + display: 'compacted', events: compactedEvent.events.slice(0, 3), rows: compactedEvent.rows, }); formattedEvents.push({ - display: "compactedExtra", + display: 'compactedExtra', events: compactedEvent.events.slice(3), rows: compactedEvent.rows, }); diff --git a/public/js/components/Calendar/Base/Grid/Line/Event.js b/public/js/components/Calendar/Base/Grid/Line/Event.js index 04d733583..4a2ff4f7c 100644 --- a/public/js/components/Calendar/Base/Grid/Line/Event.js +++ b/public/js/components/Calendar/Base/Grid/Line/Event.js @@ -3,72 +3,74 @@ import drop from '../../../../../directives/drop.js'; import CalClick from '../../../../../directives/Calendar/Click.js'; export default { - name: "GridLineEvent", + name: 'GridLineEvent', directives: { draggable, drop, - CalClick + CalClick, }, - emits: [ - 'resize-start' - ], + emits: ['resize-start'], data() { return { contextMenu: { show: false, x: 0, - y: 0 - } + y: 0, + }, }; }, inject: { - draggableEvents: "draggableEvents", + draggableEvents: 'draggableEvents', resizableEvents: { - from: "resizableEvents", - default: () => () => false + from: 'resizableEvents', + default: () => () => false, }, - mode: "mode", + mode: 'mode', contextMenuActions: { - from: "contextMenuActions", - default: () => ({}) + from: 'contextMenuActions', + default: () => ({}), }, onDrop: { - from: "onDrop", - default: () => null + from: 'onDrop', + default: () => null, }, onDropEvent: { - from: "onDropEvent", - default: () => () => {} - } + from: 'onDropEvent', + default: () => () => {}, + }, }, props: { event: { type: Object, required: true, validator(value) { - return (value.start && value.end && value.orig); - } - } + return value.start && value.end && value.orig; + }, + }, }, computed: { isHeaderOrFooter() { return ['header', 'footer'].includes(this.event.orig); }, draggable() { - return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode); + return ( + !this.isHeaderOrFooter && + this.draggableEvents(this.event.orig, this.mode) + ); }, resizable() { - return !this.isHeaderOrFooter && this.resizableEvents(this.event.orig, this.mode); + return ( + !this.isHeaderOrFooter && + this.resizableEvents(this.event.orig, this.mode) + ); }, classes() { const classes = []; if (this.isHeaderOrFooter) { classes.push('event-' + this.event.orig); } else { - if (this.event.startsHere) - classes.push('event-begin'); - if (this.event.endsHere) - classes.push('event-end'); + if (this.event.startsHere) classes.push('event-begin'); + if (this.event.endsHere) classes.push('event-end'); } classes.push(`calender_id-${this.event.orig.kalender_id}`); @@ -85,9 +87,14 @@ export default { activeContextActions() { if (this.isHeaderOrFooter) return []; const type = this.event.orig?.type ?? 'lehreinheit'; - const actions = this.contextMenuActions[type] ?? this.contextMenuActions['default'] ?? []; - return actions.filter(action => !action.visible || action.visible(this.event.orig)); - } + const actions = + this.contextMenuActions[type] ?? + this.contextMenuActions['default'] ?? + []; + return actions.filter( + (action) => !action.visible || action.visible(this.event.orig), + ); + }, }, methods: { onResizeStart(edge, evt) { @@ -95,7 +102,7 @@ export default { edge, evt, el: this.$refs.eventEl, - event: this.event + event: this.event, }); }, onRightClick(evt) { @@ -116,27 +123,24 @@ export default { evt.dataTransfer.setData('fhc-grab-offset-x', evt.clientX - rect.left); }, onDropOnCard(evt, items) { - if (this.isHeaderOrFooter || !this.onDrop) - return; + if (this.isHeaderOrFooter || !this.onDrop) return; const list = Array.isArray(items) ? items : [items]; const obj = list[0]; - if (!obj) - return; + if (!obj) return; - if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit') - { + if ((evt.ctrlKey || evt.metaKey) && obj.type === 'lehreinheit') { return this.onDrop({ item: [obj], ctrlKey: true, - targetKalenderId: this.event.orig?.kalender_id ?? null + targetKalenderId: this.event.orig?.kalender_id ?? null, }); } return this.onDropEvent(evt, items, this.event.start.startOf('day')); }, }, - template:` + template: `
- ` -} \ No newline at end of file + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index cfbdf22f7..9495dfda0 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -14,43 +14,43 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import CoreSearchbar from "../searchbar/searchbar.js"; -import NavLanguage from "../navigation/Language.js"; -import VerticalSplit from "../verticalsplit/verticalsplit.js"; -import FhcCalendar from "../Calendar/Tempus.js"; -import FhcCoursepicker from "./Coursepicker.js"; -import LectureSelection from "./Filters/LectureSelection.js"; -import VerbandSelection from "./Filters/VerbandSelection.js"; -import RoomSelection from "./Filters/RoomSelection.js"; -import ParkingSlot from "./ParkingSlot.js"; -import ApiKalender from "../../api/factory/tempus/kalender.js"; -import ApiSearchbar from "../../api/factory/searchbar.js"; -import ApiRenderers from "../../api/factory/renderers.js"; -import ApiTempusConfig from "../../api/factory/tempus/config.js"; -import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js"; -import AppMenu from "../AppMenu.js"; -import drop from "../../directives/drop.js"; -import AppConfig from "../AppConfig.js"; +import CoreSearchbar from '../searchbar/searchbar.js'; +import NavLanguage from '../navigation/Language.js'; +import VerticalSplit from '../verticalsplit/verticalsplit.js'; +import FhcCalendar from '../Calendar/Tempus.js'; +import FhcCoursepicker from './Coursepicker.js'; +import LectureSelection from './Filters/LectureSelection.js'; +import VerbandSelection from './Filters/VerbandSelection.js'; +import RoomSelection from './Filters/RoomSelection.js'; +import ParkingSlot from './ParkingSlot.js'; +import ApiKalender from '../../api/factory/tempus/kalender.js'; +import ApiSearchbar from '../../api/factory/searchbar.js'; +import ApiRenderers from '../../api/factory/renderers.js'; +import ApiTempusConfig from '../../api/factory/tempus/config.js'; +import ApiOperationalResourceToCalender from '../../api/factory/operationalResourceToCalender.js'; +import AppMenu from '../AppMenu.js'; +import drop from '../../directives/drop.js'; +import AppConfig from '../AppConfig.js'; -import BsModal from "../Bootstrap/Modal.js"; +import BsModal from '../Bootstrap/Modal.js'; import BaseTreemenu from '../Base/Treemenu.js'; -import ApiStudiengangTree from "../../api/factory/tempus/studiengangtree.js"; -import ApiInfo from "../../api/factory/tempus/info.js"; -import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js"; -import FormInput from "../../../js/components/Form/Input.js"; -import Reservierung from "./Reservierung.js"; -import { getTempusShortcuts } from "./shortcuts.js"; -import KeyboardShortcuts from "./KeyboardShortcuts.js"; -import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js"; -import MultiWeekPlanModal from "./MultiWeekPlanModal.js"; -import { getTempusSearchbarOptions } from "./Filters/searchbarOptions.js"; -import RaumauswahlModal from "./RaumauswahlModal.js"; -import LehreinheitModal from "./LehreinheitModal.js"; +import ApiStudiengangTree from '../../api/factory/tempus/studiengangtree.js'; +import ApiInfo from '../../api/factory/tempus/info.js'; +import StvStudiensemester from '../Stv/Studentenverwaltung/Studiensemester.js'; +import FormInput from '../../../js/components/Form/Input.js'; +import Reservierung from './Reservierung.js'; +import { getTempusShortcuts } from './shortcuts.js'; +import KeyboardShortcuts from './KeyboardShortcuts.js'; +import { useContextMenuActions } from '../../composables/Tempus/ContextMenuActions.js'; +import MultiWeekPlanModal from './MultiWeekPlanModal.js'; +import { getTempusSearchbarOptions } from './Filters/searchbarOptions.js'; +import RaumauswahlModal from './RaumauswahlModal.js'; +import LehreinheitModal from './LehreinheitModal.js'; export default { - name: "Tempus", + name: 'Tempus', components: { CoreSearchbar, VerticalSplit, @@ -72,7 +72,7 @@ export default { KeyboardShortcuts, MultiWeekPlanModal, RaumauswahlModal, - LehreinheitModal + LehreinheitModal, }, props: { defaultSemester: String, @@ -98,7 +98,8 @@ export default { contextMenuActions: useContextMenuActions({ openRaumauswahl: (orig) => this.$refs.raumModal.show(orig), openLehreinheit: (orig) => this.$refs.lehreinheitModal.show(orig), - openResourcesAssignmentModal: (orig) => this.openResourcesAssignmentModal(orig), + openResourcesAssignmentModal: (orig) => + this.openResourcesAssignmentModal(orig), openHistory: (orig) => this.openHistory(orig), deleteEntry: (orig) => this.deleteEntry(orig), syncToLecturer: (orig) => this.syncToLecturer(orig), @@ -113,7 +114,7 @@ export default { data() { return { appconfig: {}, - currentMode: "week", + currentMode: 'week', configEndpoints: ApiTempusConfig, endpoint: ApiStudiengangTree, raumVorschlaege: [], @@ -133,7 +134,7 @@ export default { }, renderers: null, ort_kurzbz: null, - view: "room", + view: 'room', parkedKeys: new Set(), lecturers: [], studiengaenge: [], @@ -146,14 +147,14 @@ export default { studiensemester_kurzbz: null, visibleStatusArray: {}, - visibleStatus: ["all"], + visibleStatus: ['all'], selectedStudiensemester: this.studiensemester_kurzbz ?? this.defaultSemester, calendarDate: luxon.DateTime.now() .setZone(this.config.timezone) .toISODate(), historyEntries: [], - previewRole: "planer", + previewRole: 'planer', multiWeekModal: { show: false, lehreinheitId: null, @@ -170,6 +171,7 @@ export default { assignedResources: [], areFormButtonsDisplayed: false, }, + currentlyUpdatedEvent: null, }; }, computed: { @@ -189,9 +191,9 @@ export default { })); }, visibleStatusValue() { - if (this.visibleStatus.includes("all")) + if (this.visibleStatus.includes('all')) return this.visibleStatusOptions.filter( - (visibleStatus) => visibleStatus.key === "all", + (visibleStatus) => visibleStatus.key === 'all', ); return this.visibleStatus.map((status) => ({ key: status, @@ -287,7 +289,6 @@ export default { }, onSelectVerband({ path, stg_kz, semester, orgform_kurzbz, name }) { - let exists = this.studiengaenge.some( (stg) => stg.stg_kz == stg_kz && @@ -321,12 +322,12 @@ export default { if (this.lastRange) this.handleRange(this.lastRange); }, addToFilter: function (filter, type) { - if (type === "ort") { + if (type === 'ort') { const ort_kurzbz = filter.ort_kurzbz; if (!this.rooms.some((room) => room.ort_kurzbz === ort_kurzbz)) { this.rooms.push({ ort_kurzbz }); } - } else if (type === "mitarbeiter") { + } else if (type === 'mitarbeiter') { const uid = filter.uid; const label = filter.name; if (!this.lecturers.some((l) => l.uid === uid)) { @@ -371,83 +372,148 @@ export default { .call(ApiTempusConfig.updateCollision()) .then(() => { this.$refs.config.reload(); - this.$fhcAlert.alertSuccess(this.$p.t("ui/settings_saved")); + this.$fhcAlert.alertSuccess(this.$p.t('ui/settings_saved')); }) .catch(this.$fhcAlert.handleSystemErrors); }, toggleStatus(selected) { if (!selected || selected.length === 0) { - this.visibleStatus = ["all"]; + this.visibleStatus = ['all']; return; } - const hasAll = selected.includes("all"); - const hadAll = this.visibleStatus.includes("all"); + const hasAll = selected.includes('all'); + const hadAll = this.visibleStatus.includes('all'); if (hasAll && !hadAll) { - this.visibleStatus = ["all"]; + this.visibleStatus = ['all']; return; } - this.visibleStatus = selected.filter((k) => k !== "all"); - if (this.visibleStatus.length === 0) this.visibleStatus = ["all"]; + this.visibleStatus = selected.filter((k) => k !== 'all'); + if (this.visibleStatus.length === 0) this.visibleStatus = ['all']; }, searchfunction(params) { return this.$api.call(ApiSearchbar.search(params)); }, getPromiseFunc(start, end) { - const hasRooms = this.rooms.length > 0; + const hasRoom = !!this.ort_kurzbz; const hasLektoren = this.lecturers.length > 0; - const hasStg = this.studiengaenge.length > 0; + const hasStg = !!this.stg; 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); + if (hasRoom) filter.ort = this.ort_kurzbz; + if (hasStg) filter.stg = this.stg; + if (hasLektoren) filter.uid = this.lecturers.map((l) => l.uid); - if (this.previewRole === "lektor") - return [ + let response = null; + if (this.previewRole === 'lektor') + response = [ this.$api.call( ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()), ), ]; - if (this.previewRole === "student") - return [ + if (this.previewRole === 'student') + response = [ this.$api.call( ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()), ), ]; - return [ + response = [ this.$api.call( ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()), ), ]; + + if (response) { + response[0].then((result) => { + if (!this.currentlyUpdatedEvent) return; + + document.querySelectorAll('.updated-event').forEach((el) => { + el.classList.remove('updated-event'); + }); + document.querySelectorAll('.updated-event-long').forEach((el) => { + el.classList.remove('updated-event-long'); + }); + document.querySelectorAll('.deemphasized-event').forEach((el) => { + el.classList.remove('deemphasized-event'); + }); + document + .querySelectorAll('.deemphasized-event-long') + .forEach((el) => { + el.classList.remove('deemphasized-event-long'); + }); + + setTimeout(() => { + const eventEl = document.querySelector( + `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_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'; + let deemphasizedUpdateClassName = isInsideScrolledView + ? 'deemphasized-event' + : 'deemphasized-event-long'; + + if (!isInsideScrolledView) timeout = 500; + + setTimeout(() => { + eventEl.classList.add(emphasizeUpdateClassName); + document + .querySelectorAll('.fhc-calendar-base-grid-line-event') + .forEach((el) => { + if (el !== eventEl) { + el.classList.add(deemphasizedUpdateClassName); + } + }); + }, timeout); + + this.currentlyUpdatedEvent = null; + }, 100); + }); + } + + return response; }, toDateTime(value, timezone) { if (luxon.DateTime.isDateTime(value)) return value; if (value?.date?.isValid) return value.date; - if (typeof value === "number") + if (typeof value === 'number') return luxon.DateTime.fromMillis(value, { zone: timezone }); if (value instanceof Date) return luxon.DateTime.fromJSDate(value, { zone: timezone }); - if (typeof value === "string") + if (typeof value === 'string') return luxon.DateTime.fromISO(value, { zone: timezone }); - return luxon.DateTime.invalid("invalid datetime"); + return luxon.DateTime.invalid('invalid datetime'); }, getLastEndOfSameDay(startDT, ends) { if (!ends?.length) return null; @@ -483,15 +549,15 @@ export default { const endDT = luxon.DateTime.fromISO(end); if (!startDT.isValid || !endDT.isValid) { - alert("Ungültiges Datum"); + 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"), + start_time: startDT.toFormat('yyyy-MM-dd HH:mm'), + end_time: endDT.toFormat('yyyy-MM-dd HH:mm'), }; }, @@ -508,7 +574,7 @@ export default { const updatedInfos = { ort_kurzbz: this.rooms.length ? this.rooms.map((room) => room.ort_kurzbz) - : obj.orig.ort_kurzbz ?? [], + : (obj.orig.ort_kurzbz ?? []), start_time, end_time, }; @@ -518,18 +584,22 @@ export default { ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos), ) .then(() => { - if (onSuccess) onSuccess(); + if (onSuccess) { + onSuccess(); + this.$refs.calendar.$refs.calendar.$refs.mode.$refs.view.$refs.grid.disableAutoScroll(); + this.currentlyUpdatedEvent = obj.orig; + } }); }, resizeHandler(payload) { - if (this.previewRole !== "planer") + if (this.previewRole !== 'planer') //TODO (david) testzweck return; const { item, start, end } = payload; const obj = item[0]; if (!obj?.orig?.kalender_id) - return alert("Kein gültiges Kalender-Event zum Resizen"); + return alert('Kein gültiges Kalender-Event zum Resizen'); const dates = this._parseDates(start, end); @@ -549,19 +619,19 @@ export default { }, dropHandler(payload) { - if (this.previewRole !== "planer") + if (this.previewRole !== 'planer') //TODO (david) testzweck return; const { item, start, end } = payload; - if (!item?.length) return alert("Keine Daten gedroppt"); + if (!item?.length) return alert('Keine Daten gedroppt'); const obj = item[0]; - if (!obj?.type) return alert("Unbekannter Drop-Typ"); + if (!obj?.type) return alert('Unbekannter Drop-Typ'); if (payload.targetKalenderId) { - if (obj.type !== "lehreinheit" || !obj.orig?.lehreinheit_id) + if (obj.type !== 'lehreinheit' || !obj.orig?.lehreinheit_id) return alert( - "Nur Lehreinheiten können einem Termin hinzugefügt werden", + 'Nur Lehreinheiten können einem Termin hinzugefügt werden', ); return this.$api @@ -573,7 +643,7 @@ export default { ) .then(() => { this.$refs.calendar.resetEventLoader(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }); } @@ -582,10 +652,10 @@ export default { const { startDT, endDT, start_time, end_time } = dates; - if (obj.type === "reservierung") { + if (obj.type === 'reservierung') { this.reservierungPending = true; this.$refs.reservierung.show(start_time, end_time); - } else if (obj.type === "lehreinheit") { + } else if (obj.type === 'lehreinheit') { if (obj.multiweek) { this.openMultiWeekPreview( obj.orig.lehreinheit_id, @@ -602,7 +672,7 @@ export default { obj.orig.lehreinheit_id, this.rooms.length ? this.rooms.map((r) => r.ort_kurzbz) - : obj.orig.ort_kurzbz ?? [], + : (obj.orig.ort_kurzbz ?? []), start_time, end_time, ), @@ -611,9 +681,9 @@ export default { .then((result) => { this.$refs.calendar.resetEventLoader(); this.$refs.coursepicker.reload(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }); - } else if (obj.type === "kalender") { + } else if (obj.type === 'kalender') { return this._updateKalenderEvent( obj, startDT, @@ -626,11 +696,11 @@ export default { id: obj.orig.kalender_id, }); this.$refs.calendar.resetEventLoader(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }, ); } else { - alert("Unbekannter Drop-Typ: " + obj.type); + alert('Unbekannter Drop-Typ: ' + obj.type); } }, openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) { @@ -646,17 +716,17 @@ export default { onMultiWeekConfirmed() { this.$refs.calendar.resetEventLoader(); this.$refs.coursepicker.reload(); - this.bcc.postMessage("dropped"); + this.bcc.postMessage('dropped'); }, handleRange(range) { if (!range?.start || !range?.end) return; - if (this.currentMode === "week") { + if (this.currentMode === 'week') { //Workaround because, updateRange is emitting 2 times - const startDay = range.start.startOf("day"); - const endDay = range.end.startOf("day"); + const startDay = range.start.startOf('day'); + const endDay = range.end.startOf('day'); - const days = Math.round(endDay.diff(startDay, "days").days) + 1; + const days = Math.round(endDay.diff(startDay, 'days').days) + 1; if (days > 8) return; } @@ -764,20 +834,20 @@ export default { calenderId, ), ); - if (getAssignedResources.meta.status === "success") { + if (getAssignedResources.meta.status === 'success') { return getAssignedResources.data .filter((unit) => !!unit) .map((unit) => { return { isNoteTextareaShown: - unit.anmerkung && unit.anmerkung.trim() !== "", + unit.anmerkung && unit.anmerkung.trim() !== '', ...unit, }; }) .filter((unit) => !!unit); } else { this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_fetch_error_message"), + this.$p.t('ui', 'failed_assigned_resources_fetch_error_message'), ); } @@ -789,11 +859,11 @@ export default { calendarID, ), ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { + if (getSchedulableResourcesByCalendar.meta.status === 'success') { return getSchedulableResourcesByCalendar.data; } else { this.$fhcAlert.alertError( - this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"), + this.$p.t('ui', 'failed_schedulable_resources_fetch_error_message'), ); } @@ -830,7 +900,7 @@ export default { if (index !== -1) { this.resourcesAssignmentModal.assignedResources[ index - ].isNoteTextareaShown = + ].isNoteTextareaShown = !this.resourcesAssignmentModal.assignedResources[index] .isNoteTextareaShown; } @@ -867,14 +937,14 @@ export default { assignedResources, ), ); - if (getSchedulableResourcesByCalendar.meta.status === "success") { + if (getSchedulableResourcesByCalendar.meta.status === 'success') { this.$fhcAlert.alertSuccess( - this.$p.t("ui", "assigned_resources_save_success_message"), + this.$p.t('ui', 'assigned_resources_save_success_message'), ); await this.refreshResourcesAssignmentModalData(calenderItem); } else { this.$fhcAlert.alertError( - this.$p.t("ui", "failed_assigned_resources_save_error_message"), + this.$p.t('ui', 'failed_assigned_resources_save_error_message'), ); } @@ -925,7 +995,7 @@ export default { this.$refs.parking.unpark({ type: event.type, id: event.kalender_id }); } else { this.$refs.parking.park(null, { - type: "kalender", + type: 'kalender', id: event.kalender_id, orig: event, }); @@ -960,14 +1030,14 @@ export default { this.$refs.calendar.resetEventLoader(); }, }, - "resourcesAssignmentModal.selectedAvailableResource": function (newVal) { + 'resourcesAssignmentModal.selectedAvailableResource': function (newVal) { if (!newVal) return; this.resourcesAssignmentModal.assignedResources.push({ betriebsmittel_kalender_id: null, betriebsmittel_id: newVal.data.betriebsmittel_id, beschreibung: newVal.data.beschreibung, - anmerkung: "", + anmerkung: '', isNoteTextareaShown: false, }); @@ -976,9 +1046,9 @@ export default { }, mounted() { this.reservierungPending = false; - this.bcc = new BroadcastChannel("fhc-dnd"); - this.bcc.addEventListener("message", (e) => { - if (e.data === "dropped" && !this.reservierungPending) + this.bcc = new BroadcastChannel('fhc-dnd'); + this.bcc.addEventListener('message', (e) => { + if (e.data === 'dropped' && !this.reservierungPending) this.$refs.calendar.resetEventLoader(); }); }, @@ -1020,9 +1090,9 @@ export default { `link[href="${data[rendertype].calendarEventStyles}"]`, ) ) { - var link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; + var link = document.createElement('link'); + link.type = 'text/css'; + link.rel = 'stylesheet'; link.href = data[rendertype].calendarEventStyles; head.appendChild(link); } @@ -1042,7 +1112,7 @@ export default { this.$api.call(ApiTempusConfig.getHeader()).then((res) => { this.visibleStatusArray = res.data.visible_status; - this.visibleStatus = ["all"]; + this.visibleStatus = ['all']; }); this.$api.call(ApiInfo.getStudiengaenge()).then((res) => { this.studiengaenge_all = res.data; diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index ed1c01ee0..8de2218ce 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,7 +1,12 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc) { +export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = true, isLoaderInitiallyVisible = true) { + let hasFirstLoadOccurred = false; let loading_id = 0; + + let tempEventsHolder = []; + let rangeIntervalHolder = null; + const events = Vue.ref([]); const loadingEvents = Vue.ref([]); const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value)); @@ -100,13 +105,15 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { if (start.ts >= end.ts) return result; - - loadingEvents.value.push({ - loading_id: loading_id++, - type: "loading", - isostart: start.toISODate() + 'T' + start.toISOTime(), - isoend: end.toISODate() + 'T' + end.toISOTime() - }); + + if (isLoaderVisible && (!hasFirstLoadOccurred && isLoaderInitiallyVisible)) { + loadingEvents.value.push({ + loading_id: loading_id++, + type: "loading", + isostart: start.toISODate() + 'T' + start.toISOTime(), + isoend: end.toISODate() + 'T' + end.toISOTime() + }); + } return mergePromiseArr(getPromiseFunc(start, end), result); }; @@ -115,7 +122,14 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { const range = Vue.toValue(rangeInterval); if (!(range instanceof luxon.Interval)) return; + + if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) { + hasFirstLoadOccurred = false; + rangeIntervalHolder = range; + } + const promises = markEventsLoaded(range.start, range.end); + Promise .allSettled(promises) .then(results => { @@ -127,18 +141,23 @@ export function useEventLoader(rangeInterval, getPromiseFunc) { if (res.value.meta.lv) lv.value = res.value.meta.lv; - events.value = events.value.concat(res.value.data); + tempEventsHolder = tempEventsHolder.concat(res.value.data); loadingEvents.value = []; } }) + + events.value = tempEventsHolder; + }); + + hasFirstLoadOccurred = true; }; Vue.watchEffect(reload); const reset = () => { loading_id = 0; - events.value = []; + tempEventsHolder = []; loadingEvents.value = []; eventsLoaded.splice(0, eventsLoaded.length); From 6e6cb8ae1148ec5590e4e4c9b0a461dd00472f0a Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 22 Jun 2026 15:49:16 +0200 Subject: [PATCH 11/18] Second cherry pick of modifing event loading --- public/css/Tempus.css | 179 +++++++++++--------- public/js/components/Tempus/Tempus.js | 230 ++++++++++++++++++-------- public/js/composables/EventLoader.js | 108 ++++++------ 3 files changed, 323 insertions(+), 194 deletions(-) diff --git a/public/css/Tempus.css b/public/css/Tempus.css index 72384c9c7..2dbf9ee05 100644 --- a/public/css/Tempus.css +++ b/public/css/Tempus.css @@ -6,19 +6,19 @@ @import './components/Notiz.css'; html { - font-size: .875em; - height: 100%; + font-size: 0.875em; + height: 100%; } body { /*display: flex;*/ - height: 100%; + height: 100%; } .heightfull { - height: 95%; + height: 95%; } .navbar-dark .navbar-brand:focus { - box-shadow: 0 0 0 .25rem rgba(13,110,253,.25); + box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25); z-index: 3; } #main { @@ -28,13 +28,13 @@ body { height: 100%; } .searchbar { - margin-right: 0!important; + margin-right: 0 !important; } .searchbar > .input-group { - margin-right: 0!important; + margin-right: 0 !important; } .searchbar > .input-group > * { - border-radius: 0!important; + border-radius: 0 !important; } #sidebarMenu { @@ -55,7 +55,7 @@ body { @media (min-width: 768px) { #sidebarMenu { - visibility: visible!important; + visibility: visible !important; transform: none; position: inherit; z-index: 1; @@ -64,27 +64,24 @@ body { .toast.toast-success { color: #0f5132; - background-color: #d1e7dd!important; - border-color: #badbcc!important; + background-color: #d1e7dd !important; + border-color: #badbcc !important; } .toast.toast-danger { color: #842029; - background-color: #f8d7da!important; - border-color: #f5c2c7!important; + background-color: #f8d7da !important; + border-color: #f5c2c7 !important; } .has-filter .fa-filter { color: var(--bs-success); } - - - #parkingslot { border: 1px dashed; min-height: 5vh; max-height: 15vh; - color: #AAAAAA; + color: #aaaaaa; text-align: center; font-size: large; margin-top: 1.25rem; @@ -96,7 +93,7 @@ body { } .course-picker { - border: 1px dashed #AAAAAA; + border: 1px dashed #aaaaaa; display: flex; flex-direction: column; flex: 1 1 0; @@ -112,23 +109,23 @@ body { .parkingevent { border: 1px dashed !important; - border-color: #AAAAAA; + border-color: #aaaaaa; padding: 0.5rem; margin-bottom: 0.5rem; margin-left: 0.5rem; margin-right: 0.5rem; } -:root{ +:root { --fhc-calendar-pane-height: calc(100vh - 120px); } .eckerltest { - box-shadow: 3px 3px 3px #ccc; + box-shadow: 3px 3px 3px #ccc; } .verband-selection { - border: 1px dashed #AAAAAA; + border: 1px dashed #aaaaaa; margin-bottom: 0.5rem; max-height: 450px; overflow-y: auto; @@ -137,7 +134,7 @@ body { } .lecture-selection { - border: 1px dashed #AAAAAA; + border: 1px dashed #aaaaaa; margin-bottom: 0.5rem; max-height: 20vh; overflow-y: auto; @@ -146,7 +143,7 @@ body { } .room-selection { - border: 1px dashed #AAAAAA; + border: 1px dashed #aaaaaa; margin-bottom: 0.5rem; padding: 0.5rem 0 0.5rem 0.5rem; } @@ -161,93 +158,121 @@ body { transform: scale(1.1); } - - .bg-lecturer-wish { - opacity: .15; + opacity: 0.15; } .bg-lecturer-block { background: rgba(255, 0, 0, 0.15); } - .wish-w--2 { - background-color: #FF2200; + background-color: #ff2200; } .wish-w--1 { - background-color: #FF9922; + background-color: #ff9922; } .wish-w-1 { - background-color: #CCFFCC; + background-color: #ccffcc; } .wish-w-2 { - background-color: #48FA66; + background-color: #48fa66; } +.fhc-calendar-base-grid-line-event { + position: relative !important; +} .updated-event { - position: relative; - z-index: 10; - animation: modernFocus 1s ease-out; + position: relative !important; + z-index: 10; + animation: modernFocus 1s ease-out; + border-radius: 0.25rem; } .updated-event-long { - position: relative; - z-index: 10; - animation: modernFocus 1.8s ease-out; + position: relative !important; + z-index: 10; + animation: modernFocus 1.8s ease-out; + border-radius: 0.25rem; } @keyframes modernFocus { - 0% { - background-color: #cfd4d8; - box-shadow: - 0 0 0 0 rgba(120, 120, 120, 0.8), - 0 0 0 0 rgba(255, 255, 255, 0.6); - } + 0% { + background-color: #cfd4d8; + box-shadow: + 0 0 0 0 rgba(120, 120, 120, 0.8), + 0 0 0 0 rgba(255, 255, 255, 0.6); + } - 30% { - background-color: #eef1f3; - box-shadow: - 0 0 0 8px rgba(120, 120, 120, 0.25), - 0 0 30px 8px rgba(255, 255, 255, 0.7); - } + 30% { + background-color: #eef1f3; + box-shadow: + 0 0 0 8px rgba(120, 120, 120, 0.25), + 0 0 30px 8px rgba(255, 255, 255, 0.7); + } - 60% { - background-color: #f7f8f9; - box-shadow: - 0 0 0 14px rgba(120, 120, 120, 0.15), - 0 0 45px 12px rgba(255, 255, 255, 0.5); - } + 60% { + background-color: #f7f8f9; + box-shadow: + 0 0 0 14px rgba(120, 120, 120, 0.15), + 0 0 45px 12px rgba(255, 255, 255, 0.5); + } - 100% { - background-color: inherit; - box-shadow: - 0 0 0 24px rgba(120, 120, 120, 0), - 0 0 60px 20px rgba(255, 255, 255, 0); - } + 100% { + background-color: inherit; + box-shadow: + 0 0 0 24px rgba(120, 120, 120, 0), + 0 0 60px 20px rgba(255, 255, 255, 0); + } } .deemphasized-event { - animation: deEmphasize 1s ease-out; + animation: deEmphasize 1s ease-out; } .deemphasized-event-long { - animation: deEmphasize 1.8s ease-out; + animation: deEmphasize 1.8s ease-out; } @keyframes deEmphasize { - 0% { - opacity: 1; - filter: saturate(1) brightness(1); - } + 0% { + opacity: 1; + filter: saturate(1) brightness(1); + } - 30% { - opacity: 0.55; - filter: saturate(0.4) brightness(0.4); - } + 30% { + opacity: 0.55; + filter: saturate(0.4) brightness(0.4); + } - 100% { - opacity: 1; - filter: saturate(1) brightness(1); - } -} \ No newline at end of file + 100% { + opacity: 1; + filter: saturate(1) brightness(1); + } +} + +.spinner-overlay { + position: absolute; + inset: 0; /* top:0 right:0 bottom:0 left:0 */ + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + background: rgba(255, 255, 255, 1); + opacity: 0.4; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #ddd; + border-top: 4px solid #007bff; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 9495dfda0..0a6838295 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -428,72 +428,7 @@ export default { if (response) { response[0].then((result) => { - if (!this.currentlyUpdatedEvent) return; - - document.querySelectorAll('.updated-event').forEach((el) => { - el.classList.remove('updated-event'); - }); - document.querySelectorAll('.updated-event-long').forEach((el) => { - el.classList.remove('updated-event-long'); - }); - document.querySelectorAll('.deemphasized-event').forEach((el) => { - el.classList.remove('deemphasized-event'); - }); - document - .querySelectorAll('.deemphasized-event-long') - .forEach((el) => { - el.classList.remove('deemphasized-event-long'); - }); - - setTimeout(() => { - const eventEl = document.querySelector( - `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_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'; - let deemphasizedUpdateClassName = isInsideScrolledView - ? 'deemphasized-event' - : 'deemphasized-event-long'; - - if (!isInsideScrolledView) timeout = 500; - - setTimeout(() => { - eventEl.classList.add(emphasizeUpdateClassName); - document - .querySelectorAll('.fhc-calendar-base-grid-line-event') - .forEach((el) => { - if (el !== eventEl) { - el.classList.add(deemphasizedUpdateClassName); - } - }); - }, timeout); - - this.currentlyUpdatedEvent = null; - }, 100); + this.scrollToAndEmphasizeUpdatedEvent(); }); } @@ -605,6 +540,20 @@ export default { if (!dates) return; + if ( + luxon.DateTime.fromISO(obj.orig.isostart).toMillis() === + dates.startDT.toMillis() && + luxon.DateTime.fromISO(obj.orig.isoend).toMillis() === + dates.endDT.toMillis() + ) + return; + + this.updateKalenderEventElementDisplay( + obj.orig.kalender_id, + dates.startDT, + dates.endDT, + ); + return this._updateKalenderEvent( obj, dates.startDT, @@ -650,8 +599,22 @@ export default { const dates = this._parseDates(start, end); if (!dates) return; + if ( + luxon.DateTime.fromISO(obj.orig.isostart).toMillis() === + dates.startDT.toMillis() && + luxon.DateTime.fromISO(obj.orig.isoend).toMillis() === + dates.endDT.toMillis() + ) + return; + const { startDT, endDT, start_time, end_time } = dates; + this.updateKalenderEventElementDisplay( + obj.orig.kalender_id, + startDT, + endDT, + ); + if (obj.type === 'reservierung') { this.reservierungPending = true; this.$refs.reservierung.show(start_time, end_time); @@ -1013,6 +976,141 @@ export default { focusSearchbar() { this.$refs.searchbar?.$refs?.input.focus(); }, + scrollToAndEmphasizeUpdatedEvent() { + if (!this.currentlyUpdatedEvent) return; + + document + .querySelectorAll('.fhc-calendar-base-grid .part-body') + .forEach((el) => { + el.classList.remove('deemphasized-event', 'deemphasized-event-long'); + }); + 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', + 'deemphasized-event', + 'deemphasized-event-long', + ); + }); + + setTimeout(() => { + const eventEl = document.querySelector( + `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_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'; + let deemphasizedUpdateClassName = isInsideScrolledView + ? 'deemphasized-event' + : 'deemphasized-event-long'; + + if (!isInsideScrolledView) timeout = 300; + + setTimeout(() => { + eventEl.classList.add(emphasizeUpdateClassName); + + document + .querySelectorAll( + '.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event', + ) + .forEach((el) => { + if (el !== eventEl) { + el.classList.add(deemphasizedUpdateClassName); + } + }); + document + .querySelectorAll('.fhc-calendar-base-grid .part-body') + .forEach((el) => { + if (el !== eventEl) { + el.classList.add(deemphasizedUpdateClassName); + } + }); + }, timeout); + + this.currentlyUpdatedEvent = null; + }, 100); + }, + updateKalenderEventElementDisplay(calendarId, startDT, endDT) { + if (!calendarId) return alert('Kein gültiges Kalender-Event zum Resizen'); + + let newPotentialStart = + startDT.diff(luxon.DateTime.fromISO('2026-06-22T00:00')).toMillis() ?? + 1; + let newPotentialEnd = endDT + .diff(luxon.DateTime.fromISO('2026-06-22T00:00')) + .toMillis(); + + let element = document.querySelector(`[data-id="event-${calendarId}"]`); + if (!element) return alert('Kein gültiges Kalender-Event zum Resizen'); + + setTimeout(() => { + 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 calendar = document.querySelector('.fhc-calendar-base-grid'); + 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', + }); + } + }, }, watch: { lecturers: { diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index 8de2218ce..93a1b101b 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,22 +1,28 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = true, isLoaderInitiallyVisible = true) { +export function useEventLoader( + rangeInterval, + getPromiseFunc, + isLoaderVisible = true, + isLoaderInitiallyVisible = true, +) { let hasFirstLoadOccurred = false; let loading_id = 0; - + let tempEventsHolder = []; let rangeIntervalHolder = null; const events = Vue.ref([]); const loadingEvents = Vue.ref([]); - const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value)); + const allEvents = Vue.computed(() => + events.value.concat(loadingEvents.value), + ); const lv = Vue.ref(null); const eventsLoaded = []; const mergePromiseArr = (n, o) => { - if (Array.isArray(n)) - return o.concat(n); - return o.push(n), o; + if (Array.isArray(n)) return o.concat(n); + return (o.push(n), o); }; const markEventsLoaded = (start, end) => { @@ -25,10 +31,10 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = // empty: add new chunk eventsLoaded.push(start.ts, end.ts); } else { - if (eventsLoaded[eventsLoaded.length-1] + 1 == start.ts) { + if (eventsLoaded[eventsLoaded.length - 1] + 1 == start.ts) { // add to the end of last chunk - eventsLoaded[eventsLoaded.length-1] = end.ts; - } else if (eventsLoaded[eventsLoaded.length-1] < start.ts) { + eventsLoaded[eventsLoaded.length - 1] = end.ts; + } else if (eventsLoaded[eventsLoaded.length - 1] < start.ts) { // add new chunk after the last chunk eventsLoaded.push(start.ts, end.ts); } else if (eventsLoaded[0] == end.ts + 1) { @@ -37,14 +43,13 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = } else if (eventsLoaded[0] > end.ts) { eventsLoaded.unshift(start.ts, end.ts); } else { - let index = eventsLoaded.findIndex(e => e >= start.ts); + let index = eventsLoaded.findIndex((e) => e >= start.ts); if (index % 2) { // starts inside an existing chunk - if (eventsLoaded[index] >= end.ts) - return []; // Already loaded + if (eventsLoaded[index] >= end.ts) return []; // Already loaded - let indexIsLast = (index == eventsLoaded.length - 1); + let indexIsLast = index == eventsLoaded.length - 1; if (indexIsLast || eventsLoaded[index + 1] > end.ts) { // extend an existing chunk @@ -53,14 +58,16 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = start = start.plus(nStart - start.ts); if (!indexIsLast && eventsLoaded[index + 1] == end.ts + 1) eventsLoaded.splice(index, 2); - else - eventsLoaded[index] = end.ts; + else eventsLoaded[index] = end.ts; } else { // merge exising chunks // and load the rest if necessary if (eventsLoaded[index + 2] < end.ts) { let rStart = eventsLoaded[index + 2] + 1; - result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); + result = mergePromiseArr( + markEventsLoaded(start.plus(rStart - start.ts), end), + result, + ); } let nStart = eventsLoaded[index] + 1; @@ -76,24 +83,32 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = // and load the rest if necessary if (eventsLoaded[1] < end.ts) { let rStart = eventsLoaded[1] + 1; - result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); + result = mergePromiseArr( + markEventsLoaded(start.plus(rStart - start.ts), end), + result, + ); } let nEnd = eventsLoaded[0] - 1; end = end.plus(nEnd - end.ts); eventsLoaded[0] = start.ts; } else if (eventsLoaded[index] == start.ts) { // starts at the same position as an existing chunk - if (eventsLoaded[index + 1] >= end.ts) - return []; // Already loaded + if (eventsLoaded[index + 1] >= end.ts) return []; // Already loaded // load the rest let rStart = eventsLoaded[index + 1] + 1; - result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); + result = mergePromiseArr( + markEventsLoaded(start.plus(rStart - start.ts), end), + result, + ); } else { // extend an existing chunk // and load the rest if necessary if (eventsLoaded[index + 1] < end.ts) { let rStart = eventsLoaded[index + 1] + 1; - result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); + result = mergePromiseArr( + markEventsLoaded(start.plus(rStart - start.ts), end), + result, + ); } let nEnd = eventsLoaded[index] - 1; end = end.plus(nEnd - end.ts); @@ -103,15 +118,14 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = } } - if (start.ts >= end.ts) - return result; - - if (isLoaderVisible && (!hasFirstLoadOccurred && isLoaderInitiallyVisible)) { + if (start.ts >= end.ts) return result; + + if (isLoaderVisible && !hasFirstLoadOccurred && isLoaderInitiallyVisible) { loadingEvents.value.push({ loading_id: loading_id++, - type: "loading", + type: 'loading', isostart: start.toISODate() + 'T' + start.toISOTime(), - isoend: end.toISODate() + 'T' + end.toISOTime() + isoend: end.toISODate() + 'T' + end.toISOTime(), }); } @@ -120,36 +134,28 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = const reload = () => { const range = Vue.toValue(rangeInterval); - if (!(range instanceof luxon.Interval)) - return; + if (!(range instanceof luxon.Interval)) return; if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) { hasFirstLoadOccurred = false; rangeIntervalHolder = range; } - + const promises = markEventsLoaded(range.start, range.end); - Promise - .allSettled(promises) - .then(results => { - results.forEach(res => { - if ( - res.status === 'fulfilled' - && res.value.meta.status === "success" - ) { - if (res.value.meta.lv) - lv.value = res.value.meta.lv; + Promise.allSettled(promises).then((results) => { + results.forEach((res) => { + if (res.status === 'fulfilled' && res.value.meta.status === 'success') { + if (res.value.meta.lv) lv.value = res.value.meta.lv; - tempEventsHolder = tempEventsHolder.concat(res.value.data); - loadingEvents.value = []; - } - }) - - events.value = tempEventsHolder; - + tempEventsHolder = tempEventsHolder.concat(res.value.data); + loadingEvents.value = []; + } }); + events.value = tempEventsHolder; + }); + hasFirstLoadOccurred = true; }; @@ -158,11 +164,11 @@ export function useEventLoader(rangeInterval, getPromiseFunc, isLoaderVisible = const reset = () => { loading_id = 0; tempEventsHolder = []; - loadingEvents.value = []; + //loadingEvents.value = []; eventsLoaded.splice(0, eventsLoaded.length); reload(); - } + }; - return { events: allEvents, lv, reset } -} \ No newline at end of file + return { events: allEvents, lv, reset }; +} From f0b1689a065e0f0f964bfc762403d9295d996b59 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Tue, 1 Sep 2026 17:50:54 +0200 Subject: [PATCH 12/18] Third cherry pick of modifing event loading --- public/css/Tempus.css | 25 ---- public/js/components/Calendar/Tempus.js | 176 +++++++++++++----------- public/js/components/Tempus/Tempus.js | 51 ++----- 3 files changed, 108 insertions(+), 144 deletions(-) diff --git a/public/css/Tempus.css b/public/css/Tempus.css index 2dbf9ee05..2afcea66b 100644 --- a/public/css/Tempus.css +++ b/public/css/Tempus.css @@ -226,31 +226,6 @@ body { } } -.deemphasized-event { - animation: deEmphasize 1s ease-out; -} - -.deemphasized-event-long { - animation: deEmphasize 1.8s ease-out; -} - -@keyframes deEmphasize { - 0% { - opacity: 1; - filter: saturate(1) brightness(1); - } - - 30% { - opacity: 0.55; - filter: saturate(0.4) brightness(0.4); - } - - 100% { - opacity: 1; - filter: saturate(1) brightness(1); - } -} - .spinner-overlay { position: absolute; inset: 0; /* top:0 right:0 bottom:0 left:0 */ diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 174b9c963..ae039e0b1 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -1,4 +1,4 @@ -import FhcCalendar from "./Base.js"; +import FhcCalendar from './Base.js'; import { useEventLoader } from '../../composables/EventLoader.js'; @@ -8,20 +8,19 @@ import ModeTable from './Mode/Table.js'; import ApiKalender from '../../api/factory/tempus/kalender.js'; import draggable from '../../directives/draggable.js'; - export default { - name: "CalendarTempus", + name: 'CalendarTempus', components: { - FhcCalendar + FhcCalendar, }, inject: { - renderers: {from: 'renderers'}, + renderers: { from: 'renderers' }, appConfig: { from: 'appConfig', default: { - visible_status: 'all' - } - } + visible_status: 'all', + }, + }, }, directives: { draggable, @@ -29,45 +28,45 @@ export default { props: { timezone: { type: String, - required: true + required: true, }, date: { type: [Date, String, Number, luxon.DateTime], - default: luxon.DateTime.local() + default: luxon.DateTime.local(), }, mode: { type: String, - default: 'Week' + default: 'Week', }, getPromiseFunc: { type: Function, - required: true + required: true, }, parkedEvents: { type: Object, - default: () => new Set() + default: () => new Set(), }, visibleLecturers: { type: Array, - default: null + default: null, }, extraBackgrounds: { type: Array, - default: () => [] + default: () => [], }, visibleStatus: { type: Array, - default: () => ['all'] + default: () => ['all'], }, }, emits: [ - "update:date", - "update:mode", - "update:range", - "drop", - "resize", - "event-hover", - "event-unhover" + 'update:date', + 'update:mode', + 'update:range', + 'drop', + 'resize', + 'event-hover', + 'event-unhover', ], data() { @@ -80,11 +79,11 @@ export default { modeOptions: { day: { emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')), - emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')) + emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')), }, week: { - collapseEmptyDays: false - } + collapseEmptyDays: false, + }, }, currentMode: this.mode, teachingunits: null, @@ -97,62 +96,62 @@ export default { 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 }) - }]; + 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 || []) - ]; + return [...past, ...(this.extraBackgrounds || [])]; }, - visibleEvents() - { + visibleEvents() { let list = this.events; - if (Array.isArray(this.visibleLecturers)) - { + 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)); + 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)); + return list.filter((event) => + this.visibleStatus.includes(event.status_kurzbz), + ); }, }, methods: { eventStyle(event) { - if (!event.farbe) - return undefined; + if (!event.farbe) return undefined; return '--event-bg:#' + event.farbe; }, updateRange(rangeInterval) { this.rangeInterval = rangeInterval; this.$emit('update:range', rangeInterval); }, - ondrop(payload){ + ondrop(payload) { this.$emit('drop', payload); }, - onresize(payload){ + onresize(payload) { this.$emit('resize', payload); }, resetEventLoader() { @@ -164,13 +163,34 @@ export default { navigateNext() { this.$refs.calendar.clickNext(); }, + 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); + const { events, lv, reset } = useEventLoader( + rangeInterval, + props.getPromiseFunc, + ); - Vue.watch(lv, newValue => { + Vue.watch(lv, (newValue) => { context.emit('update:lv', newValue); }); @@ -178,31 +198,27 @@ export default { rangeInterval, events, lv, - reset + 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.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.$api.call(ApiKalender.getCalendarHours()).then((res) => { + this.hoursplan = { + start: res.data.start, + end: res.data.end, + }; + }); }, - template: /* html */` + template: /* html */ `
- ` -} \ No newline at end of file + `, +}; diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 0a6838295..ade9d9535 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -549,7 +549,7 @@ export default { return; this.updateKalenderEventElementDisplay( - obj.orig.kalender_id, + obj.orig.eindeutige_kalender_gruppen_id, dates.startDT, dates.endDT, ); @@ -610,7 +610,7 @@ export default { const { startDT, endDT, start_time, end_time } = dates; this.updateKalenderEventElementDisplay( - obj.orig.kalender_id, + obj.orig.eindeutige_kalender_gruppen_id, startDT, endDT, ); @@ -979,11 +979,6 @@ export default { scrollToAndEmphasizeUpdatedEvent() { if (!this.currentlyUpdatedEvent) return; - document - .querySelectorAll('.fhc-calendar-base-grid .part-body') - .forEach((el) => { - el.classList.remove('deemphasized-event', 'deemphasized-event-long'); - }); document .querySelectorAll( '.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event', @@ -998,14 +993,12 @@ export default { 'updating-event', 'updated-event', 'updated-event-long', - 'deemphasized-event', - 'deemphasized-event-long', ); }); setTimeout(() => { const eventEl = document.querySelector( - `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_gruppen_id}"]`, + `[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_kalender_gruppen_id}"]`, ); if (!eventEl) return; @@ -1033,47 +1026,27 @@ export default { let emphasizeUpdateClassName = isInsideScrolledView ? 'updated-event' : 'updated-event-long'; - let deemphasizedUpdateClassName = isInsideScrolledView - ? 'deemphasized-event' - : 'deemphasized-event-long'; if (!isInsideScrolledView) timeout = 300; setTimeout(() => { eventEl.classList.add(emphasizeUpdateClassName); - - document - .querySelectorAll( - '.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event', - ) - .forEach((el) => { - if (el !== eventEl) { - el.classList.add(deemphasizedUpdateClassName); - } - }); - document - .querySelectorAll('.fhc-calendar-base-grid .part-body') - .forEach((el) => { - if (el !== eventEl) { - el.classList.add(deemphasizedUpdateClassName); - } - }); }, timeout); this.currentlyUpdatedEvent = null; }, 100); }, - updateKalenderEventElementDisplay(calendarId, startDT, endDT) { - if (!calendarId) return alert('Kein gültiges Kalender-Event zum Resizen'); + updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) { + if (!calendarGruppenId) + return alert('Kein gültiges Kalender-Event zum Resizen'); - let newPotentialStart = - startDT.diff(luxon.DateTime.fromISO('2026-06-22T00:00')).toMillis() ?? - 1; - let newPotentialEnd = endDT - .diff(luxon.DateTime.fromISO('2026-06-22T00:00')) - .toMillis(); + let startOfDay = startDT.startOf('day'); + let newPotentialStart = startDT.diff(startOfDay).toMillis() ?? 1; + let newPotentialEnd = endDT.diff(startOfDay).toMillis(); - let element = document.querySelector(`[data-id="event-${calendarId}"]`); + let element = document.querySelector( + `[data-group-id="event-group-${calendarGruppenId}"]`, + ); if (!element) return alert('Kein gültiges Kalender-Event zum Resizen'); setTimeout(() => { From 4ecd4b5a656e082ca0095ad65d403dbd8566436e Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 9 Sep 2026 12:21:46 +0200 Subject: [PATCH 13/18] BUG: Fixed issue witd drag drop animation --- public/js/components/Calendar/Tempus.js | 8 +++- public/js/components/Tempus/Tempus.js | 64 +++++++++++++++++++++---- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 998b73886..3bd593797 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -76,7 +76,8 @@ export default { "resize", "event-hover", "event-unhover", - "open-reservierung" + "open-reservierung", + "events-reloaded", ], data() { @@ -101,6 +102,11 @@ export default { showRaster: true, }; }, + watch: { + events() { + this.$emit('events-reloaded'); + } + }, computed: { backgrounds() { let now = luxon.DateTime.now().setZone(this.timezone); diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 32fe0bf13..0bb92c77d 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -438,15 +438,24 @@ export default { return this.$api.call(ApiSearchbar.search(params)); }, getPromiseFunc(start, end) { - const hasRoom = !!this.ort_kurzbz; + const hasRooms = this.rooms.length > 0; const hasLektoren = this.lecturers.length > 0; - const hasStg = !!this.stg; + const hasStg = this.studiengaenge.length > 0; const filter = {}; - if (hasRoom) filter.ort = this.ort_kurzbz; - if (hasStg) filter.stg = this.stg; - if (hasLektoren) filter.uid = this.lecturers.map((l) => l.uid); + 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') @@ -532,7 +541,7 @@ export default { const { item, start, end } = payload; const obj = item[0]; if (!obj?.orig?.kalender_id) - return alert('Kein gültiges Kalender-Event zum Resizen'); + return; const dates = this._parseDates(start, end); @@ -967,18 +976,42 @@ export default { }, updateKalenderEventElementDisplay(calendarGruppenId, startDT, endDT) { if (!calendarGruppenId) - return alert('Kein gültiges Kalender-Event zum Resizen'); + return; let startOfDay = startDT.startOf('day'); let newPotentialStart = startDT.diff(startOfDay).toMillis() ?? 1; let newPotentialEnd = endDT.diff(startOfDay).toMillis(); - let element = document.querySelector( + const calendar = this.$refs.calendar?.$el; + let element = calendar?.querySelector( `[data-group-id="event-group-${calendarGruppenId}"]`, ); - if (!element) return alert('Kein gültiges Kalender-Event zum Resizen'); + 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); @@ -993,7 +1026,6 @@ export default { element.appendChild(outerDiv); - const calendar = document.querySelector('.fhc-calendar-base-grid'); const eventRect = element.getBoundingClientRect(); const offset = 300; @@ -1013,6 +1045,17 @@ export default { }); } }, + clearTemporaryEvents() { + const calendar = this.$refs.calendar?.$el; + if (!calendar) return; + + const tempEvents = calendar.querySelectorAll( + '.tempus-temporary-calendar-event', + ); + tempEvents.forEach((event) => { + event.remove(); + }); + } }, watch: { lecturers: { @@ -1162,6 +1205,7 @@ export default { @open-reservierung="openReservierung" :extra-backgrounds="extraBackgrounds" @update:range="handleRange" + @events-reloaded="clearTemporaryEvents" class="responsive-calendar" /> From ed02955b497f1b1cc11bd9684c81610551ca9883 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 9 Sep 2026 13:08:49 +0200 Subject: [PATCH 14/18] BUG: Fixed issue with event reload span --- .../composables/Tempus/TempusEventLoader.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js index 84bfa8dfb..4a21c8585 100644 --- a/public/js/composables/Tempus/TempusEventLoader.js +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -30,9 +30,6 @@ export function useEventLoader( currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts && isCacheEnabled ) { - console.log( - "cached range is the same as requested range, skipping reload 2", - ); return; } @@ -51,9 +48,6 @@ export function useEventLoader( cachedEventsEndTimestamp === currentlyDisplayedDateRange.end.ts && isCacheEnabled ) { - console.log( - "cached range is the same as requested range, skipping reload", - ); return; } @@ -123,9 +117,15 @@ export function useEventLoader( if (cachedEventsStartTimestamp && cachedEventsEndTimestamp && isCacheEnabled) { if ( - startTimestamp < cachedEventsStartTimestamp && - endTimestamp > cachedEventsEndTimestamp - ) { + ( + (startTimestamp < cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp) || + (startTimestamp < cachedEventsStartTimestamp && + endTimestamp < cachedEventsEndTimestamp && Math.abs(getLuxonDateFromMillis(endTimestamp).diff(getLuxonDateFromMillis(cachedEventsStartTimestamp), 'days')) > 1) || + (startTimestamp > cachedEventsStartTimestamp && + endTimestamp > cachedEventsEndTimestamp && Math.abs(getLuxonDateFromMillis(startTimestamp).diff(getLuxonDateFromMillis(cachedEventsEndTimestamp), 'days')) > 1) + ) + ) { modifiedRequestStartTimestamp = startTimestamp; modifiedRequestEndTimestamp = endTimestamp; cachedEventsStartTimestamp = startTimestamp; @@ -157,7 +157,7 @@ export function useEventLoader( allowedCacheStartTimestamp = startTimestamp - cachePadding; allowedCacheEndTimestamp = endTimestamp + cachePadding; - return mergePromiseElements( + return mergePromiseElements( getPromiseFunc( getLuxonDateFromMillis(modifiedRequestStartTimestamp), getLuxonDateFromMillis(modifiedRequestEndTimestamp), From c91a6c6376c3f6a31d00fd50c9842e8915e6b79c Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 9 Sep 2026 13:09:48 +0200 Subject: [PATCH 15/18] Reverted to original eventLoader --- public/js/composables/EventLoader.js | 47 ++-------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index 2946fc6ad..ed1c01ee0 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,6 +1,6 @@ // TODO(chris): load events that are longer than the interval without doubling it -export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedRange = false, cacheMultiplier = 1) { +export function useEventLoader(rangeInterval, getPromiseFunc) { let loading_id = 0; const events = Vue.ref([]); const loadingEvents = Vue.ref([]); @@ -14,23 +14,6 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR return o.push(n), o; }; - const filterEventsOutsideRange = validRange => { - events.value = events.value.filter(event => { - const start = luxon.DateTime.fromISO(event.isostart).ts; - const end = luxon.DateTime.fromISO(event.isoend).ts; - return start <= validRange.end.ts && end >= validRange.start.ts; - }); - - const loadedRanges = []; - for (let i = 0; i < eventsLoaded.length; i += 2) { - const start = Math.max(eventsLoaded[i], validRange.start.ts); - const end = Math.min(eventsLoaded[i + 1], validRange.end.ts); - if (start < end) - loadedRanges.push(start, end); - } - eventsLoaded.splice(0, eventsLoaded.length, ...loadedRanges); - }; - const markEventsLoaded = (start, end) => { let result = []; if (!eventsLoaded.length) { @@ -100,23 +83,6 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR // load the rest let rStart = eventsLoaded[index + 1] + 1; result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); - } else if ( - Vue.toValue(preserveRequestedRange) - && end.ts < eventsLoaded[index] - ) { - // requested range is entirely inside the gap - // between the previous and next loaded chunks - const touchesPrevious = eventsLoaded[index - 1] + 1 == start.ts; - const touchesNext = end.ts + 1 == eventsLoaded[index]; - - if (touchesPrevious && touchesNext) - eventsLoaded.splice(index - 1, 2); - else if (touchesPrevious) - eventsLoaded[index - 1] = end.ts; - else if (touchesNext) - eventsLoaded[index] = start.ts; - else - eventsLoaded.splice(index, 0, start.ts, end.ts); } else { // extend an existing chunk // and load the rest if necessary @@ -149,13 +115,6 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR const range = Vue.toValue(rangeInterval); if (!(range instanceof luxon.Interval)) return; - const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0); - const cachePadding = Math.round(range.length() * cacheSize); - const validRange = luxon.Interval.fromDateTimes( - range.start.minus(cachePadding ?? 0), - range.end.plus(cachePadding ?? 0) - ); - const promises = markEventsLoaded(range.start, range.end); Promise .allSettled(promises) @@ -169,8 +128,6 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR lv.value = res.value.meta.lv; events.value = events.value.concat(res.value.data); - - filterEventsOutsideRange(validRange); loadingEvents.value = []; } }) @@ -189,4 +146,4 @@ export function useEventLoader(rangeInterval, getPromiseFunc, preserveRequestedR } return { events: allEvents, lv, reset } -} +} \ No newline at end of file From 5d0c6c1b4e602f07e3b19f9c4a5c194bbc999467 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 9 Sep 2026 13:57:47 +0200 Subject: [PATCH 16/18] Dedicated tempus event loader updated --- public/js/components/Calendar/Tempus.js | 4 +- public/js/components/Tempus/Tempus.js | 17 ++++---- .../composables/Tempus/TempusEventLoader.js | 40 ++++++++++++++++--- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 07a1747b6..3feefbd97 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -183,8 +183,8 @@ export default { onresize(payload) { this.$emit('resize', payload); }, - resetEventLoader() { - this.reset(); + resetEventLoader(arePreviousEventsCleared = true) { + this.reset(arePreviousEventsCleared); }, navigatePrev() { this.$refs.calendar.clickPrev(); diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 442dff8d8..ddd13d2e9 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -165,7 +165,7 @@ export default { if (!orig?.kalender_id) return; await this.deleteEntryCall(orig); - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); }, @@ -176,7 +176,7 @@ export default { await Promise.allSettled( validList.map((orig) => this.deleteEntryCall(orig)), ); - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); }, async deleteEntryCall(orig) { @@ -588,7 +588,7 @@ export default { dates.start_time, dates.end_time, () => { - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); }, @@ -619,7 +619,7 @@ export default { ), ) .then(() => { - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); this.bcc.postMessage('dropped'); @@ -690,7 +690,7 @@ export default { return; } - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); this.bcc.postMessage('dropped'); @@ -708,7 +708,7 @@ export default { ), ) .then(() => { - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.bcc.postMessage('dropped'); this.rebuildRaumvorschlag(); @@ -733,7 +733,7 @@ export default { type: obj.type, id: obj.orig.kalender_id, }); - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.rebuildRaumvorschlag(); this.bcc.postMessage('dropped'); }, @@ -753,7 +753,7 @@ export default { this.multiWeekModal.show = false; }, onMultiWeekConfirmed() { - this.$refs.calendar.resetEventLoader(); + this.$refs.calendar.resetEventLoader(false); this.$refs.sidebar.reloadCoursepicker(); this.rebuildRaumvorschlag(); this.bcc.postMessage('dropped'); @@ -1238,7 +1238,6 @@ export default { ref="verbandMenu" @select-verband-and-close="onSelectVerbandAndClose" /> - { 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 = res.value.data; }); @@ -93,6 +98,27 @@ export function useEventLoader( ).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), ); @@ -101,8 +127,10 @@ export function useEventLoader( Vue.watchEffect(reload); - const reset = () => { - allEvents.value = []; + const reset = (arePreviousEventsCleared = true) => { + if (arePreviousEventsCleared) { + allEvents.value = []; + } reload(false); }; @@ -167,6 +195,8 @@ export function useEventLoader( }; const addVisualForEventsLoading = (startTimestamp, endTimestamp) => { + if (hasFirstLoadOccurred) return; + allEvents.value.push({ loading_id: 1, type: "loading", @@ -191,7 +221,7 @@ export function useEventLoader( return events; }; - const ensureCacheRangeIsAllowedRange = ( + const ensureCacheRangeIsInAllowedRange = ( allowedCacheStartTimestamp, allowedCacheEndTimestamp, ) => { From 76fe4f567c01b1a52dd1fed66e018f00fd552340 Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Wed, 9 Sep 2026 13:58:43 +0200 Subject: [PATCH 17/18] Reverted default event loader to previous version --- public/js/composables/EventLoader.js | 125 +++++++++++---------------- 1 file changed, 49 insertions(+), 76 deletions(-) diff --git a/public/js/composables/EventLoader.js b/public/js/composables/EventLoader.js index 93a1b101b..a23bde241 100644 --- a/public/js/composables/EventLoader.js +++ b/public/js/composables/EventLoader.js @@ -1,28 +1,15 @@ -// TODO(chris): load events that are longer than the interval without doubling it - -export function useEventLoader( - rangeInterval, - getPromiseFunc, - isLoaderVisible = true, - isLoaderInitiallyVisible = true, -) { - let hasFirstLoadOccurred = false; +export function useEventLoader(rangeInterval, getPromiseFunc) { let loading_id = 0; - - let tempEventsHolder = []; - let rangeIntervalHolder = null; - const events = Vue.ref([]); const loadingEvents = Vue.ref([]); - const allEvents = Vue.computed(() => - events.value.concat(loadingEvents.value), - ); + const allEvents = Vue.computed(() => events.value.concat(loadingEvents.value)); const lv = Vue.ref(null); const eventsLoaded = []; const mergePromiseArr = (n, o) => { - if (Array.isArray(n)) return o.concat(n); - return (o.push(n), o); + if (Array.isArray(n)) + return o.concat(n); + return o.push(n), o; }; const markEventsLoaded = (start, end) => { @@ -31,10 +18,10 @@ export function useEventLoader( // empty: add new chunk eventsLoaded.push(start.ts, end.ts); } else { - if (eventsLoaded[eventsLoaded.length - 1] + 1 == start.ts) { + if (eventsLoaded[eventsLoaded.length-1] + 1 == start.ts) { // add to the end of last chunk - eventsLoaded[eventsLoaded.length - 1] = end.ts; - } else if (eventsLoaded[eventsLoaded.length - 1] < start.ts) { + eventsLoaded[eventsLoaded.length-1] = end.ts; + } else if (eventsLoaded[eventsLoaded.length-1] < start.ts) { // add new chunk after the last chunk eventsLoaded.push(start.ts, end.ts); } else if (eventsLoaded[0] == end.ts + 1) { @@ -43,13 +30,14 @@ export function useEventLoader( } else if (eventsLoaded[0] > end.ts) { eventsLoaded.unshift(start.ts, end.ts); } else { - let index = eventsLoaded.findIndex((e) => e >= start.ts); + let index = eventsLoaded.findIndex(e => e >= start.ts); if (index % 2) { // starts inside an existing chunk - if (eventsLoaded[index] >= end.ts) return []; // Already loaded + if (eventsLoaded[index] >= end.ts) + return []; // Already loaded - let indexIsLast = index == eventsLoaded.length - 1; + let indexIsLast = (index == eventsLoaded.length - 1); if (indexIsLast || eventsLoaded[index + 1] > end.ts) { // extend an existing chunk @@ -58,16 +46,14 @@ export function useEventLoader( start = start.plus(nStart - start.ts); if (!indexIsLast && eventsLoaded[index + 1] == end.ts + 1) eventsLoaded.splice(index, 2); - else eventsLoaded[index] = end.ts; + else + eventsLoaded[index] = end.ts; } else { // merge exising chunks // and load the rest if necessary if (eventsLoaded[index + 2] < end.ts) { let rStart = eventsLoaded[index + 2] + 1; - result = mergePromiseArr( - markEventsLoaded(start.plus(rStart - start.ts), end), - result, - ); + result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); } let nStart = eventsLoaded[index] + 1; @@ -83,32 +69,24 @@ export function useEventLoader( // and load the rest if necessary if (eventsLoaded[1] < end.ts) { let rStart = eventsLoaded[1] + 1; - result = mergePromiseArr( - markEventsLoaded(start.plus(rStart - start.ts), end), - result, - ); + result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); } let nEnd = eventsLoaded[0] - 1; end = end.plus(nEnd - end.ts); eventsLoaded[0] = start.ts; } else if (eventsLoaded[index] == start.ts) { // starts at the same position as an existing chunk - if (eventsLoaded[index + 1] >= end.ts) return []; // Already loaded + if (eventsLoaded[index + 1] >= end.ts) + return []; // Already loaded // load the rest let rStart = eventsLoaded[index + 1] + 1; - result = mergePromiseArr( - markEventsLoaded(start.plus(rStart - start.ts), end), - result, - ); + result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); } else { // extend an existing chunk // and load the rest if necessary if (eventsLoaded[index + 1] < end.ts) { let rStart = eventsLoaded[index + 1] + 1; - result = mergePromiseArr( - markEventsLoaded(start.plus(rStart - start.ts), end), - result, - ); + result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result); } let nEnd = eventsLoaded[index] - 1; end = end.plus(nEnd - end.ts); @@ -118,57 +96,52 @@ export function useEventLoader( } } - if (start.ts >= end.ts) return result; + if (start.ts >= end.ts) + return result; - if (isLoaderVisible && !hasFirstLoadOccurred && isLoaderInitiallyVisible) { - loadingEvents.value.push({ - loading_id: loading_id++, - type: 'loading', - isostart: start.toISODate() + 'T' + start.toISOTime(), - isoend: end.toISODate() + 'T' + end.toISOTime(), - }); - } + loadingEvents.value.push({ + loading_id: loading_id++, + type: "loading", + isostart: start.toISODate() + 'T' + start.toISOTime(), + isoend: end.toISODate() + 'T' + end.toISOTime() + }); return mergePromiseArr(getPromiseFunc(start, end), result); }; const reload = () => { const range = Vue.toValue(rangeInterval); - if (!(range instanceof luxon.Interval)) return; - - if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) { - hasFirstLoadOccurred = false; - rangeIntervalHolder = range; - } - + if (!(range instanceof luxon.Interval)) + return; const promises = markEventsLoaded(range.start, range.end); + Promise + .allSettled(promises) + .then(results => { + results.forEach(res => { + if ( + res.status === 'fulfilled' + && res.value.meta.status === "success" + ) { + if (res.value.meta.lv) + lv.value = res.value.meta.lv; - Promise.allSettled(promises).then((results) => { - results.forEach((res) => { - if (res.status === 'fulfilled' && res.value.meta.status === 'success') { - if (res.value.meta.lv) lv.value = res.value.meta.lv; - - tempEventsHolder = tempEventsHolder.concat(res.value.data); - loadingEvents.value = []; - } + events.value = events.value.concat(res.value.data); + loadingEvents.value = []; + } + }) }); - - events.value = tempEventsHolder; - }); - - hasFirstLoadOccurred = true; }; Vue.watchEffect(reload); const reset = () => { loading_id = 0; - tempEventsHolder = []; - //loadingEvents.value = []; + events.value = []; + loadingEvents.value = []; eventsLoaded.splice(0, eventsLoaded.length); reload(); - }; + } - return { events: allEvents, lv, reset }; -} + return { events: allEvents, lv, reset } +} \ No newline at end of file From a99730b3697efe8dab6bb1514080ceff7eb2d05e Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Fri, 11 Sep 2026 09:58:47 +0200 Subject: [PATCH 18/18] BUG: Fixed issue with reverting failed updated events --- public/js/components/Calendar/Base.js | 7 ++++++- public/js/components/Calendar/Tempus.js | 12 ++++++++++++ public/js/components/Tempus/Tempus.js | 8 ++++++++ public/js/composables/Tempus/TempusEventLoader.js | 3 +++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/public/js/components/Calendar/Base.js b/public/js/components/Calendar/Base.js index 360c9afa2..b9124c17b 100644 --- a/public/js/components/Calendar/Base.js +++ b/public/js/components/Calendar/Base.js @@ -123,6 +123,10 @@ export default { draggableEvents: [Boolean, Array, Function], dropableEvents: [Boolean, Array, Function], resizableEvents: [Boolean, Array, Function], + eventReloadKey: { + type: Number, + default: 0, + }, onDragover: Function, onDrop: Function, onResize: Function @@ -321,6 +325,7 @@ export default { ` -} \ No newline at end of file +} diff --git a/public/js/components/Calendar/Tempus.js b/public/js/components/Calendar/Tempus.js index 3feefbd97..4ecde52a6 100644 --- a/public/js/components/Calendar/Tempus.js +++ b/public/js/components/Calendar/Tempus.js @@ -86,6 +86,8 @@ export default { data() { return { + eventReloadKey: 0, + refreshEventsAfterReload: false, modes: { week: Vue.markRaw(ModeWeek), month: Vue.markRaw(ModeMonth), @@ -108,6 +110,11 @@ export default { }, watch: { events() { + if (this.refreshEventsAfterReload) { + this.eventReloadKey += 1; + this.refreshEventsAfterReload = false; + } + this.$emit('events-reloaded'); } }, @@ -186,6 +193,10 @@ export default { resetEventLoader(arePreviousEventsCleared = true) { this.reset(arePreviousEventsCleared); }, + reloadEvents() { + this.refreshEventsAfterReload = true; + this.resetEventLoader(false); + }, navigatePrev() { this.$refs.calendar.clickPrev(); }, @@ -259,6 +270,7 @@ export default { :timezone="timezone" :locale="$p.user_locale.value" :events="visibleEvents || []" + :event-reload-key="eventReloadKey" :backgrounds="backgrounds" :time-grid="showRaster ? teachingunits : null" :hours-plan="hoursplan" diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index ddd13d2e9..918614712 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -551,6 +551,14 @@ export default { 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; }); }, diff --git a/public/js/composables/Tempus/TempusEventLoader.js b/public/js/composables/Tempus/TempusEventLoader.js index ff107d31a..79fca1cb0 100644 --- a/public/js/composables/Tempus/TempusEventLoader.js +++ b/public/js/composables/Tempus/TempusEventLoader.js @@ -122,6 +122,8 @@ export function useEventLoader( allEvents.value = removeVisualForEventsLoading( ensureEventsAreInValidCacheRange(tempAllEvents), ); + + hasFirstLoadOccurred = true; }); }; @@ -130,6 +132,7 @@ export function useEventLoader( const reset = (arePreviousEventsCleared = true) => { if (arePreviousEventsCleared) { allEvents.value = []; + hasFirstLoadOccurred = false; } reload(false); };