Compare commits

..
13 changed files with 556 additions and 545 deletions
@@ -104,9 +104,6 @@ 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(
@@ -114,9 +111,7 @@ 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,
$isCollisionCheckEnabled,
$maxDailyEventLimit
isset($filter->stg) ? $filter->stg : null
);
$this->terminateWithSuccess($stundenplan_data);
+4 -19
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, $maxDailyEventLimit = null)
private function _mapEvents($data, $collisionCheck = true)
{
$stundenplan_data = [];
@@ -243,6 +243,7 @@ class KalenderLib
$parsedResources = is_string($resources) ? json_decode($resources, true) : $resources;
$events[$id] = (object) [
'kalender_id' => $id,
'eindeutige_kalender_gruppen_id' => $row->eindeutige_kalender_gruppen_id,
'type' => $row->typ,
'beginn' => $von->format('H:i:s'),
@@ -261,7 +262,6 @@ class KalenderLib
'lehrveranstaltung_id' => $row->lehrveranstaltung_id,
'verplante_stunden' => $row->verplante_stunden,
'organisationseinheit' => isset($row->oe_kurzbz) ? $row->oe_kurzbz : '',
'kalender_id' => $id,
'lehreinheit_id' => [],
'lektor' => [],
'teilnehmer_gruppe' => [],
@@ -365,21 +365,6 @@ 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)
@@ -450,7 +435,7 @@ class KalenderLib
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data);
}
public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null, $collisionCheck = true, $maxDailyEventLimit = null)
public function getPlanForPlanner($start_date, $end_date, $ort = null, $uids = null, $studiengaenge = null)
{
$this->_getBasePlan($start_date, $end_date);
@@ -557,7 +542,7 @@ class KalenderLib
$data = $this->_ci->KalenderModel->load();
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
return $this->_mapEvents($data);
}
public function getPlanForStudent($start_date, $end_date)
+98 -29
View File
@@ -6,19 +6,19 @@
@import './components/Notiz.css';
html {
font-size: .875em;
height: 100%;
font-size: 0.875em;
height: 100%;
}
body {
/*display: flex;*/
height: 100%;
height: 100%;
}
.heightfull {
height: 95%;
height: 95%;
}
.navbar-dark .navbar-brand:focus {
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
z-index: 3;
}
#main {
@@ -28,13 +28,13 @@ body {
height: 100%;
}
.searchbar {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group {
margin-right: 0!important;
margin-right: 0 !important;
}
.searchbar > .input-group > * {
border-radius: 0!important;
border-radius: 0 !important;
}
#sidebarMenu {
@@ -55,7 +55,7 @@ body {
@media (min-width: 768px) {
#sidebarMenu {
visibility: visible!important;
visibility: visible !important;
transform: none;
position: inherit;
z-index: 1;
@@ -64,27 +64,24 @@ body {
.toast.toast-success {
color: #0f5132;
background-color: #d1e7dd!important;
border-color: #badbcc!important;
background-color: #d1e7dd !important;
border-color: #badbcc !important;
}
.toast.toast-danger {
color: #842029;
background-color: #f8d7da!important;
border-color: #f5c2c7!important;
background-color: #f8d7da !important;
border-color: #f5c2c7 !important;
}
.has-filter .fa-filter {
color: var(--bs-success);
}
#parkingslot {
border: 1px dashed;
min-height: 5vh;
max-height: 15vh;
color: #AAAAAA;
color: #aaaaaa;
text-align: center;
font-size: large;
margin-top: 1.25rem;
@@ -96,7 +93,7 @@ body {
}
.course-picker {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
display: flex;
flex-direction: column;
flex: 1 1 0;
@@ -132,11 +129,11 @@ body {
}
.eckerltest {
box-shadow: 3px 3px 3px #ccc;
box-shadow: 3px 3px 3px #ccc;
}
.verband-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 450px;
overflow-y: auto;
@@ -145,7 +142,7 @@ body {
}
.lecture-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
max-height: 20vh;
overflow-y: auto;
@@ -154,7 +151,7 @@ body {
}
.room-selection {
border: 1px dashed #AAAAAA;
border: 1px dashed #aaaaaa;
margin-bottom: 0.5rem;
padding: 0.5rem 0 0.5rem 0.5rem;
}
@@ -169,10 +166,8 @@ body {
transform: scale(1.1);
}
.bg-lecturer-wish {
opacity: .15;
opacity: 0.15;
}
.bg-lecturer-block {
background: rgba(255, 0, 0, 0.15);
@@ -195,18 +190,21 @@ body {
.wish-w--2 {
background-color: #FF2200;
background-color: #ff2200;
}
.wish-w--1 {
background-color: #FF9922;
background-color: #ff9922;
}
.wish-w-1 {
background-color: #CCFFCC;
background-color: #ccffcc;
}
.wish-w-2 {
background-color: #48FA66;
background-color: #48fa66;
}
.fhc-calendar-base-grid-line-event {
position: relative !important;
}
.room-option {
display: grid;
@@ -214,3 +212,74 @@ body {
align-items: center;
width: 100%;
}
.updated-event {
position: relative !important;
z-index: 10;
animation: modernFocus 1s ease-out;
border-radius: 0.25rem;
}
.updated-event-long {
position: relative !important;
z-index: 10;
animation: modernFocus 1.8s ease-out;
border-radius: 0.25rem;
}
@keyframes modernFocus {
0% {
background-color: #cfd4d8;
box-shadow:
0 0 0 0 rgba(120, 120, 120, 0.8),
0 0 0 0 rgba(255, 255, 255, 0.6);
}
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);
}
}
.spinner-overlay {
position: absolute;
inset: 0; /* top:0 right:0 bottom:0 left:0 */
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
background: rgba(255, 255, 255, 1);
opacity: 0.4;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #ddd;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
+5 -21
View File
@@ -1,17 +1,11 @@
export default {
getPlan(filter, start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
getPlan(filter, start_date, end_date)
{
return {
method: 'post',
url: '/api/frontend/v1/tempus/Kalender/getPlan',
params: {
...filter,
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
params: { ...filter, start_date, end_date }
};
},
getRaumvorschlagSlots(lehreinheit_id, start_date, end_date)
@@ -27,25 +21,15 @@ export default {
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanLecturer',
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
params: { start_date, end_date }
};
},
getPlanStudent(start_date, end_date, collisionCheck = true, maxDailyEventLimit = null)
getPlanStudent(start_date, end_date)
{
return {
method: 'get',
url: '/api/frontend/v1/tempus/Kalender/getPlanStudent',
params: {
start_date,
end_date,
collisionCheck,
...(maxDailyEventLimit == null ? {} : { maxDailyEventLimit })
}
params: { start_date, end_date }
};
},
+17 -19
View File
@@ -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,
});
@@ -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: `
<div
class="fhc-calendar-base-grid-line-event event"
:class="classes"
@@ -195,5 +199,5 @@ export default {
</ul>
</teleport>
</div>
`
}
`,
};
+12 -3
View File
@@ -27,9 +27,18 @@ export default {
},
computed: {
range() {
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
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');
}
}
return luxon.Interval.fromDateTimes(first, last);
}
+11 -4
View File
@@ -27,9 +27,16 @@ export default {
},
computed: {
range() {
const targetDate = this.focusDate.plus({ weeks: this.rangeOffset });
const first = targetDate.startOf('week', { useLocaleWeeks: true });
const last = targetDate.endOf('week', { useLocaleWeeks: true });
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 });
}
}
return luxon.Interval.fromDateTimes(first, last);
}
@@ -91,7 +98,7 @@ export default {
this.$emit('update:range', this.range);
this.$refs.view.$refs.grid.enableAutoScroll();
},
template: /* html */`
template: `
<div
class="fhc-calendar-mode-week flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
+89 -75
View File
@@ -1,6 +1,6 @@
import FhcCalendar from "./Base.js";
import FhcCalendar from './Base.js';
import { useEventLoader } from '../../composables/Tempus/TempusEventLoader.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
@@ -8,11 +8,10 @@ import ModeTable from './Mode/Table.js';
import ApiKalender from '../../api/factory/tempus/kalender.js';
import draggable from '../../directives/draggable.js';
export default {
name: "CalendarTempus",
name: 'CalendarTempus',
components: {
FhcCalendar
FhcCalendar,
},
inject: {
renderers: {from: 'renderers'},
@@ -23,9 +22,9 @@ export default {
appConfig: {
from: 'appConfig',
default: {
visible_status: 'all'
}
}
visible_status: 'all',
},
},
},
directives: {
draggable,
@@ -33,39 +32,35 @@ export default {
props: {
timezone: {
type: String,
required: true
required: true,
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
default: luxon.DateTime.local(),
},
mode: {
type: String,
default: 'Week'
default: 'Week',
},
getPromiseFunc: {
type: Function,
required: true
},
cacheMultiplier: {
type: Number,
default: 1
required: true,
},
parkedEvents: {
type: Object,
default: () => new Set()
default: () => new Set(),
},
visibleLecturers: {
type: Array,
default: null
default: null,
},
extraBackgrounds: {
type: Array,
default: () => []
default: () => [],
},
visibleStatus: {
type: Array,
default: () => ['all']
default: () => ['all'],
},
showEvents: {
type: Boolean,
@@ -81,7 +76,8 @@ export default {
"resize",
"event-hover",
"event-unhover",
"open-reservierung"
"open-reservierung",
"events-reloaded",
],
data() {
@@ -94,11 +90,11 @@ export default {
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
},
week: {
collapseEmptyDays: false
}
collapseEmptyDays: false,
},
},
currentMode: this.mode,
teachingunits: null,
@@ -106,57 +102,62 @@ export default {
showRaster: true,
};
},
watch: {
events() {
this.$emit('events-reloaded');
}
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
let past = [];
if (this.mode == 'Month')
{
past = [{
class: 'background-past',
end: now.startOf('day')
}];
}
else
{
past = [{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}];
if (this.mode == 'Month') {
past = [
{
class: 'background-past',
end: now.startOf('day'),
},
];
} else {
past = [
{
class: 'background-past',
end: now,
label: now
.startOf('minute')
.toISOTime({ suppressSeconds: true, includeOffset: false }),
},
];
}
return [
...past,
...(this.extraBackgrounds || [])
];
return [...past, ...(this.extraBackgrounds || [])];
},
visibleEvents()
{
visibleEvents() {
let list = this.events;
if (Array.isArray(this.visibleLecturers))
{
if (Array.isArray(this.visibleLecturers)) {
const visibleLectures = new Set(this.visibleLecturers);
list = list.filter(event => {
if (!event.lektor?.length)
return true;
return event.lektor.some(lektor => visibleLectures.has(lektor.mitarbeiter_uid));
list = list.filter((event) => {
if (!event.lektor?.length) return true;
return event.lektor.some((lektor) =>
visibleLectures.has(lektor.mitarbeiter_uid),
);
});
}
if (!this.visibleStatus.length || this.visibleStatus.includes('all'))
return list;
return list.filter(event => this.visibleStatus.includes(event.status_kurzbz));
return list.filter((event) =>
this.visibleStatus.includes(event.status_kurzbz),
);
},
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
if (!event.farbe) return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
@@ -175,7 +176,7 @@ export default {
ondrop(payload){
this.$emit('drop', payload);
},
onresize(payload){
onresize(payload) {
this.$emit('resize', payload);
},
resetEventLoader() {
@@ -187,6 +188,24 @@ export default {
navigateNext() {
this.$refs.calendar.clickNext();
},
clearOutCalendarEventEmphasis() {
this.$refs.calendar.$el
.querySelectorAll(
'.fhc-calendar-base-grid .fhc-calendar-base-grid-line-event',
)
.forEach((el) => {
const spinner = el.querySelector('.spinner-overlay');
if (spinner) {
spinner.remove();
}
el.classList.remove(
'updating-event',
'updated-event',
'updated-event-long',
);
});
},
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
@@ -194,10 +213,9 @@ export default {
const { events, lv, reset } = useEventLoader(
rangeInterval,
props.getPromiseFunc,
() => props.cacheMultiplier
);
Vue.watch(lv, newValue => {
Vue.watch(lv, (newValue) => {
context.emit('update:lv', newValue);
});
@@ -205,31 +223,27 @@ export default {
rangeInterval,
events,
lv,
reset
reset,
};
},
created() {
this.$api
.call(ApiKalender.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
this.$api.call(ApiKalender.getStunden()).then((res) => {
return (this.teachingunits = res.data.map((el) => ({
id: el.stunde,
start: el.beginn,
end: el.ende,
})));
});
this.$api
.call(ApiKalender.getCalendarHours())
.then(res => {
this.hoursplan = {
start: res.data.start,
end: res.data.end
};
});
this.$api.call(ApiKalender.getCalendarHours()).then((res) => {
this.hoursplan = {
start: res.data.start,
end: res.data.end,
};
});
},
template: /* html */`
template: /* html */ `
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
+193 -29
View File
@@ -131,12 +131,12 @@ export default {
endTime: null,
},
studiengaenge_all: [],
maxDailyEventLimitForMonthView: 10,
raumvorschlagPreview: null,
raumvorschlagLoading: false,
showEvents: true,
reservierungPending: false,
bcc: null,
currentlyUpdatedEvent: null,
};
},
computed: {
@@ -438,9 +438,6 @@ 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;
@@ -460,41 +457,34 @@ export default {
if (hasLektoren)
filter.uid = this.lecturers.map((lecture) => lecture.uid);
let response = null;
if (this.previewRole === 'lektor')
return [
response = [
this.$api.call(
ApiKalender.getPlanLecturer(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()),
),
];
if (this.previewRole === 'student')
return [
response = [
this.$api.call(
ApiKalender.getPlanStudent(
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
),
];
return [
response = [
this.$api.call(
ApiKalender.getPlan(
filter,
start.toISODate(),
end.toISODate(),
collisionCheck,
maxDailyEventLimit,
),
ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
),
];
if (response) {
response[0].then((result) => {
this.scrollToAndEmphasizeUpdatedEvent();
});
}
return response;
},
_parseDates(start, end) {
const startDT = luxon.DateTime.fromISO(start);
@@ -536,7 +526,11 @@ 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;
}
});
},
@@ -547,12 +541,26 @@ export default {
const { item, start, end } = payload;
const obj = item[0];
if (!obj?.orig?.kalender_id)
return alert('Kein gültiges Kalender-Event zum Resizen');
return;
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
this.updateKalenderEventElementDisplay(
obj.orig.eindeutige_kalender_gruppen_id,
dates.startDT,
dates.endDT,
);
return this._updateKalenderEvent(
obj,
dates.startDT,
@@ -601,8 +609,22 @@ export default {
const dates = this._parseDates(start, end);
if (!dates) return;
if (
luxon.DateTime.fromISO(obj.orig.isostart).toMillis() ===
dates.startDT.toMillis() &&
luxon.DateTime.fromISO(obj.orig.isoend).toMillis() ===
dates.endDT.toMillis()
)
return;
const { startDT, endDT, start_time, end_time } = dates;
this.updateKalenderEventElementDisplay(
obj.orig.eindeutige_kalender_gruppen_id,
startDT,
endDT,
);
if (obj.type === 'reservierung') {
this.reservierungPending = true;
this.$refs.reservierung.show(start_time, end_time);
@@ -892,6 +914,148 @@ export default {
focusSearchbar() {
this.$refs.header?.focusSearchbar();
},
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: {
lecturers: {
@@ -1041,8 +1205,8 @@ export default {
@open-reservierung="openReservierung"
:extra-backgrounds="extraBackgrounds"
@update:range="handleRange"
@events-reloaded="clearTemporaryEvents"
class="responsive-calendar"
:cache-multiplier="currentMode === 'week' ? 1 : 0"
/>
</template>
</horizontal-split>
+75 -50
View File
@@ -1,17 +1,28 @@
// 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));
const allEvents = Vue.computed(() =>
events.value.concat(loadingEvents.value),
);
const lv = Vue.ref(null);
const eventsLoaded = [];
const mergePromiseArr = (n, o) => {
if (Array.isArray(n))
return o.concat(n);
return o.push(n), o;
if (Array.isArray(n)) return o.concat(n);
return (o.push(n), o);
};
const markEventsLoaded = (start, end) => {
@@ -20,10 +31,10 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
// empty: add new chunk
eventsLoaded.push(start.ts, end.ts);
} else {
if (eventsLoaded[eventsLoaded.length-1] + 1 == start.ts) {
if (eventsLoaded[eventsLoaded.length - 1] + 1 == start.ts) {
// add to the end of last chunk
eventsLoaded[eventsLoaded.length-1] = end.ts;
} else if (eventsLoaded[eventsLoaded.length-1] < start.ts) {
eventsLoaded[eventsLoaded.length - 1] = end.ts;
} else if (eventsLoaded[eventsLoaded.length - 1] < start.ts) {
// add new chunk after the last chunk
eventsLoaded.push(start.ts, end.ts);
} else if (eventsLoaded[0] == end.ts + 1) {
@@ -32,14 +43,13 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
} else if (eventsLoaded[0] > end.ts) {
eventsLoaded.unshift(start.ts, end.ts);
} else {
let index = eventsLoaded.findIndex(e => e >= start.ts);
let index = eventsLoaded.findIndex((e) => e >= start.ts);
if (index % 2) {
// starts inside an existing chunk
if (eventsLoaded[index] >= end.ts)
return []; // Already loaded
if (eventsLoaded[index] >= end.ts) return []; // Already loaded
let indexIsLast = (index == eventsLoaded.length - 1);
let indexIsLast = index == eventsLoaded.length - 1;
if (indexIsLast || eventsLoaded[index + 1] > end.ts) {
// extend an existing chunk
@@ -48,14 +58,16 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
start = start.plus(nStart - start.ts);
if (!indexIsLast && eventsLoaded[index + 1] == end.ts + 1)
eventsLoaded.splice(index, 2);
else
eventsLoaded[index] = end.ts;
else eventsLoaded[index] = end.ts;
} else {
// merge exising chunks
// and load the rest if necessary
if (eventsLoaded[index + 2] < end.ts) {
let rStart = eventsLoaded[index + 2] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nStart = eventsLoaded[index] + 1;
@@ -71,24 +83,32 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
// and load the rest if necessary
if (eventsLoaded[1] < end.ts) {
let rStart = eventsLoaded[1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nEnd = eventsLoaded[0] - 1;
end = end.plus(nEnd - end.ts);
eventsLoaded[0] = start.ts;
} else if (eventsLoaded[index] == start.ts) {
// starts at the same position as an existing chunk
if (eventsLoaded[index + 1] >= end.ts)
return []; // Already loaded
if (eventsLoaded[index + 1] >= end.ts) return []; // Already loaded
// load the rest
let rStart = eventsLoaded[index + 1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
} else {
// extend an existing chunk
// and load the rest if necessary
if (eventsLoaded[index + 1] < end.ts) {
let rStart = eventsLoaded[index + 1] + 1;
result = mergePromiseArr(markEventsLoaded(start.plus(rStart - start.ts), end), result);
result = mergePromiseArr(
markEventsLoaded(start.plus(rStart - start.ts), end),
result,
);
}
let nEnd = eventsLoaded[index] - 1;
end = end.plus(nEnd - end.ts);
@@ -98,52 +118,57 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
}
}
if (start.ts >= end.ts)
return result;
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);
};
const reload = () => {
const range = Vue.toValue(rangeInterval);
if (!(range instanceof luxon.Interval))
return;
const promises = markEventsLoaded(range.start, range.end);
Promise
.allSettled(promises)
.then(results => {
results.forEach(res => {
if (
res.status === 'fulfilled'
&& res.value.meta.status === "success"
) {
if (res.value.meta.lv)
lv.value = res.value.meta.lv;
if (!(range instanceof luxon.Interval)) return;
events.value = events.value.concat(res.value.data);
loadingEvents.value = [];
}
})
if (!rangeIntervalHolder || !range.equals(rangeIntervalHolder)) {
hasFirstLoadOccurred = false;
rangeIntervalHolder = range;
}
const promises = markEventsLoaded(range.start, range.end);
Promise.allSettled(promises).then((results) => {
results.forEach((res) => {
if (res.status === 'fulfilled' && res.value.meta.status === 'success') {
if (res.value.meta.lv) lv.value = res.value.meta.lv;
tempEventsHolder = tempEventsHolder.concat(res.value.data);
loadingEvents.value = [];
}
});
events.value = tempEventsHolder;
});
hasFirstLoadOccurred = true;
};
Vue.watchEffect(reload);
const reset = () => {
loading_id = 0;
events.value = [];
loadingEvents.value = [];
tempEventsHolder = [];
//loadingEvents.value = [];
eventsLoaded.splice(0, eventsLoaded.length);
reload();
}
};
return { events: allEvents, lv, reset }
}
return { events: allEvents, lv, reset };
}
@@ -1,233 +0,0 @@
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);
};
+6 -16
View File
@@ -1,17 +1,11 @@
const clickListeners = [];
function removeClickListener(el) {
const index = clickListeners.findIndex(data => data.el == el);
if (index < 0)
return;
el.removeEventListener('click', clickListeners[index].listener);
clickListeners.splice(index, 1);
}
function saveAddClickListener(el, source, value) {
removeClickListener(el);
const index = clickListeners.findIndex(data => data.el == el);
if (index >= 0) {
el.removeEventListener('click', clickListeners[index].listener);
clickListeners.splice(index, 1);
}
const listener = evt => {
evt.preventDefault();
evt.stopPropagation();
@@ -51,9 +45,5 @@ export default {
if (binding.arg != 'container') {
saveAddClickListener(el, binding.arg, binding.value);
}
},
beforeUnmount(el, binding) {
if (binding.arg != 'container')
removeClickListener(el);
}
}
}