Merge branch 'feature-25999/C4_cleanup' into merge_FHC4_C4

This commit is contained in:
Harald Bamberger
2025-02-24 08:50:04 +01:00
16 changed files with 169 additions and 58 deletions
+1 -11
View File
@@ -61,17 +61,7 @@ $route['api/v1/organisation/[O|o]rganisationseinheit/(:any)'] = 'api/v1/organisa
$route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1';
$route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
$route['Cis/Stundenplan'] = 'Cis/Stundenplan/index/null/null/null';
$route['Cis/Stundenplan/(:num)'] = 'Cis/Stundenplan/index/null/null/$1';
$route['Cis/Stundenplan/(:num)/(:any)'] = 'Cis/Stundenplan/index/null/$2/$1';
// Specific route (for mode: month|week|day, focusdate, lv_id optional)
$route['Cis/Stundenplan/([M|m]onth|[W|w]eek|[D|d]ay)'] = 'Cis/Stundenplan/index/$1';
$route['Cis/Stundenplan/([M|m]onth|[W|w]eek|[D|d]ay)(/(:any))?'] = 'Cis/Stundenplan/index/$1/$3';
$route['Cis/Stundenplan/([M|m]onth|[W|w]eek|[D|d]ay)(/(:any))?(/(:num))?'] = 'Cis/Stundenplan/index/$1/$3/$5';
$route['Cis/Stundenplan/.*'] = 'Cis/Stundenplan/index/$1';
// load routes from extensions
$subdir = 'application/config/extensions';
+1 -10
View File
@@ -23,19 +23,10 @@ class Stundenplan extends Auth_Controller
/**
* @return void
*/
public function index($mode = 'Week', $focus_date = null, $lv_id = null)
public function index()
{
// convert string "null" to actual null values -> ci3 reroute fix
$mode = ($mode === 'null') ? 'Week' : ucfirst(strtolower($mode));
$focus_date = ($focus_date === 'null') ? date('Y-m-d') : $focus_date;
$lv_id = ($lv_id === 'null') ? null : $lv_id;
if($mode) $mode = ucfirst(strtolower($mode));
$viewData = array(
'mode' => $mode,
'focus_date' => $focus_date,
'lv_id' => $lv_id,
'uid'=>getAuthUID(),
);
@@ -153,6 +153,11 @@ class Stundenplan extends FHCAPI_Controller
$stundenplan_data = $this->StundenplanModel->getStundenplanLVA($start_date, $end_date, $lv_id);
$stundenplan_data = $this->getDataOrTerminateWithError($stundenplan_data) ?? [];
$this->expand_object_information($stundenplan_data);
// query lv itself in case its Stundenplan is being queried and it has no entries
$this->load->model('education/Lehrveranstaltung_model','LehrveranstaltungModel');
$lv = getData($this->LehrveranstaltungModel->load($lv_id))[0];
$this->addMeta('lv', $lv);
$this->terminateWithSuccess($stundenplan_data);
}
+10 -6
View File
@@ -37,8 +37,10 @@ const router = VueRouter.createRouter({
redirect: (to) => {
return { // redirect to longer Rauminfo url and map params
name: "RoomInformation",
params: {
ort_kurzbz: to.params.ort_kurzbz
params: { // in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz,
mode: DEFAULT_MODE_RAUMINFO,
focus_date: new Date().toISOString().split("T")[0]
},
};
},
@@ -57,7 +59,8 @@ const router = VueRouter.createRouter({
: DEFAULT_MODE_RAUMINFO;
// default to today date if not provided
const focus_date = route.params.focus_date || new Date().toISOString().split("T")[0];
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
@@ -137,8 +140,9 @@ const router = VueRouter.createRouter({
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_STUNDENPLAN;
// default to today date if not provided
const focus_date = route.params.focus_date || new Date().toISOString().split("T")[0];
// default to today date if not provided or string forms invalid date
const d = new Date(route.params.focus_date)
const focus_date = !isNaN(d) ? route.params.focus_date : new Date().toISOString().split("T")[0];
// for consistency reasons format the props into one object but actually use a new name to we dont collide with
// existing viewData declaration written from codeigniter 3 into routerview tag
return {
@@ -227,7 +231,7 @@ const app = Vue.createApp({
// let click event propagate normally if we dont route internally
const res = this.$router.resolve(route)
if(!res?.matched?.length) return
if(!res?.matched?.length || res.name === 'Fallback') return
event.preventDefault(); // Prevent browser navigation
+4 -2
View File
@@ -95,7 +95,8 @@ export default {
'select:event',
'change:range',
'update:minimized',
'selectedEvent'
'selectedEvent',
'change:offset'
],
data() {
return {
@@ -240,7 +241,8 @@ export default {
},
template: /*html*/`
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)" @input="handleInput" >
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)"
@input="handleInput" @change:offset="$emit('change:offset', $event)">
<template #calendarDownloads>
<slot name="calendarDownloads" ></slot>
</template>
+2
View File
@@ -27,9 +27,11 @@ export default {
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -1 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 1 });
},
selectEvent(event) {
this.$emit('input', ['select:event', event]);
+4 -2
View File
@@ -36,6 +36,7 @@ export default {
'calendarClientHeight',
'setSelectedEvent',
'selectedEvent',
'rowMinHeight'
],
props: {
year: Number,
@@ -372,11 +373,12 @@ export default {
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
<div class="hours">
<div v-for="hour in hours" style="min-height:100px" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
<div v-for="hour in hours" :style="'min-height:' + rowMinHeight " :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-if="lookingAtToday && !noEventsCondition" :style="overlayStyle"></div>
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast :selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden " >
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast
:selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden" >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
+2
View File
@@ -35,9 +35,11 @@ export default {
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: -1, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 1, d: 0 });
},
selectDay(day) {
let m = day.getMonth();
+3 -3
View File
@@ -143,16 +143,16 @@ export default {
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25"
@click.prevent="changeToWeek(week)">{{week.no}}</a>
<a href="#"
@click.prevent="clickEvent(day,week)"
@click="clickEvent(day,week)"
@mouseover="highlight(week,day)"
@mouseleave="highlightedWeek = null; highlightedDay = null"
v-for="day in week.days"
:key="day"
:class="getDayClass(week, day)"
>
<span class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span @click="clickEvent(day,week)" class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
<div @click="setSelectedEvent(event);" v-for="event in events[day.toDateString()]" :key="event.id"
<div v-for="event in events[day.toDateString()]" :key="event.id"
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<slot name="monthPage" :event="event" :day="day" >
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
+2
View File
@@ -27,9 +27,11 @@ export default {
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: 0, m: 0, d: -7 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 0, m: 0, d: 7 });
},
selectEvent(event) {
this.$emit('input', ['select:event',event]);
+9 -3
View File
@@ -23,7 +23,8 @@ export default {
'noMonthView',
'isSliding',
'selectedEvent',
'setSelectedEvent'
'setSelectedEvent',
'rowMinHeight'
],
props: {
year: Number,
@@ -38,7 +39,7 @@ export default {
computed: {
getGridStyle() {
return {
'min-height': '100px',
'min-height': this.rowMinHeight,
// this.size is the magic number anyway which directs font-size,
// which in turn influences a lot of layout
width: '42px'
@@ -272,6 +273,11 @@ export default {
this.$emit('updateMode', 'month');
}
},
changeToDay(day) {
this.date.set(day);
this.focusDate.set(day);
this.$emit('updateMode', 'day');
},
dateToMinutesOfDay(day) {
// subtract 7 from the total hours because the hours range from 7 to 24
return Math.floor(((day.getHours()-7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
@@ -320,7 +326,7 @@ export default {
<div ref="page" class="fhc-calendar-week-page" style="min-width: 700px;">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToMonth(day)">
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToDay(day)">
<div class="fw-bold">{{dayText[day]?.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText[day]?.datum}}</a>
</div>
+1 -1
View File
@@ -32,7 +32,7 @@ export default {
},
},
template: `
<div class="fhc-calendar-weeks">
<div class="fhc-calendar-weeks h-100">
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
+2
View File
@@ -41,9 +41,11 @@ export default {
},
prev() {
this.$refs.pane.prev();
this.$emit('change:offset', { y: -1, m: 0, d: 0 });
},
next() {
this.$refs.pane.next();
this.$emit('change:offset', { y: 1, m: 0, d: 0 });
}
},
created() {
@@ -18,6 +18,13 @@ export default {
return [...Array(this.end - this.start).keys()].map(i => i + this.start);
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
},
template: `
<div class="fhc-calendar-years-page d-flex flex-wrap">
<div v-for="year in years" :key="year" class="d-grid col-4">
@@ -10,6 +10,14 @@ const RoomInformation = {
props:{
propsViewData: {
type: Object
},
rowMinHeight: {
type: String,
default: '100px'
},
eventMaxHeight: {
type: String,
default: '125px'
}
},
components: {
@@ -17,6 +25,12 @@ const RoomInformation = {
LvModal,
LvInfo,
},
provide() {
return {
rowMinHeight: this.rowMinHeight,
eventMaxHeight: this.eventMaxHeight
}
},
data() {
return {
events: null,
@@ -79,14 +93,37 @@ const RoomInformation = {
this.currentDay = day;
},
handleOffset: function(offset) {
this.currentDay = new Date(
this.currentDay.getFullYear() + offset.y,
this.currentDay.getMonth() + offset.m,
this.currentDay.getDate() + offset.d
)
const date = this.currentDay.getFullYear() + "-" +
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
String(this.currentDay.getDate()).padStart(2, "0");
this.$router.push({
name: "Stundenplan",
params: {
mode: this.calendarMode,
focus_date: date,
lv_id: this.propsViewData?.lv_id || null
}
})
},
handleChangeMode(mode) {
const modeCapitalized = mode.charAt(0).toUpperCase() + mode.slice(1)
const date = this.currentDay.getFullYear() + "-" +
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
String(this.currentDay.getDate()).padStart(2, "0");
this.$router.push({
name: "RoomInformation",
params: {
mode: modeCapitalized,
focus_date: this.currentDay.toISOString().split("T")[0],
focus_date: date,
ort_kurzbz: this.propsViewData.ort_kurzbz
}
})
@@ -167,7 +204,8 @@ const RoomInformation = {
ref="calendar"
@selectedEvent="setSelectedEvent"
:initial-date="currentDay"
@change:range="updateRange"
@change:range="updateRange"
@change:offset="handleOffset"
:events="events"
:initial-mode="propsViewData.mode"
show-weeks
@@ -16,6 +16,7 @@ const Stundenplan = {
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,
@@ -24,7 +25,21 @@ const Stundenplan = {
}
},
props: {
propsViewData: Object
propsViewData: Object,
rowMinHeight: {
type: String,
default: '100px'
},
eventMaxHeight: {
type: String,
default: '125px'
}
},
provide() {
return {
rowMinHeight: this.rowMinHeight,
eventMaxHeight: this.eventMaxHeight
}
},
watch: {
weekFirstDay: {
@@ -59,8 +74,17 @@ const Stundenplan = {
let ende = new Date(this.studiensemester_ende);
ende = Math.floor(ende.getTime() / 1000);
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') }];
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') }
];
},
weekFirstDay: function () {
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
@@ -94,7 +118,7 @@ const Stundenplan = {
const date = day.getFullYear() + "-" +
String(day.getMonth() + 1).padStart(2, "0") + "-" +
String(day.getDate()).padStart(2, "0");
this.$router.push({
name: "Stundenplan",
params: {
@@ -106,28 +130,52 @@ const Stundenplan = {
this.currentDay = day;
},
handleOffset: function(offset) {
this.currentDay = new Date(
this.currentDay.getFullYear() + offset.y,
this.currentDay.getMonth() + offset.m,
this.currentDay.getDate() + offset.d
)
const date = this.currentDay.getFullYear() + "-" +
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
String(this.currentDay.getDate()).padStart(2, "0");
this.$router.push({
name: "Stundenplan",
params: {
mode: this.calendarMode,
focus_date: date,
lv_id: this.propsViewData?.lv_id || null
}
})
},
handleChangeMode(mode) {
const modeCapitalized = mode.charAt(0).toUpperCase() + mode.slice(1)
const date = this.currentDay.getFullYear() + "-" +
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
String(this.currentDay.getDate()).padStart(2, "0");
this.$router.push({
name: "Stundenplan",
params: {
mode: modeCapitalized,
focus_date: this.currentDay.toISOString().split("T")[0],
focus_date: date,
lv_id: this.propsViewData?.lv_id ?? null
}
})
this.calendarMode = mode
},
showModal: function(event){
showModal: function(e, event){
e.stopPropagation()
this.currentlySelectedEvent = event;
Vue.nextTick(() => {
this.$refs.lvmodal.show();
});
},
updateRange: function ({start,end}) {
let checkDate = (date) => {
return date.m != this.eventCalendarDate.m || date.y != this.eventCalendarDate.y;
}
@@ -157,7 +205,9 @@ const Stundenplan = {
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) {
@@ -194,7 +244,11 @@ const Stundenplan = {
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
},
template:/*html*/`
<h2>{{$p.t('lehre/stundenplan')}}</h2>
<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" />
<fhc-calendar
@@ -202,6 +256,7 @@ const Stundenplan = {
@selectedEvent="setSelectedEvent"
:initial-date="currentDay"
@change:range="updateRange"
@change:offset="handleOffset"
:events="events"
:initial-mode="propsViewData.mode"
show-weeks
@@ -220,12 +275,15 @@ const Stundenplan = {
</div>
</template>
<template #monthPage="{event,day}">
<span class="fhc-entry" >
{{event.topic}}
</span>
<div @click="showModal($event, event); ">
<span class="fhc-entry">
{{event.topic}}
</span>
</div>
</template>
<template #weekPage="{event,day}">
<div @click="showModal(event?.orig); " type="button"
<div @click="showModal($event, event?.orig);" type="button"
class=" position-relative border border-secondary border d-flex flex-col align-items-center
justify-content-evenly h-100" style="overflow: auto;">
@@ -243,22 +301,22 @@ const Stundenplan = {
</div>
</template>
<template #dayPage="{event,day,mobile}">
<div @click="mobile? showModal(event?.orig):null" type="button" class="fhc-entry border border-secondary border row m-0 h-100 justify-content-center align-items-center text-center">
<div @click="mobile? showModal($event, event?.orig):null" type="button" class="fhc-entry border border-secondary border row m-0 h-100 justify-content-center align-items-center text-center">
<div class="col-auto" v-if="event?.orig?.beginn && event?.orig?.ende" >
<div class="d-flex flex-column p-4 border-end border-secondary">
<span class="small">{{convertTime(event.orig.beginn.split(":"))}}</span>
<span class="small">{{convertTime(event.orig.ende.split(":"))}}</span>
</div>
</div>
<div class="col ">
<div class="col">
<p>{{ $p.t('lehre/lehrveranstaltung') }}:</p>
<p class="m-0">{{event?.orig.topic}}</p>
</div>
<div class="col ">
<div class="col" :style="'max-height: ' + eventMaxHeight + '; overflow: auto;'">
<p>{{ $p.t('lehre/lektor') }}:</p>
<p class="m-0" v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</p>
</div>
<div class="col ">
<div class="col">
<p>{{ $p.t('profil/Ort') }}: </p>
<p class="m-0">{{event?.orig.ort_kurzbz}}</p>
</div>