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