this.renderers),
- appConfig: Vue.computed(() => this.appconfig),
- contextMenuActions: Vue.computed(() => this.contextMenuActions),
- }
- },
- data() {
- return {
- appconfig: {},
- 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,
- //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'
- }
- },
- computed: {
- contextMenuActions() {
- return {
- lehreinheit: [
- {
- label: 'Raumauswahl',
- icon: 'fa-solid fa-door-open',
- action: this.openRaumauswahl
- },
- {
- label: 'Freischalten für Voransicht',
- icon: 'fa-solid fa-chalkboard-user',
- action: (orig) => this.$api.call(ApiKalender.syncToLecturer(orig.kalender_id)).then(() => this.$refs.calendar.resetEventLoader())
- },
- {
- label: 'Freischalten für Live',
- icon: 'fa-solid fa-user-graduate',
- action: (orig) => this.$api.call(ApiKalender.syncToStudent(orig.kalender_id)).then(() => this.$refs.calendar.resetEventLoader())
- },
- {
- label: 'History',
- icon: 'fa-solid fa-clock-rotate-left',
- action: this.openHistory
- },
- {
- label: 'Delete',
- icon: 'fa-solid fa-calendar-xmark',
- action: this.deleteEntry
- },
- ],
- reservierung: [
- {
- label: 'Delete',
- icon: 'fa-solid fa-calendar-xmark',
- action: this.deleteEntry
- },
- ]
- };
- },
- currentDay() {
- return luxon.DateTime.now().setZone(this.config.timezone).toISODate();
- },
- currentMode() {
- return 'week';
- },
- 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] }));
- },
- },
- 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 deleteEntry(orig)
- {
- if (!orig?.kalender_id)
- return;
-
- await this.$api.call(ApiKalender.deleteEntry(
- orig?.kalender_id
- )).then(result => {
- this.$refs.calendar.resetEventLoader();
- });
- },
- 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();
- });
- },
- 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() {
- console.log("handleChangeMode")
- },
- 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);
-
- if (this.previewRole === 'lektor')
- return [this.$api.call(ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()))];
-
- if (this.previewRole === 'student')
- return [this.$api.call(ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()))];
-
- return [this.$api.call(ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()))];
- },
- 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)
- {
- 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();
- });
- },
-
- 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 alert("Kein gültiges Kalender-Event zum Resizen");
-
- const dates = this._parseDates(start, end);
-
- if (!dates)
- return;
-
- return this._updateKalenderEvent(obj, dates.startDT, dates.endDT, dates.start_time, dates.end_time, () => {
- this.$refs.calendar.resetEventLoader();
- });
- },
-
- 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");
-
- const dates = this._parseDates(start, end);
- if (!dates) return;
-
- const { startDT, endDT, start_time, end_time } = dates;
-
- if (obj.type === 'reservierung')
- {
- this.reservierungPending = true;
- this.$refs.reservierung.show(start_time, end_time);
- }
- else if (obj.type === 'lehreinheit')
- {
- 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(() => {
- this.$refs.calendar.resetEventLoader();
- 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.bcc.postMessage('dropped');
-
- });
- }
- else
- {
- alert("Unbekannter Drop-Typ: " + obj.type);
- }
- },
- 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())
- }
- },
- watch: {
- lecturers: {
- deep: true,
- handler() {
- this.rebuildExtraBackgrounds();
- }
- }
- },
- 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: `
+ name: "Tempus",
+ components: {
+ CoreSearchbar,
+ VerticalSplit,
+ FhcCalendar,
+ FhcCoursepicker,
+ LectureSelection,
+ ParkingSlot,
+ AppConfig,
+ AppMenu,
+ NavLanguage,
+ BsModal,
+ StvVerband,
+ StvStudiensemester,
+ Multiselect: primevue.multiselect,
+ FormInput,
+ Reservierung,
+ },
+ 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: Vue.computed(() => this.contextMenuActions),
+ };
+ },
+ data() {
+ return {
+ appconfig: {},
+ 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,
+ //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,
+ },
+ };
+ },
+ computed: {
+ contextMenuActions() {
+ return {
+ lehreinheit: [
+ {
+ label: "Raumauswahl",
+ icon: "fa-solid fa-door-open",
+ action: this.openRaumauswahl,
+ },
+ {
+ label: "Ressourcen zuordnen",
+ icon: "fa-solid fa-list",
+ action: this.openResourcesAssignmentModal,
+ },
+ {
+ label: "Freischalten für Voransicht",
+ icon: "fa-solid fa-chalkboard-user",
+ action: (orig) =>
+ this.$api
+ .call(ApiKalender.syncToLecturer(orig.kalender_id))
+ .then(() => this.$refs.calendar.resetEventLoader()),
+ },
+ {
+ label: "Freischalten für Live",
+ icon: "fa-solid fa-user-graduate",
+ action: (orig) =>
+ this.$api
+ .call(ApiKalender.syncToStudent(orig.kalender_id))
+ .then(() => this.$refs.calendar.resetEventLoader()),
+ },
+ {
+ label: "History",
+ icon: "fa-solid fa-clock-rotate-left",
+ action: this.openHistory,
+ },
+ {
+ label: "Delete",
+ icon: "fa-solid fa-calendar-xmark",
+ action: this.deleteEntry,
+ },
+ ],
+ reservierung: [
+ {
+ label: "Delete",
+ icon: "fa-solid fa-calendar-xmark",
+ action: this.deleteEntry,
+ },
+ ],
+ };
+ },
+ currentDay() {
+ return luxon.DateTime.now().setZone(this.config.timezone).toISODate();
+ },
+ currentMode() {
+ return "week";
+ },
+ 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],
+ }));
+ },
+ 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.$api
+ .call(ApiKalender.deleteEntry(orig?.kalender_id))
+ .then((result) => {
+ this.$refs.calendar.resetEventLoader();
+ });
+ },
+ 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();
+ });
+ },
+ 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() {
+ console.log("handleChangeMode");
+ },
+ 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);
+
+ if (this.previewRole === "lektor")
+ return [
+ this.$api.call(
+ ApiKalender.getPlanLecturer(start.toISODate(), end.toISODate()),
+ ),
+ ];
+
+ if (this.previewRole === "student")
+ return [
+ this.$api.call(
+ ApiKalender.getPlanStudent(start.toISODate(), end.toISODate()),
+ ),
+ ];
+
+ return [
+ this.$api.call(
+ ApiKalender.getPlan(filter, start.toISODate(), end.toISODate()),
+ ),
+ ];
+ },
+ 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) {
+ 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();
+ });
+ },
+
+ 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 alert("Kein gültiges Kalender-Event zum Resizen");
+
+ const dates = this._parseDates(start, end);
+
+ if (!dates) return;
+
+ return this._updateKalenderEvent(
+ obj,
+ dates.startDT,
+ dates.endDT,
+ dates.start_time,
+ dates.end_time,
+ () => {
+ this.$refs.calendar.resetEventLoader();
+ },
+ );
+ },
+
+ 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");
+
+ const dates = this._parseDates(start, end);
+ if (!dates) return;
+
+ const { startDT, endDT, start_time, end_time } = dates;
+
+ if (obj.type === "reservierung") {
+ this.reservierungPending = true;
+ this.$refs.reservierung.show(start_time, end_time);
+ } else if (obj.type === "lehreinheit") {
+ 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(() => {
+ this.$refs.calendar.resetEventLoader();
+ 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.bcc.postMessage("dropped");
+ },
+ );
+ } else {
+ alert("Unbekannter Drop-Typ: " + obj.type);
+ }
+ },
+ 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.getAssignedResourcesByCalenderId(
+ 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,
+ };
+ },
+ },
+ 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*/ `
`,
+};
diff --git a/system/dbupdate_3.4.php b/system/dbupdate_3.4.php
index 7a4e844cc..aaaa4b95b 100644
--- a/system/dbupdate_3.4.php
+++ b/system/dbupdate_3.4.php
@@ -96,6 +96,7 @@ require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php');
require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinheitstyp_programm.php');
require_once('dbupdate_3.4/70376_lohnguide.php');
require_once('dbupdate_3.4/75888_reihungstest_mehrfachdurchfuehrung.php');
+require_once('dbupdate_3.4/77375_ressourcen.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '
Pruefe Tabellen und Attribute!
';
diff --git a/system/dbupdate_3.4/77375_ressourcen.php b/system/dbupdate_3.4/77375_ressourcen.php
new file mode 100644
index 000000000..25b7f0a45
--- /dev/null
+++ b/system/dbupdate_3.4/77375_ressourcen.php
@@ -0,0 +1,87 @@
+db_query("SELECT 1 FROM information_schema.columns WHERE table_schema = 'lehre' AND table_name = 'tbl_kalender' AND column_name = 'eindeutige_gruppen_id'");
+if($db->db_num_rows($result) === 0)
+{
+
+ $qry = "ALTER TABLE lehre.tbl_kalender
+ ADD COLUMN IF NOT EXISTS eindeutige_gruppen_id UUID";
+
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_kalender: '.$db->db_last_error().'';
+ else
+ echo '
lehre.tbl_kalender column eindeutige_gruppen_id hinzugefuegt';
+}
+
+$result = $db->db_query("SELECT count(*) FROM public.tbl_variablenname WHERE name = 'ignore_resources_collisions'");
+if($db->db_num_rows($result) === 0)
+{
+ $qry = "INSERT INTO public.tbl_variablenname
+ (name, defaultwert)
+ VALUES('ignore_resources_collisions', 'false')";
+}
+
+if(!$result = @$db->db_query("SELECT 1 FROM lehre.tbl_betriebsmittel_kalender LIMIT 1"))
+{
+ $qry = '
+ CREATE TABLE lehre.tbl_betriebsmittel_kalender (
+ "betriebsmittel_kalender_id" INTEGER NOT NULL,
+ "eindeutige_kalender_gruppen_id" UUID NOT NULL,
+ "betriebsmittel_id" INTEGER NOT NULL,
+ "anmerkung" TEXT,
+ "quelle" VARCHAR(32),
+ "insertamum" TIMESTAMP WITH TIME ZONE DEFAULT now(),
+ "insertvon" VARCHAR(32),
+ "updateamum" TIMESTAMP WITH TIME ZONE DEFAULT now(),
+ "updatevon" VARCHAR(32),
+ CONSTRAINT pk_betriebsmittel_kalender_id PRIMARY KEY("betriebsmittel_kalender_id"),
+ CONSTRAINT fk_betriebsmittel_id FOREIGN KEY("betriebsmittel_id") REFERENCES wawi.tbl_betriebsmittel("betriebsmittel_id") ON DELETE CASCADE,
+ CONSTRAINT eindeutige_kalender_gruppen_id_betriebsmittel_id UNIQUE (eindeutige_kalender_gruppen_id, betriebsmittel_id)
+ );';
+
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
lehre.tbl_betriebsmittel_kalender table created';
+
+ $db->db_query('CREATE SEQUENCE IF NOT EXISTS lehre.seq_tbl_betriebsmittel_kalender_betriebsmittel_kalender_id
+ INCREMENT BY 1
+ NO MAXVALUE
+ NO MINVALUE
+ CACHE 1;');
+
+ $db->db_query("ALTER TABLE lehre.tbl_betriebsmittel_kalender ALTER COLUMN betriebsmittel_kalender_id SET DEFAULT nextval('lehre.seq_tbl_betriebsmittel_kalender_betriebsmittel_kalender_id');");
+
+
+ $qry = 'GRANT SELECT ON TABLE lehre.tbl_betriebsmittel_kalender TO web;';
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
Granted privileges to
web on lehre.tbl_betriebsmittel_kalender';
+
+ $qry = 'GRANT USAGE ON lehre.seq_tbl_betriebsmittel_kalender_betriebsmittel_kalender_id TO web;';
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
Granted privileges to
web on lehre.tbl_betriebsmittel_kalender';
+
+
+ $qry = 'GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE lehre.tbl_betriebsmittel_kalender TO vilesci;';
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
Granted privileges to
vilesci on lehre.tbl_betriebsmittel_kalender';
+
+ $qry = 'GRANT USAGE, INSERT ON lehre.seq_tbl_betriebsmittel_kalender_betriebsmittel_kalender_id TO vilesci;';
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
Granted privileges to
vilesci on lehre.tbl_betriebsmittel_kalender';
+
+ $qry = 'GRANT USAGE, UPDATE ON lehre.seq_tbl_betriebsmittel_kalender_betriebsmittel_kalender_id TO vilesci;';
+ if(!$db->db_query($qry))
+ echo '
lehre.tbl_betriebsmittel_kalender: '.$db->db_last_error().'';
+ else
+ echo '
Granted privileges to
vilesci on lehre.tbl_betriebsmittel_kalender';
+}
\ No newline at end of file
diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php
index 84ff56712..3317fcf3f 100644
--- a/system/phrasesupdate.php
+++ b/system/phrasesupdate.php
@@ -47608,6 +47608,26 @@ array(
)
)
),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'resource_collision',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Ressourcen Kollision',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Resource Collision',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
array(
'app' => 'core',
'category' => 'ui',
@@ -47668,6 +47688,26 @@ array(
)
)
),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'ignore_resources_collisions',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Ressourcenkollisionen ignorieren',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Ignore resource collision',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
array(
'app' => 'core',
'category' => 'ui',
@@ -58616,6 +58656,166 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'failed_assigned_resources_fetch_error_message',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Fehler beim Abrufen der zugewiesenen Ressourcen. Bitte versuchen Sie es erneut.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Error fetching assigned resources. Please try again.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'failed_schedulable_resources_fetch_error_message',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Fehler beim Abrufen der zuweisbaren Ressourcen. Bitte versuchen Sie es erneut.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Error fetching schedulable resources. Please try again.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'failed_assigned_resources_save_error_message',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Fehler beim Speichern der zugewiesenen Ressourcen. Bitte versuchen Sie es erneut.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Error saving assigned resources. Please try again.',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'resource_assignment_modal_title',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Ressourcen-Zuweisung',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Resource Assignment',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'assigned_resources_subtitle',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Zugewiesene Ressourcen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Assigned Resources',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'no_assigned_resources',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Keine zugewiesenen Ressourcen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'No Assigned Resources',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'available_resources_label',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Verfügbare Ressourcen',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Available Resources',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
+ array(
+ 'app' => 'core',
+ 'category' => 'ui',
+ 'phrase' => 'assigned_resources_save_success_message',
+ 'insertvon' => 'system',
+ 'phrases' => array(
+ array(
+ 'sprache' => 'German',
+ 'text' => 'Zugewiesene Ressourcen erfolgreich gespeichert',
+ 'description' => '',
+ 'insertvon' => 'system'
+ ),
+ array(
+ 'sprache' => 'English',
+ 'text' => 'Assigned Resources successfully saved',
+ 'description' => '',
+ 'insertvon' => 'system'
+ )
+ )
+ ),
// ### Phrases Dashboard Admin END
);
diff --git a/system/seeders/014_betriebsmittel.sql b/system/seeders/014_betriebsmittel.sql
new file mode 100644
index 000000000..6239ccfa9
--- /dev/null
+++ b/system/seeders/014_betriebsmittel.sql
@@ -0,0 +1,11 @@
+INSERT INTO wawi.tbl_betriebsmitteltyp
+(betriebsmitteltyp, beschreibung, anzahl, kaution, typ_code, mastershapename)
+VALUES('Inventar', NULL, NULL, NULL, NULL, NULL);
+
+INSERT INTO wawi.tbl_betriebsmittel
+(betriebsmittel_id, beschreibung, betriebsmitteltyp, nummer, reservieren, updateamum, updatevon, insertamum, insertvon, ext_id, inventarnummer, oe_kurzbz, ort_kurzbz, hersteller, seriennummer, bestellung_id, bestelldetail_id, afa, verwendung, anmerkung, leasing_bis, inventuramum, inventurvon, anschaffungswert, anschaffungsdatum, tiefe, hoehe, breite, nummer2, verplanen)
+VALUES
+(1, 'pc', 'Laptop', NULL, false, '2024-03-07 08:17:33.000', 'demoadmin', '2015-12-01 14:55:32.000', 'demoadmin', NULL, '00001', NULL, NULL, 'pc', NULL, NULL, NULL, 3, NULL, 'Angelegt von demoadmin für die test', NULL, '2015-12-01 14:55:32.000', 'demoadmin', NULL, NULL, NULL, NULL, NULL, NULL, true),
+(2, 'projector', 'Inventar', NULL, false, '2024-03-07 08:17:33.000', 'demoadmin', '2015-12-01 14:55:32.000', 'demoadmin', NULL, '00002', NULL, NULL, 'projector', NULL, NULL, NULL, 3, NULL, 'Angelegt von demoadmin für die test', NULL, '2015-12-01 14:55:32.000', 'demoadmin', NULL, NULL, NULL, NULL, NULL, NULL, true),
+(3, 'workstation', 'Inventar', NULL, false, '2024-03-07 08:17:33.000', 'demoadmin', '2015-12-01 14:55:32.000', 'demoadmin', NULL, '00003', NULL, NULL, 'workstation', NULL, NULL, NULL, 3, NULL, 'Angelegt von demoadmin für die test', NULL, '2015-12-01 14:55:32.000', 'demoadmin', NULL, NULL, NULL, NULL, NULL, NULL, true),
+(4, 'pc2', 'Laptop', NULL, false, '2024-03-07 08:17:33.000', 'demoadmin', '2015-12-01 14:55:32.000', 'demoadmin', NULL, '00004', NULL, NULL, 'pc2', NULL, NULL, NULL, 3, NULL, 'Angelegt von demoadmin für die test', NULL, '2015-12-01 14:55:32.000', 'demoadmin', NULL, NULL, NULL, NULL, NULL, NULL, true);
diff --git a/system/seeders/014_kalender.sql b/system/seeders/015_kalender.sql
similarity index 83%
rename from system/seeders/014_kalender.sql
rename to system/seeders/015_kalender.sql
index e4a3cb0b7..0a525bd59 100644
--- a/system/seeders/014_kalender.sql
+++ b/system/seeders/015_kalender.sql
@@ -143,6 +143,7 @@ VALUES
(51006, 26);
-- ////////////////////////////////////////////////////////////////////////////////////////////////////
+
-- Calendar setup for Stundenraster disabled tests
INSERT INTO lehre.tbl_kalender
(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, insertamum, insertvon, updateamum, updatevon) VALUES
@@ -155,9 +156,45 @@ INSERT INTO lehre.tbl_kalender_lehreinheit
VALUES
(51002, 27),
(51006, 28);
-
-- ////////////////////////////////////////////////////////////////////////////////////////////////////
+
+-- Calendar setup for resources tests
+INSERT INTO lehre.tbl_kalender
+(von, bis, typ, status_kurzbz, vorgaenger_kalender_id, eindeutige_gruppen_id, insertamum, insertvon, updateamum, updatevon)
+VALUES
+(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '19:30:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '20:15:00', 'lehreinheit', 'live', NULL, 'ec1bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'),
+(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '20:15:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '21:00:00', 'lehreinheit', 'live', NULL, 'ec2bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'),
+(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '17:50:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '18:35:00', 'lehreinheit', 'live', NULL, 'ec3bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin'),
+(date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '18:35:00', date_trunc('week', CURRENT_DATE) + INTERVAL '6 day' + TIME '19:20:00', 'lehreinheit', 'live', NULL, 'ec4bd001-facf-4b21-a671-bcb2d4f037fd', now(), 'demoadmin', NULL, 'demoadmin')
+;
+
+INSERT INTO lehre.tbl_kalender_lehreinheit
+(lehreinheit_id, kalender_id)
+VALUES
+(51004, 29),
+(51006, 30),
+(51006, 31),
+(51004, 32);
+
+INSERT INTO public.tbl_variablenname
+(name, defaultwert)
+VALUES('ignore_resources_collisions', 'false');
+
+INSERT INTO public.tbl_variable
+(name, uid, wert)
+VALUES('ignore_resources_collisions', 'demoadmin', 'false');
+
+INSERT INTO lehre.tbl_betriebsmittel_kalender
+(eindeutige_kalender_gruppen_id, betriebsmittel_id, anmerkung, quelle, insertamum, insertvon, updateamum, updatevon)
+VALUES
+('ec1bd001-facf-4b21-a671-bcb2d4f037fd'::uuid, 1, '', 'tempus_neu', '2026-06-17 16:48:59.669', 'demoadmin', '2026-06-17 17:13:23.000', 'demoadmin'),
+('ec2bd001-facf-4b21-a671-bcb2d4f037fd'::uuid, 1, '', 'tempus_neu', '2026-06-17 16:48:59.669', 'demoadmin', '2026-06-17 17:13:23.000', 'demoadmin'),
+('ec3bd001-facf-4b21-a671-bcb2d4f037fd'::uuid, 2, '', 'tempus_neu', '2026-06-17 16:48:59.669', 'demoadmin', '2026-06-17 17:13:23.000', 'demoadmin'),
+('ec4bd001-facf-4b21-a671-bcb2d4f037fd'::uuid, 3, '', 'tempus_neu', '2026-06-17 16:48:59.669', 'demoadmin', '2026-06-17 17:13:23.000', 'demoadmin');
+-- //////////////////////////////////////////////////////////////////////////////////////////////////--
+
+
INSERT INTO lehre.tbl_kalender_event
(kalender_id, titel, beschreibung)
VALUES(26, 'Test reservation', NULL);
@@ -169,3 +206,5 @@ VALUES(26, 'teilnehmer', 'demolektor4', NULL, NULL, NULL, NULL, NULL, NULL, NULL
INSERT INTO campus.tbl_zeitsperre
(zeitsperre_id, zeitsperretyp_kurzbz, mitarbeiter_uid, bezeichnung, vondatum, vonstunde, bisdatum, bisstunde, vertretung_uid, updateamum, updatevon, insertamum, insertvon, erreichbarkeit_kurzbz, freigabeamum, freigabevon)
VALUES(1, 'DienstV', 'demolektor1', 'Test', date_trunc('week', CURRENT_DATE) + INTERVAL '5 day', NULL, date_trunc('week', CURRENT_DATE) + INTERVAL '5 day', NULL, 'demoadmin', '2007-04-02 11:04:31.000', 'demoadmin', '2007-04-02 11:04:31.000', 'demoadmin', 't', NULL, NULL);
+
+
diff --git a/tests/cypress/cypress/e2e/specs/api/tempus/tempus.collisions.cy.js b/tests/cypress/cypress/e2e/specs/api/tempus/tempus.collisions.cy.js
index 1a163433e..ae00b672c 100644
--- a/tests/cypress/cypress/e2e/specs/api/tempus/tempus.collisions.cy.js
+++ b/tests/cypress/cypress/e2e/specs/api/tempus/tempus.collisions.cy.js
@@ -3,33 +3,55 @@ import { getDateForDay } from "../../../../support/helpers/date";
const TARGETED_STUDY_PLAN_SHORT_CODE = "STG5";
+const resetEventsBeforeTest = () => {
+ tempusApi
+ .getPlannerEvents(getDateForDay("monday"), getDateForDay("monday"))
+ .then((events) => {
+ events.forEach((event) => {
+ tempusApi.deleteKalenderEvent(event.kalender_id);
+ });
+ });
+ tempusApi
+ .getPlannerEvents(getDateForDay("tuesday"), getDateForDay("tuesday"))
+ .then((events) => {
+ events.forEach((event) => {
+ if (
+ event.type === "lehreinheit" &&
+ event.beginn === "20:15:00" &&
+ event.ende === "21:00:00"
+ ) {
+ tempusApi.updateKalenderEvent(
+ event.kalender_id,
+ `${event.datum} 19:30`,
+ `${event.datum} 20:15`,
+ );
+ }
+ });
+ });
+ tempusApi
+ .getPlannerEvents(getDateForDay("sunday"), getDateForDay("sunday"))
+ .then((events) => {
+ events.forEach((event) => {
+ if (
+ event.type === "lehreinheit" &&
+ event.beginn === "18:35:00" &&
+ event.ende === "19:20:00" &&
+ event.lektor.some((lector) => lector.kurzbz === "DemoLKT1")
+ ) {
+ tempusApi.updateKalenderEvent(
+ event.kalender_id,
+ `${event.datum} 17:50`,
+ `${event.datum} 18:35`,
+ );
+ }
+ });
+ });
+};
+
describe("Tempus Kalender API", () => {
beforeEach(() => {
cy.login();
- tempusApi
- .getPlannerEvents(getDateForDay("monday"), getDateForDay("monday"))
- .then((events) => {
- events.forEach((event) => {
- tempusApi.deleteKalenderEvent(event.kalender_id);
- });
- });
- tempusApi
- .getPlannerEvents(getDateForDay("tuesday"), getDateForDay("tuesday"))
- .then((events) => {
- events.forEach((event) => {
- if (
- event.type === "lehreinheit" &&
- event.beginn === "20:15:00" &&
- event.ende === "21:00:00"
- ) {
- tempusApi.updateKalenderEvent(
- event.kalender_id,
- `${event.datum} 19:30`,
- `${event.datum} 20:15`,
- );
- }
- });
- });
+ resetEventsBeforeTest();
});
it("event creation works for non collision case", () => {
@@ -97,7 +119,6 @@ describe("Tempus Kalender API", () => {
tempusApi
.createKalenderEvent(lehreinheitId, startDateTime, endDateTime)
.then((response) => {
- console.log(response.body.errors);
expect(response.status).to.eq(500);
expect(response.body).to.have.nested.property(
"meta.status",
@@ -106,13 +127,7 @@ describe("Tempus Kalender API", () => {
expect(response.body.errors).to.be.an("array");
let hasTimeLockCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("zeitsperre kollision") ||
- error.message
- .toLowerCase()
- .includes("time lock collision"),
+ (error) => error.errorCode === "absences_collision",
);
expect(
hasTimeLockCollisionError,
@@ -154,7 +169,6 @@ describe("Tempus Kalender API", () => {
tempusApi
.createKalenderEvent(lehreinheitId, startDateTime, endDateTime)
.then((response) => {
- console.log(response.body.errors);
expect(response.status).to.eq(500);
expect(response.body).to.have.nested.property(
"meta.status",
@@ -162,11 +176,7 @@ describe("Tempus Kalender API", () => {
);
expect(response.body.errors).to.be.an("array");
let hasStudentGroupCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("verband kollision") ||
- error.message.toLowerCase().includes("student collision"),
+ (error) => error.errorCode === "verband_collision",
);
expect(
hasStudentGroupCollisionError,
@@ -219,7 +229,6 @@ describe("Tempus Kalender API", () => {
endDateTime,
)
.then((response) => {
- console.log(response.body.errors);
expect(response.status).to.eq(500);
expect(response.body).to.have.nested.property(
"meta.status",
@@ -227,13 +236,7 @@ describe("Tempus Kalender API", () => {
);
expect(response.body.errors).to.be.an("array");
let hasStudentCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("studierende kollision") ||
- error.message
- .toLowerCase()
- .includes("student collision"),
+ (error) => error.errorCode === "student_collision",
);
expect(
hasStudentCollisionError,
@@ -286,7 +289,6 @@ describe("Tempus Kalender API", () => {
endDateTime,
)
.then((response) => {
- console.log(response.body.errors);
expect(response.status).to.eq(500);
expect(response.body).to.have.nested.property(
"meta.status",
@@ -294,13 +296,7 @@ describe("Tempus Kalender API", () => {
);
expect(response.body.errors).to.be.an("array");
let hasLectorCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("lektorin kollision") ||
- error.message
- .toLowerCase()
- .includes("lector collision"),
+ (error) => error.errorCode === "lector_collision",
);
expect(
hasLectorCollisionError,
@@ -390,9 +386,7 @@ describe("Tempus Kalender API", () => {
expect(response.body.errors).to.be.an("array");
let hasRoomCollisionError = response.body.errors.some(
- (error) =>
- error.message.toLowerCase().includes("raum kollision") ||
- error.message.toLowerCase().includes("room collision"),
+ (error) => error.errorCode === "room_collision",
);
expect(
hasRoomCollisionError,
@@ -440,15 +434,12 @@ describe("Tempus Kalender API", () => {
"error",
);
expect(response.body.errors).to.be.an("array");
- console.log(response.body.errors);
let hasStudentGroupCollisionError = response.body.errors.some(
- (error) =>
- error.message.toLowerCase().includes("verband kollision") ||
- error.message.toLowerCase().includes("student collision"),
+ (error) => error.errorCode === "verband_collision",
);
expect(
hasStudentGroupCollisionError,
- "response contains student collision error",
+ "response contains student group collision error",
).to.be.true;
});
});
@@ -490,13 +481,8 @@ describe("Tempus Kalender API", () => {
"error",
);
expect(response.body.errors).to.be.an("array");
- console.log(response.body.errors);
let hasDirectStudentCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("studierende kollision") ||
- error.message.toLowerCase().includes("student collision"),
+ (error) => error.errorCode === "student_collision",
);
expect(
hasDirectStudentCollisionError,
@@ -515,7 +501,6 @@ describe("Tempus Kalender API", () => {
tempusApi
.getPlannerEvents(getDateForDay("saturday"), getDateForDay("saturday"))
.then((events) => {
- console.log(events);
const sourceEvent = events.find(
(event) =>
event.type === "lehreinheit" &&
@@ -543,11 +528,8 @@ describe("Tempus Kalender API", () => {
"error",
);
expect(response.body.errors).to.be.an("array");
- console.log(response.body.errors);
let hasLectorCollisionError = response.body.errors.some(
- (error) =>
- error.message.toLowerCase().includes("lektorin kollision") ||
- error.message.toLowerCase().includes("lector collision"),
+ (error) => error.errorCode === "lector_collision",
);
expect(
hasLectorCollisionError,
@@ -596,11 +578,7 @@ describe("Tempus Kalender API", () => {
expect(response.body.errors).to.be.an("array");
let hasTimeLockCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("zeitsperre kollision") ||
- error.message.toLowerCase().includes("time lock collision"),
+ (error) => error.errorCode === "absences_collision",
);
expect(
hasTimeLockCollisionError,
@@ -611,6 +589,90 @@ describe("Tempus Kalender API", () => {
});
});
+ it("event update works for entries without collision", () => {
+ let settingsData = tempusApi.getSettingsData();
+ tempusApi.updateSettingsData(settingsData).then((response) => {
+ tempusApi
+ .getPlannerEvents(getDateForDay("sunday"), getDateForDay("sunday"))
+ .then((events) => {
+ const sourceEvent = events.find(
+ (event) =>
+ event.beginn === "17:50:00" &&
+ event.ende === "18:35:00" &&
+ event.lektor.some((lector) => lector.kurzbz === "DemoLKT1"),
+ );
+ expect(
+ sourceEvent,
+ "source event with fixed time and resources for collision test",
+ ).to.exist;
+
+ const startDateTime = `${sourceEvent.datum} 18:35`;
+ const endDateTime = `${sourceEvent.datum} 19:20`;
+
+ tempusApi
+ .updateKalenderEvent(
+ sourceEvent.kalender_id,
+ startDateTime,
+ endDateTime,
+ )
+ .then((response) => {
+ console.log(response);
+ expect(response.status).to.eq(200);
+ expect(response.body).to.have.nested.property(
+ "meta.status",
+ "success",
+ );
+ });
+ });
+ });
+ });
+
+ it("prohibited event update due to resources collision", () => {
+ let settingsData = tempusApi.getSettingsData();
+ tempusApi.updateSettingsData(settingsData).then((response) => {
+ tempusApi
+ .getPlannerEvents(getDateForDay("sunday"), getDateForDay("sunday"))
+ .then((events) => {
+ const sourceEvent = events.find(
+ (event) =>
+ event.beginn === "19:30:00" &&
+ event.ende === "20:15:00" &&
+ event.lektor.some((lector) => lector.kurzbz === "DemoLKT4"),
+ );
+ expect(
+ sourceEvent,
+ "source event with fixed time and resources for collision test",
+ ).to.exist;
+
+ const startDateTime = `${sourceEvent.datum} 20:15`;
+ const endDateTime = `${sourceEvent.datum} 21:00`;
+
+ tempusApi
+ .updateKalenderEvent(
+ sourceEvent.kalender_id,
+ startDateTime,
+ endDateTime,
+ )
+ .then((response) => {
+ console.log(response);
+ expect(response.status).to.eq(500);
+ expect(response.body).to.have.nested.property(
+ "meta.status",
+ "error",
+ );
+ expect(response.body.errors).to.be.an("array");
+ let hasResourcesCollisionError = response.body.errors.some(
+ (error) => error.errorCode === "resource_collision",
+ );
+ expect(
+ hasResourcesCollisionError,
+ "response contains resources collision error",
+ ).to.be.true;
+ });
+ });
+ });
+ });
+
it("prohibited event update due to lector - reservation collision", () => {
let settingsData = tempusApi.getSettingsData();
settingsData.kollision_student = false;
@@ -647,13 +709,8 @@ describe("Tempus Kalender API", () => {
"error",
);
expect(response.body.errors).to.be.an("array");
- console.log(response.body.errors);
let hasReservationCollisionError = response.body.errors.some(
- (error) =>
- error.message
- .toLowerCase()
- .includes("reservierung kollision") ||
- error.message.toLowerCase().includes("reservation collision"),
+ (error) => error.errorCode === "reservation_collision",
);
expect(
hasReservationCollisionError,
@@ -700,15 +757,12 @@ describe("Tempus Kalender API", () => {
"error",
);
expect(response.body.errors).to.be.an("array");
- console.log(response.body.errors);
let hasReservationCollisionError = response.body.errors.some(
- (error) =>
- error.message.toLowerCase().includes("lektorin kollision") ||
- error.message.toLowerCase().includes("reservation collision"),
+ (error) => error.errorCode === "lector_collision",
);
expect(
hasReservationCollisionError,
- "response contains reservation collision error",
+ "response contains lector collision error",
).to.be.true;
});
});
diff --git a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js
index b8c7db85b..59d34abe8 100644
--- a/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js
+++ b/tests/cypress/cypress/e2e/specs/ui/tempus/tempus.smoke.cy.js
@@ -57,6 +57,19 @@ context("Tempus smoke tests", () => {
tempusPage.getRaumauswahlModal().should("be.visible");
});
+ it("shows resources modal when selecting Ressourcen zuordnen from event context menu", () => {
+ tempusPage.waitForCalendarToFinishLoading();
+ tempusPage
+ .getCalendarEventsWithLehreinheit()
+ .should("have.length.greaterThan", 0);
+
+ tempusPage.getCalendarEventsWithLehreinheit().first().rightclick();
+ tempusPage.getEventContextMenuOption("Ressourcen zuordnen").click();
+ waitForOk("@fetchResourceSuggestions");
+
+ tempusPage.getResourcesModal().should("be.visible");
+ });
+
it("shows history modal when selecting History from event context menu", () => {
tempusPage.waitForCalendarToFinishLoading();
tempusPage.getCalendarEvents().should("have.length.greaterThan", 0);
diff --git a/tests/cypress/cypress/support/api/tempusApi.js b/tests/cypress/cypress/support/api/tempusApi.js
index 852e2b146..8ef542303 100644
--- a/tests/cypress/cypress/support/api/tempusApi.js
+++ b/tests/cypress/cypress/support/api/tempusApi.js
@@ -92,6 +92,7 @@ export const tempusApi = {
kollision_student: false,
ignore_reservierung: false,
ignore_zeitsperre: false,
+ ignore_resources_collisions: false,
}),
updateSettingsData: (options) =>
diff --git a/tests/cypress/cypress/support/pages/tempus.po.js b/tests/cypress/cypress/support/pages/tempus.po.js
index 3e87fc8bf..baf3519a0 100644
--- a/tests/cypress/cypress/support/pages/tempus.po.js
+++ b/tests/cypress/cypress/support/pages/tempus.po.js
@@ -64,6 +64,10 @@ class TempusPage {
method: "GET",
url: "**/tempus/Kalender/getRaumvorschlag**",
}).as("fetchRoomSuggestions");
+ cy.intercept({
+ method: "GET",
+ url: "**/betriebsmittel/OperationalResourceToCalenderAPI/getSchedulableResourcesByCalendar/32",
+ }).as("fetchResourcesSuggestions");
cy.intercept({
method: "GET",
url: "**/tempus/coursepicker/getByStg**",
@@ -284,6 +288,10 @@ class TempusPage {
.closest(".bootstrap-modal.show");
getRaumauswahlRoomOptions = () =>
this.getRaumauswahlModal().find(".list-group-item");
+ getResourcesModal = () =>
+ cy
+ .get("[data-cy='resourcesAssignmentModal']")
+ .closest(".bootstrap-modal.show");
getCalendarEventRoom = (id) =>
this.getCalendarEventById(id).find(".event-place");
getStundenrasterToggle = () =>