diff --git a/public/js/components/Tempus/Tempus.js b/public/js/components/Tempus/Tempus.js index 6bad39517..a7e7c3442 100644 --- a/public/js/components/Tempus/Tempus.js +++ b/public/js/components/Tempus/Tempus.js @@ -14,14 +14,26 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import VueDatePicker from '../vueDatepicker.js.php' 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 ApiKalender from '../../api/factory/kalender.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 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"; export default { name: "Tempus", @@ -29,7 +41,14 @@ export default { CoreSearchbar, VerticalSplit, FhcCalendar, - FhcCoursepicker + FhcCoursepicker, + LectureSelection, + ParkingSlot, + AppConfig, + AppMenu, + NavLanguage, + BsModal, + StvVerband }, props: { defaultSemester: String, @@ -39,19 +58,28 @@ export default { 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, - $reloadList: () => { - this.$refs.stvList.reload(); - }, + 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', @@ -60,7 +88,8 @@ export default { types: [ //"student", "raum", - //"mitarbeiter" + "mitarbeiter", + "mitarbeiter_ohne_zuordnung" ], actions: { raum: { @@ -70,7 +99,17 @@ export default { }, childactions: [ ] - } + }, + employee: { + defaultaction: { + type: "function", + action: (data) => { + this.setEmp(data); + } + }, + childactions: [ + ] + }, } }, lv_id: null, @@ -86,98 +125,379 @@ export default { geschlechter: [] }, renderers: null, - ort_kurzbz: 'EDV_A5.08', + 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 + }, } }, - methods: { - setOrt: function(data) - { - // Wenn bei der Suche ein Ort ausgewaehlt wird, dann wir der Ort gesetzt und ein Reload getriggert durch den watcher - this.ort_kurzbz = data.ort_kurzbz; - }, - handleChangeDate() { - }, - handleChangeMode() { - }, - searchfunction(params) { - return this.$api.call(ApiSearchbar.search(params)); - }, - getPromiseFunc(start, end) { - return [ - this.$api.call(ApiKalender.getRoomplan(this.ort_kurzbz, '2025-10-01','2025-10-30')),//start.toISODate(), end.toISODate())), - ]; - }, - parkingdrop: function(evt) - { - evt.preventDefault(); - var data = JSON.parse(evt.dataTransfer.getData("text")); - alert('parked Data:'+data.id); - console.log(data); - }, - dropHandler: function(event, start, end) - { - let day = start.date.toFormat('yyyy-MM-dd'); - let time = start.date.toFormat('hh:mm'); - - let dropdata = JSON.parse(event.dataTransfer.getData('text')) - - if(dropdata.type=='kalender') - { - let kalender_id = dropdata.id; - - Promise.allSettled([ - this.$api.call(ApiKalender.updateKalenderEvent(kalender_id, this.ort_kurzbz, day+' '+time, null)) - ]).then((result) => { - let promise_events = []; - result.forEach((promise_result) => { - if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") - { - // TODO - reload - } - }) - }); - } - else if(dropdata.type=='lehreinheit') - { - // TODO Calculate end time - let lehreinheit_id = dropdata.id; - let start_time = day+' '+time; - let end_time = start.date.plus({ minutes: 45 }).toFormat('yyyy-MM-dd hh:mm'); - alert("mode:"+dropdata.mode); - - Promise.allSettled([ - this.$api.call(ApiKalender.addKalenderEvent(lehreinheit_id, this.ort_kurzbz, start_time, end_time)) - ]).then((result) => { - let promise_events = []; - result.forEach((promise_result) => { - if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") { - - // TODO - reload - } - }) - }); - } - else - { - alert("Unbekannte Daten gedroppt"); - } - }, - onRightClick: function(evt) { - this.$refs.EventContextMenu.show(evt); - } - }, - watch: { - ort_kurzbz: function (newValue, oldValue) { - // Raumansicht laden wenn der Ort geaendert wird - } - }, computed: { + contextMenuActions() { + return { + lehreinheit: [ + { + label: 'Raumauswahl', + icon: 'fa-solid fa-door-open', + action: this.openRaumauswahl + } + ] + }; + }, 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); + } + }, + methods: { + async openRaumauswahl(orig) { + if (!orig?.lehreinheit_id) + return; + this.raumModal = orig; + + await this.$api.call(ApiKalender.getRaumvorschlag( + orig.isostart, + orig.isoend, + orig.lehreinheit_id[0] + )).then(result => { + + this.raumVorschlaege = result.data ?? []; + this.$refs.raumModal.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(); + }, + onSelectVerband({link, name}) + { + let stg = null; + let semester = null; + let studiensemester_kurzbz = this.selectedStudiensemester; + 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; + if (studiensemester_kurzbz) + this.studiensemester_kurzbz = studiensemester_kurzbz; + + 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); + }, + handleChangeDate() { + console.log("handleChangeDate"); + }, + handleChangeMode() { + console.log("handleChangeMode") + }, + 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); + + 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; + }, + dropHandler(payload) { + 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 startDT = luxon.DateTime.fromISO(start); + const endDT = luxon.DateTime.fromISO(end); + + if (!startDT.isValid || !endDT.isValid) + return alert("Ungültiges Datum"); + + const start_time = startDT.toFormat('yyyy-MM-dd HH:mm'); + const end_time = endDT.toFormat('yyyy-MM-dd HH:mm'); + + + if (obj.type === 'lehreinheit') + { + 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(); + }); + } + else if (obj.type === 'kalender') + { + let updatedInfos = { + ort_kurzbz: this.ort_kurzbz ? this.ort_kurzbz : obj.orig.ort_kurzbz, + start_time: start_time, + end_time: end_time + } + + this.$api.call( + ApiKalender.updateKalenderEvent( + obj.orig.kalender_id, + updatedInfos + ) + ).then(() => { + this.$refs.parking.unpark({ + type: obj.type, + id: obj.orig.kalender_id + }); + this.$refs.calendar.resetEventLoader(); + }); + } + 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(); + } + }, + watch: { + lecturers: { + deep: true, + handler() { + this.rebuildExtraBackgrounds(); + } + } }, async created() { @@ -218,70 +538,184 @@ export default { this.renderers[rendertype].calendarEvent = calendarEvent; } }); - }, - mounted() { - - }, template: `
-
+ + + + + +
` -}; +}; \ No newline at end of file