mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 06:54:04 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e90b1841 | ||
|
|
a99730b369 | ||
|
|
26b779e864 | ||
|
|
76fe4f567c | ||
|
|
5d0c6c1b4e | ||
|
|
45b6c190b6 | ||
|
|
c91a6c6376 | ||
|
|
ed02955b49 | ||
|
|
4ecd4b5a65 | ||
|
|
6ea1d4610d | ||
|
|
63af8efb44 | ||
|
|
f0b1689a06 | ||
|
|
6e6cb8ae11 | ||
|
|
1bbe4368bb | ||
|
|
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);
|
||||
|
||||
@@ -242,7 +242,7 @@ class KalenderLib
|
||||
return $this->_ci->KalenderModel->execReadOnlyQuery($query);
|
||||
}
|
||||
|
||||
private function _mapEvents($data, $collisionCheck = true)
|
||||
private function _mapEvents($data, $collisionCheck = true, $maxDailyEventLimit = null)
|
||||
{
|
||||
$stundenplan_data = [];
|
||||
|
||||
@@ -263,6 +263,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'),
|
||||
@@ -281,7 +282,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' => [],
|
||||
@@ -385,6 +385,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)
|
||||
@@ -455,7 +470,7 @@ class KalenderLib
|
||||
$data = $this->_loadFilteredBasePlan($kalender_entry->von, $kalender_entry->bis);
|
||||
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->_buildBasePlanQuery();
|
||||
|
||||
@@ -562,7 +577,7 @@ class KalenderLib
|
||||
|
||||
$data = $this->_loadFilteredBasePlan($start_date, $end_date);
|
||||
|
||||
return $this->_mapEvents($data);
|
||||
return $this->_mapEvents($data, $collisionCheck, $maxDailyEventLimit);
|
||||
}
|
||||
|
||||
public function getPlanForStudent($start_date, $end_date)
|
||||
|
||||
+98
-29
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -123,6 +123,10 @@ export default {
|
||||
draggableEvents: [Boolean, Array, Function],
|
||||
dropableEvents: [Boolean, Array, Function],
|
||||
resizableEvents: [Boolean, Array, Function],
|
||||
eventReloadKey: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
onDragover: Function,
|
||||
onDrop: Function,
|
||||
onResize: Function
|
||||
@@ -321,6 +325,7 @@ export default {
|
||||
</base-header>
|
||||
<component
|
||||
:is="modes ? modes[cMode] : null || 'div'"
|
||||
:key="eventReloadKey"
|
||||
ref="mode"
|
||||
v-model:current-date="cDate"
|
||||
:range-end="pickedRangeEnd"
|
||||
@@ -343,4 +348,4 @@ export default {
|
||||
</bs-modal>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -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 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';
|
||||
@@ -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,35 +32,39 @@ 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
|
||||
required: true,
|
||||
},
|
||||
cacheMultiplier: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
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,
|
||||
@@ -77,11 +80,14 @@ export default {
|
||||
"resize",
|
||||
"event-hover",
|
||||
"event-unhover",
|
||||
"open-reservierung"
|
||||
"open-reservierung",
|
||||
"events-reloaded",
|
||||
],
|
||||
|
||||
data() {
|
||||
return {
|
||||
eventReloadKey: 0,
|
||||
refreshEventsAfterReload: false,
|
||||
modes: {
|
||||
week: Vue.markRaw(ModeWeek),
|
||||
month: Vue.markRaw(ModeMonth),
|
||||
@@ -90,11 +96,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,
|
||||
@@ -102,57 +108,67 @@ export default {
|
||||
showRaster: true,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
events() {
|
||||
if (this.refreshEventsAfterReload) {
|
||||
this.eventReloadKey += 1;
|
||||
this.refreshEventsAfterReload = false;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -171,11 +187,15 @@ export default {
|
||||
ondrop(payload){
|
||||
this.$emit('drop', payload);
|
||||
},
|
||||
onresize(payload){
|
||||
onresize(payload) {
|
||||
this.$emit('resize', payload);
|
||||
},
|
||||
resetEventLoader() {
|
||||
this.reset();
|
||||
resetEventLoader(arePreviousEventsCleared = true) {
|
||||
this.reset(arePreviousEventsCleared);
|
||||
},
|
||||
reloadEvents() {
|
||||
this.refreshEventsAfterReload = true;
|
||||
this.resetEventLoader(false);
|
||||
},
|
||||
navigatePrev() {
|
||||
this.$refs.calendar.clickPrev();
|
||||
@@ -183,13 +203,35 @@ 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);
|
||||
|
||||
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
|
||||
const { events, lv, reset } = useEventLoader(
|
||||
rangeInterval,
|
||||
props.getPromiseFunc,
|
||||
() => props.cacheMultiplier
|
||||
);
|
||||
|
||||
Vue.watch(lv, newValue => {
|
||||
Vue.watch(lv, (newValue) => {
|
||||
context.emit('update:lv', newValue);
|
||||
});
|
||||
|
||||
@@ -197,31 +239,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"
|
||||
@@ -232,6 +270,7 @@ export default {
|
||||
:timezone="timezone"
|
||||
:locale="$p.user_locale.value"
|
||||
:events="visibleEvents || []"
|
||||
:event-reload-key="eventReloadKey"
|
||||
:backgrounds="backgrounds"
|
||||
:time-grid="showRaster ? teachingunits : null"
|
||||
:hours-plan="hoursplan"
|
||||
|
||||
@@ -131,11 +131,13 @@ export default {
|
||||
endTime: null,
|
||||
},
|
||||
studiengaenge_all: [],
|
||||
maxDailyEventLimitForMonthView: 10,
|
||||
raumvorschlagPreview: null,
|
||||
raumvorschlagLoading: false,
|
||||
showEvents: true,
|
||||
reservierungPending: false,
|
||||
bcc: null,
|
||||
currentlyUpdatedEvent: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -163,7 +165,7 @@ export default {
|
||||
if (!orig?.kalender_id) return;
|
||||
|
||||
await this.deleteEntryCall(orig);
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.rebuildRaumvorschlag();
|
||||
},
|
||||
@@ -174,7 +176,7 @@ export default {
|
||||
await Promise.allSettled(
|
||||
validList.map((orig) => this.deleteEntryCall(orig)),
|
||||
);
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
},
|
||||
async deleteEntryCall(orig) {
|
||||
@@ -437,6 +439,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;
|
||||
@@ -456,25 +461,50 @@ 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()),
|
||||
ApiKalender.getPlanLecturer(
|
||||
start.toISODate(),
|
||||
end.toISODate(),
|
||||
collisionCheck,
|
||||
maxDailyEventLimit,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
if (this.previewRole === 'student')
|
||||
return [
|
||||
response = [
|
||||
this.$api.call(
|
||||
ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
|
||||
ApiKalender.getPlanStudent(
|
||||
start.toISODate(),
|
||||
end.toISODate(),
|
||||
collisionCheck,
|
||||
maxDailyEventLimit,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return [
|
||||
response = [
|
||||
this.$api.call(
|
||||
ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
|
||||
ApiKalender.getPlan(
|
||||
filter,
|
||||
start.toISODate(),
|
||||
end.toISODate(),
|
||||
collisionCheck,
|
||||
maxDailyEventLimit,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
if (response) {
|
||||
response[0].then((result) => {
|
||||
this.scrollToAndEmphasizeUpdatedEvent();
|
||||
});
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
_parseDates(start, end) {
|
||||
const startDT = luxon.DateTime.fromISO(start);
|
||||
@@ -516,7 +546,19 @@ 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;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.currentlyUpdatedEvent = null;
|
||||
this.$refs.calendar.clearOutCalendarEventEmphasis();
|
||||
this.$nextTick(() => {
|
||||
this.$refs.calendar.reloadEvents();
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -527,12 +569,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,
|
||||
@@ -540,7 +596,7 @@ export default {
|
||||
dates.start_time,
|
||||
dates.end_time,
|
||||
() => {
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.rebuildRaumvorschlag();
|
||||
},
|
||||
@@ -571,7 +627,7 @@ export default {
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.rebuildRaumvorschlag();
|
||||
this.bcc.postMessage('dropped');
|
||||
@@ -581,8 +637,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);
|
||||
@@ -628,7 +698,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.rebuildRaumvorschlag();
|
||||
this.bcc.postMessage('dropped');
|
||||
@@ -646,7 +716,7 @@ export default {
|
||||
),
|
||||
)
|
||||
.then(() => {
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.bcc.postMessage('dropped');
|
||||
this.rebuildRaumvorschlag();
|
||||
@@ -671,7 +741,7 @@ export default {
|
||||
type: obj.type,
|
||||
id: obj.orig.kalender_id,
|
||||
});
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.rebuildRaumvorschlag();
|
||||
this.bcc.postMessage('dropped');
|
||||
},
|
||||
@@ -691,7 +761,7 @@ export default {
|
||||
this.multiWeekModal.show = false;
|
||||
},
|
||||
onMultiWeekConfirmed() {
|
||||
this.$refs.calendar.resetEventLoader();
|
||||
this.$refs.calendar.resetEventLoader(false);
|
||||
this.$refs.sidebar.reloadCoursepicker();
|
||||
this.rebuildRaumvorschlag();
|
||||
this.bcc.postMessage('dropped');
|
||||
@@ -872,6 +942,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: {
|
||||
@@ -1021,7 +1233,9 @@ 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>
|
||||
@@ -1032,7 +1246,6 @@ export default {
|
||||
ref="verbandMenu"
|
||||
@select-verband-and-close="onSelectVerbandAndClose"
|
||||
/>
|
||||
<raumauswahl-modal ref="raumModal" @saved="$refs.calendar.resetEventLoader()"/>
|
||||
<raumauswahl-modal ref="raumModal" @saved="() => {$refs.calendar.resetEventLoader(); $refs.sidebar.reloadCoursepicker(); rebuildRaumvorschlag();}"/>
|
||||
<lehreinheit-modal ref="lehreinheitModal" @saved="$refs.calendar.resetEventLoader(); $refs.sidebar.reloadCoursepicker();"/>
|
||||
<resources-assignment-modal
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// TODO(chris): load events that are longer than the interval without doubling it
|
||||
|
||||
export function useEventLoader(rangeInterval, getPromiseFunc) {
|
||||
let loading_id = 0;
|
||||
const events = Vue.ref([]);
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
export function useEventLoader(
|
||||
rangeInterval,
|
||||
getPromiseFunc,
|
||||
cacheMultiplier = 1,
|
||||
) {
|
||||
let hasFirstLoadOccurred = false;
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
ensureCacheRangeIsInAllowedRange(
|
||||
allowedCacheStartTimestamp,
|
||||
allowedCacheEndTimestamp,
|
||||
);
|
||||
|
||||
if (promises.length === 0) {
|
||||
allEvents.value = removeVisualForEventsLoading(
|
||||
ensureEventsAreInValidCacheRange(allEvents.value),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
hasFirstLoadOccurred = false;
|
||||
|
||||
addVisualForEventsLoading();
|
||||
|
||||
Promise.allSettled(promises).then((results) => {
|
||||
let newlyLoadedEvents = [];
|
||||
let hasSuccessfullyLoadedEvents = false;
|
||||
|
||||
results.forEach((res) => {
|
||||
if (
|
||||
!(res.status === "fulfilled" && res.value.meta.status === "success")
|
||||
)
|
||||
return;
|
||||
|
||||
hasSuccessfullyLoadedEvents = true;
|
||||
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(),
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
allEvents.value = removeVisualForEventsLoading(
|
||||
ensureEventsAreInValidCacheRange(tempAllEvents),
|
||||
);
|
||||
|
||||
hasFirstLoadOccurred = true;
|
||||
});
|
||||
};
|
||||
|
||||
Vue.watchEffect(reload);
|
||||
|
||||
const reset = (arePreviousEventsCleared = true) => {
|
||||
if (arePreviousEventsCleared) {
|
||||
allEvents.value = [];
|
||||
hasFirstLoadOccurred = false;
|
||||
}
|
||||
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) => {
|
||||
if (hasFirstLoadOccurred) return;
|
||||
|
||||
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 ensureCacheRangeIsInAllowedRange = (
|
||||
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