Merge branch 'feature-25999/C4_cleanup' into merge_FHC4_C4

This commit is contained in:
Harald Bamberger
2025-02-05 16:57:52 +01:00
26 changed files with 495 additions and 158 deletions
+2 -2
View File
@@ -25,9 +25,9 @@ class Stundenplan extends Auth_Controller
*/
public function index($lv_id = null)
{
$viewData = array(
'lv_id' => $lv_id
'lv_id' => $lv_id,
'uid'=>getAuthUID(),
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Stundenplan']);
@@ -0,0 +1,52 @@
<?php
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH')) exit('No direct script access allowed');
class AuthInfo extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getAuthUID' => self::PERM_LOGGED,
]);
$this->uid = getAuthUID();
$this->pid = getAuthPersonID();
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* returns the uid of the currently logged in user
* @access public
*
*/
public function getAuthUID()
{
$this->terminateWithSuccess(['uid'=>$this->uid]);
}
}
@@ -33,6 +33,7 @@ class Stundenplan extends FHCAPI_Controller
'Reservierungen' => self::PERM_LOGGED,
'getStundenplan' => self::PERM_LOGGED,
'getLehreinheitStudiensemester' => self::PERM_LOGGED,
'studiensemesterDateInterval' => self::PERM_LOGGED,
]);
$this->load->library('LogLib');
@@ -56,6 +57,15 @@ class Stundenplan extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------
// Public methods
//TODO: delete this function if we don't use the old calendar export endpoints anymore
public function studiensemesterDateInterval($date){
$this->load->model('organisation/Studiensemester_model','StudiensemesterModel');
$studiensemester =$this->StudiensemesterModel->getByDate(date_format(date_create($date),'Y-m-d'));
$studiensemester =current($this->getDataOrTerminateWithError($studiensemester));
$this->terminateWithSuccess($studiensemester);
}
/**
* fetches Stunden layout from database
* @access public
@@ -545,7 +555,7 @@ class Stundenplan extends FHCAPI_Controller
private function studienSemesterErmitteln($start_date,$end_date){
// gets all studiensemester from the student from start_date to end_date
$semester_range = $this->StudiensemesterModel->getByDate($start_date,$end_date);
$semester_range = $this->StudiensemesterModel->getByDateRange($start_date,$end_date);
$semester_range = array_map(
function($sem)
{
@@ -170,13 +170,31 @@ class Studiensemester_model extends DB_Model
return $this->execQuery($query, array($studiensemester_kurzbz, $studiengang_kz));
}
/**
* Gets a Studiensemester for a date
* @param $date
* @return string
*/
public function getByDate($date)
{
// gets the studiensemster of a date or the next closest previous studiensemester if a date is not within a studiensemester
$query = "
SELECT studiensemester_kurzbz, start, ende
FROM public.tbl_studiensemester
WHERE ( ende >= ?::date AND start <= ?::date ) OR ( ende >= ?::date + '-45 days'::interval AND start <= ?::date + '-45 days'::interval )
ORDER BY start DESC
LIMIT 1";
return $this->execQuery($query, array($date,$date,$date,$date));
}
/**
* Gets all Studiensemester between two dates
* @param $from
* @param $to
* @return array|null
*/
public function getByDate($from, $to)
public function getByDateRange($from, $to)
{
if (date_format(date_create($from), 'Y-m-d') > (date_format(date_create($to), 'Y-m-d')))
return success(array());
+1 -1
View File
@@ -10,7 +10,7 @@ $this->load->view(
);
?>
<iframe style="width:100%; height:100%;" id="Infoterminal" src="<?php echo base_url() . 'cis/infoterminal/'; ?>" name="Infoterminal" frameborder="0" >
<iframe style="width:100%; height:100%;" id="Infoterminal" src="<?php echo base_url() . 'cis/infoterminal/?forcelogin=true'; ?>" name="Infoterminal" frameborder="0" >
No iFrames
</iframe>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
+8
View File
@@ -38,6 +38,14 @@ require_once('../../include/authentication.class.php');
require_once('../../include/addon.class.php');
require_once('../../include/'.EXT_FKT_PATH.'/serviceterminal.inc.php');
// 2025-02-05 ma0080 add query parameter to force login e.g. when used in iframe in CIS4.0 begin
if( isset($_GET['forcelogin']) && !isset($_SERVER['PHP_AUTH_USER']) ) {
header('WWW-Authenticate: Basic Realm="' . AUTH_NAME . '"');
http_response_code(401);
die();
}
// 2025-02-05 ma0080 add query parameter to force login e.g. when used in iframe in CIS4.0 end
if (!$db = new basis_db())
$db=false;
+2 -2
View File
@@ -1,9 +1,9 @@
.sprachen-entry{
background-color: var(--fhc-cis-primary);
background-color: var(--fhc-cis-primary-hover);
}
[selected="true"].sprachen-entry {
background-color: var(--fhc-cis-primary-hover);
background-color: var(--fhc-cis-primary);
}
.sprachen-entry.btn {
+50 -33
View File
@@ -131,19 +131,19 @@
.fhc-calendar-md .fhc-calendar-month-page-day.active {
border-color: var(--bs-secondary);
}
.fhc-calendar-lg .fhc-calendar-month-page-day .events,
.fhc-calendar-md .fhc-calendar-month-page-day .events {
display: block;
overflow: auto;
font-size: 0.7em;
}
.fhc-calendar-lg .fhc-calendar-month-page-day .events span,
.fhc-calendar-md .fhc-calendar-month-page-day .events span {
display: block;
margin: 0.2em;
padding: 0.1em 0.4em;
border-radius: 0.1em;
}
/*.fhc-calendar-lg .fhc-calendar-month-page-day .events,*/
/*.fhc-calendar-md .fhc-calendar-month-page-day .events {*/
/* display: block;*/
/* overflow: auto;*/
/* font-size: 0.7em;*/
/*}*/
/*.fhc-calendar-lg .fhc-calendar-month-page-day .events span,*/
/*.fhc-calendar-md .fhc-calendar-month-page-day .events span {*/
/* display: block;*/
/* margin: 0.2em;*/
/* padding: 0.1em 0.4em;*/
/* border-radius: 0.1em;*/
/*}*/
.fhc-calendar-lg .fhc-calendar-years .col-4,
.fhc-calendar-md .fhc-calendar-years .col-4 {
padding: 0.09375em 0;
@@ -185,8 +185,25 @@
aspect-ratio: 1;
}
.fhc-calendar-past {
background-color:#F5E9D7;
border-color: #E8E8E8;
opacity: 0.5;
}
.fhc-calendar-month-page-day-highlight {
background-color: #f5f5f5;
/*background-color: #f5f5f5;*/
/*background-color: red;*/
}
.fhc-highlight-week {
/*border-color: black !important;*/
}
.fhc-highlight-day {
border-width: 2px !important;
border-color: black !important;
}
.fhc-calendar-sm .fhc-calendar-month-page-day.active .no,
@@ -200,25 +217,25 @@
background-color: rgba(var(--bs-secondary-rgb), 0.25);
border-radius: 50%;
}
.fhc-calendar-sm .fhc-calendar-month-page-day .no,
.fhc-calendar-xs .fhc-calendar-month-page-day .no {
display: flex;
align-items: center;
justify-content: center;
width: 80%;
height: 80%;
margin: 10%;
}
.fhc-calendar-sm .fhc-calendar-month-page-day .events,
.fhc-calendar-xs .fhc-calendar-month-page-day .events {
position: absolute;
bottom: 0;
left: 10%;
width: 80%;
height: 10%;
overflow: hidden;
display: flex;
}
/*.fhc-calendar-sm .fhc-calendar-month-page-day .no,*/
/*.fhc-calendar-xs .fhc-calendar-month-page-day .no {*/
/* display: flex;*/
/* align-items: center;*/
/* justify-content: center;*/
/* width: 80%;*/
/* height: 80%;*/
/* margin: 10%;*/
/*}*/
/*.fhc-calendar-sm .fhc-calendar-month-page-day .events,*/
/*.fhc-calendar-xs .fhc-calendar-month-page-day .events {*/
/* position: absolute;*/
/* bottom: 0;*/
/* left: 10%;*/
/* width: 80%;*/
/* height: 10%;*/
/* overflow: hidden;*/
/* display: flex;*/
/*}*/
.fhc-calendar-sm .fhc-calendar-month-page-day .events span,
.fhc-calendar-xs .fhc-calendar-month-page-day .events span {
overflow: hidden;
+10
View File
@@ -0,0 +1,10 @@
export default {
getAuthUID() {
return this.$fhcApi.get(
'/api/frontend/v1/AuthInfo/getAuthUID',
{ }
);
},
};
+2
View File
@@ -36,6 +36,7 @@ import addons from "./addons.js";
import studiengang from "./studiengang.js";
import menu from "./menu.js";
import dashboard from "./dashboard.js";
import authinfo from "./authinfo.js";
export default {
search,
@@ -59,4 +60,5 @@ export default {
addons,
studiengang,
menu,
authinfo,
};
+6
View File
@@ -36,4 +36,10 @@ export default {
{}
);
},
studiensemesterDateInterval(date) {
return this.$fhcApi.get(
`/api/frontend/v1/Stundenplan/studiensemesterDateInterval/${date}`,
{}
);
},
};
+12
View File
@@ -78,6 +78,11 @@ const app = Vue.createApp({
appSideMenuEntries: {}
}),
components: {},
computed: {
isMobile() {
return /Mobi|Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
@@ -97,6 +102,13 @@ const app = Vue.createApp({
if(!res?.matched?.length) return
event.preventDefault(); // Prevent browser navigation
if(this.isMobile) { // toggle the menu
const navMain = document.getElementById('nav-main');
// fix unwanted toggle from off to on for some links on mobile
if(navMain.classList.contains('show')) document.getElementById('nav-main-btn').click();
}
this.$router.push(route);
}
}
+31 -19
View File
@@ -8,7 +8,9 @@ import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
// TODO(chris): week/month toggle
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
const today = todayDate.getTime()
export default {
components: {
@@ -22,9 +24,14 @@ export default {
},
provide() {
return {
today,
todayDate,
date: this.date,
focusDate: this.focusDate,
size: Vue.computed({ get: () => this.size, set: v => this.size = v }),
size: Vue.computed({ get: () => this.size }),
calendarHeight: Vue.computed({ get: () => this.calendarHeight }),
calendarWidth: Vue.computed({ get: () => this.calendarWidth }),
events: Vue.computed(() => this.eventsPerDay),
filteredEvents: Vue.computed(() => this.filteredEvents),
minimized: Vue.computed({ get: () => this.minimized, set: v => this.$emit('update:minimized', v) }),
@@ -32,11 +39,9 @@ export default {
noMonthView: this.noMonthView,
noWeekView: this.noWeekView,
eventsAreNull: Vue.computed(() => this.events === null),
classHeader: this.classHeader,
mode: Vue.computed(()=>this.mode),
selectedEvent: Vue.computed(() => this.selectedEvent),
setSelectedEvent: (event)=>{this.selectedEvent = event;},
widget: this.widget
};
},
props: {
@@ -53,17 +58,9 @@ export default {
type: String,
default: 'month'
},
classHeader: {
type: [String, Object, Array],
default: ''
},
minimized: Boolean,
noWeekView: Boolean,
noMonthView: Boolean,
widget: {
type: Boolean,
default: false
}
noMonthView: Boolean
},
watch:{
selectedEvent:{
@@ -106,11 +103,14 @@ export default {
date: new CalendarDate(),
focusDate: new CalendarDate(),
size: 0,
containerWidth: 0,
containerHeight: 0,
selectedEvent:null,
}
},
computed: {
sizeClass() {
sizeClass() {
// mainly determines calendar font-size
return 'fhc-calendar-' + ['xs', 'sm', 'md', 'lg'][this.size];
},
mode: {
@@ -206,16 +206,25 @@ export default {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
let w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
// TODO(chris): rework sizing
if (w > 600)
const w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
const h = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
// https://getbootstrap.com/docs/5.0/layout/breakpoints/
// bootstrap breakpoints watch window size and this function monitors container size of calendar itself.
// calendar is using bootstrap breakpoints which influence layout, which retriggers this function
// -> some width constellations will loop so we dont use values around bs5 breakpoints
// ['xs', 'sm', 'md', 'lg'][this.size]
if (w >= 600)
this.size = 3;
else if (w > 350)
else if (w >= 350)
this.size = 2;
else if (w > 250)
else if (w >= 250)
this.size = 1;
else
this.size = 0;
this.containerWidth = w
this.containerHeight = h
}
}).observe(this.$refs.container);
}
@@ -227,6 +236,9 @@ 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" >
<template #calendarDownloads>
<slot name="calendarDownloads" ></slot>
</template>
<template #monthPage="{event,day}">
<slot name="monthPage" :event="event" :day="day" ></slot>
</template>
+5 -1
View File
@@ -40,7 +40,11 @@ export default {
},
template: /*html*/`
<div class="fhc-calendar-day">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')"/>
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-day-page :active="slot.active" :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #dayPage="{event,day,mobile}">
+54 -15
View File
@@ -17,11 +17,14 @@ export default {
data() {
return {
hourPosition: null,
curHourPosition: null,
hourPositionTime: null,
lvMenu: null,
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -100,19 +103,13 @@ export default {
}
},
dayText(){
if(!this.size || !this.day)return {};
if(!this.day)return {};
return {
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
}
},
dayGridStyle() {
return {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
},
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
@@ -135,6 +132,22 @@ export default {
right: 0,
}
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '7rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
left: 0,
right: 0,
}
},
noEventsCondition() {
return !this.isSliding && this.filteredEvents?.length === 0;
},
@@ -192,8 +205,32 @@ export default {
smallestTimeFrame() {
return [30, 15, 10, 5][this.size];
},
lookingAtToday() {
return this.date.compare(this.todayDate)
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
dayGridStyle(day) {
const styleObj = {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(this.date.compare(this.todayDate)) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
// styleObj.opacity = 0.5; // would opaque the whole column
}
return styleObj
},
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$fhcApi.factory.stundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]).then(
@@ -270,6 +307,7 @@ export default {
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
console.log('height: ', height)
position = height * (timePercentage / 100);
this.hourPosition = position;
@@ -290,13 +328,9 @@ export default {
},
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'hidden'
},
template: /*html*/`
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
@@ -308,7 +342,7 @@ export default {
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
</div>
</div>
<div id="scroll g-0" style="height: 100%; overflow: scroll;">
<div id="scroll g-0" style="height: 100%; overflow-y: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
@@ -318,6 +352,11 @@ export default {
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<Transition>
<div v-if="lookingAtToday && !noEventsCondition" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">Keine Lehrveranstaltungen</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
@@ -325,7 +364,7 @@ export default {
<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>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle">
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<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)">
@@ -348,7 +387,7 @@ export default {
</div>
</div>
</div>
<div class="d-xl-block col-xl-6 p-4" style="max-height: 100%">
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
<div style="z-index:0; max-height: 100%" class="sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: 100%; overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
+5 -24
View File
@@ -13,11 +13,9 @@ export default {
inject: [
'eventsAreNull',
'size',
'classHeader',
'mode',
'noWeekView',
'noMonthView',
'widget'
],
props: {
title: String
@@ -28,30 +26,13 @@ export default {
'next',
'click'
],
computed: {
getHeaderOffsetClass() {
return 'col offset-0' + (this.widget ? '' : ' offset-md-3')
},
myClassHeader() {
// TODO(chris): + {'btn-sm': !this.size}
let c = this.classHeader;
if (Array.isArray(c)) {
if (!this.size)
c.push('btn-sm');
} else if (typeof c === 'string' || c instanceof String) {
if (!this.size)
c += ' btn-sm';
} else {
c['btn-sm'] = !this.size;
}
return c;
}
},
template: /*html*/`
<div class="calendar-header card-header w-100" :class="classHeader">
<div class="calendar-header card-header w-100">
<div class="row align-items-center ">
<div :class="getHeaderOffsetClass" :style="{'padding-left':headerPadding}">
<div class=" col-12 col-md-3 d-flex justify-content-center justify-content-md-start align-items-center">
<slot name="calendarDownloads"></slot>
</div>
<div class="col-12 col-md-6" :style="{'padding-left':headerPadding}">
<div class="row align-items-center justify-content-center">
<div class="col-auto ">
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm':!this.size}" @click="$emit('prev')"><i class="fa fa-chevron-left"></i></button>
@@ -8,7 +8,6 @@ export default {
'size',
'minimized',
'date',
'classHeader'
],
data() {
return {
+5 -1
View File
@@ -61,7 +61,11 @@ export default {
},
template: `
<div class="fhc-calendar-month">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'months')" />
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'months')" >
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" >
<template #monthPage="{event,day}">
+45 -15
View File
@@ -9,6 +9,8 @@ export default {
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -16,7 +18,7 @@ export default {
'showWeeks',
'noWeekView',
'selectedEvent',
'setSelectedEvent',
'setSelectedEvent'
],
props: {
year: Number,
@@ -63,14 +65,17 @@ export default {
methods: {
getDayClass(week, day) {
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
const isHighlighted = this.isHighlighted(week, day)
const isHighlightedWeek = this.isHighlightedWeek(week)
const isHighlightedDay = this.isHighlightedDay(day)
const isThisDate = this.date.compare(day)
const isNotThisMonth = day.getMonth() != this.month
const isInThePast = this.date.isInPast(day)
if(isHighlighted) classstring += ' fhc-calendar-month-page-day-highlight'
if(isThisDate) classstring += ' active'
if(isNotThisMonth || isInThePast) classstring += ' opacity-50'
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
if(isHighlightedDay) classstring += ' fhc-highlight-day'
if(isNotThisMonth) classstring += ' opacity-25'
if(isInThePast) classstring += ' fhc-calendar-past'
return classstring
},
selectDay(day) {
@@ -85,12 +90,14 @@ export default {
}
},
highlight(week, day){
console.log('highlight method')
this.highlightedWeek = week.no;
this.highlightedDay = day;
},
isHighlighted(week, day) {
return this.noWeekView ? day == this.highlightedDay : week.no == this.highlightedWeek;
isHighlightedDay(day) {
return day == this.highlightedDay
},
isHighlightedWeek(week) {
return week.no == this.highlightedWeek
},
clickEvent(day,week) {
if(!this.noWeekView)
@@ -99,11 +106,31 @@ export default {
this.$emit('updateMode', 'day');
}
this.selectDay(day);
}
},
getNumberStyle(day) {
const styleObj = {}
styleObj.display = 'inline-block';
styleObj.height = '32px';
styleObj['line-height'] = '32px';
styleObj['text-align'] = 'center';
styleObj['font-weight'] = 'bold';
styleObj['font-size'] = '14px';
if(day.getDate() === this.todayDate.getDate()
&& day.getMonth() === this.todayDate.getMonth()
&& day.getFullYear() === this.todayDate.getFullYear()) {
styleObj['background-color'] = '#00649c'; // fh blau
styleObj.color = 'white';
}
return styleObj
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
if(container) container.style['overflow-y'] = 'auto'
},
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
@@ -113,7 +140,8 @@ export default {
</div>
<template v-for="week in weeks"
:key="week.no">
<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="#" 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)"
@mouseover="highlight(week,day)"
@@ -122,9 +150,10 @@ export default {
:key="day"
:class="getDayClass(week, day)"
>
<span class="no">{{day.getDate()}}</span>
<span 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" :style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
<div @click="setSelectedEvent(event);" 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>
</slot>
@@ -132,5 +161,6 @@ export default {
</span>
</a>
</template>
</div>`
</div>
`
}
+5 -1
View File
@@ -40,7 +40,11 @@ export default {
},
template: /*html*/`
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')"/>
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')">
<template #calendarDownloads>
<slot name="calendarDownloads"></slot>
</template>
</calendar-header>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #weekPage="{event,day}">
+93 -8
View File
@@ -9,9 +9,13 @@ export default {
return{
hourPosition:null,
hourPositionTime:null,
resizeObserver: null,
width: 0
}
},
inject: [
'today',
'todayDate',
'date',
'focusDate',
'size',
@@ -32,6 +36,13 @@ export default {
'input',
],
computed: {
laneWidth() {
return this.width / this.days.length
},
curTime() {
const now = new Date();
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
},
pageHeaderStyle(){
return {
'z-index': 4,
@@ -91,7 +102,9 @@ export default {
let nextDay = new Date(day);
nextDay.setDate(nextDay.getDate()+1);
nextDay.setMilliseconds(nextDay.getMilliseconds()-1);
let d = {events:[],lanes:1};
let d = {events:[],lanes:1, isPast: false};
d.isPast = nextDay.getTime() < this.today
d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate()
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) {
@@ -118,6 +131,32 @@ export default {
},
smallestTimeFrame() {
return [30,15,10,5][this.size];
},
lookingAtToday() {
return this.days.some(d =>
d.getFullYear() === this.todayDate.getFullYear() &&
d.getMonth() === this.todayDate.getMonth() &&
d.getDate() === this.todayDate.getDate()
)
},
curIndicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '1rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.getDayTimePercent + '%',
width: this.laneWidth + 'px' // todo: manage the real value of 1fr somehow
}
},
getDayTimePercent() {
const now = new Date(Date.now())
const currentMinutes = now.getMinutes() + now.getHours() * 60
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
return timePercentage
}
},
methods: {
@@ -135,10 +174,23 @@ export default {
}
},
dayGridStyle(day) {
return {
const styleObj = {
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
if(day.isPast) {
styleObj['background-color'] = '#F5E9D7'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
} else if (day.isToday) {
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
styleObj['border-color'] = '#E8E8E8';
styleObj.opacity = 0.5;
}
return styleObj
},
eventGridStyle(day, event) {
return {
@@ -208,17 +260,44 @@ export default {
this.setSelectedEvent(event);
this.focusDate.set(new CalendarDate(new Date(event.datum)));
this.$emit('input', event)
}
},
initResizeObserver() {
const events = this.$refs['eventsRef'+this.week];
if (!events) return;
this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
if(width > 0) this.width = width
}
});
this.resizeObserver.observe(events);
},
destroyResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
},
},
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
if(container) {
container.style['overflow-y'] = 'scroll'
container.style['overflow-x'] = 'auto'
}
this.initResizeObserver();
},
beforeUnmount() {
this.destroyResizeObserver();
},
template: /*html*/`
<div class="fhc-calendar-week-page" style="min-width: 700px;">
<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)">
@@ -233,19 +312,25 @@ export default {
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="events">
<div class="events" :ref="'eventsRef'+week">
<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>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<Transition>
<div v-if="day.isToday" class="position-absolute border-top small" :style="curIndicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
</div>
</Transition>
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)"
:selected="event.orig == selectedEvent"
:style="eventGridStyle(day,event)"
class="mx-2 small rounded overflow-hidden fhc-entry "
v-contrast >
<slot name="weekPage" :event="event" :day="day">
<slot name="weekPage" :event="event" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</slot>
</div>
</div>
@@ -10,19 +10,45 @@ export const Stundenplan = {
return {
events: null,
calendarDate: new CalendarDate(new Date()),
eventCalendarDate: new CalendarDate(new Date()),
currentlySelectedEvent: null,
currentDay: new Date(),
minimized: false,
viewData: JSON.parse(this.viewDataString ?? '{}'),
studiensemester_kurzbz:null,
studiensemester_start:null,
studiensemester_ende:null,
uid:null,
}
},
props: [
"viewDataString"
"viewData",
],
watch: {
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,
}
},
components: {
FhcCalendar, LvModal, LvMenu, LvInfo
},
computed:{
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);
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') }];
},
lv_id() { // computed so we can theoretically change path/lva selection and reload without page refresh
const pathParts = window.location.pathname.split('/').filter(Boolean);
const id = pathParts[pathParts.length - 1];
@@ -35,14 +61,17 @@ export const Stundenplan = {
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
},
monthFirstDay: function () {
return this.calendarDateToString(this.calendarDate.cdFirstDayOfCalendarMonth);
return this.calendarDateToString(this.eventCalendarDate.cdFirstDayOfCalendarMonth);
},
monthLastDay: function () {
return this.calendarDateToString(this.calendarDate.cdLastDayOfCalendarMonth);
return this.calendarDateToString(this.eventCalendarDate.cdLastDayOfCalendarMonth);
},
},
methods:{
fetchStudiensemesterDetails: async function (date) {
return this.$fhcApi.factory.stundenplan.studiensemesterDateInterval(date);
},
convertTime: function([hour,minute]){
let date = new Date();
date.setHours(hour);
@@ -66,14 +95,15 @@ export const Stundenplan = {
updateRange: function ({start,end}) {
let checkDate = (date) => {
return date.m != this.calendarDate.m || date.y != this.calendarDate.y;
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
if (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.calendarDate = new CalendarDate(end);
this.eventCalendarDate = new CalendarDate(end);
Vue.nextTick(() => {
this.loadEvents();
});
@@ -120,13 +150,30 @@ export const Stundenplan = {
},
created()
{
this.$fhcApi.factory.authinfo.getAuthUID().then((res) => res.data)
.then(data=>{
this.uid = data.uid;
})
this.loadEvents();
},
beforeUnmount() {
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
},
template:/*html*/`
<h2>{{$p.t('lehre/stundenplan')}}</h2>
<hr>
<lv-modal v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ref="lvmodal" />
<fhc-calendar @selectedEvent="setSelectedEvent" :initial-date="currentDay" @change:range="updateRange" :events="events" initial-mode="week" show-weeks @select:day="selectDay" v-model:minimized="minimized">
<template #calendarDownloads>
<div v-for="{title,icon,link} in downloadLinks">
<a :href="link" :title="title" class="py-1 px-2 m-1 btn btn-outline-secondary">
<div class="d-flex flex-column">
<i :class="icon"></i>
<span class="small">{{title}}</span>
</div>
</a>
</div>
</template>
<template #monthPage="{event,day}">
<span class="fhc-entry" >
{{event.topic}}
+9 -9
View File
@@ -120,8 +120,6 @@ export default {
.catch((err) => {
console.error("ERROR: ", err);
});
},
mounted() {
@@ -141,10 +139,13 @@ export default {
}
}).observe(this.$refs.container);
}
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
});
Vue.nextTick(()=>{
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
});
})
},
template: /*html*/ `
<div ref="container" class="widgets-news h-100" :class="sizeClass" :style="getNewsWidgetStyle">
@@ -177,9 +178,8 @@ export default {
<div class="container h-100" style="padding: 0px;" ref="carocontainer">
<div id="FhcCarouselContainer" style="height: 100%;" ref="carousel" class="carousel slide fhc-carousel" data-bs-ride="carousel" data-bs-interval="false">
<div class="carousel-inner" ref="carouselInner" style="height: 100%; max-width: 100%;">
<div ref="carouselItems" v-for="(news, index) in newsList" class="carousel-item " style="overflow-y: auto; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content">
</div>
<div class="carousel-inner" ref="carouselInner" style="height: 100%; max-width: 100%;">
<div ref="carouselItems" v-for="(news, index) in newsList" class="carousel-item " style="overflow-y: auto; overflow-x: hidden; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content"/>
</div>
<button @click="setPrev" @focus="$event.target.blur()" style="z-index: 100; color: black; overflow: hidden; margin-left: 10px; width:35px;" data-bs-target="#FhcCarouselContainer" class="carousel-control-prev" type="button">
<div class="border rounded-circle" style="padding-left: 0.4rem; padding-right: 0.4rem; background-color:rgba(138,138,138,0.4)">
@@ -178,7 +178,7 @@ export default {
<div v-if="events === null" class="d-flex h-100 justify-content-center align-items-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<template ref="allWeek" v-else-if="allEventsGrouped.size" v-for="([key, value], index) in allEventsGrouped" :key="index" style="margin-top: 8px;">
<template v-else-if="allEventsGrouped.size" v-for="([key, value], index) in allEventsGrouped" :key="index" style="margin-top: 8px;">
<div class="card-header d-grid p-0">
<button class="btn btn-link link-secondary text-decoration-none" @click="setCalendarMaximized">{{ key.format({dateStyle: "full"})}}</button>
</div>
+1 -6
View File
@@ -199,11 +199,7 @@ class CalendarDate {
return true;
return false;
}
isInPast(d) {
if (this.isDate(d))
return (this.y > d.getFullYear() || this.m > d.getMonth() || this.d > d.getDate());
return false
}
setLocale(locale) {
this.weekStart = CalendarDate.getWeekStart(locale);
}
@@ -261,5 +257,4 @@ CalendarDate.getWeekStart = function(locale) {
}
}
export default CalendarDate
+13 -11
View File
@@ -1,16 +1,18 @@
<?php
if (!$result = @$db->db_query("SELECT LC_Time FROM public.tbl_sprache WHERE LIMIT 1")) {
if ($result = $db->db_query("SELECT * FROM information_schema.columns WHERE column_name = 'lc_time' AND table_name = 'tbl_sprache' AND table_schema = 'public';")) {
if ($db->db_num_rows($result) == 0)
{
$qry = "
ALTER TABLE public.tbl_sprache ADD lc_time VARCHAR(255) ;
UPDATE public.tbl_sprache SET lc_time = 'en-GB' where locale ='en-US';
UPDATE public.tbl_sprache SET lc_time = 'de-AT' where locale ='de-AT';
";
$qry = "
ALTER TABLE public.tbl_sprache ADD LC_Time VARCHAR(255) ;
UPDATE public.tbl_sprache SET LC_Time = 'en-GB' where locale ='en-US';
UPDATE public.tbl_sprache SET LC_Time = 'de-AT' where locale ='de-AT';
";
if (!$db->db_query($qry))
echo '<strong>public.tbl_sprache: ' . $db->db_last_error() . '</strong><br>';
else
echo '<br>public.tbl_sprache: column LC_Time was successfully added';
if (!$db->db_query($qry))
echo '<strong>public.tbl_sprache: ' . $db->db_last_error() . '</strong><br>';
else
echo '<br>public.tbl_sprache: column lc_time was successfully added';
}
}