diff --git a/application/controllers/api/frontend/v1/LvPlan.php b/application/controllers/api/frontend/v1/LvPlan.php index 99dc8bf59..dc87732b9 100644 --- a/application/controllers/api/frontend/v1/LvPlan.php +++ b/application/controllers/api/frontend/v1/LvPlan.php @@ -48,6 +48,7 @@ class LvPlan extends FHCAPI_Controller 'getStudiengaenge' => self::PERM_LOGGED, 'getLehrverband' => self::PERM_LOGGED, 'permissionOtherLvPlan' => self::PERM_LOGGED, + 'compactibleEventTypes' => self::PERM_LOGGED, ]); $this->load->library('LogLib'); @@ -393,6 +394,16 @@ class LvPlan extends FHCAPI_Controller $this->terminateWithSuccess($this->permissionlib->isBerechtigt('basis/other_lv_plan')); } + /** + * get event types which can be compacted in lv plan display + * + * @return void + */ + public function compactibleEventTypes() + { + $this->terminateWithSuccess(["lehreinheit", "reservierung"]); + } + /** * fetch moodle events * diff --git a/public/js/api/factory/lvPlan.js b/public/js/api/factory/lvPlan.js index e6285ec9a..ac369c0e8 100644 --- a/public/js/api/factory/lvPlan.js +++ b/public/js/api/factory/lvPlan.js @@ -129,5 +129,11 @@ export default { method: 'get', url: '/api/frontend/v1/lvPlan/permissionOtherLvPlan', } - } + }, + getCompactibleEventTypes(){ + return { + method: 'get', + url: '/api/frontend/v1/lvPlan/compactibleEventTypes', + } + }, }; \ No newline at end of file diff --git a/public/js/apps/Cis.js b/public/js/apps/Cis.js index 7590ac9ef..1c3e8b14c 100644 --- a/public/js/apps/Cis.js +++ b/public/js/apps/Cis.js @@ -11,7 +11,7 @@ import Raumsuche from "../components/Cis/Raumsuche/Raumsuche.js"; import CmsNews from "../components/Cis/Cms/News.js"; import CmsContent from "../components/Cis/Cms/Content.js"; import Info from "../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; -import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../components/Cis/Mylv/RoomInformation.js"; +import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../components/Cis/Mylv/RoomInformation.js"; import AbgabetoolStudent from "../components/Cis/Abgabetool/AbgabetoolStudent.js"; import AbgabetoolMitarbeiter from "../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js"; import AbgabetoolAssistenz from "../components/Cis/Abgabetool/AbgabetoolAssistenz.js"; @@ -24,6 +24,7 @@ import ApiRouteInfo from '../api/factory/routeinfo.js'; import {capitalize} from "../helpers/StringHelpers.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; +const isMobile = window.matchMedia("(max-width: 767px)").matches; const router = VueRouter.createRouter({ history: VueRouter.createWebHistory(`/${ciPath}`), @@ -86,7 +87,7 @@ const router = VueRouter.createRouter({ name: "RoomInformation", params: { // in this case always populate other params since they are not optional ort_kurzbz: to.params.ort_kurzbz, - mode: DEFAULT_MODE_RAUMINFO, + mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP, focus_date: new Date().toISOString().split("T")[0] }, }; @@ -103,7 +104,7 @@ const router = VueRouter.createRouter({ const mode = route.params.mode && validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()) ? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase() - : DEFAULT_MODE_RAUMINFO; + : (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP); // default to today date if not provided const d = new Date(route.params.focus_date) diff --git a/public/js/components/Calendar/Base/Grid/Line.js b/public/js/components/Calendar/Base/Grid/Line.js index 9a6c2f579..64a71c5f0 100644 --- a/public/js/components/Calendar/Base/Grid/Line.js +++ b/public/js/components/Calendar/Base/Grid/Line.js @@ -1,5 +1,5 @@ -import LineEvent from './Line/Event.js'; -import LineBackground from './Line/Background.js'; +import LineEvent from "./Line/Event.js"; +import LineBackground from "./Line/Background.js"; /** * TODO(chris): @@ -10,54 +10,117 @@ export default { name: "GridLine", components: { LineEvent, - LineBackground - }, - inject: { - axisRow: "axisRow" + LineBackground, }, + inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"], props: { date: { type: luxon.DateTime, - required: true + required: true, }, start: { type: luxon.DateTime, - required: true + required: true, }, end: { type: luxon.DateTime, - required: true + required: true, }, events: { type: Array, - default: [] + default: [], }, backgrounds: { type: Array, - default: [] - } + default: [], + }, }, computed: { - eventsWithRowInfo() { - const events = []; - this.events.forEach(event => { - const rows = [1, -1]; + formattedEvents() { + let formattedEvents = this.events.map((event) => { + event.rows = [1, -1]; if (event.startsHere) { - rows[0] = 't_' + event.start.diff(this.date).toMillis(); + event.rows[0] = + "t_" + event.start.diff(this.date).toMillis(); } if (event.endsHere) { - rows[1] = 't_' + event.end.diff(this.date).toMillis(); + event.rows[1] = "t_" + event.end.diff(this.date).toMillis(); } - events.push({ - ...event, - rows - }); + return event; }); - return events; - } + + if (this.shouldCompactEvents && this.compactibleEventTypes?.length) { + formattedEvents = + this.compactEvents(formattedEvents, this.compactibleEventTypes); + } + + return formattedEvents; + }, }, - template: /* html */` + methods: { + compactEvents(events, compactibleEventTypes) { + let formattedEvents = events + .filter( + (event) => + !compactibleEventTypes.includes(event.type), + ) + .map((event) => { + event.display = "default"; + return event; + }); + let eventsToBeCompacted = events.filter((event) => + compactibleEventTypes.includes(event.type), + ); + let compactedEvents = []; + + eventsToBeCompacted.forEach((event) => { + let existingCompactedEvent = compactedEvents.find( + (compactedEvent) => + event.rows[0] === compactedEvent.rows[0] && + event.rows[1] === compactedEvent.rows[1], + ); + + if (!existingCompactedEvent) { + compactedEvents.push({ + events: [ + { + farbe: event.orig.farbe, + }, + ], + rows: event.rows, + }); + } else { + existingCompactedEvent.events.push({ + farbe: event.orig.farbe, + }); + } + }); + + compactedEvents.forEach((compactedEvent) => { + if (compactedEvent.events.length < 4) { + formattedEvents.push({ + display: "compacted", + ...compactedEvent, + }); + } else { + formattedEvents.push({ + display: "compacted", + events: compactedEvent.events.slice(0, 3), + rows: compactedEvent.rows, + }); + formattedEvents.push({ + display: "compactedExtra", + events: compactedEvent.events.slice(3), + rows: compactedEvent.rows, + }); + } + }); + + return formattedEvents; + }, + }, + template: /* html */ `
- - - +
- ` -} + `, +}; diff --git a/public/js/components/Calendar/LvPlan.js b/public/js/components/Calendar/LvPlan.js index 60de7d171..14e2b4382 100644 --- a/public/js/components/Calendar/LvPlan.js +++ b/public/js/components/Calendar/LvPlan.js @@ -8,12 +8,14 @@ import { useRenderers } from '../../composables/Renderers.js'; import ModeDay from './Mode/Day.js'; import ModeWeek from './Mode/Week.js'; import ModeMonth from './Mode/Month.js'; +import ModeList from './Mode/List.js'; export default { name: "CalendarLvPlan", components: { FhcCalendar }, + inject: ["isMobile"], props: { date: { type: [Date, String, Number, luxon.DateTime], @@ -21,13 +23,23 @@ export default { }, mode: { type: String, - default: 'Week' + default: 'Day' }, getPromiseFunc: { type: Function, required: true } }, + provide() { + return { + shouldCompactEvents: Vue.computed( + () => this.$props.mode === "Month" && this.isMobile, + ), + compactibleEventTypes: Vue.computed( + () => this.compactibleEventTypes, + ), + }; + }, emits: [ "update:date", "update:mode", @@ -36,11 +48,6 @@ export default { data() { return { timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone, - modes: { - day: Vue.markRaw(ModeDay), - week: Vue.markRaw(ModeWeek), - month: Vue.markRaw(ModeMonth) - }, modeOptions: { day: { emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')), @@ -48,9 +55,13 @@ export default { }, week: { collapseEmptyDays: false - } + }, + list: { + length: 7, + }, }, - teachingunits: null + teachingunits: null, + compactibleEventTypes: [], }; }, computed: { @@ -72,7 +83,20 @@ export default { label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false }) } ]; - } + }, + modes() { + let modes = { + day: Vue.markRaw(ModeDay), + month: Vue.markRaw(ModeMonth), + }; + if (this.isMobile) { + modes.list = Vue.markRaw(ModeList); + } else { + modes.week = Vue.markRaw(ModeWeek); + } + + return modes; + }, }, methods: { eventStyle(event) { @@ -86,7 +110,21 @@ export default { }, resetEventLoader() { this.reset(); - } + }, + async getStunden() { + let stundenResponse = await this.$api.call(ApiLvPlan.getStunden()); + this.teachingunits = stundenResponse.data.map((el) => ({ + id: el.stunde, + start: el.beginn, + end: el.ende, + })); + }, + async getCompactibleEventTypes() { + let compactibleEventTypesResponse = await this.$api.call( + ApiLvPlan.getCompactibleEventTypes(), + ); + this.compactibleEventTypes = compactibleEventTypesResponse.data; + }, }, setup(props, context) { const rangeInterval = Vue.ref(null); @@ -107,16 +145,9 @@ export default { renderers }; }, - created() { - this.$api - .call(ApiLvPlan.getStunden()) - .then(res => { - return this.teachingunits = res.data.map(el => ({ - id: el.stunde, - start: el.beginn, - end: el.ende - })); - }); + async created() { + await this.getStunden(); + await this.getCompactibleEventTypes(); }, template: /* html */` item.studiengang_kz === this.formData.stgkz + (item) => item.studiengang_kz === this.formData.stgkz, ); return currentStg?.max_semester; }, @@ -55,19 +57,32 @@ export default { return this.propsViewData?.focus_date; }, currentMode() { - if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase())) - return DEFAULT_MODE_LVPLAN; + let validModes = ["day", "month"]; + validModes.push(this.isMobile ? "list" : "week"); + + const defaultMode = this.isMobile + ? DEFAULT_MODE_LVPLAN_MOBILE + : DEFAULT_MODE_LVPLAN_DESKTOP; + + if ( + !this.propsViewData?.mode || + !validModes.includes(this.propsViewData?.mode.toLowerCase()) + ) + return defaultMode; return this.propsViewData?.mode; }, downloadLinks() { - if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid) + if ( + !this.studiensemester_start || + !this.studiensemester_ende || + !this.uid + ) return false; let type = false; - type = this.isStudent ? 'student' : type; - type = this.isMitarbeiter ? 'lektor' : type; - if (false === type) - { + type = this.isStudent ? "student" : type; + type = this.isMitarbeiter ? "lektor" : type; + if (false === type) { return; } @@ -79,35 +94,63 @@ export default { .fromISO(this.studiensemester_ende, opts) .toUnixInteger(); - const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root - + 'cis/private/lvplan/stpl_kalender.php' - + '?type=' + type - + '&pers_uid=' + this.uid - + '&begin=' + start - + '&ende=' + ende; + const download_link = + FHC_JS_DATA_STORAGE_OBJECT.app_root + + "cis/private/lvplan/stpl_kalender.php" + + "?type=" + + type + + "&pers_uid=" + + this.uid + + "&begin=" + + start + + "&ende=" + + ende; return [ - { title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' }, - { title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' }, - { title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' }, - { title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' } + { + title: "excel", + icon: "fa-solid fa-file-excel", + link: download_link + "&format=excel", + }, + { + title: "csv", + icon: "fa-solid fa-file-csv", + link: download_link + "&format=csv", + }, + { + title: "ical1", + icon: "fa-regular fa-calendar", + link: download_link + "&format=ical&version=1&target=ical", + }, + { + title: "ical2", + icon: "fa-regular fa-calendar", + link: download_link + "&format=ical&version=2&target=ical", + }, ]; - } + }, }, methods: { - loadLvPlan(){ - if(!this.formData.stgkz){ - this.$fhcAlert.alertError(this.$p.t('LvPlan', 'chooseStg')); + loadLvPlan() { + if (!this.formData.stgkz) { + this.$fhcAlert.alertError(this.$p.t("LvPlan", "chooseStg")); return; } - if(!this.formData.sem && (this.formData.verband || this.formData.gruppe)){ - this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_SemMissing')); + if ( + !this.formData.sem && + (this.formData.verband || this.formData.gruppe) + ) { + this.$fhcAlert.alertError( + this.$p.t("LvPlan", "error_SemMissing"), + ); return; } - if(!this.formData.verband && this.formData.gruppe){ - this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_VerbandMissing')); + if (!this.formData.verband && this.formData.gruppe) { + this.$fhcAlert.alertError( + this.$p.t("LvPlan", "error_VerbandMissing"), + ); return; } @@ -121,18 +164,17 @@ export default { }; //ensure logic: no value after a null value in route - if(params.sem == null) - { + if (params.sem == null) { params.verband = null; params.gruppe = null; } - if(params.verband == null) { + if (params.verband == null) { params.gruppe = null; } //delete all null values to avoid null in router Object.keys(params).forEach( - key => params[key] == null && delete params[key] + (key) => params[key] == null && delete params[key], ); this.$router.push({ @@ -146,32 +188,41 @@ export default { if(!this.listSem) this.listSem = [...Array(this.maxSemester).keys()].map(i => i + 1); }, - loadListVerband(){ + loadListVerband() { this.$api .call(ApiLvPlan.getLehrverband(this.formData.stgkz, this.formData.sem, this.formData.verband)) .then(result => { const data = result.data; - const mappedData = data.map(item => item.verband); - this.listVerband = [...new Set(mappedData.filter(v => - v !== null && - v !== undefined && - String(v).trim() !== "" - ))] - .sort(); + const mappedData = data.map((item) => item.verband); + this.listVerband = [ + ...new Set( + mappedData.filter( + (v) => + v !== null && + v !== undefined && + String(v).trim() !== "", + ), + ), + ].sort(); }) .catch(this.$fhcAlert.handleSystemError); }, - loadListGroup(){ + loadListGroup() { this.$api .call(ApiLvPlan.getGruppe(this.formData.stgkz, this.formData.sem, this.formData.verband)) .then(result => { const data = result.data; - const mappedData = data.map(item => item.gruppe); - this.listGroup = [...new Set(mappedData.filter(v => - v !== null && - v !== undefined && - String(v).trim() !== ""))] - .sort(); + const mappedData = data.map((item) => item.gruppe); + this.listGroup = [ + ...new Set( + mappedData.filter( + (v) => + v !== null && + v !== undefined && + String(v).trim() !== "", + ), + ), + ].sort(); }) .catch(this.$fhcAlert.handleSystemError); }, @@ -196,18 +247,30 @@ export default { }, updateRange(rangeInterval) { this.$api - .call(ApiLvPlan.studiensemesterDateInterval( - rangeInterval.end.startOf('week').toISODate() - )) - .then(res => { - this.studiensemester_kurzbz = res.data.studiensemester_kurzbz; + .call( + ApiLvPlan.studiensemesterDateInterval( + rangeInterval.end.startOf("week").toISODate(), + ), + ) + .then((res) => { + this.studiensemester_kurzbz = + res.data.studiensemester_kurzbz; this.studiensemester_start = res.data.start; this.studiensemester_ende = res.data.ende; }); }, getPromiseFunc(start, end) { return [ - this.$api.call(ApiLvPlan.eventsStgOrg(start, end, this.formData.stgkz, this.formData.sem, this.formData.verband, this.formData.gruppe)) + this.$api.call( + ApiLvPlan.eventsStgOrg( + start, + end, + this.formData.stgkz, + this.formData.sem, + this.formData.verband, + this.formData.gruppe, + ), + ), ]; }, async fetchAuthInfo() { @@ -347,6 +410,4 @@ export default { `, - - -}; \ No newline at end of file +}; diff --git a/public/js/components/Cis/Mylv/RoomInformation.js b/public/js/components/Cis/Mylv/RoomInformation.js index 85741cab2..3a07e0a76 100644 --- a/public/js/components/Cis/Mylv/RoomInformation.js +++ b/public/js/components/Cis/Mylv/RoomInformation.js @@ -2,7 +2,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js"; import ApiLvPlan from '../../../api/factory/lvPlan.js'; -export const DEFAULT_MODE_RAUMINFO = 'Week' +export const DEFAULT_MODE_RAUMINFO_MOBILE = 'List'; +export const DEFAULT_MODE_RAUMINFO_DESKTOP = 'Week'; export default { name: "RoomInformation", @@ -13,12 +14,14 @@ export default { viewData: Object, // NOTE(chris): this is inherited from router-view propsViewData: Object }, + inject: ["isMobile"], computed: { currentDay() { return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate(); }, currentMode() { - return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO; + const defaultMode = this.isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP; + return this.propsViewData?.mode || defaultMode; } }, methods:{