mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c91a6c6376 | ||
|
|
ed02955b49 | ||
|
|
63af8efb44 | ||
|
|
669e03b915 | ||
|
|
1b23662a54 | ||
|
|
a5715571e8 | ||
|
|
0df66047fe | ||
|
|
36a11ec2fd | ||
|
|
7759884d8d | ||
|
|
9352d5cd46 | ||
|
|
cc8e7493e2 | ||
|
|
bb6dfd4d64 | ||
|
|
4bc858dc48 |
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user