Compare commits

...
9 changed files with 335 additions and 43 deletions
@@ -104,6 +104,9 @@ 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;
$maxDailyEventLimit = $this->_ci->input->post('maxDailyEventLimit', TRUE) ?? false;
$filter = $this->_checkFilter(self::ALLOWED_PLAN_FILTER);
$stundenplan_data = $this->_ci->kalenderlib->getPlanForPlanner(
@@ -111,7 +114,9 @@ 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,
$maxDailyEventLimit
);
$this->terminateWithSuccess($stundenplan_data);
+18 -3
View File
@@ -222,7 +222,7 @@ class KalenderLib
$this->_ci->KalenderModel->addOrder('tbl_kalender.eindeutige_kalender_gruppen_id', 'DESC');
}
private function _mapEvents($data, $collisionCheck = true)
private function _mapEvents($data, $collisionCheck = true, $maxDailyEventLimit = null)
{
$stundenplan_data = [];
@@ -365,6 +365,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)
@@ -435,7 +450,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, $maxDailyEventLimit = null)
{
$this->_getBasePlan($start_date, $end_date);
@@ -542,7 +557,7 @@ class KalenderLib
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data);
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
}
public function getPlanForStudent($start_date, $end_date)
+21 -5
View File
@@ -1,11 +1,17 @@
export default {
getPlan(filter, start_date, end_date)
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 }
params: {
...filter,
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
getRaumvorschlagSlots(lehreinheit_id, start_date, end_date)
@@ -21,15 +27,25 @@ export default {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer',
params: { start_date, end_date }
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
getPlanStudent(start_date, end_date)
getPlanStudent(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanStudent',
params: { start_date, end_date }
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
};
},
+3 -12
View File
@@ -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);
}
+4 -11
View File
@@ -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 */`
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
+10 -2
View File
@@ -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';
@@ -47,6 +47,10 @@ export default {
type: Function,
required: true
},
cacheMultiplier: {
type: Number,
default: 1
},
parkedEvents: {
type: Object,
default: () => new Set()
@@ -187,7 +191,11 @@ export default {
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
() => props.cacheMultiplier
);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
+24 -3
View File
@@ -131,6 +131,7 @@ export default {
endTime: null,
},
studiengaenge_all: [],
maxDailyEventLimitForMonthView: 10,
raumvorschlagPreview: null,
raumvorschlagLoading: false,
showEvents: true,
@@ -437,6 +438,9 @@ export default {
return this.$api.call(ApiSearchbar.search(params));
},
getPromiseFunc(start, end) {
const isMonth = this.currentMode?.toLowerCase() === "month";
const collisionCheck = !isMonth;
const maxDailyEventLimit = isMonth ? this.maxDailyEventLimitForMonthView : null;
const hasRooms = this.rooms.length > 0;
const hasLektoren = this.lecturers.length > 0;
const hasStg = this.studiengaenge.length > 0;
@@ -459,20 +463,36 @@ export default {
if (this.previewRole === 'lektor')
return [
this.$api.call(
ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()),
ApiKalender.getPlanLecturer(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
if (this.previewRole === 'student')
return [
this.$api.call(
ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
ApiKalender.getPlanStudent(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
return [
this.$api.call(
ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
ApiKalender.getPlan(
filter,
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
),
];
},
@@ -1022,6 +1042,7 @@ export default {
:extra-backgrounds="extraBackgrounds"
@update:range="handleRange"
class="responsive-calendar"
:cache-multiplier="currentMode === 'week' ? 1 : 0"
/>
</template>
</horizontal-split>
@@ -0,0 +1,233 @@
export function useEventLoader(
rangeInterval,
getPromiseFunc,
cacheMultiplier = 1,
) {
let allEvents = Vue.ref([]);
const lv = Vue.ref(null);
let newlyLoadedEvents = [];
let cachedEventsStartTimestamp = null;
let cachedEventsEndTimestamp = null;
let allowedCacheStartTimestamp = null;
let allowedCacheEndTimestamp = null;
let currentlyDisplayedDateRange;
const getCachePadding = () => {
const cacheSize = Math.max(Vue.toValue(cacheMultiplier), 0);
return Math.round(currentlyDisplayedDateRange.length() * cacheSize) + 1;
};
const reload = (isCacheEnabled = true) => {
if (
currentlyDisplayedDateRange &&
currentlyDisplayedDateRange?.start?.ts ===
rangeInterval?.value?.start?.ts &&
currentlyDisplayedDateRange?.end?.ts === rangeInterval?.value?.end?.ts &&
isCacheEnabled
) {
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 &&
isCacheEnabled
) {
return;
}
const promises = requestEvents(
currentlyDisplayedDateRange.start.ts,
currentlyDisplayedDateRange.end.ts,
isCacheEnabled
);
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_kalender_gruppen_id,
event,
]),
).values(),
);
allEvents.value = removeVisualForEventsLoading(
ensureEventsAreInValidCacheRange(tempAllEvents),
);
});
};
Vue.watchEffect(reload);
const reset = () => {
allEvents.value = [];
reload(false);
};
const requestEvents = (startTimestamp, endTimestamp, isCacheEnabled = true) => {
let result = [];
if (!startTimestamp || !endTimestamp) return result;
if (startTimestamp >= endTimestamp) return result;
let modifiedRequestStartTimestamp = startTimestamp;
let modifiedRequestEndTimestamp = endTimestamp;
if (cachedEventsStartTimestamp && cachedEventsEndTimestamp && isCacheEnabled) {
if (
(
(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;
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 if (isCacheEnabled) {
cachedEventsStartTimestamp = modifiedRequestStartTimestamp;
cachedEventsEndTimestamp = modifiedRequestEndTimestamp;
}
const cachePadding = getCachePadding();
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,
) => {
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);
};
+16 -6
View File
@@ -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);
}
}
}