Implementation CIS => LV Plan

This commit is contained in:
chfhtw
2025-07-21 15:40:51 +02:00
parent 20e8964274
commit 91bb167c42
+185 -303
View File
@@ -1,353 +1,235 @@
import FhcCalendar from "../../Calendar/Calendar.js";
import CalendarDate from "../../../composables/CalendarDate.js";
import FhcCalendar from "../../Calendar/Base.js";
import LvModal from "../Mylv/LvModal.js";
import LvMenu from "../Mylv/LvMenu.js"
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import { useEventLoader } from '../../../composables/EventLoader.js';
import ModeDay from '../../Calendar/Mode/Day.js';
import ModeWeek from '../../Calendar/Mode/Week.js';
import ModeMonth from '../../Calendar/Mode/Month.js';
export const DEFAULT_MODE_LVPLAN = 'Week'
const LvPlan = {
export default {
name: 'LvPlan',
components: {
FhcCalendar,
LvModal,
LvMenu
},
inject: [
"renderers"
],
props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
},
data() {
return {
events: null,
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound')),
compact: false
},
week: {
collapseEmptyDays: false
}
},
currentDay: this.propsViewData?.focus_date,
calendarMode: this.propsViewData?.mode ?? DEFAULT_MODE_LVPLAN,
calendarDate: new CalendarDate(new Date()),
eventCalendarDate: new CalendarDate(new Date()),
currentlySelectedEvent: null,
currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
lv: null,
minimized: false,
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isModalContentResolved: false,
isModalTitleResolved: false,
isShowModal: false,
}
},
props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object,
rowMinHeight: {
type: String,
default: '100px'
},
eventMaxHeight: {
type: String,
default: '125px'
}
},
provide() {
return {
rowMinHeight: this.rowMinHeight,
eventMaxHeight: this.eventMaxHeight
}
},
inject:["renderers"],
watch: {
/* events:{
handler: function(newValue){
if(newValue == null)
setTimeout(()=>{
if(this.events == null){
this.loadEvents();
}
},500);
},
immediate: true,
}, */
modalLoaded:{
handler: function (newValue) {
if (this.isShowModal && newValue.isModalContentResolved && newValue.isModalTitleResolved) {
this.$nextTick(() => {
if(this.$refs.lvmodal) this.$refs.lvmodal.show();
this.isShowModal = false;
});
}
},
immediate: true
},
weekFirstDay: {
handler: async function (newValue) {
let data = await this.fetchStudiensemesterDetails(newValue);
let { studiensemester_kurzbz, start, ende } = data.data;
this.studiensemester_kurzbz = studiensemester_kurzbz;
this.studiensemester_start = start;
this.studiensemester_ende = ende;
},
immediate: true,
},
// forward/backward on history entries happening in lvplan
'propsViewData.lv_id'(newVal) {
// relevant if lv_id can be changed from within this component
},
'propsViewData.mode'(newVal) {
if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
},
'propsViewData.focus_date'(newVal) {
this.currentDate = new Date(newVal)
}
},
components: {
FhcCalendar, LvModal, LvMenu
};
},
computed:{
modalLoaded: function(){
return { isModalContentResolved: this.isModalContentResolved, isModalTitleResolved:this.isModalTitleResolved};
},
downloadLinks: function(){
if(!this.studiensemester_start || !this.studiensemester_ende || !this.uid )return;
let start = new Date(this.studiensemester_start);
start = Math.floor(start.getTime()/1000);
let ende = new Date(this.studiensemester_ende);
ende = Math.floor(ende.getTime() / 1000);
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.toUnixInteger();
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'cis/private/lvplan/stpl_kalender.php'
+ '?type=student'
+ '&pers_uid=' + this.uid
+ '&begin=' + start
+ '&ende=' + ende;
let download_link =
(format, version = "", target = "") =>
`${FHC_JS_DATA_STORAGE_OBJECT.app_root}cis/private/lvplan/stpl_kalender.php?type=student&pers_uid=
${this.uid}&begin=${start}&ende=${ende}&format=${format}
${version ? '&version=' + version : ''}${target ? '&target=' + target : ''}`;
return [
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link('excel') },
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link('csv') },
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link('ical', '1', 'ical') },
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link('ical', '2', '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' }
];
},
weekFirstDay: function () {
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
},
weekLastDay: function () {
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
},
monthFirstDay: function () {
return this.calendarDateToString(this.eventCalendarDate.cdFirstDayOfCalendarMonth);
},
monthLastDay: function () {
return this.calendarDateToString(this.eventCalendarDate.cdLastDayOfCalendarMonth);
},
},
methods:{
modalTitleResolved: function () {
this.isModalTitleResolved = true;
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
modalContentResolved: function () {
this.isModalContentResolved = true;
},
// component renderers fetches from different addons
modalTitleComponent(type){
return this.renderers[type]?.modalTitle;
},
modalContentComponent(type) {
return this.renderers[type]?.modalContent;
},
calendarEventComponent(type){
return this.renderers[type]?.calendarEvent;
},
fetchStudiensemesterDetails: async function (date) {
return this.$api.call(ApiLvPlan.studiensemesterDateInterval(date));
},
setSelectedEvent: function (event) {
this.currentlySelectedEvent = event;
},
selectDay: function(day){
const date = day.getFullYear() + "-" +
String(day.getMonth() + 1).padStart(2, "0") + "-" +
String(day.getDate()).padStart(2, "0");
const capitalizedMode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
handleChangeDate(day) {
const focus_date = day.toISODate();
const mode = this.calendarMode[0].toUpperCase() + this.calendarMode.slice(1);
this.$router.push({
name: "LvPlan",
params: {
mode: capitalizedMode,
focus_date: date,
mode,
focus_date,
lv_id: this.propsViewData?.lv_id || null
}
})
this.currentDay = day;
},
handleChangeMode(mode) {
let m = mode[0].toUpperCase() + mode.slice(1)
if(m === this.calendarMode) return; // TODO(chris): check for date and lv_id too!
const date = this.currentDay.getFullYear() + "-" +
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
String(this.currentDay.getDate()).padStart(2, "0");
if (m == 'Weeks' || m == 'Years' || m == 'Months') return;
handleChangeMode(newMode) {
const mode = newMode[0].toUpperCase() + newMode.slice(1)
const focus_date = (this.currentDay instanceof luxon.DateTime)
? this.currentDay.toISODate()
: this.currentDay;
this.$router.push({
name: "LvPlan",
params: {
mode: m,
focus_date: date,
mode,
focus_date,
lv_id: this.propsViewData?.lv_id ?? null
}
})
this.calendarMode = m
},
showModal: function(event){
this.currentlySelectedEvent = event;
Vue.nextTick(()=>{
if(this.isModalContentResolved && this.isModalTitleResolved){
if(this.$refs.lvmodal) this.$refs.lvmodal.show();
}
else{
this.isShowModal = true;
}
})
},
updateRange: function ({start,end, mounted}) {
let checkDate = (date) => {
return date.m != this.eventCalendarDate.m || date.y != this.eventCalendarDate.y;
}
this.calendarDate = new CalendarDate(end);
// only load month data if the month or year has changed
// or we receive a reload flag from the mounted routine of the components
// or this handler is being called from the mounted lifecycle of a component
if (mounted || (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end)))){
// reset the events before querying the new events to activate the loading spinner
this.events = null;
this.eventCalendarDate = new CalendarDate(end);
this.loadEvents();
}
},
calendarDateToString: function (calendarDate) {
return calendarDate instanceof CalendarDate ?
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
null;
},
loadEvents: function(){
Promise.allSettled([
this.$api.call(ApiLvPlan.LvPlanEvents(this.monthFirstDay, this.monthLastDay, this.propsViewData.lv_id)),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(this.monthFirstDay, this.monthLastDay))
]).then((result) => {
let promise_events = [];
result.forEach((promise_result) => {
if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") {
if(promise_result.value.meta?.lv) this.lv = promise_result.value.meta.lv
let data = promise_result.value.data;
// adding additional information to the events
if (data && data.forEach) {
data.forEach((el, i) => {
el.id = i;
if (el.type === 'reservierung') {
el.color = '#' + (el.farbe || 'FFFFFF');
} else {
el.color = '#' + (el.farbe || 'CCCCCC');
}
el.start = new Date(el.datum + ' ' + el.beginn);
el.end = new Date(el.datum + ' ' + el.ende);
});
}
promise_events = promise_events.concat(data);
}
})
this.events = promise_events;
});
this.calendarMode = mode;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
this.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;
});
}
},
setup(props) {
const $api = Vue.inject('$api');
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, (start, end) => {
return [
$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), props.propsViewData.lv_id)),
$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
];
});
return {
rangeInterval,
events,
lv
};
},
created() {
this.$api
.call(ApiAuthinfo.getAuthUID())
.then(res => res.data)
.then(data => {
this.uid = data.uid;
.then(res => {
this.uid = res.data.uid;
});
// this.loadEvents();
},
beforeUnmount() {
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
/*window.addEventListener("resize", () => {
this.modeOptions.day.compact = document.body.offsetWidth < 1200;
});*/
},
template:/*html*/`
<template v-if="renderers">
<h2>
{{$p.t('lehre/stundenplan')}}
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">{{studiensemester_kurzbz}}</span>
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv"> {{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english}}</span>
</h2>
<hr>
<lv-modal v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ref="lvmodal" >
<template #modalTitle>
<Suspense @pending="isModalTitleResolved=false" @resolve="modalTitleResolved">
<component :is="modalTitleComponent(currentlySelectedEvent.type)" v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ></component>
</Suspense>
</template>
<template #modalContent>
<Suspense @pending="isModalContentResolved=false" @resolve="modalContentResolved">
<component :is="modalContentComponent(currentlySelectedEvent.type)" v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ></component>
</Suspense>
</template>
</lv-modal>
<div class="d-flex flex-column h-100" v-if="renderers">
<h2 @click="modeOptions.week.collapseEmptyDays = !modeOptions.week.collapseEmptyDays">
{{ $p.t('lehre/stundenplan') }}
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">
{{ studiensemester_kurzbz }}
</span>
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv">
{{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english }}
</span>
</h2>
<hr>
<fhc-calendar
ref="calendar"
:date="currentDay"
:modes="modes"
:modeOptions="modeOptions"
:mode="propsViewData.mode.toLowerCase()"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
:timezone="viewData.timezone"
:locale="$p.user_locale.value"
show-btns
:events="events || []"
:backgrounds="[]"
<fhc-calendar
ref="calendar"
@selectedEvent="setSelectedEvent"
:initial-date="currentDay"
@change:range="updateRange"
@change:offset="handleOffset"
:events="events"
:initial-mode="propsViewData.mode"
show-weeks
@select:day="selectDay"
@change:mode="handleChangeMode"
v-model:minimized="minimized"
>
<template #calendarDownloads>
<div v-for="{title,icon,link} in downloadLinks">
<a :href="link" :aria-label="title" class="py-1 px-2 m-1 btn btn-outline-secondary card">
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span class="small">{{title}}</span>
show-weeks
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
>
<div v-for="{ title, icon, link } in downloadLinks">
<a
:href="link"
:aria-label="title"
class="py-1 px-2 m-1 btn btn-outline-secondary card"
>
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span class="small">{{ title }}</span>
</div>
</a>
</div>
</a>
</div>
</template>
<template #monthPage="{event,day}">
<div @click="showModal(event)" class="monthPageContainer " >
<component :is="calendarEventComponent(event.type)" :event="event" ></component>
</div>
</template>
<template #weekPage="{event,day}">
<div @click="showModal(event)" type = "button"
class="weekPageContainer position-relative h-100"
:class="{'p-1':event.allDayEvent}"
style = "overflow: auto;" >
<component :is="calendarEventComponent(event.type)" :event="event" ></component>
</div>
</template>
<template #dayPage="{event,day,mobile}">
<div @click="mobile? showModal(event):null" type="button" class="dayPageContainer fhc-entry m-0 h-100 text-center">
<component :is="calendarEventComponent(event.type)" :event="event"></component>
</div>
</template>
<template #pageMobilContent="{lvMenu, event}">
<component :is="modalContentComponent(currentlySelectedEvent.type)" v-if="event" :event="event" :lvMenu="lvMenu" ></component>
</template>
<template #pageMobilContentEmpty >
<h3>{{ $p.t('lehre/noLvFound') }}</h3>
</template>
</fhc-calendar>
</template>`
}
export default LvPlan;
</div>
</template>
</fhc-calendar>
</div>`
};