From 1bbe4368bb783845ee7cca897bb6d1c04e9333fe Mon Sep 17 00:00:00 2001 From: Ivymaster Date: Mon, 22 Jun 2026 15:06:53 +0200 Subject: [PATCH] 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);