Merge branch 'feature-25999/C4_cleanup_rc'

This commit is contained in:
Harald Bamberger
2024-12-04 12:44:08 +01:00
36 changed files with 729 additions and 254 deletions
+2
View File
@@ -5,3 +5,5 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
// CMS Content Id for CIS4 Menu Root
$config['cis_menu_root_content_id'] = 11066;
// send Mails for ProfilUpdate
$config['cis_send_profil_update_mails'] = true;
@@ -36,6 +36,7 @@ class ProfilUpdate extends Auth_Controller
'getTopic' => ['basis/cis:r'],
]);
$this->load->config('cis');
$this->load->model('person/Profil_update_model', 'ProfilUpdateModel');
$this->load->model('person/Kontakt_model', 'KontaktModel');
@@ -111,6 +112,10 @@ class ProfilUpdate extends Auth_Controller
private function sendEmail_onProfilUpdate_response($uid, $topic, $status)
{
if($this->config->item('cis_send_profil_update_mails') === false)
{
return;
}
$this->load->helper('hlp_sancho_helper');
$email = $uid . "@" . DOMAIN;
@@ -138,6 +143,10 @@ class ProfilUpdate extends Auth_Controller
private function sendEmail_onProfilUpdate_insertion($uid, $profil_update_id, $topic)
{
if($this->config->item('cis_send_profil_update_mails') === false)
{
return;
}
$this->load->helper('hlp_sancho_helper');
$emails = [];
+5 -7
View File
@@ -28,14 +28,12 @@ class Cis4 extends Auth_Controller
public function index()
{
$this->load->model('person/Person_model','PersonModel');
$begruesung = $this->PersonModel->getFirstName(getAuthUID());
if(isError($begruesung))
{
show_error("name couldn't be loaded for username ".getAuthUID());
}
$begruesung = getData($begruesung);
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$viewData = array(
'name' => $begruesung
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id
);
$this->load->view('CisVue/Dashboard.php',['viewData' => $viewData]);
@@ -125,7 +125,7 @@ class Cms extends FHCAPI_Controller
//get the data or terminate with error
$news = $this->getDataOrTerminateWithError($news);
// collect the content of the news
foreach($news as $news_element){
$this->addMeta("content_id",$news_element->content_id);
@@ -134,12 +134,17 @@ class Cms extends FHCAPI_Controller
$this->NewsModel->resetQuery();
$content = $this->cmslib->getContent($news_element->content_id);
$content = $this->getDataOrTerminateWithError($content);
$content = getData($content);
$news_element->content_obj = $content;
}
$withContent = function($news) {
return $news->content_obj != null;
};
$newsWithContent = array_filter($news, $withContent);
$this->terminateWithSuccess($news);
$this->terminateWithSuccess($newsWithContent);
}
@@ -29,7 +29,9 @@ class Phrasen extends FHCAPI_Controller
{
parent::__construct([
'loadModule' => self::PERM_ANONYMOUS,
'setLanguage' => self::PERM_ANONYMOUS
'setLanguage' => self::PERM_ANONYMOUS,
'getLanguage' => self::PERM_ANONYMOUS,
'getAllLanguages' => self::PERM_ANONYMOUS,
]);
$this->load->helper('hlp_language');
@@ -60,4 +62,23 @@ class Phrasen extends FHCAPI_Controller
$phrases = $this->p->setPhrases($categories, $language);
$this->terminateWithSuccess($phrases);
}
// gets the langauge of the currently logged in user session and otherwhise the system language
public function getLanguage()
{
$lang = getUserLanguage();
$this->terminateWithSuccess($lang);
}
// gets all languages that are set as active in the database
public function getAllLanguages()
{
$langs = getDBActiveLanguages();
$langs = $this->getDataOrTerminateWithError($langs);
$langs = array_map(function($lang){
return $lang->sprache;
}, $langs);
$this->terminateWithSuccess($langs);
}
}
@@ -47,6 +47,8 @@ class ProfilUpdate extends FHCAPI_Controller
'show' => self::PERM_LOGGED,
]);
$this->load->config('cis');
// Load language phrases
$this->loadPhrases(
array(
@@ -504,6 +506,10 @@ class ProfilUpdate extends FHCAPI_Controller
private function sendEmail_onProfilUpdate_insertion($uid, $profil_update_id, $topic)
{
if($this->config->item('cis_send_profil_update_mails') === false)
{
return;
}
$this->load->helper('hlp_sancho_helper');
$emails = [];
@@ -573,6 +579,11 @@ class ProfilUpdate extends FHCAPI_Controller
private function sendEmail_onProfilUpdate_response($uid, $topic, $status)
{
if($this->config->item('cis_send_profil_update_mails') === false)
{
return;
}
$this->load->helper('hlp_sancho_helper');
$email = $uid . "@" . DOMAIN;
@@ -151,7 +151,12 @@ class Stundenplan extends FHCAPI_Controller
// getting the student_lehrverbaende of the student in the different studiensemester
$student_lehrverband = $this->fetchStudentlehrverbandFromStudiensemester($semester_range);
$stundenplan_data = $this->StundenplanModel->stundenplanGruppierung($this->StundenplanModel->getStundenplanQuery($start_date, $end_date, $semester_range, $benutzer_gruppen, $student_lehrverband));
$stundenplan_query = $this->StundenplanModel->getStundenplanQuery($start_date, $end_date, $semester_range, $benutzer_gruppen, $student_lehrverband);
if(!$stundenplan_query)
{
$this->terminateWithSuccess([]);
}
$stundenplan_data = $this->StundenplanModel->stundenplanGruppierung($stundenplan_query);
$stundenplan_data = $this->getDataOrTerminateWithError($stundenplan_data) ?? [];
$this->expand_object_information($stundenplan_data);
@@ -135,7 +135,7 @@ class Stundenplan_model extends DB_Model
/**
* function that takes a query that fetches lehre.vw_stundenplan rows and groups them so that they can be displayed in a calendar
* groups rows of a subquery that fetches data from the lehre.vw_stundenplan table
* @param string $stundenplanViewQuery the subquery used to group the result
*
* @return stdClass
@@ -190,7 +190,7 @@ class Stundenplan_model extends DB_Model
* NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
* @param string $uid the user id that is used to fetch the stundenplan rows from the lehre.vw_stundenplan table
*
* @return string
* @return mixed
*/
public function getStundenplanQuery($start_date, $end_date,$semester,$gruppen,$studentlehrverbaende){
@@ -206,7 +206,13 @@ class Stundenplan_model extends DB_Model
}
return $result;
};
// if both the gruppen and the studentlehrverbaende are empty we early return
if($emptyCheck($gruppen) && $emptyCheck($studentlehrverbaende))
{
return false;
}
$query =
"select sp.*
from lehre.vw_stundenplan sp
@@ -233,15 +239,15 @@ class Stundenplan_model extends DB_Model
// converts the array of gruppen strings into a sql IN (_,_,_) chain
$query .="(sp.gruppe_kurzbz IN (" .implode(',',$gruppen[$sem_date]).") AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende)." )";
// adds the OR sql chain only if the $studentlehrverbaende array is not empty
// DOES not include the sql OR if the $studentlehrverbaende are empty and it is the last gruppen element in the iteration
if(key($semester) != $sem || !$emptyCheck($studentlehrverbaende))
{
$query .="OR";
}
$query .="OR";
}
}
// if there are no studentlehrverbaende and the gruppen are not empty, we can remove the last OR added after the groups
if($emptyCheck($studentlehrverbaende) && !$emptyCheck($gruppen))
{
$query = substr($query, 0, -2);
}
foreach($semester as $sem=>$semester_date_range)
{
@@ -253,20 +259,24 @@ class Stundenplan_model extends DB_Model
}
foreach($studentlehrverbaende[$sem_date] as $key=>$lehrverband)
{
// adds the OR sql chain only if its not the first element in the first semester of the $studentlehrverbaende array
if($sem != array_keys($semester)[0] || $key != array_keys($semester)[0])
{
$query .="OR";
}
$query .= "((sp.studiengang_kz = ".$this->escape($lehrverband->studiengang_kz)." AND sp.semester = ".$this->escape($lehrverband->semester)." AND sp.verband = ".$this->escape($lehrverband->verband)." AND sp.gruppe = ".$this->escape($lehrverband->gruppe)." AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende).")";
// Eintraege fuer den ganzen Verband
$query .= "OR (sp.studiengang_kz = ".$this->escape($lehrverband->studiengang_kz)." AND sp.semester = ".$this->escape($lehrverband->semester)." AND sp.verband = ".$this->escape($lehrverband->verband)." AND (sp.gruppe is null OR sp.gruppe='') AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende).")";
// Eintraege fuer das ganze Semester
$query .= "OR (sp.studiengang_kz = ".$this->escape($lehrverband->studiengang_kz)." AND sp.semester = ".$this->escape($lehrverband->semester)." AND (sp.verband is null OR sp.verband='') AND sp.datum BETWEEN ".$this->escape($sem_date_range->start)." AND ".$this->escape($sem_date_range->ende).") AND gruppe_kurzbz is null)";
$query .="OR";
}
}
}
// if the studentlehrverbaende is not empty we can remove the last OR that was added to the query
if(!$emptyCheck($studentlehrverbaende))
{
$query = substr($query, 0, -2);
}
// closes the AND sql chain only if it was opened previously
if(!$emptyCheck($gruppen) || !$emptyCheck($studentlehrverbaende))
{
+39 -1
View File
@@ -182,6 +182,10 @@ html {
transform: rotate(-90deg);
}
#nav-sprachen{
transition: none;
}
/* searchbar */
#nav-search {
z-index: 1;
@@ -478,15 +482,28 @@ html {
}
.fhc-entry:hover{
background-color:#0088d6 !important;
background-color:#005585 !important;
color:white !important;
}
.fhc-entry.btn:focus {
box-shadow: none !important;
}
.fhc-entry.btn {
border-radius: 0 !important;
}
.fhc-entry {
transition-property: background,color;
transition-duration: 0.3s,0.2s;
transition-timing-function: ease-out,ease-out;
}
[selected].fhc-entry {
background-color: #00649C !important;
}
@media screen and ( max-width: 767px ) {
#nav-search {
position: static;
@@ -527,4 +544,25 @@ html {
padding-left: 2.5rem;
overflow-wrap: anywhere;
}
}
/* classes used for the Vue <Transition> component*/
.v-enter-active,
.v-leave-active {
transition: opacity 0.2s ease-out;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.height-enter-active,
.height-leave-active {
transition: height 0.3s ease-out;
}
.height-enter-from,
.height-leave-to {
height: 0px;
}
+63 -18
View File
@@ -1,27 +1,72 @@
.widgets-news .card-header {
flex-direction: column;
align-items: flex-start !important;
}
:root{
--news-widget-height: 1;
}
.widgets-news .news-content > div,
.widgets-news .news-content .row:nth-child(1),
.widgets-news .news-content .news-list,
.widgets-news .news-content .news-list-item,
.widgets-news .news-content .card-body
{
height: 100%;
}
.widgets-news img
{
max-width: 100%;
}
.widgets-news .card-body{
overflow: hidden;
}
.fhc-news-menu-item {
padding: 0.375rem;
color: var(--fhc-cis-menu-lvl-1-color);
min-height: 5%;
max-height: 30%;
width: 100%;
justify-content: space-between;
align-items: center;
background-color: #00649c;
border: 1px solid #f1f1f1;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease; /* Smooth transition */
}
.fhc-news-menu-item:hover {
background-color: var(--fhc-cis-menu-lvl-1-bg-hover);
border-color: #f1f1f1;
}
.fhc-news-menu-item:active {
background-color: var(--fhc-cis-menu-lvl-1-color-hover);
border-color: #f1f1f1;
}
.fhc-news-menu-item.selected {
background-color: var(--fhc-cis-menu-lvl-1-bg-hover);
border-right: 2px solid #fff;
}
.fhc-news-menu-item:focus {
outline: none;
}
.fhc-news-menu-item-betreff
{
width: 100%;
text-align: center;
max-height: 100%;
height: 100%;
}
.fhc-news-menu-item-date {
text-align: end;
width: 100%;
max-height: 100%;
height: 100%;
}
.fhc-carousel .carousel-item {
transition: transform 0.375s ease-in-out, opacity 0.75s ease-in-out;
}
.fhc-carousel .carousel-item-next,
.fhc-carousel .carousel-item-prev {
transition: transform 0.44s ease-in-out, opacity 0.8s ease-in-out;
}
.fhc-carousel .carousel-item-start,
.fhc-carousel .carousel-item-end {
transition: transform 0.44s ease-in-out, opacity 0.8s ease-in-out;
}
+6
View File
@@ -22,5 +22,11 @@ export default {
setLanguage(categories,language) {
const payload = {categories, language}
return this.$fhcApi.post('/api/frontend/v1/phrasen/setLanguage', payload);
},
getLanguage() {
return this.$fhcApi.get('/api/frontend/v1/phrasen/getLanguage', {});
},
getActiveDbLanguages() {
return this.$fhcApi.get('/api/frontend/v1/phrasen/getAllLanguages', {});
}
};
+20 -4
View File
@@ -2,6 +2,8 @@ import FhcCalendar from "../../components/Calendar/Calendar.js";
import Phrasen from "../../plugin/Phrasen.js";
import CalendarDate from "../../composables/CalendarDate.js";
import LvModal from "../../components/Cis/Mylv/LvModal.js";
import LvInfo from "../../components/Cis/Mylv/LvInfo.js"
import LvMenu from "../../components/Cis/Mylv/LvMenu.js"
const app = Vue.createApp({
@@ -17,7 +19,7 @@ const app = Vue.createApp({
}
},
components: {
FhcCalendar, LvModal
FhcCalendar, LvModal, LvMenu, LvInfo
},
computed:{
weekFirstDay: function () {
@@ -35,6 +37,9 @@ const app = Vue.createApp({
},
methods:{
setSelectedEvent: function (event) {
this.currentlySelectedEvent = event;
},
getLvID: function () {
this.lv_id = window.location.pathname
},
@@ -111,7 +116,7 @@ const app = Vue.createApp({
<h2>{{$p.t('lehre/stundenplan')}}</h2>
<hr>
<lv-modal v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ref="lvmodal" />
<fhc-calendar :initial-date="currentDay" @change:range="updateRange" :events="events" initial-mode="week" show-weeks @select:day="selectDay" v-model:minimized="minimized">
<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 #monthPage="{event,day,isSelected}">
<span class="fhc-entry" :class="{'selectedEvent':isSelected}" style="color:white" :style="{'background-color': event.color}">
{{event.topic}}
@@ -124,8 +129,8 @@ const app = Vue.createApp({
<span>{{event?.orig.ort_kurzbz}}</span>
</div>
</template>
<template #dayPage="{event,day}">
<div type="button" class="fhc-entry border border-secondary border row h-100 justify-content-center align-items-center text-center">
<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 class="col ">
<p>Lehrveranstaltung:</p>
<p class="m-0">{{event?.orig.topic}}</p>
@@ -140,6 +145,17 @@ const app = Vue.createApp({
</div>
</div>
</template>
<template #pageMobilContent="{lvMenu}">
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
<div class="w-100">
<lv-info :event="currentlySelectedEvent" />
</div>
<h3 >Lehrveranstaltungs Menu</h3>
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-show="lvMenu" :menu="lvMenu" />
</template>
<template #pageMobilContentEmpty >
<h3>Keine Lehrveranstaltungen</h3>
</template>
</fhc-calendar>
`
});
+16 -3
View File
@@ -66,6 +66,12 @@ export default {
}
},
watch:{
selectedEvent:{
handler(newSelectedEvent) {
this.$emit('selectedEvent', newSelectedEvent);
},
immediate: true,
},
// scroll to the first event if the html element was found
scrollTime({focusDate,scrollTime}){
// return early if the scrollTime is not set
@@ -89,7 +95,8 @@ export default {
'select:day',
'select:event',
'change:range',
'update:minimized'
'update:minimized',
'selectedEvent'
],
data() {
return {
@@ -223,8 +230,14 @@ export default {
<template #weekPage="{event,day,isSelected}">
<slot name="weekPage" :event="event" :day="day" :isSelected="isSelected"></slot>
</template>
<template #dayPage="{event,day}">
<slot name="dayPage" :event="event" :day="day"></slot>
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile"></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu"></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
<template #minimizedPage="{event,day}">
<slot name="minimizedPage" :event="event" :day="day"></slot>
+8 -2
View File
@@ -43,8 +43,14 @@ export default {
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')"/>
<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}">
<slot name="dayPage" :event="event" :day="day" ></slot>
<template #dayPage="{event,day,mobile}">
<slot name="dayPage" :event="event" :day="day" :mobile="mobile" ></slot>
</template>
<template #pageMobilContent="{lvMenu}">
<slot name="pageMobilContent" :lvMenu="lvMenu" ></slot>
</template>
<template #pageMobilContentEmpty>
<slot name="pageMobilContentEmpty" ></slot>
</template>
</calendar-day-page>
</calendar-pane>
+49 -43
View File
@@ -1,6 +1,4 @@
import CalendarDate from '../../../composables/CalendarDate.js';
import LvMenu from "../../../components/Cis/Mylv/LvMenu.js"
import LvInfo from "../../../components/Cis/Mylv/LvInfo.js"
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
function ggt(m, n) { return n == 0 ? m : ggt(n, m % n); }
@@ -8,8 +6,6 @@ function kgv(m, n) { return (m * n) / ggt(m, n); }
export default {
components:{
LvMenu,
LvInfo,
LvModal,
},
data() {
@@ -79,9 +75,26 @@ export default {
this.fetchLvMenu(event);
},
immediate:true,
},
isSliding:{
handler(value){
if(value)
{
this.setSelectedEvent(null);
}
}
}
},
computed: {
pageHeaderStyle(){
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
dayGridStyle(){
return {
'grid-template-columns': '1 1fr',
@@ -191,12 +204,16 @@ export default {
'z-index': 0,
}
},
showModal: function (evt) {
let event = evt.orig;
this.setSelectedEvent(event);
Vue.nextTick(() => {
this.$refs.lvmodal.show();
});
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
},
eventClick(evt) {
let event = evt.orig;
@@ -258,36 +275,26 @@ export default {
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
eventGridStyle(day,event){
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
}
},
template: /*html*/`
<div class="fhc-calendar-day-page ">
<!-- lvModal for mobile view -->
<lv-modal v-if="selectedEvent" :event="selectedEvent" ref="lvmodal" />
<div class="row m-0">
<div class="col-12 col-xl-6 p-0">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="{'z-index':4,'grid-template-columns': 'repeat(' + day.length + ', 1fr)', 'grid-template-rows':1}" style="position:sticky; top:0; " >
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" class="flex-grow-1" :title="day.toLocaleString(undefined, {dateStyle:'short'})" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{day.toLocaleString(undefined, {weekday: size < 2 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{day.toLocaleString(undefined, [{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{dateStyle:'short'}][this.size])}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="" >
<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>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
<Transition>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</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}">
@@ -297,16 +304,16 @@ export default {
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle">
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" :class="{'selectedEvent':event.orig == selectedEvent}" class="mx-2 small rounded overflow-hidden " >
<!-- desktop version opens the lvMenu next to the calendar -->
<!-- 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">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<!-- mobile version opens the lvModal in a modal -->
<div class="d-block d-xl-none h-100" @click.prevent="showModal(event)">
<slot name="dayPage" :event="event" :day="day">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
</div>
@@ -318,18 +325,17 @@ export default {
</div>
</div>
<div class="d-none d-xl-block col-xl-6 p-0">
<div class="p-5 sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="z-index:0" class="p-5 sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: calc(var(--fhc-calendar-pane-height) - 100px); overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
<div class="w-100">
<lv-info :event="selectedEvent" />
</div>
<h3 >Lehrveranstaltungs Menu</h3>
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-show="lvMenu" :menu="lvMenu" />
<slot name="pageMobilContent" :lvMenu="lvMenu" >
<p>this is a slot placeholder</p>
</slot>
</template>
<template v-else-if="noEventsCondition">
<h3>Keine Lehrveranstaltungen</h3>
<slot name="pageMobilContentEmpty" >
<h3>This is an slot placeholder</h3>
</slot>
</template>
<template v-else>
<div class="p-4 d-flex w-100 justify-content-center align-items-center">
+61 -8
View File
@@ -31,6 +31,27 @@ export default {
'input',
],
computed: {
pageHeaderStyle(){
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.days.length + ', 1fr)',
'grid-template-rows': 1,
position: 'sticky',
top: 0,
}
},
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
},
hours(){
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
@@ -88,6 +109,36 @@ export default {
}
},
methods: {
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0,
}
},
dayGridStyle(day) {
return {
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
},
eventGridStyle(day, event) {
return {
'z-index': 1,
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
'grid-row-start': this.dateToMinutesOfDay(event.start),
'grid-row-end': this.dateToMinutesOfDay(event.end),
'background-color': event.orig.color,
'--test': this.dateToMinutesOfDay(event.end),
}
},
calcHourPosition(event) {
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
@@ -155,23 +206,25 @@ export default {
<div class="fhc-calendar-week-page">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="{'z-index':4,'grid-template-columns': 'repeat(' + days.length + ', 1fr)', 'grid-template-rows':1}" style="position:sticky; top:0; " >
<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="day.toLocaleString(undefined, {dateStyle:'short'})" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{day.toLocaleString(undefined, {weekday: size < 2 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{day.toLocaleString(undefined, [{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{dateStyle:'short'}][this.size])}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="" >
<div :id="'scroll'+hour+focusDate.d+week" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" style="pointer-events: none;" :style="{top:getAbsolutePositionForHour(hour),left:0,right:0,'z-index':0}"></div>
<div v-if="hourPosition" class="position-absolute border-top small" style="pointer-events: none; padding-left:3.5rem; margin-top:-1px;z-index:2;border-color:#00649C !important" :style="{top:hourPosition+'px',left:0,right:0}">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
<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>
<Transition>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div class="events">
<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="{'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)', 'grid-template-rows': 'repeat(' + (hours.length * 60 / smallestTimeFrame) + ', 1fr)'}">
<div :style="{'background-color':event.orig.color}" class="mx-2 small rounded overflow-hidden " @click.prevent="weekPageClick(event.orig, day)" :style="{'z-index':1,'grid-column-start': 1+(event.lane-1)*day.lanes/event.maxLane, 'grid-column-end': 1+event.lane*day.lanes/event.maxLane, 'grid-row-start': dateToMinutesOfDay(event.start), 'grid-row-end': dateToMinutesOfDay(event.end) ,'--test': dateToMinutesOfDay(event.end)}" v-for="event in day.events" :key="event">
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)" :style="eventGridStyle(day,event)" class="mx-2 small rounded overflow-hidden " >
<slot name="weekPage" :event="event" :day="day" :isSelected="event.orig == selectedEvent" >
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
+1 -1
View File
@@ -53,7 +53,7 @@ export default {
},
template: /*html*/ `
<!-- div that contains the content -->
<component :is="computeContentType" v-if="content" :content="content" />
<component :is="computeContentType" v-if="content" :content="content" :content_id="content_id" />
<p v-else>No content is available to display</p>
`,
};
+4 -4
View File
@@ -12,7 +12,7 @@ export default {
RaumContent,
},
props:{
contentID:{
content_id:{
type: Number
},
ort_kurzbz:{
@@ -34,8 +34,8 @@ export default {
// this method is always called when the modal is shown
modalShown: function(){
if(this.contentID){
this.$fhcApi.factory.cms.content(this.contentID).then(res =>{
if(this.content_id){
this.$fhcApi.factory.cms.content(this.content_id).then(res =>{
this.content = res.data.content;
this.type = res.data.type;
@@ -55,7 +55,7 @@ export default {
<span v-else>Raum Informationen</span>
</template>
<template #default>
<RaumContent v-if="content" :content="content"></RaumContent>
<RaumContent v-if="content" :content="content" :content_id="content_id"></RaumContent>
<div v-else>Der Content für diesen Raum konnte nicht geladen werden</div>
</template>
<template #footer>
@@ -5,6 +5,9 @@ export default {
type:String,
required:true,
},
content_id:{
type:Number,
}
},
mounted(){
// replaces the tablesorter with the tabulator
@@ -21,6 +24,34 @@ export default {
}
})
}
// tries to wrap the Raum titel with a link tag that redirects to the Reservierungen of that Raum
let title = document.getElementsByTagName("h1");
title = title.length ? title[0] : null;
if (title)
{
let room_name = title.innerText;
let room_name_reg_exp = new RegExp("\\w*\\s([a-zA-Z][0-9\\.]+)$");
let room_name_reg_exp_result = room_name.match(room_name_reg_exp);
if(room_name_reg_exp_result)
{
room_name = room_name_reg_exp_result[0];
room_name = room_name.replace(" ","_");
let link_element = document.createElement("a");
link_element.href = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/CisVue/Cms/getRoomInformation/" + room_name;
link_element.appendChild(title.cloneNode(true));
title.replaceWith(link_element);
}
else
{
console.error(`the regular expression did not match the room name: ${room_name}`);
}
}
else
{
console.error(`was not able to get the title of the raum_contentmittitel by searching for the first h1 element`);
}
},
template: /*html*/ `
<!-- div that contains the content -->
+3 -2
View File
@@ -37,7 +37,8 @@ export default {
template: /*html*/ `
<h2 >News</h2>
<hr/>
<pagination :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
<pagination :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
</pagination>
<div v-html="content"></div>
</pagination>`,
`,
};
+25 -56
View File
@@ -1,10 +1,12 @@
import CisMenuEntry from "./Menu/Entry.js";
import FhcSearchbar from "../searchbar/searchbar.js";
import CisSprachen from "./Sprachen.js"
export default {
components: {
CisMenuEntry,
FhcSearchbar
FhcSearchbar,
CisSprachen,
},
props: {
menu: Array,
@@ -49,34 +51,18 @@ export default {
}
},
methods: {
getLanguageButtonClass(lang) {
let classString = 'btn btn-level-2 rounded-0'
const langCookie = (function(lang) {
const cookieString = document.cookie;
const cookies = cookieString.split('; ');
for (let cookie of cookies) {
const [key, value] = cookie.split('=');
if (key === lang) {
return decodeURIComponent(value);
}
}
return null; // Return null if the cookie is not found
})('sprache');
if(langCookie === lang) classString += ' fhc-active';
return classString
},
toggleCollapsibles(target){
switch(target){
case 'settings':
this.navUserDropdown?.hide();
break;
case 'navUserDropdown':
this.$refs.searchbar?.settingsDropdown?.hide();
break;
checkSettingsVisibility: function (event) {
// hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.navUserDropdown.contains(event.target)) {
this.navUserDropdown.hide();
}
},
handleShowNavUser(){
document.addEventListener("click", this.checkSettingsVisibility);
},
handleHideNavUser(){
document.removeEventListener("click", this.checkSettingsVisibility);
},
makeParentContentActive(content_id, collection=this.entries, parent=null){
for(let entry of collection){
if(entry.content_id == content_id){
@@ -93,20 +79,6 @@ export default {
setActiveEntry(content_id){
this.activeEntry = content_id;
},
handleChangeLanguage(lang) {
this.$p.setLanguage(lang, this.$fhcApi)
const gerButton = this.$refs.ger
const engButton = this.$refs.eng
if(lang === 'German') {
gerButton.classList.add('fhc-active')
engButton.classList.remove('fhc-active')
} else if(lang === 'English') {
engButton.classList.add('fhc-active')
gerButton.classList.remove('fhc-active')
}
}
},
mounted(){
this.entries = this.menu;
@@ -116,14 +88,10 @@ export default {
});
},
template: /*html*/`
<!--<p>CISVUE HEADER</p>
<p>highest count : {{highestMatchingUrlCount}}</p>
<p>active entry content_id : {{activeEntry}}</p>
-->
<button id="nav-main-btn" class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<fhc-searchbar @showSettings="toggleCollapsibles" ref="searchbar" id="nav-search" class="fhc-searchbar w-100" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<a id="nav-logo" class="d-none d-lg-block" :href="rootUrl">
<img :src="logoUrl" alt="Logo">
</a>
@@ -131,27 +99,28 @@ export default {
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
<img :src="avatarUrl" class="avatar rounded-circle"/>
</button>
<ul ref="navUserDropdown" @[\`show.bs.collapse\`]="toggleCollapsibles('navUserDropdown')" id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<ul ref="navUserDropdown"
@[\`shown.bs.collapse\`]="handleShowNavUser"
@[\`hide.bs.collapse\`]="handleHideNavUser"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<li class="btn-level-2"><a class="btn btn-level-2 rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
<li class="fhc-languages btn-level-2" style="text-align: center;">
<div class="btn-group">
<a :class="getLanguageButtonClass('German')" ref="ger" href="#" @click="handleChangeLanguage('German')">Deutsch</a>
<a :class="getLanguageButtonClass('English')" ref="eng" href="#" @click="handleChangeLanguage('English')">English</a>
</div>
<li class="btn-level-2">
<cis-sprachen></cis-sprachen>
</li>
<li class="btn-level-2"><hr class="dropdown-divider p-0 "></li>
<li class="btn-level-2"><hr class="dropdown-divider m-0 "></li>
<li><a class="btn btn-level-2 rounded-0 d-block" :href="logoutUrl">Logout</a></li>
</ul>
</div>
<nav id="nav-main" class="offcanvas offcanvas-start bg-dark" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<div id="nav-main-sticky">
<div id="nav-main-toggle" class="position-static d-none d-lg-block bg-dark">
<button type="button" class="btn bg-dark text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target="#nav-main-menu" aria-expanded="true" aria-controls="nav-main-menu">
<button type="button" class="btn bg-dark text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
<i class="fa fa-arrow-circle-left"></i>
</button>
</div>
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="collapse collapse-horizontal show">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
</div>
-5
View File
@@ -196,11 +196,6 @@ export default {
this.checkActiveUrl(new URL(window.location.href));
},
template: /*html*/`
<!-- DEBUGGIING PRINTS
<p>entry content_id: {{JSON.stringify(entry.content_id,null,2)}}</p>
<p>entry menu: {{JSON.stringify(entry.menu_open,null,2)}}</p>
<p>highest count : {{urlCount}}</p>
-->
<div v-if="entry.template_kurzbz == 'include'">
INCLUDE
</div>
+15 -1
View File
@@ -11,6 +11,15 @@ export default {
}
},
computed: {
lektorenLinks: function(){
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
let lektorenLinks ={};
this.event.lektor.forEach((lektor)=>{
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
})
return lektorenLinks;
},
start_time: function () {
if (!this.event.start) return 'N/A';
if (!this.event.start instanceof Date) {
@@ -67,7 +76,12 @@ export default {
$p.t('lehre','lektor')+':'
:''
}}</th>
<td>{{event.lektor.map(lektor=>lektor.kurzbz).join("/")}}</td>
<td>
<div v-for="lektor in event.lektor" class="d-block">
<a v-if="lektorenLinks[lektor.kurzbz]" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
{{lektor.kurzbz}}
</div>
</td>
</tr>
<tr>
<th>{{
+11 -2
View File
@@ -17,6 +17,10 @@ export default {
type:String,
default:"title"
},
showMenu:{
type:Boolean,
default:true,
},
/*
* NOTE(chris):
* Hack to expose in "emits" declared events to $props which we use
@@ -39,6 +43,9 @@ export default {
methods:{
onModalShow: function()
{
// do not load the menu if the menu is not getting rendered
if(!this.showMenu) return;
if (this.event.type == 'lehreinheit') {
this.$fhcApi.factory.stundenplan.getLehreinheitStudiensemester(this.event.lehreinheit_id[0]).then(
res=>res.data
@@ -69,8 +76,10 @@ export default {
<template v-slot:default>
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
<lv-info :event="event"></lv-info>
<h3 >Lehrveranstaltungs Menu</h3>
<lv-menu :menu="menu"></lv-menu>
<template v-if="showMenu">
<h3 >Lehrveranstaltungs Menu</h3>
<lv-menu :menu="menu"></lv-menu>
</template>
</template>
<!-- optional footer -->
<template v-slot:footer >
@@ -1,6 +1,7 @@
import FhcCalendar from "../../Calendar/Calendar.js";
import CalendarDate from "../../../composables/CalendarDate.js";
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
import LvInfo from "../../../components/Cis/Mylv/LvInfo.js"
export default{
props:{
@@ -10,7 +11,9 @@ export default{
}
},
components: {
FhcCalendar
FhcCalendar,
LvModal,
LvInfo,
},
data() {
return {
@@ -40,6 +43,9 @@ export default{
},
},
methods:{
setSelectedEvent: function(event){
this.currentlySelectedEvent = event;
},
getLvID: function () {
this.lv_id = window.location.pathname
},
@@ -51,6 +57,7 @@ export default{
Vue.nextTick(() => {
this.$refs.lvmodal.show();
});
},
updateRange: function ({ start, end }) {
@@ -112,7 +119,8 @@ export default{
this.loadEvents();
},
template: /*html*/`
<fhc-calendar :initial-date="currentDay" @change:range="updateRange" :events="events" initial-mode="week" show-weeks @select:day="selectDay" v-model:minimized="minimized">
<lv-modal v-if="currentlySelectedEvent" :showMenu="false" :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 #monthPage="{event,day,isSelected}">
<span class="fhc-entry" :class="{'selectedEvent':isSelected}" style="color:white" :style="{'background-color': event.color}">
{{event.topic}}
@@ -125,8 +133,8 @@ export default{
<span>{{event?.orig.ort_kurzbz}}</span>
</div>
</template>
<template #dayPage="{event,day}">
<div type="button" class="fhc-entry border border-secondary border row h-100 justify-content-center align-items-center text-center">
<template #dayPage="{event,day,mobile}">
<div @click="mobile? showModal(event?.orig):null" type="button" class="fhc-entry border border-secondary border row h-100 justify-content-center align-items-center text-center">
<div class="col ">
<p>Lehrveranstaltung:</p>
<p class="m-0">{{event?.orig.topic}}</p>
@@ -141,6 +149,15 @@ export default{
</div>
</div>
</template>
<template #pageMobilContent>
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
<div class="w-100">
<lv-info :event="currentlySelectedEvent" />
</div>
</template>
<template #pageMobilContentEmpty >
<h3>Keine Raum Reservierung</h3>
</template>
</fhc-calendar>
`,
};
@@ -19,8 +19,16 @@ export default {
info: null,
}),
computed: {
lektorNames() {
return this.info.lektoren.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
lektorNamesLinks(){
let lektorenLinks = {};
this.info.lektoren.forEach(e => {
let name = ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim();
lektorenLinks[name] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${e.uid}`;
});
return lektorenLinks;
},
lektorNames(){
return this.info.lektoren.map((e)=>((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
},
lvLeitung() {
return this.info.lvLeitung && this.info.lvLeitung.length ? this.info.lvLeitung.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim()) : null;
@@ -69,16 +77,11 @@ export default {
this.info = infos[this.lehrveranstaltung_id];
} else {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Info/' + this.studien_semester + '/' + this.lehrveranstaltung_id).then(res => {
this.info = infos[this.lehrveranstaltung_id] = res.data.retval || [];
}).catch(() => this.info = {});
}
},
template: /*html*/`
<!-- debugging print
<p>{{JSON.stringify(info,null,2)}}</p>
-->
<h1>{{$p.t('lvinfo/lehrveranstaltungsinformationen')}}</h1>
<hr>
<div v-if="!info" class="text-center">
@@ -110,8 +113,8 @@ export default {
<th>{{$p.t('lehre/lehrbeauftragter')}}</th>
<td>
<ul v-if="lektorNames.length" class="list-unstyled mb-0">
<li v-for="name in lektorNames" :key="name">
<!-- TODO(chris): link? -->
<li v-for="name in new Set(lektorNames)" :key="name">
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
{{name}}
</li>
</ul>
@@ -125,7 +128,7 @@ export default {
<td>
<ul class="list-unstyled mb-0">
<li v-for="name in lvLeitung" :key="name">
<!-- TODO(chris): link? -->
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
{{name}}
</li>
</ul>
+31
View File
@@ -0,0 +1,31 @@
export default {
data(){
return {
allActiveLanguages: null,
}
},
methods:{
changeLanguage: function(lang){
if(this.allActiveLanguages.some(l => l === lang))
{
this.$p.setLanguage(lang, this.$fhcApi);
}
},
},
mounted(){
this.$fhcApi.factory.phrasen.getActiveDbLanguages()
.then(res => res.data)
.then(
(langs) => {
this.allActiveLanguages = langs;
}
);
},
template:/*html*/`
<div class="container">
<div class="row justify-content-center align-items-center flex-nowrap overflow-hidden">
<button v-for="lang in allActiveLanguages" @click.prevent="changeLanguage(lang)" class="col text-white fhc-entry btn text-center w-100" :selected="$p.user_language.value==lang?'':null">{{lang}}</button>
</div>
</div>
`,
};
@@ -22,6 +22,7 @@ export default {
provide() {
return {
editMode: Vue.computed(()=>this.editMode),
viewData: Vue.computed(()=>Vue.reactive(this.viewData)),
}
},
computed: {
+15 -7
View File
@@ -1,9 +1,11 @@
import BsModal from "../Bootstrap/Modal.js";
import CachedWidgetLoader from "../../composables/Dashboard/CachedWidgetLoader.js";
import HeightTransition from "../Tranistion/HeightTransition.js";
export default {
components: {
BsModal,
HeightTransition
},
data: () => ({
component: "",
@@ -118,7 +120,9 @@ export default {
</div>
<div v-else-if="!hidden || editMode" class="dashboard-item card overflow-hidden h-100" :class="arguments && arguments.className ? arguments.className : ''">
<div v-if="widget" class="card-header d-flex ps-0 pe-2">
<span v-if="editMode" drag-action="move" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
<Transition>
<span v-if="editMode" drag-action="move" class="col-auto mx-2 px-2 cursor-move"><i class="fa-solid fa-grip-vertical"></i></span>
</Transition>
<span class="col mx-2 px-2">{{ widget.setup.name }}</span>
<a v-if="widget.setup.cis4link" :href="getWidgetC4Link(widget)" class="ms-auto mb-2">
<i class="fa fa-arrow-up-right-from-square me-1"></i>
@@ -127,9 +131,11 @@ export default {
<a v-if="custom && editMode" class="col-auto px-1" href="#" @click.prevent="$emit('remove')">
<i class="fa-solid fa-trash"></i>
</a>
<div v-else-if="editMode" class="col-auto px-1 form-switch">
<input class="form-check-input ms-0" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
</div>
<Transition>
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
<input class="form-check-input ms-0" type="checkbox" role="switch" id="flexSwitchCheckChecked" :checked="!hidden" @input="$emit('remove', hidden)">
</div>
</Transition>
</div>
<div v-if="ready" class="card-body overflow-hidden" style="padding: 0px;">
<component :is="component" v-model:shared-data="sharedData" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
@@ -148,8 +154,10 @@ export default {
<button type="button" class="btn btn-primary" @click="changeConfig">Save changes</button>
</template>
</bs-modal>
<div v-if="editMode && isResizeable" class="card-footer d-flex justify-content-end p-0">
<span drag-action="resize" class="col-auto px-1 cursor-nw-resize"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
</div>
<height-transition>
<div v-if="editMode && isResizeable" class="card-footer d-flex justify-content-end p-0">
<span drag-action="resize" class="col-auto px-1 cursor-nw-resize"><i class="fa-solid fa-up-right-and-down-left-from-center mirror-x"></i></span>
</div>
</height-transition>
</div>`,
};
+118 -20
View File
@@ -1,14 +1,17 @@
import AbstractWidget from './Abstract';
import BsModal from '../Bootstrap/Modal';
const MAX_LOADED_NEWS = 10;
const MAX_LOADED_NEWS = 30;
export default {
name: "WidgetsNews",
components: {BsModal},
components: {
BsModal
},
data: () => ({
allNewsList: [],
singleNews: {},
selected: null
}),
mixins: [AbstractWidget],
computed: {
@@ -17,10 +20,12 @@ export default {
},
newsList() {
//Return news amount depending on widget width and size
let quantity = this.width;
// let quantity = this.width;
let quantity = MAX_LOADED_NEWS;
if (this.width === 1) {
quantity = this.height === 1 ? 4 : 10;
quantity = this.height === 1 ? 4 : MAX_LOADED_NEWS;
}
return this.allNewsList.slice(0, quantity);
@@ -31,12 +36,18 @@ export default {
"skin/images/fh_technikum_wien_illustration_klein.png"
);
},
activeNews() {
return this.allNewsList.find(news => news.minimized === false) ?? this.allNewsList[0] ?? null
}
},
created() {
this.$fhcApi.factory.cms
.news(MAX_LOADED_NEWS)
.then((res) => {
this.allNewsList = res.data;
this.allNewsList = Array.from(Object.values(res.data));
this.selected = this.allNewsList.length ? this.allNewsList[0] : null
})
.catch((err) => {
console.error("ERROR: ", err.response.data);
@@ -45,6 +56,63 @@ export default {
this.$emit("setConfig", false);
},
methods: {
setNext(){
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const nextIndex = thisIndex == (this.allNewsList.length - 1) ? 0 : thisIndex + 1
this.setSelected(this.allNewsList[nextIndex])
},
setPrev() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1
this.setSelected(this.allNewsList[prevIndex], 'prev')
},
getMenuItemClass(news) {
let classString = ''
if(this.selected && this.selected.news_id === news.news_id) {
classString += 'selected'
}
return classString
},
getDynClassCarouselItem(news, index) {
// sets classes prev/active/next for bootstrap carousel
let classString = ''
// return active class to news === selected OR very first news
if((this.selected.news_id === news.news_id) || (this.selected === null && index === 0)) {
classString = 'active';
} else { // set prev/next class for news
const selectedIndex = this.newsList.indexOf(this.selected)
const ownIndex = this.newsList.indexOf(news)
const isPrev = (ownIndex + 1) === selectedIndex || (ownIndex === this.newsList.length - 1 && selectedIndex === 0)
if(isPrev) {
classString += ' carousel-item-prev'
}
const isNext = (ownIndex - 1) === selectedIndex || (ownIndex === 0 && selectedIndex === this.newsList.length - 1)
if(isNext) {
classString += ' carousel-item-next'
}
}
return classString;
},
setSelected(news, direction = "next") {
if (this.selected && news && this.selected === news) return
const oldCard = document.getElementById('card-'+this.selected.news_id)
// TODO: to show animation of non neighbour item through menu reapply css classes
if(direction === 'next') {
// set nextCard .carousel-item-next.carousel-item-start
oldCard.classList.add('carousel-item-start')
} else {
// set prevCard .carousel-item-prev.carousel-item-end
oldCard.classList.add('carousel-item-end')
}
this.selected = news
},
contentURI: function (content_id) {
return (
FHC_JS_DATA_STORAGE_OBJECT.app_root +
@@ -67,23 +135,53 @@ export default {
},
template: /*html*/ `
<div class="widgets-news h-100" :style="getNewsWidgetStyle">
<div class="d-flex flex-column h-100 ">
<div class="d-flex flex-column h-100">
<div class="h-100" style="overflow-y: auto" v-if="width == 1">
<div v-for="(news, index) in newsList" :key="news.id" class="mt-2">
<div v-for="(news, index) in newsList" :key="news.news_id" class="mt-2">
<div v-if="index > 0 " class="fhc-seperator"></div>
<a :href="contentURI(news.content_id)" >{{ news.content_obj.betreff?news.content_obj.betreff:getDate(news.insertamum) }}</a><br>
<span class="small text-muted">{{ formatDateTime(news.insertamum) }}</span>
</div>
</div>
<div v-else-if="width > 1 && height === 1" class="h-100" :class="'row row-cols-' + width">
<div class="h-100" v-for="news in newsList" :key="news.id">
<div class="news-content h-100" :style="'--news-widget-height: '+height" ref="htmlContent" v-html="news.content_obj.content"></div>
</div>
</div>
<div v-else class="h-100" :class="'row row-cols-' + width + ' gx-2'">
<div class="h-100" v-for="news in newsList" :key="news.id">
<div class="news-content h-100" :style="'--news-widget-height: '+height" ref="htmlContent" v-html="news.content_obj.content"></div>
</div>
<a :href="contentURI(news.content_id)" >{{ news.content_obj.betreff?news.content_obj.betreff:getDate(news.insertamum) }}</a><br>
<span class="small text-muted">{{ formatDateTime(news.insertamum) }}</span>
</div>
</div>
<div v-else class="row h-100">
<!-- TODO: mobile responsiveness of this part-->
<div :class="'col-'+(width == 2? 6 : 4) + ' h-100 g-0'" style="overflow: auto;">
<template v-for="news in newsList" :key="'menu-'+news.news_id">
<div class="row fhc-news-menu-item" @click="setSelected(news)" :class="getMenuItemClass(news)" style="margin-right: 0px; margin-left: 0px;">
<div class="col-8 fhc-news-menu-item-betreff" style="overflow-y: hidden;"><p>{{news.content_obj.betreff ?? ''}}</p></div>
<span class="fhc-news-menu-item-date fw-bold"
>{{ news.datum ?? ''}}</span>
</div>
</template>
</div>
<div :class="'col-'+(width == 2? 6 : 8) + ' h-100'" style="padding-left: 0px; padding-right: 0px;" ref="htmlContent">
<div class="container h-100" style="padding: 0px;" ref="carocontainer">
<div id="carouselExample" style="height: 100%;" class="carousel slide fhc-carousel" data-bs-ride="carousel"
data-bs-interval="false"
ref="carocontrols">
<div class="carousel-indicators">
<button v-for="(news, index) in newsList" :id="'indicator-'+news_news_id" type="button" data-bs-target="#carouselExample" data-bs-slide-to="index"></button>
</div>
<div class="carousel-inner" style="height: 100%; max-width: 100%;">
<div v-for="(news, index) in newsList" class="carousel-item" :class="getDynClassCarouselItem(news, index)" style="overflow-y: auto; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content">
</div>
</div>
<!-- TODO: prev/next button styling && placement-->
<button @click="setPrev" style="z-index: 9999; color: black; opacity: 1;" data-bs-target="#carouselExample" class="carousel-control-prev" type="button">
<i class="fa fa-chevron-left"></i>
</button>
<button @click="setNext" style="z-index: 9999; color: black; opacity: 1;" data-bs-target="#carouselExample" class="carousel-control-next" type="button">
<i class="fa fa-chevron-right"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>`,
@@ -188,8 +188,13 @@ export default {
template: /*html*/`
<div class="dashboard-widget-stundenplan d-flex flex-column h-100">
<lv-modal v-if="selectedEvent" ref="lvmodal" :event="selectedEvent" />
<content-modal :contentID="roomInfoContentID" dialogClass="modal-lg" ref="contentModal"/>
<content-modal :content_id="roomInfoContentID" dialogClass="modal-lg" ref="contentModal"/>
<fhc-calendar @change:range="updateRange" :initial-date="currentDay" class="border-0" class-header="p-0" @select:day="selectDay" :widget="true" v-model:minimized="minimized" :events="events" no-week-view :show-weeks="false" >
<template #monthPage="{event,day,isSelected}">
<span class="fhc-entry" :class="{'selectedEvent':isSelected}" style="color:white" :style="{'background-color': event.color}">
{{event.topic}}
</span>
</template>
<template #minimizedPage >
<div class="flex-grow-1" style="overflow-y: auto; overflow-x: hidden">
<div v-if="events === null" class="d-flex h-100 justify-content-center align-items-center">
+25 -23
View File
@@ -356,29 +356,31 @@ export default {
@drop="dragEnd"
@mousemove="updateCursor"
@mouseleave="mouseLeave">
<grid-item
v-for="item in (mode == 0 && active? placedItems_withPlaceholders : placedItems)"
:key="item.id"
:item="item"
@start-move="startMove"
@start-resize="startResize"
@end-drag="dragCancel"
@drop-drag="dragEnd"
class="position-absolute"
:active="active"
:style="{
top: 'calc(' + item.y + ' * var(--fhc-dg-row-height))',
left: 'calc(' + item.x + ' * var(--fhc-dg-col-width))',
width: 'calc(' + item.w + ' * var(--fhc-dg-col-width))',
height: 'calc(' + item.h + ' * var(--fhc-dg-row-height))',
paddingTop: 'var(--fhc-dg-item-padding-top)',
paddingLeft: 'var(--fhc-dg-item-padding-horizontal)',
paddingRight: 'var(--fhc-dg-item-padding-horizontal)'
}">
<template v-slot="item">
<slot v-bind="item.data" v-bind="item" :x="item.x" :y="item.y" ></slot>
</template>
</grid-item>
<TransitionGroup tag="div">
<grid-item
v-for="item in (mode == 0 && active? placedItems_withPlaceholders : placedItems)"
:key="item.id"
:item="item"
@start-move="startMove"
@start-resize="startResize"
@end-drag="dragCancel"
@drop-drag="dragEnd"
class="position-absolute"
:active="active"
:style="{
top: 'calc(' + item.y + ' * var(--fhc-dg-row-height))',
left: 'calc(' + item.x + ' * var(--fhc-dg-col-width))',
width: 'calc(' + item.w + ' * var(--fhc-dg-col-width))',
height: 'calc(' + item.h + ' * var(--fhc-dg-row-height))',
paddingTop: 'var(--fhc-dg-item-padding-top)',
paddingLeft: 'var(--fhc-dg-item-padding-horizontal)',
paddingRight: 'var(--fhc-dg-item-padding-horizontal)'
}">
<template v-slot="item">
<slot v-bind="item.data" v-bind="item" :x="item.x" :y="item.y" ></slot>
</template>
</grid-item>
</TransitionGroup>
</div>`
}
+10 -6
View File
@@ -23,11 +23,15 @@ export default {
},
mounted() {},
template: /*html*/ `
<paginator v-model:rows="page_size" @page="(data)=>$emit('page',{...data, page:data.page+1})" :rows="page_size" :totalRecords="maxPageCount" :rowsPerPageOptions="[10, 20, 30]" ></paginator>
<slot>
Placeholder
</slot>
<!-- Desktop -->
<div class="d-none d-md-block">
<paginator v-model:rows="page_size" @page="(data)=>$emit('page',{...data, page:data.page+1})" :rows="page_size" :totalRecords="maxPageCount" :rowsPerPageOptions="[10, 20, 30]" >
</paginator>
</div>
<!-- Mobile -->
<div class="d-block d-md-none">
<paginator v-model:rows="page_size" @page="(data)=>$emit('page',{...data, page:data.page+1})" :rows="page_size" :totalRecords="maxPageCount" :rowsPerPageOptions="[10, 20, 30]" template="FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink RowsPerPageDropdown">
</paginator>
</div>
`,
};
@@ -0,0 +1,24 @@
export default {
data(){
return {
}
},
methods:{
onEnter(el,done){
el.style.height = '0';
el.style.height = el.scrollHeight + 'px';
},
onLeave(el,done){
el.style.height = el.scrollHeight + 'px';
el.style.height = '0';
}
},
template:
/*html*/`
<Transition name="height" @enter="onEnter" @leave="onLeave">
<slot>
</slot>
</Transition>
`,
};
+19 -5
View File
@@ -7,12 +7,10 @@ import prestudent from "./prestudent.js";
export default {
props: [ "searchoptions", "searchfunction" ],
emits: ['showSettings'],
data: function() {
return {
searchtimer: null,
hidetimer: null,
showsettings: false,
searchsettings: {
searchstr: '',
types: [],
@@ -65,8 +63,10 @@ export default {
</div>
</div>
<div id="searchSettings" ref="settings" @[\`show.bs.collapse\`]="$emit('showSettings','settings')"
class="top-100 end-0 searchbar_settings text-white collapse" tabindex="-1">
<div id="searchSettings" ref="settings"
@[\`shown.bs.collapse\`]="handleShowSettings"
@[\`hide.bs.collapse\`]="handleHideSettings"
class="top-100 end-0 searchbar_settings text-white collapse" tabindex="-1">
<div class="d-flex flex-column m-3" v-if="this.searchoptions.types.length > 0">
<span class="fw-light mb-2">Suche filtern nach:</span>
<template v-for="(type, index) in this.searchoptions.types" :key="type">
@@ -87,7 +87,6 @@ export default {
},
beforeMount: function() {
this.updateSearchOptions();
},
mounted(){
this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, {
@@ -102,6 +101,21 @@ export default {
}
},
methods: {
checkSettingsVisibility: function(event) {
// hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.settings.contains(event.target))
{
this.settingsDropdown.hide();
}
},
handleShowSettings: function() {
// adds the event listener checkSettingsVisibility only when the collapsible is shown
document.addEventListener("click", this.checkSettingsVisibility);
},
handleHideSettings: function () {
// removes the event listener checkSettingsVisibility when the collapsible is hidden
document.removeEventListener("click", this.checkSettingsVisibility);
},
updateSearchOptions: function() {
this.searchsettings.types = [];
for( const idx in this.searchoptions.types ) {
+5
View File
@@ -1,6 +1,7 @@
import FhcApi from './FhcApi.js';
const categories = Vue.reactive({});
const user_language = Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language);
const loadingModules = {};
let reload = false;
@@ -30,6 +31,9 @@ const phrasen = {
categories[row.category][row.phrase] = row.text
})
// update the reactive data that holds the current active user_language
user_language.value = language;
return res
})
},
@@ -81,6 +85,7 @@ export default {
t: phrasen.t,
loadCategory: cat => phrasen.loadCategory.call(app, cat),
setLanguage: phrasen.setLanguage,
user_language: user_language,
t_ref: phrasen.t_ref
};
app.provide('$p', app.config.globalProperties.$p);