mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-15 14:02:17 +00:00
1578 lines
50 KiB
JavaScript
1578 lines
50 KiB
JavaScript
/**
|
|
* Copyright (C) 2024 fhcomplete.org
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
import CoreSearchbar from "../searchbar/searchbar.js";
|
|
import NavLanguage from "../navigation/Language.js";
|
|
import VerticalSplit from "../verticalsplit/verticalsplit.js";
|
|
import FhcCalendar from "../Calendar/Tempus.js";
|
|
import FhcCoursepicker from "../Tempus/Coursepicker.js";
|
|
import LectureSelection from "../Tempus/LectureSelection.js";
|
|
import ParkingSlot from "../Tempus/ParkingSlot.js";
|
|
import ApiKalender from "../../api/factory/tempus/kalender.js";
|
|
import ApiSearchbar from "../../api/factory/searchbar.js";
|
|
import ApiRenderers from "../../api/factory/renderers.js";
|
|
import ApiTempusConfig from "../../api/factory/tempus/config.js";
|
|
import ApiBetriebsmittel from "../../api/factory/betriebsmittel.js";
|
|
import ApiOperationalResourceToCalender from "../../api/factory/operationalResourceToCalender.js";
|
|
import AppMenu from "../AppMenu.js";
|
|
import drop from "../../directives/drop.js";
|
|
import AppConfig from "../AppConfig.js";
|
|
import BsModal from "../Bootstrap/Modal.js";
|
|
|
|
import StvVerband from "../Stv/Studentenverwaltung/Verband.js";
|
|
import ApiStudiengangTree from "../../api/lehrveranstaltung/studiengangtree.js";
|
|
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
|
|
import FormInput from "../../../js/components/Form/Input.js";
|
|
import Reservierung from "./Reservierung.js";
|
|
import { getTempusShortcuts } from "./shortcuts.js";
|
|
import KeyboardShortcuts from "./KeyboardShortcuts.js";
|
|
import { useContextMenuActions } from "../../composables/Tempus/ContextMenuActions.js";
|
|
import MultiWeekPlanModal from "./MultiWeekPlanModal.js";
|
|
|
|
export default {
|
|
name: "Tempus",
|
|
components: {
|
|
CoreSearchbar,
|
|
VerticalSplit,
|
|
FhcCalendar,
|
|
FhcCoursepicker,
|
|
LectureSelection,
|
|
ParkingSlot,
|
|
AppConfig,
|
|
AppMenu,
|
|
NavLanguage,
|
|
BsModal,
|
|
StvVerband,
|
|
StvStudiensemester,
|
|
Multiselect: primevue.multiselect,
|
|
FormInput,
|
|
Reservierung,
|
|
KeyboardShortcuts,
|
|
MultiWeekPlanModal
|
|
},
|
|
props: {
|
|
defaultSemester: String,
|
|
config: Object,
|
|
permissions: Object,
|
|
tempusRoot: String,
|
|
cisRoot: String,
|
|
activeAddons: String, // semicolon separated list of active addons
|
|
viewData: Object,
|
|
logoutUrl: String,
|
|
avatarUrl: String,
|
|
},
|
|
directives: {
|
|
drop,
|
|
},
|
|
provide() {
|
|
return {
|
|
cisRoot: this.cisRoot,
|
|
defaultSemester: this.defaultSemester,
|
|
currentSemester: this.defaultSemester,
|
|
renderers: Vue.computed(() => this.renderers),
|
|
appConfig: Vue.computed(() => this.appconfig),
|
|
contextMenuActions: useContextMenuActions({
|
|
openRaumauswahl: (orig) => this.openRaumauswahl(orig),
|
|
openResourcesAssignmentModal: (orig) => this.openResourcesAssignmentModal(orig),
|
|
openHistory: (orig) => this.openHistory(orig),
|
|
deleteEntry: (orig) => this.deleteEntry(orig),
|
|
syncToLecturer: (orig) => this.syncToLecturer(orig),
|
|
syncToStudent: (orig) => this.syncToStudent(orig),
|
|
}),
|
|
tableActions: {
|
|
deleteEntries: (origList) => this.deleteEntries(origList),
|
|
openRaumauswahl: (orig) => this.openRaumauswahl(orig),
|
|
},
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
appconfig: {},
|
|
currentMode: 'week',
|
|
configEndpoints: ApiTempusConfig,
|
|
endpoint: ApiStudiengangTree,
|
|
raumVorschlaege: [],
|
|
selected: [],
|
|
searchbaroptions: {
|
|
origin: "tempus",
|
|
cssclass: "position-relative",
|
|
calcheightonly: true,
|
|
types: [
|
|
//"student",
|
|
"raum",
|
|
"mitarbeiter",
|
|
"mitarbeiter_ohne_zuordnung",
|
|
],
|
|
actions: {
|
|
raum: {
|
|
defaultaction: {
|
|
type: "function",
|
|
action: this.setOrt,
|
|
},
|
|
childactions: [],
|
|
},
|
|
employee: {
|
|
defaultaction: {
|
|
type: "function",
|
|
action: (data) => {
|
|
this.setEmp(data);
|
|
},
|
|
},
|
|
childactions: [],
|
|
},
|
|
},
|
|
},
|
|
lv_id: null,
|
|
events: null,
|
|
minimized: false,
|
|
currentlySelectedEvent: null,
|
|
hoveredEvent: null,
|
|
//currentDay: new Date(),
|
|
studiensemesterKurzbz: this.defaultSemester,
|
|
lists: {
|
|
nations: [],
|
|
sprachen: [],
|
|
geschlechter: [],
|
|
},
|
|
renderers: null,
|
|
ort_kurzbz: null,
|
|
view: "room",
|
|
parkedKeys: new Set(),
|
|
lecturers: [],
|
|
overlayCache: [],
|
|
extraBackgrounds: [],
|
|
lastRange: null,
|
|
stg: null,
|
|
show_stg: null,
|
|
semester: null,
|
|
studiensemester_kurzbz: null,
|
|
raumModal: {
|
|
show: false,
|
|
loading: false,
|
|
vorschlaege: [],
|
|
event: null,
|
|
},
|
|
visibleStatusArray: {},
|
|
visibleStatus: ["all"],
|
|
selectedStudiensemester:
|
|
this.studiensemester_kurzbz ?? this.defaultSemester,
|
|
calendarDate: luxon.DateTime.now()
|
|
.setZone(this.config.timezone)
|
|
.toISODate(),
|
|
historyEntries: [],
|
|
previewRole: "planer",
|
|
resourcesAssignmentModal: {
|
|
calendar: null,
|
|
availableResources: [],
|
|
filteredAvailableResources: [],
|
|
selectedAvailableResource: null,
|
|
assignedResources: [],
|
|
areFormButtonsDisplayed: false,
|
|
},
|
|
currentlyUpdatedEvent: null,
|
|
multiWeekModal: {
|
|
show: false,
|
|
lehreinheitId: null,
|
|
ortKurzbz: null,
|
|
startTime: null,
|
|
endTime: null,
|
|
},
|
|
};
|
|
},
|
|
computed: {
|
|
currentDay() {
|
|
return luxon.DateTime.now().setZone(this.config.timezone).toISODate();
|
|
},
|
|
visibleLecturerUids() {
|
|
if (!this.lecturers.length) return null;
|
|
return this.lecturers
|
|
.filter((lecture) => lecture.showEvents)
|
|
.map((lecture) => lecture.uid);
|
|
},
|
|
visibleStatusOptions() {
|
|
return Object.entries(this.visibleStatusArray).map(([key, label]) => ({
|
|
key,
|
|
label,
|
|
}));
|
|
},
|
|
visibleStatusValue() {
|
|
if (this.visibleStatus.includes("all"))
|
|
return this.visibleStatusOptions.filter(
|
|
(visibleStatus) => visibleStatus.key === "all",
|
|
);
|
|
return this.visibleStatus.map((status) => ({
|
|
key: status,
|
|
label: this.visibleStatusArray[status],
|
|
}));
|
|
},
|
|
keyboardShortcuts() {
|
|
return getTempusShortcuts(this);
|
|
},
|
|
dropdownParsedAvailableResources() {
|
|
return this.resourcesAssignmentModal.availableResources
|
|
.map((unit) => {
|
|
return {
|
|
label: unit.beschreibung,
|
|
value: unit.betriebsmittel_id,
|
|
data: unit,
|
|
};
|
|
})
|
|
.sort((a, b) => a.label?.localeCompare(b.label));
|
|
},
|
|
},
|
|
methods: {
|
|
async openRaumauswahl(orig) {
|
|
if (!orig?.kalender_id) return;
|
|
this.raumModal = orig;
|
|
|
|
await this.$api
|
|
.call(ApiKalender.getRaumvorschlag(orig.kalender_id))
|
|
.then((result) => {
|
|
this.raumVorschlaege = result.data ?? [];
|
|
this.$refs.raumModal.show();
|
|
});
|
|
},
|
|
async openResourcesAssignmentModal(orig) {
|
|
if (!orig?.kalender_id) return;
|
|
|
|
this.resourcesAssignmentModal.calendar = orig;
|
|
this.resourcesAssignmentModal.availableResources =
|
|
await this.fetchSchedulableResourcesByCalender(orig.kalender_id);
|
|
this.resourcesAssignmentModal.filteredAvailableResources = [
|
|
...this.dropdownParsedAvailableResources,
|
|
];
|
|
|
|
this.resourcesAssignmentModal.assignedResources =
|
|
await this.fetchAssignedResourcesByCalender(orig.kalender_id);
|
|
|
|
this.$refs.resourcesAssignmentModal.show();
|
|
},
|
|
async deleteEntry(orig)
|
|
{
|
|
if (!orig?.kalender_id)
|
|
return;
|
|
|
|
await this.deleteEntryCall(orig);
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
},
|
|
async deleteEntries(origList)
|
|
{
|
|
let validList = (origList ?? []).filter(orig => orig?.kalender_id);
|
|
if (!validList.length)
|
|
return;
|
|
|
|
await Promise.allSettled(validList.map(orig => this.deleteEntryCall(orig)));
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
},
|
|
async deleteEntryCall(orig)
|
|
{
|
|
await this.$api.call(ApiKalender.deleteEntry(
|
|
orig.kalender_id
|
|
)).then(() => {
|
|
this.$refs.parking.unpark({ type: orig.type, id: orig.kalender_id });
|
|
});
|
|
},
|
|
async openHistory(orig) {
|
|
if (!orig?.kalender_id) return;
|
|
await this.$api
|
|
.call(ApiKalender.getHistory(orig.kalender_id))
|
|
.then((result) => {
|
|
this.historyEntries = result.data ?? [];
|
|
this.$refs.historyModel.show();
|
|
});
|
|
},
|
|
syncToLecturer(orig) {
|
|
if (!orig?.kalender_id)
|
|
return;
|
|
return this.$api.call(ApiKalender.syncToLecturer(orig.kalender_id))
|
|
.then(() => this.$refs.calendar.resetEventLoader());
|
|
},
|
|
syncToStudent(orig) {
|
|
if (!orig?.kalender_id)
|
|
return;
|
|
return this.$api.call(ApiKalender.syncToStudent(orig.kalender_id))
|
|
.then(() => this.$refs.calendar.resetEventLoader());
|
|
},
|
|
async selectRaum(ort_kurzbz) {
|
|
const orig = this.raumModal;
|
|
await this.$api
|
|
.call(
|
|
ApiKalender.updateKalenderEvent(orig.kalender_id, {
|
|
ort_kurzbz,
|
|
start_time: orig.von,
|
|
end_time: orig.bis,
|
|
}),
|
|
)
|
|
.then(() => this.$refs.raumModal.hide());
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
setOrt: function (data) {
|
|
this.ort_kurzbz = data.ort_kurzbz;
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
onSelectVerbandAndClose(payload) {
|
|
this.onSelectVerband(payload);
|
|
bootstrap.Offcanvas.getOrCreateInstance(this.$refs.verbandMenu).hide();
|
|
},
|
|
onSelectVerband({ link, name }) {
|
|
let stg = null;
|
|
let semester = null;
|
|
this.show_stg = name;
|
|
if (typeof link === "number") stg = link;
|
|
else if (typeof link === "string") {
|
|
[stg, semester] = link.split("/");
|
|
}
|
|
this.stg = stg;
|
|
if (semester !== null) this.semester = semester;
|
|
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
setEmp: function (data) {
|
|
const uid = data.uid;
|
|
const label = data.name;
|
|
if (!this.lecturers.some((l) => l.uid === uid)) {
|
|
this.lecturers.push({
|
|
uid,
|
|
label,
|
|
showEvents: true,
|
|
overlays: { blocks: true, wishes: true },
|
|
});
|
|
}
|
|
|
|
this.$refs.calendar.resetEventLoader();
|
|
if (this.lastRange) this.handleRange(this.lastRange);
|
|
},
|
|
jumpToKw(kw) {
|
|
const num = parseInt(kw);
|
|
if (!num) return;
|
|
|
|
const date = luxon.DateTime.fromObject(
|
|
{
|
|
weekYear: luxon.DateTime.now().setZone(this.config.timezone).weekYear,
|
|
weekNumber: num,
|
|
weekday: 1,
|
|
},
|
|
{ zone: this.config.timezone },
|
|
);
|
|
this.calendarDate = date.toISODate();
|
|
},
|
|
handleChangeDate(newDate) {
|
|
if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid)
|
|
this.calendarDate = newDate.toISODate();
|
|
},
|
|
handleChangeMode(newMode, newDate) {
|
|
if (!newMode)
|
|
return;
|
|
this.currentMode = newMode;
|
|
if (newDate && luxon.DateTime.isDateTime(newDate) && newDate.isValid)
|
|
this.calendarDate = newDate.toISODate();
|
|
},
|
|
toggleStatus(selected) {
|
|
if (!selected || selected.length === 0) {
|
|
this.visibleStatus = ["all"];
|
|
return;
|
|
}
|
|
const hasAll = selected.includes("all");
|
|
const hadAll = this.visibleStatus.includes("all");
|
|
|
|
if (hasAll && !hadAll) {
|
|
this.visibleStatus = ["all"];
|
|
return;
|
|
}
|
|
this.visibleStatus = selected.filter((k) => k !== "all");
|
|
if (this.visibleStatus.length === 0) this.visibleStatus = ["all"];
|
|
},
|
|
searchfunction(params) {
|
|
return this.$api.call(ApiSearchbar.search(params));
|
|
},
|
|
getPromiseFunc(start, end) {
|
|
const hasRoom = !!this.ort_kurzbz;
|
|
const hasLektoren = this.lecturers.length > 0;
|
|
const hasStg = !!this.stg;
|
|
|
|
const filter = {};
|
|
|
|
if (hasRoom) filter.ort = this.ort_kurzbz;
|
|
if (hasStg) filter.stg = this.stg;
|
|
if (hasLektoren) filter.uid = this.lecturers.map((l) => l.uid);
|
|
|
|
let response = null;
|
|
if (this.previewRole === "lektor") {
|
|
response = [
|
|
this.$api.call(
|
|
ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()),
|
|
),
|
|
];
|
|
} else if (this.previewRole === "student") {
|
|
response = [
|
|
this.$api.call(
|
|
ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
|
|
),
|
|
];
|
|
} else {
|
|
response = [
|
|
this.$api.call(
|
|
ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
|
|
),
|
|
];
|
|
}
|
|
|
|
|
|
if (response) {
|
|
response[0].then((result) => {
|
|
this.scrollToAndEmphasizeUpdatedEvent();
|
|
});
|
|
}
|
|
|
|
return response;
|
|
},
|
|
toDateTime(value, timezone) {
|
|
if (luxon.DateTime.isDateTime(value)) return value;
|
|
|
|
if (value?.date?.isValid) return value.date;
|
|
|
|
if (typeof value === "number")
|
|
return luxon.DateTime.fromMillis(value, { zone: timezone });
|
|
|
|
if (value instanceof Date)
|
|
return luxon.DateTime.fromJSDate(value, { zone: timezone });
|
|
|
|
if (typeof value === "string")
|
|
return luxon.DateTime.fromISO(value, { zone: timezone });
|
|
|
|
return luxon.DateTime.invalid("invalid datetime");
|
|
},
|
|
getLastEndOfSameDay(startDT, ends) {
|
|
if (!ends?.length) return null;
|
|
|
|
const dayKey = startDT.toISODate();
|
|
let lastSameDay = null;
|
|
|
|
for (const end of ends) {
|
|
const dt = luxon.DateTime.isDateTime(end)
|
|
? end
|
|
: luxon.DateTime.fromISO(String(end), { zone: startDT.zoneName });
|
|
|
|
if (!dt.isValid) continue;
|
|
|
|
if (dt.toISODate() === dayKey) lastSameDay = dt;
|
|
}
|
|
|
|
return lastSameDay;
|
|
},
|
|
clampEndToGrid(startDT, durationMin, ends) {
|
|
const calculatedEnd = startDT.plus({ minutes: durationMin });
|
|
|
|
const lastGridEndSameDay = this.getLastEndOfSameDay(startDT, ends);
|
|
|
|
if (!lastGridEndSameDay) return calculatedEnd;
|
|
|
|
return calculatedEnd > lastGridEndSameDay
|
|
? lastGridEndSameDay
|
|
: calculatedEnd;
|
|
},
|
|
_parseDates(start, end) {
|
|
const startDT = luxon.DateTime.fromISO(start);
|
|
const endDT = luxon.DateTime.fromISO(end);
|
|
|
|
if (!startDT.isValid || !endDT.isValid) {
|
|
alert("Ungültiges Datum");
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
startDT,
|
|
endDT,
|
|
start_time: startDT.toFormat("yyyy-MM-dd HH:mm"),
|
|
end_time: endDT.toFormat("yyyy-MM-dd HH:mm"),
|
|
};
|
|
},
|
|
|
|
_updateKalenderEvent(
|
|
obj,
|
|
startDT,
|
|
endDT,
|
|
start_time,
|
|
end_time,
|
|
onSuccess,
|
|
onError,
|
|
) {
|
|
const origStart = luxon.DateTime.fromISO(obj.orig.isostart);
|
|
const origEnd = luxon.DateTime.fromISO(obj.orig.isoend);
|
|
|
|
if (
|
|
origStart.toMillis() === startDT.toMillis() &&
|
|
origEnd.toMillis() === endDT.toMillis()
|
|
)
|
|
return;
|
|
|
|
const updatedInfos = {
|
|
ort_kurzbz: this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz,
|
|
start_time,
|
|
end_time,
|
|
};
|
|
|
|
return this.$api
|
|
.call(
|
|
ApiKalender.updateKalenderEvent(obj.orig.kalender_id, updatedInfos),
|
|
)
|
|
.then(() => {
|
|
if (onSuccess) {
|
|
onSuccess();
|
|
this.$refs.calendar.$refs.calendar.$refs.mode.$refs.view.$refs.grid.disableAutoScroll();
|
|
this.currentlyUpdatedEvent = obj.orig;
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (onError) {
|
|
onError(error);
|
|
}
|
|
});
|
|
},
|
|
|
|
resizeHandler(payload) {
|
|
if (this.previewRole !== "planer")
|
|
//TODO (david) testzweck
|
|
return;
|
|
const { item, start, end } = payload;
|
|
const obj = item[0];
|
|
if (!obj?.orig?.kalender_id) 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.kalender_id,
|
|
dates.startDT,
|
|
dates.endDT,
|
|
);
|
|
|
|
return this._updateKalenderEvent(
|
|
obj,
|
|
dates.startDT,
|
|
dates.endDT,
|
|
dates.start_time,
|
|
dates.end_time,
|
|
() => {
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
},
|
|
() => {
|
|
this.$refs.calendar.clearOutCalendarEventEmphasis();
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
},
|
|
);
|
|
},
|
|
|
|
dropHandler(payload) {
|
|
if (this.previewRole !== "planer")
|
|
//TODO (david) testzweck
|
|
return;
|
|
const { item, start, end } = payload;
|
|
if (!item?.length) return alert("Keine Daten gedroppt");
|
|
|
|
const obj = item[0];
|
|
if (!obj?.type) return alert("Unbekannter Drop-Typ");
|
|
|
|
if (payload.targetKalenderId)
|
|
{
|
|
if (obj.type !== 'lehreinheit' || !obj.orig?.lehreinheit_id)
|
|
return alert("Nur Lehreinheiten können einem Termin hinzugefügt werden");
|
|
|
|
return this.$api.call(
|
|
ApiKalender.addToKalenderEvent(payload.targetKalenderId, obj.orig.lehreinheit_id)
|
|
).then(() => {
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.bcc.postMessage('dropped');
|
|
});
|
|
}
|
|
|
|
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.kalender_id,
|
|
startDT,
|
|
endDT,
|
|
);
|
|
|
|
if (obj.type === "reservierung") {
|
|
this.reservierungPending = true;
|
|
this.$refs.reservierung.show(start_time, end_time);
|
|
} else if (obj.type === "lehreinheit") {
|
|
|
|
if (obj.multiweek)
|
|
{
|
|
this.openMultiWeekPreview(
|
|
obj.orig.lehreinheit_id,
|
|
this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz,
|
|
start_time,
|
|
end_time
|
|
);
|
|
return;
|
|
}
|
|
|
|
return this.$api
|
|
.call(
|
|
ApiKalender.addKalenderEvent(
|
|
obj.orig.lehreinheit_id,
|
|
this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz,
|
|
start_time,
|
|
end_time,
|
|
),
|
|
)
|
|
.then(result => result.data)
|
|
.then(() => {
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
this.bcc.postMessage("dropped");
|
|
});
|
|
} else if (obj.type === "kalender") {
|
|
return this._updateKalenderEvent(
|
|
obj,
|
|
startDT,
|
|
endDT,
|
|
start_time,
|
|
end_time,
|
|
() => {
|
|
this.$refs.parking.unpark({
|
|
type: obj.type,
|
|
id: obj.orig.kalender_id,
|
|
});
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
this.bcc.postMessage("dropped");
|
|
},
|
|
() => {
|
|
this.$refs.calendar.clearOutCalendarEventEmphasis();
|
|
this.updateKalenderEventElementDisplay(
|
|
obj.orig.kalender_id,
|
|
luxon.DateTime.fromISO(obj.orig.isostart),
|
|
luxon.DateTime.fromISO(obj.orig.isoend)
|
|
);
|
|
this.$refs.calendar.clearOutCalendarEventEmphasis();
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
);
|
|
} else {
|
|
alert("Unbekannter Drop-Typ: " + obj.type);
|
|
}
|
|
},
|
|
openMultiWeekPreview(lehreinheit_id, ort_kurzbz, start_time, end_time) {
|
|
this.multiWeekModal.lehreinheitId = lehreinheit_id;
|
|
this.multiWeekModal.ortKurzbz = ort_kurzbz;
|
|
this.multiWeekModal.startTime = start_time;
|
|
this.multiWeekModal.endTime = end_time;
|
|
this.multiWeekModal.show = true;
|
|
},
|
|
closeMultiWeekModal() {
|
|
this.multiWeekModal.show = false;
|
|
},
|
|
onMultiWeekConfirmed() {
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.coursepicker.reload();
|
|
this.bcc.postMessage('dropped');
|
|
},
|
|
handleRange(range) {
|
|
if (!range?.start || !range?.end) return;
|
|
|
|
if (this.currentMode === "week") {
|
|
//Workaround because, updateRange is emitting 2 times
|
|
const startDay = range.start.startOf("day");
|
|
const endDay = range.end.startOf("day");
|
|
|
|
const days = Math.round(endDay.diff(startDay, "days").days) + 1;
|
|
if (days > 8) return;
|
|
}
|
|
|
|
this.lastRange = range;
|
|
|
|
const key = `${range.start.toISODate()}_${range.end.toISODate()}_${this.currentMode}`;
|
|
|
|
for (const lect of this.lecturers) {
|
|
this.getOverlays(lect.uid, range, key);
|
|
}
|
|
|
|
this.rebuildExtraBackgrounds();
|
|
},
|
|
|
|
getOverlays(uid, range, rangeKey) {
|
|
if (!this.overlayCache[uid]) this.overlayCache[uid] = {};
|
|
|
|
let entry = this.overlayCache[uid][rangeKey];
|
|
|
|
if (entry?.loaded || entry?.loading) return;
|
|
|
|
entry = this.overlayCache[uid][rangeKey] = {
|
|
blocks: [],
|
|
wishes: [],
|
|
loading: true,
|
|
loaded: false,
|
|
};
|
|
|
|
const promises = [];
|
|
const lect = this.lecturers.find((lecture) => lecture.uid === uid);
|
|
|
|
if (lect.overlays.wishes) {
|
|
promises.push(
|
|
this.$api
|
|
.call(
|
|
ApiKalender.getLektorZeitwuensche(
|
|
uid,
|
|
range.start.toISODate(),
|
|
range.end.toISODate(),
|
|
),
|
|
)
|
|
.then((result) => {
|
|
entry.wishes = (result.data || []).map((zeitwunsch) => ({
|
|
class: `bg-lecturer-wish bg-uid-${uid} wish-w-${zeitwunsch.gewicht}`,
|
|
start: zeitwunsch.isostart,
|
|
end: zeitwunsch.isoend,
|
|
label: zeitwunsch.label,
|
|
}));
|
|
}),
|
|
);
|
|
}
|
|
|
|
if (lect.overlays.blocks) {
|
|
promises.push(
|
|
this.$api
|
|
.call(
|
|
ApiKalender.getLektorZeitsperren(
|
|
uid,
|
|
range.start.toISODate(),
|
|
range.end.toISODate(),
|
|
),
|
|
)
|
|
.then((result) => {
|
|
entry.blocks = (result.data || []).map((zeitsperre) => ({
|
|
class: `bg-lecturer-block bg-uid-${uid}`,
|
|
start: zeitsperre.isostart,
|
|
end: zeitsperre.isoend,
|
|
label: zeitsperre.label,
|
|
}));
|
|
}),
|
|
);
|
|
}
|
|
|
|
Promise.allSettled(promises).then(() => {
|
|
entry.loading = false;
|
|
entry.loaded = true;
|
|
this.rebuildExtraBackgrounds();
|
|
});
|
|
},
|
|
|
|
rebuildExtraBackgrounds() {
|
|
if (!this.lastRange) return;
|
|
|
|
const key =
|
|
`${this.lastRange.start.toISODate()}_` +
|
|
`${this.lastRange.end.toISODate()}_` +
|
|
`${this.currentMode}`;
|
|
let res = [];
|
|
|
|
for (let lect of this.lecturers) {
|
|
const entry = this.overlayCache[lect.uid]?.[key];
|
|
if (!entry) continue;
|
|
|
|
if (lect.overlays.blocks) res.push(...(entry.blocks || []));
|
|
|
|
if (lect.overlays.wishes) res.push(...(entry.wishes || []));
|
|
}
|
|
|
|
this.extraBackgrounds = res;
|
|
},
|
|
|
|
removeLecturer(uid) {
|
|
this.lecturers = this.lecturers.filter((lecture) => lecture.uid !== uid);
|
|
delete this.overlayCache[uid];
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
clearOrt() {
|
|
this.ort_kurzbz = null;
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
clearStg() {
|
|
this.stg = null;
|
|
this.show_stg = null;
|
|
this.$refs.calendar.resetEventLoader();
|
|
},
|
|
triggerSync() {
|
|
this.$api
|
|
.call(ApiKalender.sync())
|
|
.then(this.$refs.calendar.resetEventLoader());
|
|
},
|
|
async fetchAssignedResourcesByCalender(calenderId) {
|
|
let getAssignedResources = await this.$api.call(
|
|
ApiOperationalResourceToCalender.getAssignedResourcesByCalender(
|
|
calenderId,
|
|
),
|
|
);
|
|
if (getAssignedResources.meta.status === "success") {
|
|
return getAssignedResources.data
|
|
.filter((unit) => !!unit)
|
|
.map((unit) => {
|
|
return {
|
|
isNoteTextareaShown:
|
|
unit.anmerkung && unit.anmerkung.trim() !== "",
|
|
...unit,
|
|
};
|
|
})
|
|
.filter((unit) => !!unit);
|
|
} else {
|
|
this.$fhcAlert.alertError(
|
|
this.$p.t("ui", "failed_assigned_resources_fetch_error_message"),
|
|
);
|
|
}
|
|
|
|
return [];
|
|
},
|
|
async fetchSchedulableResourcesByCalender(calendarID) {
|
|
let getSchedulableResourcesByCalendar = await this.$api.call(
|
|
ApiOperationalResourceToCalender.getSchedulableResourcesByCalendar(
|
|
calendarID,
|
|
),
|
|
);
|
|
if (getSchedulableResourcesByCalendar.meta.status === "success") {
|
|
return getSchedulableResourcesByCalendar.data;
|
|
} else {
|
|
this.$fhcAlert.alertError(
|
|
this.$p.t("ui", "failed_schedulable_resources_fetch_error_message"),
|
|
);
|
|
}
|
|
|
|
return [];
|
|
},
|
|
filterAvailableResources(event) {
|
|
this.resourcesAssignmentModal.filteredAvailableResources;
|
|
const query = event.query.toLowerCase();
|
|
if (!query) {
|
|
return (this.resourcesAssignmentModal.filteredAvailableResources = [
|
|
...this.dropdownParsedAvailableResources.filter((unit) => {
|
|
return !this.resourcesAssignmentModal.assignedResources.some(
|
|
(assigned) => assigned.betriebsmittel_id === unit.value,
|
|
);
|
|
}),
|
|
]);
|
|
}
|
|
|
|
return (this.resourcesAssignmentModal.filteredAvailableResources =
|
|
this.dropdownParsedAvailableResources
|
|
.filter((unit) => {
|
|
return !this.resourcesAssignmentModal.assignedResources.some(
|
|
(assigned) => assigned.betriebsmittel_id === unit.value,
|
|
);
|
|
})
|
|
.filter((unit) => {
|
|
return unit.label.toLowerCase().includes(query);
|
|
}));
|
|
},
|
|
toggleAssignedResourceNoteInput(resource) {
|
|
const index = this.resourcesAssignmentModal.assignedResources.findIndex(
|
|
(assigned) => assigned.betriebsmittel_id === resource.betriebsmittel_id,
|
|
);
|
|
if (index !== -1) {
|
|
this.resourcesAssignmentModal.assignedResources[
|
|
index
|
|
].isNoteTextareaShown =
|
|
!this.resourcesAssignmentModal.assignedResources[index]
|
|
.isNoteTextareaShown;
|
|
}
|
|
|
|
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
|
|
},
|
|
removeAssignedResource(resource) {
|
|
this.resourcesAssignmentModal.assignedResources =
|
|
this.resourcesAssignmentModal.assignedResources.filter(
|
|
(assigned) =>
|
|
assigned.betriebsmittel_id !== resource.betriebsmittel_id,
|
|
);
|
|
|
|
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
|
|
},
|
|
async refreshResourcesAssignmentModalData(calenderItem) {
|
|
this.resourcesAssignmentModal.availableResources =
|
|
await this.fetchSchedulableResourcesByCalender(
|
|
calenderItem.kalender_id,
|
|
);
|
|
this.resourcesAssignmentModal.filteredAvailableResources = [
|
|
...this.dropdownParsedAvailableResources,
|
|
];
|
|
|
|
this.resourcesAssignmentModal.assignedResources =
|
|
await this.fetchAssignedResourcesByCalender(calenderItem.kalender_id);
|
|
this.resourcesAssignmentModal.selectedAvailableResource = null;
|
|
this.resourcesAssignmentModal.areFormButtonsDisplayed = false;
|
|
},
|
|
async saveAssignedResourcesToCalendarItem(calenderItem, assignedResources) {
|
|
let getSchedulableResourcesByCalendar = await this.$api.call(
|
|
ApiOperationalResourceToCalender.storeResourcesToCalendarRelationship(
|
|
calenderItem.kalender_id,
|
|
assignedResources,
|
|
),
|
|
);
|
|
if (getSchedulableResourcesByCalendar.meta.status === "success") {
|
|
this.$fhcAlert.alertSuccess(
|
|
this.$p.t("ui", "assigned_resources_save_success_message"),
|
|
);
|
|
await this.refreshResourcesAssignmentModalData(calenderItem);
|
|
} else {
|
|
this.$fhcAlert.alertError(
|
|
this.$p.t("ui", "failed_assigned_resources_save_error_message"),
|
|
);
|
|
}
|
|
|
|
this.$refs.calendar.resetEventLoader();
|
|
this.$refs.resourcesAssignmentModal.hide();
|
|
},
|
|
closeResourcesAssignmentModal() {
|
|
this.resourcesAssignmentModal = {
|
|
availableResources: [],
|
|
filteredAvailableResources: [],
|
|
selectedAvailableResource: null,
|
|
assignedResources: [],
|
|
areFormButtonsDisplayed: false,
|
|
};
|
|
},
|
|
scrollToAndEmphasizeUpdatedEvent() {
|
|
if (!this.currentlyUpdatedEvent) return;
|
|
|
|
this.$refs.calendar.clearOutCalendarEventEmphasis();
|
|
|
|
setTimeout(() => {
|
|
const eventEl = document.querySelector(
|
|
`[data-group-id="event-group-${this.currentlyUpdatedEvent.eindeutige_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";
|
|
let deemphasizedUpdateClassName = isInsideScrolledView
|
|
? "deemphasized-event"
|
|
: "deemphasized-event-long";
|
|
|
|
if (!isInsideScrolledView) timeout = 300;
|
|
|
|
setTimeout(() => {
|
|
eventEl.classList.add(emphasizeUpdateClassName);
|
|
|
|
document
|
|
.querySelectorAll(
|
|
".fhc-calendar-base-grid .fhc-calendar-base-grid-line-event",
|
|
)
|
|
.forEach((el) => {
|
|
if (el !== eventEl) {
|
|
el.classList.add(deemphasizedUpdateClassName);
|
|
}
|
|
});
|
|
}, timeout);
|
|
|
|
this.currentlyUpdatedEvent = null;
|
|
}, 100);
|
|
},
|
|
updateKalenderEventElementDisplay(calendarId, startDT, endDT) {
|
|
if (!calendarId) return;
|
|
|
|
let startOfDay = startDT.startOf("day");
|
|
let newPotentialStart = startDT.diff(startOfDay).toMillis() ?? 1;
|
|
let newPotentialEnd = endDT.diff(startOfDay).toMillis();
|
|
|
|
let element = document.querySelector(`[data-id="event-${calendarId}"]`);
|
|
if (!element) return;
|
|
|
|
const calendarView = element.closest(".fhc-calendar-mode-week-view");
|
|
const targetGridLine = calendarView?.querySelector(
|
|
`.fhc-calendar-base-grid-line[data-day="${startOfDay.toISODate()}"]`,
|
|
);
|
|
if (!targetGridLine)
|
|
return alert("Kein gültiger Kalendertag zum Verschieben gefunden");
|
|
|
|
setTimeout(() => {
|
|
targetGridLine.appendChild(element);
|
|
element.style.gridRowStart = "t_" + newPotentialStart;
|
|
element.style.gridRowEnd = "t_" + newPotentialEnd;
|
|
}, 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 calendar = document.querySelector(".fhc-calendar-base-grid");
|
|
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",
|
|
});
|
|
}
|
|
},
|
|
onEventHover(event) {
|
|
this.hoveredEvent = event;
|
|
},
|
|
onEventUnhover(event)
|
|
{
|
|
if (this.hoveredEvent?.kalender_id === event?.kalender_id) {
|
|
this.hoveredEvent = null;
|
|
}
|
|
},
|
|
parkHoveredEvent()
|
|
{
|
|
const event = this.hoveredEvent;
|
|
if (!event?.kalender_id)
|
|
return;
|
|
|
|
if (this.$refs.parking.isParked(event.kalender_id))
|
|
{
|
|
this.$refs.parking.unpark({ type: event.type, id: event.kalender_id });
|
|
}
|
|
else
|
|
{
|
|
this.$refs.parking.park(null, {
|
|
type: 'kalender',
|
|
id: event.kalender_id,
|
|
orig: event
|
|
});
|
|
}
|
|
},
|
|
deleteHoveredEvent() {
|
|
const event = this.hoveredEvent;
|
|
if (!event?.kalender_id)
|
|
return;
|
|
|
|
this.deleteEntry(event);
|
|
},
|
|
clearHoveredEvent() {
|
|
this.hoveredEvent = null;
|
|
},
|
|
focusSearchbar() {
|
|
this.$refs.searchbar?.$refs?.input.focus()
|
|
},
|
|
},
|
|
watch: {
|
|
lecturers: {
|
|
deep: true,
|
|
handler() {
|
|
this.rebuildExtraBackgrounds();
|
|
},
|
|
},
|
|
"resourcesAssignmentModal.selectedAvailableResource": function (newVal) {
|
|
if (!newVal) return;
|
|
|
|
this.resourcesAssignmentModal.assignedResources.push({
|
|
betriebsmittel_kalender_id: null,
|
|
betriebsmittel_id: newVal.data.betriebsmittel_id,
|
|
beschreibung: newVal.data.beschreibung,
|
|
anmerkung: "",
|
|
isNoteTextareaShown: false,
|
|
});
|
|
|
|
this.resourcesAssignmentModal.areFormButtonsDisplayed = true;
|
|
},
|
|
},
|
|
mounted() {
|
|
this.reservierungPending = false;
|
|
this.bcc = new BroadcastChannel("fhc-dnd");
|
|
this.bcc.addEventListener("message", (e) => {
|
|
if (e.data === "dropped" && !this.reservierungPending)
|
|
this.$refs.calendar.resetEventLoader();
|
|
});
|
|
},
|
|
beforeUnmount() {
|
|
this.bcc.close();
|
|
},
|
|
async created() {
|
|
await this.$api
|
|
.call(ApiRenderers.loadTempusRenderers())
|
|
.then((res) => res.data)
|
|
.then((data) => {
|
|
for (let rendertype of Object.keys(data)) {
|
|
let modalTitle = null;
|
|
let modalContent = null;
|
|
let calendarEvent = null;
|
|
if (data[rendertype].modalTitle)
|
|
modalTitle = Vue.markRaw(
|
|
Vue.defineAsyncComponent(
|
|
() => import(data[rendertype].modalTitle),
|
|
),
|
|
);
|
|
if (data[rendertype].modalContent)
|
|
modalContent = Vue.markRaw(
|
|
Vue.defineAsyncComponent(
|
|
() => import(data[rendertype].modalContent),
|
|
),
|
|
);
|
|
if (data[rendertype].calendarEvent)
|
|
calendarEvent = Vue.markRaw(
|
|
Vue.defineAsyncComponent(
|
|
() => import(data[rendertype].calendarEvent),
|
|
),
|
|
);
|
|
|
|
if (data[rendertype].calendarEventStyles) {
|
|
var head = document.head;
|
|
if (
|
|
!head.querySelector(
|
|
`link[href="${data[rendertype].calendarEventStyles}"]`,
|
|
)
|
|
) {
|
|
var link = document.createElement("link");
|
|
link.type = "text/css";
|
|
link.rel = "stylesheet";
|
|
link.href = data[rendertype].calendarEventStyles;
|
|
head.appendChild(link);
|
|
}
|
|
}
|
|
|
|
if (this.renderers === null) {
|
|
this.renderers = {};
|
|
}
|
|
if (!this.renderers[rendertype]) {
|
|
this.renderers[rendertype] = {};
|
|
}
|
|
this.renderers[rendertype].modalTitle = modalTitle;
|
|
this.renderers[rendertype].modalContent = modalContent;
|
|
this.renderers[rendertype].calendarEvent = calendarEvent;
|
|
}
|
|
});
|
|
|
|
this.$api.call(ApiTempusConfig.getHeader()).then((res) => {
|
|
this.visibleStatusArray = res.data.visible_status;
|
|
this.visibleStatus = ["all"];
|
|
});
|
|
},
|
|
template: /*html*/ `
|
|
<div data-cy="tempus" class="tempus">
|
|
<keyboard-shortcuts :shortcuts="keyboardShortcuts" />
|
|
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
|
|
<div class="col-md-4 col-lg-3 col-xl-2 d-flex align-items-center">
|
|
<button
|
|
class="btn btn-outline-light border-0 m-1 collapsed"
|
|
type="button"
|
|
data-bs-toggle="offcanvas"
|
|
data-bs-target="#appMenu"
|
|
aria-controls="appMenu"
|
|
aria-expanded="false"
|
|
:aria-label="$p.t('ui/toggle_nav')"
|
|
>
|
|
<span class="svg-icon svg-icon-apps"></span>
|
|
</button>
|
|
<a class="navbar-brand me-0" :href="tempusRoot">Tempus</a>
|
|
</div>
|
|
<button
|
|
class="btn btn-outline-light border-0 d-md-none m-1 collapsed"
|
|
type="button"
|
|
data-bs-toggle="offcanvas"
|
|
data-bs-target="#sidebarMenu"
|
|
aria-controls="sidebarMenu"
|
|
aria-expanded="false"
|
|
:aria-label="$p.t('ui/toggle_nav')"
|
|
>
|
|
<span class="fa-solid fa-table-list"></span>
|
|
</button>
|
|
<core-searchbar
|
|
ref="searchbar"
|
|
:searchoptions="searchbaroptions"
|
|
:searchfunction="searchfunction"
|
|
class="searchbar position-relative w-100"
|
|
show-btn-submit
|
|
></core-searchbar>
|
|
<div id="nav-user" class="dropdown">
|
|
<button
|
|
id="nav-user-btn"
|
|
class="btn btn-link rounded-0 py-0"
|
|
type="button"
|
|
data-bs-toggle="dropdown"
|
|
data-bs-target="#nav-user-menu"
|
|
aria-expanded="false"
|
|
aria-controls="nav-user-menu"
|
|
>
|
|
<img
|
|
:src="avatarUrl"
|
|
:alt="$p.t('profilUpdate/profilBild')"
|
|
class="bg-light avatar rounded-circle border border-light"
|
|
/>
|
|
</button>
|
|
<ul
|
|
ref="navUserDropdown"
|
|
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
|
|
aria-labelledby="nav-user-btn"
|
|
>
|
|
<li>
|
|
<button
|
|
type="button"
|
|
class="dropdown-item"
|
|
data-bs-toggle="modal"
|
|
data-bs-target="#configModal"
|
|
>
|
|
{{ $p.t('ui/settings') }}
|
|
</button>
|
|
</li>
|
|
<li><hr class="dropdown-divider m-0"/></li>
|
|
<li>
|
|
<nav-language
|
|
item-class="dropdown-item border-left-dark"
|
|
/>
|
|
</li>
|
|
<li><hr class="dropdown-divider m-0"/></li>
|
|
<li>
|
|
<a class="dropdown-item" :href="logoutUrl">
|
|
{{ $p.t('ui/logout') }}
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</header>
|
|
<div class="container-fluid overflow-hidden heightfull">
|
|
<div class="row h-100">
|
|
<aside id="appMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
|
|
<div class="offcanvas-header">
|
|
Tempus
|
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
|
|
</div>
|
|
<div class="offcanvas-body">
|
|
<app-menu app-identifier="tempus" />
|
|
</div>
|
|
</aside>
|
|
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100 d-flex flex-column">
|
|
<div class="sidebar-icons d-flex flex-row align-items-start py-2 gap-1 ps-2">
|
|
<button
|
|
class="btn btn-outline-secondary"
|
|
type="button"
|
|
data-bs-toggle="offcanvas"
|
|
data-bs-target="#verbandMenu"
|
|
aria-controls="verbandMenu"
|
|
aria-expanded="false"
|
|
title="Verband"
|
|
>
|
|
<span class="fa-solid fa-university"></span>
|
|
</button>
|
|
<button
|
|
class="btn btn-outline-secondary"
|
|
type="button"
|
|
data-bs-toggle="offcanvas"
|
|
data-bs-target="#verbandMenu"
|
|
aria-controls="verbandMenu"
|
|
aria-expanded="false"
|
|
title="Verband"
|
|
>
|
|
<span class="fa-solid fa-door-open"></span>
|
|
</button>
|
|
</div>
|
|
<div class="px-2 py-1 w-100">
|
|
<!--<Multiselect
|
|
:model-value="visibleStatusValue"
|
|
@update:model-value="val => toggleStatus(val.map(o => o.key))"
|
|
option-label="label"
|
|
:options="visibleStatusOptions"
|
|
placeholder="Status filtern"
|
|
:hide-selected="false"
|
|
:show-toggle-all="false"
|
|
class="w-100"
|
|
/>-->
|
|
|
|
<div class="d-flex gap-1 py-1" data-cy="previewRoleOptionsHolder">
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="previewRole === 'planer' ? 'btn-dark' : 'btn-outline-dark'"
|
|
@click="previewRole = 'planer'; $refs.calendar.resetEventLoader()"
|
|
>
|
|
<i class="fa-solid fa-pen-ruler me-1"></i>Planer
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="previewRole === 'lektor' ? 'btn-primary' : 'btn-outline-primary'"
|
|
@click="previewRole = 'lektor'; $refs.calendar.resetEventLoader()"
|
|
>
|
|
<i class="fa-solid fa-chalkboard-user me-1"></i>Lektor
|
|
</button>
|
|
<button
|
|
class="btn btn-sm"
|
|
:class="previewRole === 'student' ? 'btn-success' : 'btn-outline-success'"
|
|
@click="previewRole = 'student'; $refs.calendar.resetEventLoader()"
|
|
>
|
|
<i class="fa-solid fa-user-graduate me-1"></i>Student
|
|
</button>
|
|
<button
|
|
class="btn btn-sm btn-outline-danger"
|
|
@click="triggerSync"
|
|
>
|
|
<i class="fa-solid fa-rotate me-1"></i>Sync
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="room-selection" v-if="ort_kurzbz">
|
|
<div class="fw-semibold px-2 d-flex align-items-center justify-content-between">
|
|
<span><i class="fa-solid fa-door-open me-2"></i>{{ ort_kurzbz }}</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-link text-danger p-0"
|
|
@click="clearOrt"
|
|
title="Raum entfernen"
|
|
>
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="room-selection" v-if="show_stg">
|
|
<div class="fw-semibold px-2 d-flex align-items-center justify-content-between">
|
|
<span><i class="fa-solid fa-university me-2"></i>{{ show_stg }}</span>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-link text-danger p-0"
|
|
@click="clearStg"
|
|
title="STG entfernen"
|
|
>
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<lecture-selection
|
|
v-if="lecturers.length"
|
|
:lecturers="lecturers"
|
|
@remove="removeLecturer"
|
|
></lecture-selection>
|
|
<div class="d-flex flex-column flex-grow-1" style="min-height: 0">
|
|
<parking-slot
|
|
ref="parking"
|
|
v-model:parked-keys="parkedKeys"
|
|
></parking-slot>
|
|
|
|
<fhc-coursepicker ref="coursepicker" :stg="stg" @select-lecturer="setEmp" @select-kw="jumpToKw" :studiensemester="selectedStudiensemester"></fhc-coursepicker>
|
|
|
|
</div>
|
|
<stv-studiensemester v-model:studiensemester-kurzbz="selectedStudiensemester"></stv-studiensemester>
|
|
|
|
</nav>
|
|
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
|
|
<fhc-calendar
|
|
ref="calendar"
|
|
:timezone="config.timezone"
|
|
:get-promise-func="getPromiseFunc"
|
|
:visible-status="visibleStatus"
|
|
:date="calendarDate"
|
|
:mode="currentMode"
|
|
:parkedEvents="parkedKeys"
|
|
:visible-lecturers="visibleLecturerUids"
|
|
@drop="dropHandler"
|
|
@resize="resizeHandler"
|
|
@update:date="handleChangeDate"
|
|
@update:mode="handleChangeMode"
|
|
@event-hover="onEventHover"
|
|
@event-unhover="onEventUnhover"
|
|
:extra-backgrounds="extraBackgrounds"
|
|
@update:range="handleRange"
|
|
class="responsive-calendar"
|
|
/>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<app-config ref="config" v-model="appconfig" :endpoints="configEndpoints"></app-config>
|
|
<div id="verbandMenu" ref="verbandMenu" class="offcanvas offcanvas-start col-md p-md-0 h-100" tabindex="-1" data-cy="verbandMenu">
|
|
<div class="offcanvas-header justify-content-end px-1 d-md-none">
|
|
<h5 class="offcanvas-title" id="verbandMenuLabel">
|
|
<i class="fa-solid fa-university me-2"></i>Verband
|
|
</h5>
|
|
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
|
|
</div>
|
|
<stv-verband :endpoint="endpoint" @select-verband="onSelectVerbandAndClose" class="col" style="height:0%"></stv-verband>
|
|
</div>
|
|
|
|
<bs-modal ref="raumModal" class="bootstrap-prompt">
|
|
<template #title>Raumauswahl</template>
|
|
<template #default>
|
|
<ul v-if="raumVorschlaege.length" class="list-group">
|
|
<li
|
|
v-for="raum in raumVorschlaege"
|
|
:key="raum.ort_kurzbz"
|
|
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
|
|
style="cursor:pointer"
|
|
@click="selectRaum(raum.ort_kurzbz)"
|
|
>
|
|
<span><i class="fa-solid fa-door-open me-2"></i>{{ raum.ort_kurzbz }}</span>
|
|
<span class="text-muted" v-tooltip="{ value: raum.details.join('\\n'), class: 'custom-tooltip' }">{{ raum.score }}</span>
|
|
</li>
|
|
</ul>
|
|
<p v-else class="text-muted mb-0">Keine freien Räume gefunden.</p>
|
|
</template>
|
|
</bs-modal>
|
|
|
|
<bs-modal
|
|
ref="resourcesAssignmentModal"
|
|
@hideBsModal="closeResourcesAssignmentModal"
|
|
class="bootstrap-prompt"
|
|
data-cy="resourcesAssignmentModal"
|
|
>
|
|
<template #title>{{$p.t('ui', 'resource_assignment_modal_title')}}</template>
|
|
<template #default>
|
|
<div class="mb-5">
|
|
<form-input
|
|
v-if="resourcesAssignmentModal.availableResources.length"
|
|
@itemSelect="(option) => { resourcesAssignmentModal.selectedAvailableResource = option.value; }"
|
|
:label="$p.t('ui', 'available_resources_label')"
|
|
:suggestions="resourcesAssignmentModal.filteredAvailableResources"
|
|
:optionValue="(option) => option.value"
|
|
:optionLabel="(option) => option.label"
|
|
@complete="filterAvailableResources"
|
|
dropdown
|
|
forceSelection
|
|
type="autocomplete"
|
|
name="availableResources"
|
|
:closeOnSelect="false"
|
|
>
|
|
</form-input>
|
|
</div>
|
|
<div>
|
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
<h6 class="mb-2 mx-auto text-bold fw-1">{{$p.t('ui', 'assigned_resources_subtitle')}}</h6>
|
|
</div>
|
|
<div v-if="resourcesAssignmentModal.assignedResources.length" class="mb-4">
|
|
<div
|
|
v-for="resource in resourcesAssignmentModal.assignedResources"
|
|
:key="resource.betriebsmittel_id"
|
|
class=" shadow-sm p-2 mb-2 bg-body rounded"
|
|
>
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<p class="m-0">{{ resource.beschreibung }}</p>
|
|
<div class="d-flex justify-content-between align-items-center gap-2">
|
|
<a href="#" @click.prevent="toggleAssignedResourceNoteInput(resource)" class="ms-auto"><i class="fa fa-edit text-primary"></i></a>
|
|
<a href="#" @click.prevent="removeAssignedResource(resource)" class="ms-auto"><i class="fa fa-trash text-danger"></i></a>
|
|
</div>
|
|
</div>
|
|
<form-input
|
|
v-if="resource.isNoteTextareaShown"
|
|
v-model="resource.anmerkung"
|
|
@input="this.resourcesAssignmentModal.areFormButtonsDisplayed = true"
|
|
:placeholder="$capitalize($p.t('global/anmerkung'))"
|
|
:rows="1"
|
|
class="flex-grow-1"
|
|
type="textarea"
|
|
name="anmerkung"
|
|
>
|
|
</form-input>
|
|
</div>
|
|
</div>
|
|
<div v-else class="d-flex align-items-center justify-content-center mb-2">
|
|
<p class="text-muted mb-0">{{$p.t('ui', 'no_assigned_resources')}}</p>
|
|
</div>
|
|
<div v-if="resourcesAssignmentModal.areFormButtonsDisplayed" class="d-flex justify-content-end gap-2">
|
|
<button type="button" class="btn btn-secondary" @click="refreshResourcesAssignmentModalData(resourcesAssignmentModal.calendar)">{{$p.t('ui', 'abbrechen')}}</button>
|
|
<button type="button" class="btn btn-primary" @click="saveAssignedResourcesToCalendarItem(resourcesAssignmentModal.calendar, resourcesAssignmentModal.assignedResources)">{{$p.t('ui', 'speichern')}}</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</bs-modal>
|
|
|
|
<bs-modal ref="historyModel" class="bootstrap-prompt" dialogClass="modal-lg" data-cy="historyModal">
|
|
<template #title>History</template>
|
|
<template #default>
|
|
<table v-if="historyEntries.length" class="table table-bordered table-hover">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Von</th>
|
|
<th>Bis</th>
|
|
<th>Status</th>
|
|
<th>Ort</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="entry in historyEntries" :key="entry.id">
|
|
<td>{{ entry.von }}</td>
|
|
<td>{{ entry.bis }}</td>
|
|
<td>{{ entry.status_kurzbz }}</td>
|
|
<td>{{ entry.ort }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
</bs-modal>
|
|
<reservierung
|
|
ref="reservierung"
|
|
:ort-kurzbz="ort_kurzbz"
|
|
@saved="reservierungPending = false; $refs.calendar.resetEventLoader()"
|
|
></reservierung>
|
|
<multi-week-plan-modal
|
|
:show="multiWeekModal.show"
|
|
:lehreinheit-id="multiWeekModal.lehreinheitId"
|
|
:ort-kurzbz="multiWeekModal.ortKurzbz"
|
|
:start-time="multiWeekModal.startTime"
|
|
:end-time="multiWeekModal.endTime"
|
|
@close="closeMultiWeekModal"
|
|
@confirmed="onMultiWeekConfirmed"
|
|
/>
|
|
</div>`
|
|
};
|