Compare commits

..
25 changed files with 710 additions and 264 deletions
+15
View File
@@ -82,6 +82,21 @@ function generateCSSsInclude($CSSs)
}
}
/**
* Generates tags for the style sheets you want to include, the parameter could by a string or an array of strings
*/
function generatePrintCSS($path)
{
$cssLink = '<link rel="stylesheet" media="print" href="%s" />';
$ci =& get_instance();
$cachetoken = '?'.$ci->config->item('fhcomplete_build_version');
if (isset($path)) {
echo sprintf($cssLink, base_url($path).$cachetoken) . PHP_EOL;
}
}
/**
* Generates global JS-Object to pass parms to other javascripts
*/
@@ -57,7 +57,7 @@
<div class="row<?php if ($lockedbyother) echo ' alert-danger' ?>">
<div class="col-lg-8">
<h3 class="page-header">
Infocenter Details: <a target="_blank" title="Studentenverwaltung" href="<?php echo site_url('/studvw/stdsem/' . $studiensemester . '/person/' . $stammdaten->person_id) ?>"><?php echo $stammdaten->vorname.' '.$stammdaten->nachname ?> <i class="fa fa-external-link" style="font-size:small"></i></a>
Infocenter Details: <a target="_blank" title="Studentenverwaltung" href="<?php echo site_url('/Studentenverwaltung/' . $studiensemester . '/person/' . $stammdaten->person_id) ?>"><?php echo $stammdaten->vorname.' '.$stammdaten->nachname ?> <i class="fa fa-external-link" style="font-size:small"></i></a>
</h3>
</div>
<div class="col-lg-4">
@@ -7,6 +7,7 @@ $includesArray = array(
'fontawesome6' => true,
'axios027' => true,
'primevue3' => true,
'printCssPath' => 'public/css/Cis4/print.css',
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
], $customJSModules ?? []),
@@ -12,6 +12,8 @@
$customJSs = isset($customJSs) ? $customJSs : null;
$customJSModules = isset($customJSModules) ? $customJSModules : null;
$skipID = isset($skipID) ? $skipID : null;
$printCssPath = isset($printCssPath) ? $printCssPath : null;
?>
<!-- Header start -->
@@ -138,6 +140,9 @@
generateCSSsIncludeIfExtensionCssExists('tags.css');
}
// Print CSS
if ($printCssPath) generatePrintCSS($printCssPath);
$extapphelper = ExtendableAppsHelper::getInstance();
$extapphelper->init($customCSSs, $customJSs, $customJSModules);
+1 -1
View File
@@ -351,7 +351,7 @@ if (!$berechtigung->isBerechtigt('admin')
{
$worksheet->write($lines,3,$elem->vorname);
}
$worksheet->writeString($lines,4,$elem->semester.$elem->verband.$elem->gruppe);
$worksheet->write($lines,4,$elem->semester.$elem->verband.$elem->gruppe);
$worksheet->write($lines,5,trim($elem->matrikelnr),$format_highlight);
$worksheet->write($lines,6, $note, $format_highlightright);
+1 -1
View File
@@ -121,7 +121,7 @@ class vertrag extends basis_db
JOIN lehre.tbl_lehreinheit USING(lehreinheit_id)
WHERE
vertrag_id=tbl_vertrag.vertrag_id
AND studiensemester_kurzbz in (SELECT studiensemester_kurzbz FROM public.tbl_studiensemester WHERE start>".$this->db_add_param($datum).")
AND studiensemester_kurzbz in (SELECT studiensemester_kurzbz FROM public.tbl_studiensemester WHERE start>=".$this->db_add_param($datum).")
)
)";
}
+9
View File
@@ -59,12 +59,21 @@
padding: .25em .5em .25em .25em;
margin-inline-end: .25rem;
}
.rangePageContainer .calendar-event-default .event-time {
display: none !important;
}
.weekPageContainer .calendar-event-default .event-text {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
}
.rangePageContainer .calendar-event-default .event-text {
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: start;
}
.dayPageContainer .calendar-event-default,
.listPageContainer .calendar-event-default {
display: flex;
+51
View File
@@ -0,0 +1,51 @@
@page {
size: landscape;
}
* {
background-color: white !important;
color: black !important;
font-size: x-small !important;
overflow: visible !important;
}
.fhc-calendar-base-slider > div {
position: relative !important;
}
header,
#skiplink,
#cis-lvplan-personal-heading,
.cis-lvplan-personal hr,
.fhc-calendar-base-header,
.fhc-calendar-mode-range .grid-allday,
.fhc-calendar-mode-range #rangePresetSelector,
.fhc-calendar-base-grid-line-background {
display: none !important;
}
.fhc-calendar-mode-range .main-header-sunday,
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-sunday {
border-bottom-width: 0 !important;
}
.fhc-calendar-base-grid-line-sunday,
.fhc-calendar-mode-range .main-header-sunday {
break-after: page !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid {
grid-template-rows: auto repeat(var(--fhc-grid-displayed-axis-main-count), auto) !important;
gap: 0px !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid * {
gap: 0px !important;
}
.fhc-calendar-mode-range .main-header:not(.main-header-empty),
.fhc-calendar-mode-range .fhc-calendar-base-grid-line {
border-top: solid !important;
border-top-color: black !important;
border-top-width: 1px !important;
}
+7 -1
View File
@@ -104,11 +104,17 @@
.fhc-calendar-base-grid-line {
grid-gap: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
.fhc-calendar-mode-range .main-header-sunday,
.fhc-calendar-mode-range .all-day-events-sunday,
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-sunday {
border: solid;
border-width: 0 0 8px 0;
border-color: var(--fhc-calendar-gap);
}
.fhc-calendar-base-label-time {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: end;
}
/* Header */
.fhc-calendar-base-header .header-picker {
+1 -1
View File
@@ -190,7 +190,7 @@ const router = VueRouter.createRouter({
}
},
{
path: `/Cis/MyLvPlan/:mode?/:focus_date?`,
path: `/Cis/MyLvPlan/:mode?/:focus_date?/:range_length?`,
name: 'MyLvPlan',
component: MyLvPlan,
props(route) {
+16 -2
View File
@@ -93,6 +93,10 @@ export default {
type: Boolean,
default: undefined
},
btnRange: {
type: Boolean,
default: undefined
},
timeGrid: Array,
draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function],
@@ -163,8 +167,16 @@ export default {
return date.setLocale(this.locale);
},
set(value) {
this.internalDate = value;
this.$emit('update:date', value, this.cMode);
let date;
let rangeLength;
if (value instanceof luxon.DateTime) {
date = value;
} else {
date = value.date;
rangeLength = value.rangeLength;
}
this.internalDate = date;
this.$emit('update:date', date, this.cMode, rangeLength);
}
},
sMode() {
@@ -282,6 +294,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-range="!!modes['range'] && (btnRange || (showBtns && btnRange !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
>
<slot name="actions" />
@@ -293,6 +306,7 @@ export default {
@update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal"
@request-modal-close="hideEventModal"
@update:date="cDate = $event"
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
>
<template v-slot="slot"><slot v-bind="slot" /></template>
+121 -70
View File
@@ -53,7 +53,11 @@ export default {
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
snapToGrid: Boolean,
arePartHeadersRepeated: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -110,6 +114,19 @@ export default {
return this.axisParts.slice(0, -1);
return this.axisParts;
},
displayedAxisMain() {
if (!this.$props.arePartHeadersRepeated)
return this.$props.axisMain;
let displayedAxisMain = [];
this.$props.axisMain.forEach((part, index) => {
if (part.weekday === 1 && index > 0) {
displayedAxisMain.push(null);
}
displayedAxisMain.push(part);
});
return displayedAxisMain;
},
start() {
return this.axisPartsWithBreaks[0].start;
},
@@ -128,15 +145,17 @@ export default {
return ends;
},
axisMainBorders() {
return this.axisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
return this.displayedAxisMain.reduce(
(res, curr) => res.concat(curr ? [curr.plus(this.start), curr.plus(this.end)] : [null, null]),
[]
);
},
eventsAllDay() {
if (!this.allDayEvents)
return [];
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
let eventsAllDay = this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
return eventsAllDay;
},
eventsNormal() {
if (!this.allDayEvents)
@@ -153,7 +172,7 @@ export default {
return this.events.find(e => e.length);
},
styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
let cols = 'repeat(' + this.displayedAxisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
cols = this.events
@@ -181,7 +200,7 @@ export default {
});
this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex];
let day = this.displayedAxisMain[mainIndex];
events.forEach(event => {
if (!event.startsHere && !event.endsHere)
return;
@@ -211,15 +230,17 @@ export default {
},
methods: {
mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array());
const result = Array.from({length: this.displayedAxisMain.length}, () => Array());
target.forEach(event => {
const start = event.start || this.axisMainBorders[0].plus(-1);
const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
for (var i = 0; i < this.axisMain.length; i++) {
let laneStart = this.axisMainBorders[i * 2];
let laneEnd = this.axisMainBorders[i * 2 + 1];
this.displayedAxisMain.forEach((part, index) => {
if (!part) return;
let laneStart = this.axisMainBorders[index * 2];
let laneEnd = this.axisMainBorders[index * 2 + 1];
if (event.orig?.allDayEvent) {
laneStart = laneStart.startOf('day');
laneEnd = laneEnd.endOf('day');
@@ -227,13 +248,13 @@ export default {
if (start < laneEnd && end > laneStart) {
const startsHere = start >= laneStart;
const endsHere = end <= laneEnd;
result[i].push({
result[index].push({
...event,
startsHere,
endsHere
});
}
}
});
});
return result;
@@ -317,22 +338,31 @@ export default {
<div
class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%;overflow:auto"
:style="'grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
:style="'--fhc-grid-displayed-axis-main-count: ' + displayedAxisMain.length + ';grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
>
<div
class="grid-header"
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(date, index) in axisMain"
:key="index"
class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
</div>
<template v-for="(date, index) in displayedAxisMain" :key="index">
<div
v-if="date"
class="main-header"
:class="{
'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length,
'main-header-sunday': date.weekday === 7,
}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<slot name="main-header" v-bind="{ index, date }" />
</div>
<div
v-else
class="main-header main-header-empty"
:style="'grid-' + axisCol + ':' + (2+index)"
></div>
</template>
</div>
<div
v-if="allDayEvents"
@@ -340,26 +370,34 @@ export default {
style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(events, index) in eventsAllDay"
:key="index"
class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<grid-line-event
v-for="(event, i) in events"
:key="i"
:event="event"
<template v-for="(events, index) in eventsAllDay" :key="index">
<div
v-if="displayedAxisMain[index]"
class="all-day-events"
:class="{'all-day-events-sunday': displayedAxisMain[index].weekday === 7}"
:style="'grid-' + axisCol + ':' + (2+index)"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</grid-line-event>
</div>
<grid-line-event
v-for="(event, i) in events"
:key="i"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</grid-line-event>
</div>
<div
v-else
class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)"
></div>
</template>
</div>
<div
ref="scroller"
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
id="grid-main-scrollable"
style="display:grid;overflow:auto"
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
>
@@ -371,9 +409,9 @@ export default {
>
<div
v-for="(part, index) in axisPartsSave"
:key="index"
:key="index"2
class="part-header"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index + ';min-width:50px;'"
>
<slot name="part-header" v-bind="{ index, part }" />
</div>
@@ -389,43 +427,56 @@ export default {
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
v-for="(date, index) in displayedAxisMain"
:key="index"
>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-body" v-bind="{ index, part }" />
<template v-if="date">
<div
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
></div>
</div>
<grid-line
:start="date.plus(start)"
:end="date.plus(end)"
:date="date"
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
style="position:relative"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
<slot name="event" v-bind="slot" />
</template>
<template #dropzone>
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-body" v-bind="{ index, part }" />
<div
v-if="!snapToGrid && dragging"
v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}"
></div>
</template>
</grid-line>
</div>
<grid-line
:start="date.plus(start)"
:end="date.plus(end)"
:date="date"
:events="eventsNormal[index]"
:backgrounds="backgrounds[index]"
style="position:relative"
:style="'grid-' + axisRow + ':1/-1;grid-' + axisCol + ':' + (1+index)"
>
<template #event="slot">
<slot name="event" v-bind="slot" />
</template>
<template #dropzone>
<div
v-if="!snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1"
v-cal-dnd:dropzone="evt => getTimestampFromMouse(evt, date)"
></div>
</template>
</grid-line>
</template>
<template v-else>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-header-repeat part-body"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
<slot name="part-header-repeat" v-bind="{ index, part }" />
</div>
</template>
</template>
</div>
</div>
@@ -123,6 +123,9 @@ export default {
template: /* html */ `
<div
class="fhc-calendar-base-grid-line"
:class="{
'fhc-calendar-base-grid-line-sunday': $props.date.weekday === 7
}"
style="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'"
>
+17 -4
View File
@@ -24,7 +24,8 @@ export default {
btnMonth: Boolean,
btnWeek: Boolean,
btnDay: Boolean,
btnList: Boolean
btnList: Boolean,
btnRange: Boolean,
},
emits: [
"next",
@@ -35,7 +36,8 @@ export default {
],
data() {
return {
open: false
open: false,
semesterOptions: [],
};
},
methods: {
@@ -53,6 +55,15 @@ export default {
</div>
<div class="header-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
v-if="btnRange"
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'range'}"
@click="clickMode($event, 'range')"
>
<i class="fa fa-calendar"></i>
</button>
<button
v-if="btnMonth"
type="button"
@@ -95,9 +106,10 @@ export default {
<div class="header-picker">
<div class="btn-group" role="group">
<button
class="btn btn-outline-secondary border-0"
v-if="$props.mode !== 'range'"
@click="$emit('prev')"
:disabled="open"
class="btn btn-outline-secondary border-0"
>
<i class="fa fa-chevron-left"></i>
</button>
@@ -110,9 +122,10 @@ export default {
:list-length="modeOptions.length"
/>
<button
class="btn btn-outline-secondary border-0"
v-if="$props.mode !== 'range'"
@click="$emit('next')"
:disabled="open"
class="btn btn-outline-secondary border-0"
>
<i class="fa fa-chevron-right"></i>
</button>
@@ -5,10 +5,11 @@ export default {
components: {
VueDatePicker
},
inject: [
"locale",
"timezone"
],
inject: {
locale: "locale",
timezone: "timezone",
rangeLength: {default: 30}
},
props: {
date: {
type: luxon.DateTime,
@@ -21,7 +22,7 @@ export default {
listLength: {
type: Number,
default: 7
}
},
},
emits: [
"update:date"
@@ -38,6 +39,8 @@ export default {
return {month: this.convertedDate.month-1, year: this.convertedDate.year};
case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1];
case "range":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.rangeLength }).ts - 1];
case "week":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day":
@@ -56,6 +59,8 @@ export default {
return this.$p.t('calendar/year_kw', { year, week });
case "list":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
case "range":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.rangeLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL);
case "day":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
default:
@@ -64,11 +69,21 @@ export default {
},
weekStart() {
return luxon.Info.getStartOfWeek(this.date)%7;
}
},
rangeConfig() {
if (this.$props.mode === "list") {
return { autoRange: listLength - 1 };
} else if (this.$props.mode === "range") {
return true;
} else {
return false;
}
},
},
methods: {
update(value) {
let date;
let rangeLength;
switch (this.mode) {
case "month":
value.month++;
@@ -78,13 +93,18 @@ export default {
case "week":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
case "range":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
let endDate = luxon.DateTime.fromJSDate(value[1]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
rangeLength = Math.floor(endDate.diff(date, "days").toObject().days) + 1;
break;
case "day":
date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break;
default:
return; // Don't update if the value is invalid!
}
this.$emit("update:date", date);
this.$emit("update:date", { date, rangeLength });
},
weekNumbers(date) {
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
@@ -97,7 +117,7 @@ export default {
:format="() => title"
:month-picker="mode == 'month'"
:week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
:range="rangeConfig"
:text-input="mode == 'day'"
:week-start="weekStart"
:week-numbers="{ type: weekNumbers }"
@@ -26,7 +26,11 @@ export default {
}
return true;
}
}
},
alignItemsClassSuffix: {
type: String,
default: "end",
},
},
computed: {
sanitizedTimestamps() {
@@ -49,7 +53,7 @@ export default {
}
},
template: `
<div class="fhc-calendar-base-label-time">
<div class="fhc-calendar-base-label-time" :class="'align-items-' + $props.alignItemsClassSuffix">
<span v-if="start">{{ start }}</span>
<span v-if="end">-</span>
<span v-if="end">{{ end }}</span>
+33 -15
View File
@@ -9,13 +9,21 @@ import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js';
import ModeRange from './Mode/Range.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: ["isMobile"],
inject: {
isMobile: {
default: false,
},
rangeLength: {
default: 30,
}
},
props: {
date: {
type: [Date, String, Number, luxon.DateTime],
@@ -28,7 +36,11 @@ export default {
getPromiseFunc: {
type: Function,
required: true
}
},
shouldIncludeRangeMode: {
type: Boolean,
default: false,
},
},
provide() {
return {
@@ -68,31 +80,37 @@ export default {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
if (this.mode == 'Month')
if (this.mode == 'Month') {
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
} else if (this.mode == 'Range') {
return [];
} else {
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
},
modes() {
let modes = {
day: Vue.markRaw(ModeDay),
month: Vue.markRaw(ModeMonth),
};
if (this.isMobile) {
modes.list = Vue.markRaw(ModeList);
} else {
if (!this.isMobile) {
modes.week = Vue.markRaw(ModeWeek);
if (this.$props.shouldIncludeRangeMode) {
modes.range = Vue.markRaw(ModeRange);
}
} else {
modes.list = Vue.markRaw(ModeList);
}
return modes;
@@ -163,7 +181,7 @@ export default {
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@update:date="(newDate, newMode) => $emit('update:date', newDate, newMode)"
@update:date="(newDate, newMode, newRangeLength) => $emit('update:date', newDate, newMode, newRangeLength)"
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange"
>
+122
View File
@@ -0,0 +1,122 @@
import BaseSlider from "../Base/Slider.js";
import RangeView from "./Range/View.js";
import FormInput from "../../Form/Input.js";
export default {
name: "ModeRange",
components: {
BaseSlider,
RangeView,
FormInput,
},
inject: {
rangeLength: {
default: 30,
},
rangeViewPresets: {
default: {},
},
},
props: {
currentDate: {
type: luxon.DateTime,
required: true,
},
},
emits: ["update:currentDate", "update:range", "click", "requestModalOpen"],
data() {
return {
selectedRangePreset: null,
};
},
computed: {
range() {
let first = this.$props.currentDate;
let last = first.plus({ days: this.rangeLength });
return luxon.Interval.fromDateTimes(first, last);
},
},
watch: {
currentDate() {
this.$emit("update:range", this.range);
},
rangeLength() {
this.$emit("update:range", this.range);
},
selectedRangePreset() {
if (!this.selectedRangePreset) return;
const preset = this.rangeViewPresets.presets.find(
(preset) => preset.name === this.selectedRangePreset,
);
if (!preset) return;
this.$emit("update:date", {
date: preset.startDate,
rangeLength:
preset.endDate.diff(preset.startDate, "days").days + 1,
});
this.selectedRangePreset = null;
},
},
methods: {
viewAttrs() {
const day = this.$props.currentDate.startOf("day");
const rangeLength = parseInt(this.rangeLength);
return { ...this.$attrs, day, rangeLength };
},
handleClickDefaults(evt) {
switch (evt.detail.source) {
case "day":
// default: Set current-date
this.$emit("update:currentDate", {
date: evt.detail.value,
});
break;
case "event":
// default: Request Modal
this.$emit("requestModalOpen", { event: evt.detail.value });
break;
}
},
},
mounted() {
this.$emit("update:range", this.range);
},
template: /*html*/ `
<div
class="fhc-calendar-mode-range flex-grow-1 position-relative d-flex flex-column"
@cal-click-default.capture="handleClickDefaults"
>
<div
v-if="rangeViewPresets?.presets?.length"
id="rangePresetSelector"
class="w-100 d-flex flex-row gap-2 justify-content-center align-items-center py-2"
>
<span>{{ rangeViewPresets.label }}</span>
<form-input
name="rangePresetSelector"
type="select"
v-model="selectedRangePreset"
>
<option
v-for="preset in rangeViewPresets.presets"
:key="preset.name"
:value="preset.name"
>
{{ preset.name }}
</option>
</form-input>
</div>
<div class="flex-grow-1">
<base-slider ref="slider" v-slot="slot">
<range-view ref="view" v-bind="viewAttrs()">
<template v-slot="slot"><slot v-bind="slot" mode="range" /></template>
</range-view>
</base-slider>
</div>
</div>
`,
};
@@ -0,0 +1,88 @@
import CalendarGrid from '../../Base/Grid.js';
import LabelDay from '../../Base/Label/Day.js';
import LabelDow from '../../Base/Label/Dow.js';
import LabelTime from '../../Base/Label/Time.js';
export default {
name: "RangeView",
components: {
CalendarGrid,
LabelDay,
LabelDow,
LabelTime
},
inject: {
timeGrid: "timeGrid",
timezone: "timezone"
},
props: {
day: {
type: luxon.DateTime,
required: true
},
collapseEmptyDays: Boolean,
rangeLength: Number,
},
computed: {
axisMain() {
return Array.from({ length: this.$props.rangeLength }, (e, i) => this.$props.day.plus({ days: i }));
},
axisParts() {
if (this.timeGrid) {
// create {start, end} array
return this.timeGrid.map(tu => {
return {
start: luxon.Duration.fromISOTime(tu.start),
end: luxon.Duration.fromISOTime(tu.end)
};
});
} else {
// create 07:00-23:00
return Array.from({ length: 17 }, (e, i) => luxon.Duration.fromObject({ hours: i + 7 }));
}
}
},
methods: {
isToday(date) {
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
}
},
template: /* html */`
<div class="fhc-calendar-mode-range-view h-100">
<calendar-grid
ref="grid"
:axis-main="axisMain"
:axis-parts="axisParts"
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
:flip-axis="true"
:arePartHeadersRepeated="true"
all-day-events
>
<template #main-header="{ date }">
<div :class="{ today: isToday(date) }">
<label-dow
v-bind="{ date }"
@cal-click="evt => evt.detail.source = 'day'"
/>
<label-day
v-bind="{ date }"
/>
</div>
</template>
<template #part-header="{ part }">
<label-time v-bind="{ part }" :alignItemsClassSuffix="'center'" />
</template>
<template #part-header-repeat="{ part }">
<label-time v-bind="{ part }" :alignItemsClassSuffix="'center'" />
</template>
<template #event="slot">
<div v-if="slot.event.type == 'loading'" class="placeholder-glow h-100 opacity-50">
<span class="placeholder w-100 h-100" />
</div>
<slot v-else v-bind="slot" />
</template>
</calendar-grid>
</div>
`
}
+46 -6
View File
@@ -2,6 +2,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import ApiStudiensemester from '../../../api/factory/studiensemester.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
@@ -23,9 +24,23 @@ export default {
isMitarbeiter: false,
isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
semesterRangePresets: {
label: null,
presets: [],
},
};
},
inject: ["isMobile"],
provide() {
return {
rangeLength: Vue.computed(() => {
if (!this.$route.params.range_length) return 30;
else if (this.$route.params.range_length > 365) return 365;
else return this.$route.params.range_length;
}),
rangeViewPresets: Vue.computed(() => this.semesterRangePresets)
};
},
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
@@ -34,7 +49,11 @@ export default {
},
currentMode() {
let validModes = ["day", "month"];
validModes.push(this.isMobile ? "list" : "week");
if (!this.isMobile) {
validModes.push("week", "range");
} else {
validModes.push("list");
}
const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
@@ -94,10 +113,10 @@ export default {
},
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
handleChangeDate(day, newMode, rangeLength) {
return this.handleChangeMode(newMode, day, rangeLength);
},
handleChangeMode(newMode, day) {
handleChangeMode(newMode, day, range_length = null) {
const mode = newMode[0].toUpperCase() + newMode.slice(1)
const focus_date = day.toISODate();
@@ -105,7 +124,8 @@ export default {
name: "MyLvPlan",
params: {
mode,
focus_date
focus_date,
range_length,
}
});
},
@@ -134,13 +154,32 @@ export default {
this.isMitarbeiter = authInfo.isMitarbeiter;
this.isStudent = authInfo.isStudent;
},
async fetchSemesters() {
const semestersResponse = await this.$api.call(ApiStudiensemester.getAll());
if (semestersResponse.meta.status === "success") {
this.semesterRangePresets = {
label: "View specific semester",
presets: semestersResponse.data.map((semester) => {
let startDate = luxon.DateTime.fromISO(semester.start);
let endDate = luxon.DateTime.fromISO(semester.ende);
return {
startDate,
endDate,
name: semester.studiensemester_kurzbz,
description: semester.bezeichnung,
};
})
};
}
},
},
async created() {
await this.fetchAuthInfo();
await this.fetchSemesters();
},
template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100">
<h2>
<h2 id="cis-lvplan-personal-heading">
{{ $p.t('lehre/stundenplan') }}
<span v-if="studiensemester_kurzbz" class="ps-3">
{{ studiensemester_kurzbz }}
@@ -154,6 +193,7 @@ export default {
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
:shouldIncludeRangeMode="true"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
+15 -13
View File
@@ -28,7 +28,6 @@ export default {
urlMatchRankings:[],
navUserDropdown:null,
menuOpen:true,
openMenuHierarchy: [],
};
},
inject: ["isNarrow", "isMobile"],
@@ -36,7 +35,7 @@ export default {
return{
setActiveEntry: this.setActiveEntry,
addUrlCount: this.addUrlCount,
setOpenMenuHierarchy: this.setOpenMenuHierarchy,
makeParentContentActive: this.makeParentContentActive,
}
},
computed:{
@@ -83,6 +82,19 @@ export default {
handleHideNavUser(){
document.removeEventListener("click", this.checkSettingsVisibility);
},
makeParentContentActive(content_id, collection=this.entries, parent=null){
if(!collection) return;
if (typeof collection == 'object' && !Array.isArray(collection) && Object.entries(collection).length > 0) {
collection = Object.values(collection);
}
for(let entry of collection){
if(entry.content_id == content_id){
this.activeEntry = parent;
}
this.makeParentContentActive(content_id, entry.childs, entry.content_id);
}
},
addUrlCount(count){
this.urlMatchRankings.push(count);
},
@@ -90,9 +102,6 @@ export default {
setActiveEntry(content_id){
this.activeEntry = content_id;
},
setOpenMenuHierarchy(openMenuHierarchy) {
this.openMenuHierarchy = openMenuHierarchy;
},
},
created(){
this.fetchMenu();
@@ -183,14 +192,7 @@ export default {
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div class="flex-grow-1">
<cis-menu-entry
v-for="entry in entries"
:key="entry.content_id"
:highestMatchingUrlCount="highestMatchingUrlCount"
:activeContent="activeEntry"
:entry="entry"
:openMenuHierarchy="openMenuHierarchy"
/>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
</div>
</div>
</div>
+94 -90
View File
@@ -8,11 +8,6 @@ export default {
},
activeContent: [String, Number],
highestMatchingUrlCount: Number,
openMenuHierarchy: Array,
precedingMenuHierarchy: {
type: Array,
default: [],
},
},
data: () => {
return {
@@ -20,7 +15,7 @@ export default {
urlCount:0,
}
},
inject: ['setActiveEntry', 'setOpenMenuHierarchy', 'addUrlCount'],
inject: ['makeParentContentActive', 'setActiveEntry','addUrlCount'],
watch:{
highestMatchingUrlCount: function(newValue)
{
@@ -30,27 +25,49 @@ export default {
this.setActiveEntry(this.entry.content_id);
}
},
openMenuHierarchy() {
if (!this.hasChilds) return;
if (this.$props.openMenuHierarchy.length && this.$props.openMenuHierarchy[0] === this.$props.entry.content_id) {
this.$props.entry.menu_open = true;
} else {
this.$props.entry.menu_open = false;
activeContent: function(newValue){
if(newValue == this.entry.content_id){
// wenn der Menupunkt nicht bereits offen ist
if (!this.entry.menu_open){
this.entry.menu_open = true;
}
}else{
if (this.searchRecursiveChild(this.entry, 'content_id',newValue)) {
this.entry.menu_open = true;
} else {
this.entry.menu_open = false;
}
}
},
'entry.menu_open': function (isMenuOpen) {
if (!this.collapse)
return;
if (isMenuOpen) {
this.collapse.show();
} else {
this.collapse.hide();
'entry.menu_open': function (newValue,oldValue) {
if (newValue)
{
// only invokes .show if this.collapse is not null
this.collapse && this.collapse.show();
}
else
{
// only invokes .hide if this.collapse is not null
this.collapse && this.collapse.hide();
if (this.activeContent == this.entry.content_id)
{
this.makeParentContentActive(this.entry.content_id);
}
}
},
},
computed: {
active: function () {
if (this.entry.menu_open){
return true;
}
else if (this.activeContent) {
return this.activeContent == this.entry.content_id;
} else {
return false;
}
},
hasFullLink() {
return this.entry.url.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
},
@@ -72,34 +89,25 @@ export default {
},
hasChilds() {
return this.entry.childs && this.entry.childs.length !== 0;
},
menuHierarchy() {
return [...this.$props.precedingMenuHierarchy, this.$props.entry.content_id];
},
menuNodeHref() {
if (this.hasChilds) {
return this.$props.entry.menu_open && this.hasFullLink
? this.$props.entry.url
: null;
} else {
return this.$props.entry.url;
}
},
}
},
methods: {
getUrlMatchPoints(url,link){
let splitted_link = link.split('/');
let splitted_url = url.href.split('/');
let count = 0;
for (let part_url of splitted_url) {
for (let part_link of splitted_link) {
if (part_url === part_link) {
for(let part_url of splitted_url)
{
for (let part_link of splitted_link)
{
if(part_url == part_link)
{
count++;
}
}
}
this.urlCount = count;
this.addUrlCount(count);
},
@@ -111,11 +119,14 @@ export default {
let url_hash = url.hash;
url_hash = url_hash.replace(url_hash_spaceSymbol_regex, " ").replace(url_hash_sharpSymbol_regex,"");
// if the url hash contains the titel of the menu
// or if the url equals the link of a menu
// then set the menu active
if (url_hash == this.entry.titel || url.href == this.entry.url) {
this.setActiveEntry(this.entry.content_id);
this.setOpenMenuHierarchy(this.menuHierarchy);
}
},
// searches the childs of an entry recursively based on the value of a property
searchRecursiveChild(entry,property,value){
if (typeof entry.childs == 'object' && !Array.isArray(entry.childs) && Object.entries(entry.childs).length > 0){
entry.childs = Object.values(entry.childs);
@@ -132,23 +143,16 @@ export default {
}
return false;
},
toggleCollapse() {
if (!this.$props.entry.menu_open) {
this.setOpenMenuHierarchy(this.menuHierarchy);
} else {
this.setOpenMenuHierarchy(this.precedingMenuHierarchy);
toggleCollapse(evt) {
if (this.active)
{
this.makeParentContentActive(this.entry.content_id);
}
},
handleClickOnMenuNode() {
if (this.hasFullLink) {
this.setActiveEntry(this.$props.entry.content_id);
if (this.hasChilds) {
this.setOpenMenuHierarchy(this.menuHierarchy);
}
} else if (this.hasChilds) {
this.toggleCollapse();
else
{
this.setActiveEntry(this.entry.content_id);
}
},
}
},
mounted() {
if (this.$refs.children) {
@@ -164,41 +168,41 @@ export default {
INCLUDE
</div>
<template v-else>
<div class="btn-group w-100">
<a
@click="handleClickOnMenuNode($event)"
:href="menuNodeHref"
:target="target"
class="btn btn-default rounded-0 text-start"
:class="{
['btn-level-' + level]: true,
'fw-bold': $props.activeContent === $props.entry.content_id
}"
:style="'padding-left: calc(var(--bs-btn-padding-x) * ' + $props.level + ');'"
>
{{ entry.titel }}
</a>
<button
v-if="hasChilds"
@click.prevent="toggleCollapse()"
:aria-expanded="entry.menu_open"
class="btn btn-default rounded-0 dropdown-toggle dropdown-toggle-split flex-grow-0"
:class="{ collapsed: !entry.menu_open }"
>
<span class="visually-hidden">Toggle Dropdown</span>
</button>
</div>
<ul v-if="hasChilds" ref="children" class="nav w-100 collapse" >
<cis-menu-entry
v-for="child in entry.childs"
:key="child"
:highestMatchingUrlCount="highestMatchingUrlCount"
:activeContent="activeContent"
:entry="child"
:level="level + 1"
:openMenuHierarchy="$props.openMenuHierarchy.slice(1)"
:precedingMenuHierarchy="menuHierarchy"
/>
</ul>
<template v-if="hasChilds">
<div class="btn-group w-100">
<a :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
['btn-level-' + level]: true,
'fw-bold':active
}">
{{ entry.titel }}
</a>
<button @click.prevent="toggleCollapse" :aria-expanded="entry.menu_open"
:class="{
'btn btn-default rounded-0 dropdown-toggle dropdown-toggle-split flex-grow-0': true,
collapsed: !entry.menu_open
}">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
</div>
<ul ref="children"
class="nav w-100 collapse">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
</ul>
</template>
<a v-else
:href="entry.url"
:target="target"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
['btn-level-' + level]: true,
'fw-bold':active
}"
@mouseup="setActiveEntry(entry.content_id)">
{{ entry.titel }}
</a>
</template>`
};
@@ -255,7 +255,7 @@ export default {
deleteButton.className = 'btn btn-outline-secondary';
const icon = document.createElement('i');
icon.className = 'fa fa-trash';
icon.title = this.$p.t('lehre/info_deleteMarkEntry');
icon.title = this.$p.t('ui/loeschen');
deleteButton.append(icon);
deleteButton.addEventListener('click', evt => {
evt.stopPropagation();
@@ -822,33 +822,18 @@ export default {
@headerFilterOn="handleHeaderFilter"
>
<template #actions>
<!-- <div>
<button
class="btn btn-outline-success sm mb-1"
:title="'Export ' + selectedPrestudents.length + ' prestudent(s) to Excel'"
>
<i class="fas fa-file-excel fa-xl"></i>
</button>
</div> -->
<template v-if="filter.length || headerFilterActive">
<div class="d-flex justify-content-center align-items-center gap-2 ps-4 position-absolute start-50 translate-middle-x">
<p class="text-danger mb-0">
<strong>{{$p.t('filter','filterActive')}}</strong>
</p>
<button
class="btn btn-outline-danger sm"
:title="$p.t('filter/filterDelete')"
@click="resetFilter"
>
<span class="fa-solid fa-filter-circle-xmark"></span>
</button>
</div>
</template>
</template>
<!--
<template #actions>
<div>
<button
class="btn btn-outline-success sm mb-1"
:title="'Export ' + selectedPrestudents.length + ' prestudent(s) to Excel'"
>
<i class="fas fa-file-excel fa-xl"></i>
</button>
</div>
</template>
-->
<template #additional>
<div class="pe-1">
@@ -888,7 +873,22 @@ export default {
<i class="fa-solid fa-refresh pe-1"></i> {{selected.length}}
</button>
</div>
</template>
<template v-if="filter.length || headerFilterActive">
<div class="d-flex justify-content-center align-items-center gap-2 ps-4 position-absolute start-50 translate-middle-x">
<p class="text-danger mb-0">
<strong>{{$p.t('filter','filterActive')}}</strong>
</p>
<button
class="btn btn-outline-danger sm"
:title="$p.t('filter/filterDelete')"
@click="resetFilter"
>
<span class="fa-solid fa-filter-circle-xmark"></span>
</button>
</div>
</template>
<template #filter>
-20
View File
@@ -62411,26 +62411,6 @@ I have been informed that I am under no obligation to consent to the transmissio
)
)
),
array(
'app' => 'core',
'category' => 'lehre',
'phrase' => 'info_deleteMarkEntry',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Noteneintrag entfernen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Remove grade entry',
'description' => '',
'insertvon' => 'system'
)
)
),
);