Compare commits

...
17 changed files with 599 additions and 75 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
*/
@@ -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);
+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;
+95
View File
@@ -0,0 +1,95 @@
@page {
size: portrait;
}
* {
background-color: white !important;
color: black !important;
font-size: x-small !important;
overflow: visible !important;
scrollbar-color: rgba(0,0,0,0) rgba(0,0,0,0) !important;
}
body
{
padding-top: 0 !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 .fhc-calendar-base-grid {
break-after: page !important;
border-bottom-style: none !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid {
grid-template-rows: auto repeat(var(--fhc-grid-axis-main-count), auto) !important;
gap: 0px !important;
border: solid !important;
border-color: black !important;
border-width: 1px !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid * {
gap: 0px !important;
}
.fhc-calendar-mode-range .part-header,
.fhc-calendar-mode-range .part-body,
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event {
break-inside: avoid;
}
/* .fhc-calendar-mode-range .fhc-calendar-base-grid-line, */
.fhc-calendar-mode-range .main-header {
border-left: solid !important;
border-left-color: black !important;
border-left-width: 1px !important;
}
.fhc-calendar-mode-range .part-header {
border-top: solid !important;
border-top-color: black !important;
border-top-width: 1px !important;
}
.fhc-calendar-mode-range .part-body {
border-top: solid !important;
border-top-color: black !important;
border-top-width: 1px !important;
border-left: solid !important;
border-left-color: black !important;
border-left-width: 1px !important;
z-index: 9998;
}
.fhc-calendar-mode-range .part-header-followed-by-break,
.fhc-calendar-mode-range .part-body-followed-by-break {
border-bottom: solid !important;
border-bottom-color: black !important;
border-bottom-width: 1px !important;
}
.fhc-calendar-mode-range .main-header {
text-align: center !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid-line * {
background-color: rgba(0,0,0,0) !important;
}
.fhc-calendar-mode-range .fhc-calendar-base-grid-line-event {
z-index: 9999 !important;
}
+13 -1
View File
@@ -101,14 +101,26 @@
.fhc-calendar-base-grid .grid-main .part-body {
background-color: var(--fhc-calendar-bg-body);
}
.fhc-calendar-mode-range .fhc-calendar-base-grid {
border-bottom-style: solid;
border-bottom-width: 5px;
border-bottom-color: var(--fhc-calendar-gap);
}
.fhc-calendar-base-grid-line {
grid-gap: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px));
}
/* todo: remove commented lines below */
/* .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>
+75 -36
View File
@@ -53,7 +53,15 @@ export default {
flipAxis: Boolean,
allDayEvents: Boolean,
axisMainCollapsible: Boolean,
snapToGrid: Boolean
snapToGrid: Boolean,
overwrittenEvents: {
type: Array | null,
default: null,
},
shouldMatchParentHeight: {
type: Boolean,
default: true,
},
},
data() {
return {
@@ -64,6 +72,9 @@ export default {
};
},
computed: {
parsedEvents() {
return this.$props.overwrittenEvents ?? this.originalEvents;
},
axisRow() {
return this.flipAxis ? 'column' : 'row';
},
@@ -128,23 +139,25 @@ export default {
return ends;
},
axisMainBorders() {
return this.axisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
return this.$props.axisMain.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.parsedEvents.filter(event => event.orig.allDayEvent));
return eventsAllDay;
},
eventsNormal() {
if (!this.allDayEvents)
return this.events;
return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
return this.mapIntoMainAxis(this.parsedEvents.filter(event => !event.orig.allDayEvent));
},
events() {
return this.mapIntoMainAxis(this.originalEvents);
return this.mapIntoMainAxis(this.parsedEvents);
},
backgrounds() {
return this.mapIntoMainAxis(this.originalBackgrounds);
@@ -153,7 +166,7 @@ export default {
return this.events.find(e => e.length);
},
styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
let cols = 'repeat(' + this.$props.axisMain.length + ', 1fr)';
if (this.axisMainCollapsible) {
if (this.hasValidEvents)
cols = this.events
@@ -181,7 +194,7 @@ export default {
});
this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex];
let day = this.$props.axisMain[mainIndex];
events.forEach(event => {
if (!event.startsHere && !event.endsHere)
return;
@@ -211,15 +224,15 @@ export default {
},
methods: {
mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array());
const result = Array.from({length: this.$props.axisMain.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.$props.axisMain.forEach((part, index) => {
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 +240,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;
@@ -308,7 +321,19 @@ export default {
} else {
this.$refs.scroller.scrollTo(0, 0);
}
}
},
isFollowedByBreak(index) {
if (index === this.axisPartsSave.length - 1) return false;
const targetPartEnd = this.axisPartsSave[index].end;
const nextPartStart = this.axisPartsSave[index + 1].start;
if (!targetPartEnd || !nextPartStart) return false;
return !(
targetPartEnd.hours === nextPartStart.hours &&
targetPartEnd.minutes === nextPartStart.minutes
);
},
},
beforeUnmount() {
this.disableAutoScroll();
@@ -316,8 +341,9 @@ export default {
template: /* html */`
<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"
:class="{'h-100': $props.shouldMatchParentHeight}"
style="display:grid;width:100%;overflow:auto"
:style="'--fhc-grid-axis-main-count: ' + $props.axisMain.length + ';grid-template-' + axisRow + 's:auto' + (allDayEvents ? ' auto ' : ' ') + '1fr;grid-template-' + axisCol + 's:auto ' + styleGridCols"
>
<div
class="grid-header"
@@ -325,10 +351,13 @@ export default {
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
>
<div
v-for="(date, index) in axisMain"
v-for="(date, index) in $props.axisMain"
:key="index"
class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}"
: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 }" />
@@ -340,26 +369,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="$props.axisMain[index]"
class="all-day-events"
:class="{'all-day-events-sunday': $props.axisMain[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'"
>
@@ -373,7 +410,8 @@ export default {
v-for="(part, index) in axisPartsSave"
:key="index"
class="part-header"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
:class="{'part-header-followed-by-break': isFollowedByBreak(index)}"
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index + ';min-width:50px;'"
>
<slot name="part-header" v-bind="{ index, part }" />
</div>
@@ -389,13 +427,14 @@ export default {
@dragover="dropAllowed ? $event.preventDefault() : null"
>
<template
v-for="(date, index) in axisMain"
v-for="(date, index) in $props.axisMain"
:key="index"
>
<div
v-for="(part, i) in axisPartsSave"
:key="i"
class="part-body"
:class="{'part-body-followed-by-break': isFollowedByBreak(i)}"
style="position:relative"
:style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
>
@@ -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,114 @@
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",
events: "events",
},
props: {
day: {
type: luxon.DateTime,
required: true,
},
collapseEmptyDays: Boolean,
rangeLength: Number,
},
computed: {
axisMainGroupedByWeek() {
const focusDayStartOfWeek = this.$props.day.startOf("week");
const prefixedDaysCount = this.$props.day.weekday - 1;
let rangeLength = this.$props.rangeLength + prefixedDaysCount;
rangeLength = rangeLength + (7 - (rangeLength % 7));
let axisMain = Array.from({ length: rangeLength }, (e, i) =>
focusDayStartOfWeek.plus({ days: i }),
);
return Object.groupBy(axisMain, (day) => {
return day.startOf("week").toISO().slice(0, 10);
});
},
weeks() {
return Object.keys(this.axisMainGroupedByWeek).sort();
},
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 }),
);
}
},
eventsGroupedByWeek() {
return Object.groupBy(
this.events.filter((event) => !event.orig.allDayEvent),
(event) => {
return event.start.startOf("week").toISO().slice(0, 10);
},
);
},
},
methods: {
isToday(date) {
return date.hasSame(
luxon.DateTime.now().setZone(this.timezone),
"day",
);
},
},
template: /* html */ `
<div class="fhc-calendar-mode-range-view h-100 overflow-y-scroll">
<calendar-grid
v-for="week in weeks"
:key="week"
:axis-main="axisMainGroupedByWeek[week]"
:axis-parts="axisParts"
:axis-main-collapsible="collapseEmptyDays"
:snap-to-grid="!!timeGrid"
:overwrittenEvents="
week in eventsGroupedByWeek ? eventsGroupedByWeek[week] : []
"
:shouldMatchParentHeight="false"
>
<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 #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"