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 */ `
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {{"+" + event.events.length}}
+
+
- `
-}
+ `,
+};
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:{