Compare commits

...
17 changed files with 569 additions and 109 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 * Generates global JS-Object to pass parms to other javascripts
*/ */
@@ -7,6 +7,7 @@ $includesArray = array(
'fontawesome6' => true, 'fontawesome6' => true,
'axios027' => true, 'axios027' => true,
'primevue3' => true, 'primevue3' => true,
'printCssPath' => 'public/css/Cis4/print.css',
'customJSModules' => array_merge([ 'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js' 'public/js/apps/Cis/Menu.js'
], $customJSModules ?? []), ], $customJSModules ?? []),
@@ -12,6 +12,8 @@
$customJSs = isset($customJSs) ? $customJSs : null; $customJSs = isset($customJSs) ? $customJSs : null;
$customJSModules = isset($customJSModules) ? $customJSModules : null; $customJSModules = isset($customJSModules) ? $customJSModules : null;
$skipID = isset($skipID) ? $skipID : null; $skipID = isset($skipID) ? $skipID : null;
$printCssPath = isset($printCssPath) ? $printCssPath : null;
?> ?>
<!-- Header start --> <!-- Header start -->
@@ -138,6 +140,9 @@
generateCSSsIncludeIfExtensionCssExists('tags.css'); generateCSSsIncludeIfExtensionCssExists('tags.css');
} }
// Print CSS
if ($printCssPath) generatePrintCSS($printCssPath);
$extapphelper = ExtendableAppsHelper::getInstance(); $extapphelper = ExtendableAppsHelper::getInstance();
$extapphelper->init($customCSSs, $customJSs, $customJSModules); $extapphelper->init($customCSSs, $customJSs, $customJSModules);
+9
View File
@@ -59,12 +59,21 @@
padding: .25em .5em .25em .25em; padding: .25em .5em .25em .25em;
margin-inline-end: .25rem; margin-inline-end: .25rem;
} }
.rangePageContainer .calendar-event-default .event-time {
display: none !important;
}
.weekPageContainer .calendar-event-default .event-text { .weekPageContainer .calendar-event-default .event-text {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-evenly; justify-content: space-evenly;
align-items: center; 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, .dayPageContainer .calendar-event-default,
.listPageContainer .calendar-event-default { .listPageContainer .calendar-event-default {
display: flex; 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 { .fhc-calendar-base-grid-line {
grid-gap: var(--fhc-calendar-gap-events, var(--fhc-calendar-gap, 1px)); 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 { .fhc-calendar-base-label-time {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
align-items: end;
} }
/* Header */ /* Header */
.fhc-calendar-base-header .header-picker { .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', name: 'MyLvPlan',
component: MyLvPlan, component: MyLvPlan,
props(route) { props(route) {
+16 -2
View File
@@ -93,6 +93,10 @@ export default {
type: Boolean, type: Boolean,
default: undefined default: undefined
}, },
btnRange: {
type: Boolean,
default: undefined
},
timeGrid: Array, timeGrid: Array,
draggableEvents: [Boolean, Array, Function], draggableEvents: [Boolean, Array, Function],
dropableEvents: [Boolean, Array, Function], dropableEvents: [Boolean, Array, Function],
@@ -163,8 +167,16 @@ export default {
return date.setLocale(this.locale); return date.setLocale(this.locale);
}, },
set(value) { set(value) {
this.internalDate = value; let date;
this.$emit('update:date', value, this.cMode); 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() { sMode() {
@@ -282,6 +294,7 @@ export default {
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))" :btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))" :btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))" :btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
:btn-range="!!modes['range'] && (btnRange || (showBtns && btnRange !== false))"
:mode-options="modeOptions ? modeOptions[cMode] : undefined" :mode-options="modeOptions ? modeOptions[cMode] : undefined"
> >
<slot name="actions" /> <slot name="actions" />
@@ -293,6 +306,7 @@ export default {
@update:range="$emit('update:range', $event)" @update:range="$emit('update:range', $event)"
@request-modal-open="showEventModal" @request-modal-open="showEventModal"
@request-modal-close="hideEventModal" @request-modal-close="hideEventModal"
@update:date="cDate = $event"
v-bind="modeOptions ? modeOptions[cMode] : null || {}" v-bind="modeOptions ? modeOptions[cMode] : null || {}"
> >
<template v-slot="slot"><slot v-bind="slot" /></template> <template v-slot="slot"><slot v-bind="slot" /></template>
+121 -70
View File
@@ -53,7 +53,11 @@ export default {
flipAxis: Boolean, flipAxis: Boolean,
allDayEvents: Boolean, allDayEvents: Boolean,
axisMainCollapsible: Boolean, axisMainCollapsible: Boolean,
snapToGrid: Boolean snapToGrid: Boolean,
arePartHeadersRepeated: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -110,6 +114,19 @@ export default {
return this.axisParts.slice(0, -1); return this.axisParts.slice(0, -1);
return this.axisParts; 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() { start() {
return this.axisPartsWithBreaks[0].start; return this.axisPartsWithBreaks[0].start;
}, },
@@ -128,15 +145,17 @@ export default {
return ends; return ends;
}, },
axisMainBorders() { axisMainBorders() {
return this.axisMain.reduce( return this.displayedAxisMain.reduce(
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]), (res, curr) => res.concat(curr ? [curr.plus(this.start), curr.plus(this.end)] : [null, null]),
[] []
); );
}, },
eventsAllDay() { eventsAllDay() {
if (!this.allDayEvents) if (!this.allDayEvents)
return []; 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() { eventsNormal() {
if (!this.allDayEvents) if (!this.allDayEvents)
@@ -153,7 +172,7 @@ export default {
return this.events.find(e => e.length); return this.events.find(e => e.length);
}, },
styleGridCols() { styleGridCols() {
let cols = 'repeat(' + this.axisMain.length + ', 1fr)'; let cols = 'repeat(' + this.displayedAxisMain.length + ', 1fr)';
if (this.axisMainCollapsible) { if (this.axisMainCollapsible) {
if (this.hasValidEvents) if (this.hasValidEvents)
cols = this.events cols = this.events
@@ -181,7 +200,7 @@ export default {
}); });
this.eventsNormal.forEach((events, mainIndex) => { this.eventsNormal.forEach((events, mainIndex) => {
let day = this.axisMain[mainIndex]; let day = this.displayedAxisMain[mainIndex];
events.forEach(event => { events.forEach(event => {
if (!event.startsHere && !event.endsHere) if (!event.startsHere && !event.endsHere)
return; return;
@@ -211,15 +230,17 @@ export default {
}, },
methods: { methods: {
mapIntoMainAxis(target) { mapIntoMainAxis(target) {
const result = Array.from({length: this.axisMain.length}, () => Array()); const result = Array.from({length: this.displayedAxisMain.length}, () => Array());
target.forEach(event => { target.forEach(event => {
const start = event.start || this.axisMainBorders[0].plus(-1); const start = event.start || this.axisMainBorders[0].plus(-1);
const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1); const end = event.end || this.axisMainBorders[this.axisMainBorders.length - 1].plus(1);
for (var i = 0; i < this.axisMain.length; i++) { this.displayedAxisMain.forEach((part, index) => {
let laneStart = this.axisMainBorders[i * 2]; if (!part) return;
let laneEnd = this.axisMainBorders[i * 2 + 1];
let laneStart = this.axisMainBorders[index * 2];
let laneEnd = this.axisMainBorders[index * 2 + 1];
if (event.orig?.allDayEvent) { if (event.orig?.allDayEvent) {
laneStart = laneStart.startOf('day'); laneStart = laneStart.startOf('day');
laneEnd = laneEnd.endOf('day'); laneEnd = laneEnd.endOf('day');
@@ -227,13 +248,13 @@ export default {
if (start < laneEnd && end > laneStart) { if (start < laneEnd && end > laneStart) {
const startsHere = start >= laneStart; const startsHere = start >= laneStart;
const endsHere = end <= laneEnd; const endsHere = end <= laneEnd;
result[i].push({ result[index].push({
...event, ...event,
startsHere, startsHere,
endsHere endsHere
}); });
} }
} });
}); });
return result; return result;
@@ -317,22 +338,31 @@ export default {
<div <div
class="fhc-calendar-base-grid" class="fhc-calendar-base-grid"
style="display:grid;width:100%;height:100%;overflow:auto" 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 <div
class="grid-header" class="grid-header"
style="display:grid" style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'" :style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
> >
<div <template v-for="(date, index) in displayedAxisMain" :key="index">
v-for="(date, index) in axisMain" <div
:key="index" v-if="date"
class="main-header" class="main-header"
:class="{'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length}" :class="{
:style="'grid-' + axisCol + ':' + (2+index)" 'collapsed-header': axisMainCollapsible && hasValidEvents && !events[index].length,
> 'main-header-sunday': date.weekday === 7,
<slot name="main-header" v-bind="{ index, date }" /> }"
</div> :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>
<div <div
v-if="allDayEvents" v-if="allDayEvents"
@@ -340,26 +370,34 @@ export default {
style="display:grid" style="display:grid"
:style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'" :style="'grid-template-' + axisCol + 's:subgrid;grid-' + axisCol + ':1/-1'"
> >
<div <template v-for="(events, index) in eventsAllDay" :key="index">
v-for="(events, index) in eventsAllDay" <div
:key="index" v-if="displayedAxisMain[index]"
class="all-day-events" class="all-day-events"
:style="'grid-' + axisCol + ':' + (2+index)" :class="{'all-day-events-sunday': displayedAxisMain[index].weekday === 7}"
> :style="'grid-' + axisCol + ':' + (2+index)"
<grid-line-event
v-for="(event, i) in events"
:key="i"
:event="event"
> >
<template v-slot="slot"> <grid-line-event
<slot name="event" v-bind="slot" /> v-for="(event, i) in events"
</template> :key="i"
</grid-line-event> :event="event"
</div> >
<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>
<div <div
ref="scroller" ref="scroller"
@scrollend="userScroll ? disableAutoScroll() : userScroll = true" @scrollend="userScroll ? disableAutoScroll() : userScroll = true"
id="grid-main-scrollable"
style="display:grid;overflow:auto" style="display:grid;overflow:auto"
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'" :style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
> >
@@ -371,9 +409,9 @@ export default {
> >
<div <div
v-for="(part, index) in axisPartsSave" v-for="(part, index) in axisPartsSave"
:key="index" :key="index"2
class="part-header" 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 }" /> <slot name="part-header" v-bind="{ index, part }" />
</div> </div>
@@ -389,43 +427,56 @@ export default {
@dragover="dropAllowed ? $event.preventDefault() : null" @dragover="dropAllowed ? $event.preventDefault() : null"
> >
<template <template
v-for="(date, index) in axisMain" v-for="(date, index) in displayedAxisMain"
:key="index" :key="index"
> >
<div <template v-if="date">
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 <div
v-if="snapToGrid && dragging" v-for="(part, i) in axisPartsSave"
style="position:absolute;inset:0;z-index:1" :key="i"
v-cal-dnd:dropzone.once="{date: date.plus(part.start || part), ends: ends.slice(ends.findIndex(end => end > date))}" class="part-body"
></div> style="position:relative"
</div> :style="'grid-' + axisCol + ':' + (1+index) + ';grid-' + axisRow + ':ps_' + i + '/pe_' + i"
<grid-line >
:start="date.plus(start)" <slot name="part-body" v-bind="{ index, part }" />
: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 <div
v-if="!snapToGrid && dragging" v-if="snapToGrid && dragging"
style="position:absolute;inset:0;z-index:1" 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> ></div>
</template> </div>
</grid-line> <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> </template>
</div> </div>
</div> </div>
@@ -123,6 +123,9 @@ export default {
template: /* html */ ` template: /* html */ `
<div <div
class="fhc-calendar-base-grid-line" 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="position:relative;display:grid;grid-auto-flow:dense"
:style="'grid-template-' + axisRow + 's:subgrid'" :style="'grid-template-' + axisRow + 's:subgrid'"
> >
+17 -4
View File
@@ -24,7 +24,8 @@ export default {
btnMonth: Boolean, btnMonth: Boolean,
btnWeek: Boolean, btnWeek: Boolean,
btnDay: Boolean, btnDay: Boolean,
btnList: Boolean btnList: Boolean,
btnRange: Boolean,
}, },
emits: [ emits: [
"next", "next",
@@ -35,7 +36,8 @@ export default {
], ],
data() { data() {
return { return {
open: false open: false,
semesterOptions: [],
}; };
}, },
methods: { methods: {
@@ -53,6 +55,15 @@ export default {
</div> </div>
<div class="header-modes"> <div class="header-modes">
<div class="d-flex gap-1 justify-content-end" role="group"> <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 <button
v-if="btnMonth" v-if="btnMonth"
type="button" type="button"
@@ -95,9 +106,10 @@ export default {
<div class="header-picker"> <div class="header-picker">
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button <button
class="btn btn-outline-secondary border-0" v-if="$props.mode !== 'range'"
@click="$emit('prev')" @click="$emit('prev')"
:disabled="open" :disabled="open"
class="btn btn-outline-secondary border-0"
> >
<i class="fa fa-chevron-left"></i> <i class="fa fa-chevron-left"></i>
</button> </button>
@@ -110,9 +122,10 @@ export default {
:list-length="modeOptions.length" :list-length="modeOptions.length"
/> />
<button <button
class="btn btn-outline-secondary border-0" v-if="$props.mode !== 'range'"
@click="$emit('next')" @click="$emit('next')"
:disabled="open" :disabled="open"
class="btn btn-outline-secondary border-0"
> >
<i class="fa fa-chevron-right"></i> <i class="fa fa-chevron-right"></i>
</button> </button>
@@ -5,10 +5,11 @@ export default {
components: { components: {
VueDatePicker VueDatePicker
}, },
inject: [ inject: {
"locale", locale: "locale",
"timezone" timezone: "timezone",
], rangeLength: {default: 30}
},
props: { props: {
date: { date: {
type: luxon.DateTime, type: luxon.DateTime,
@@ -21,7 +22,7 @@ export default {
listLength: { listLength: {
type: Number, type: Number,
default: 7 default: 7
} },
}, },
emits: [ emits: [
"update:date" "update:date"
@@ -38,6 +39,8 @@ export default {
return {month: this.convertedDate.month-1, year: this.convertedDate.year}; return {month: this.convertedDate.month-1, year: this.convertedDate.year};
case "list": case "list":
return [this.convertedDate.startOf('day').ts, this.convertedDate.startOf('day').plus({ days: this.listLength }).ts - 1]; 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": case "week":
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts]; return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
case "day": case "day":
@@ -56,6 +59,8 @@ export default {
return this.$p.t('calendar/year_kw', { year, week }); return this.$p.t('calendar/year_kw', { year, week });
case "list": case "list":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL) + '-' + this.date.plus({ days: this.listLength - 1 }).toLocaleString(luxon.DateTime.DATE_FULL); 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": case "day":
return this.date.toLocaleString(luxon.DateTime.DATE_FULL); return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
default: default:
@@ -64,11 +69,21 @@ export default {
}, },
weekStart() { weekStart() {
return luxon.Info.getStartOfWeek(this.date)%7; 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: { methods: {
update(value) { update(value) {
let date; let date;
let rangeLength;
switch (this.mode) { switch (this.mode) {
case "month": case "month":
value.month++; value.month++;
@@ -78,13 +93,18 @@ export default {
case "week": case "week":
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break; 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": case "day":
date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale); date = luxon.DateTime.fromJSDate(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
break; break;
default: default:
return; // Don't update if the value is invalid! return; // Don't update if the value is invalid!
} }
this.$emit("update:date", date); this.$emit("update:date", { date, rangeLength });
}, },
weekNumbers(date) { weekNumbers(date) {
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber; return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
@@ -97,7 +117,7 @@ export default {
:format="() => title" :format="() => title"
:month-picker="mode == 'month'" :month-picker="mode == 'month'"
:week-picker="mode == 'week'" :week-picker="mode == 'week'"
:range="mode == 'list' ? { autoRange: listLength - 1 } : false" :range="rangeConfig"
:text-input="mode == 'day'" :text-input="mode == 'day'"
:week-start="weekStart" :week-start="weekStart"
:week-numbers="{ type: weekNumbers }" :week-numbers="{ type: weekNumbers }"
@@ -26,7 +26,11 @@ export default {
} }
return true; return true;
} }
} },
alignItemsClassSuffix: {
type: String,
default: "end",
},
}, },
computed: { computed: {
sanitizedTimestamps() { sanitizedTimestamps() {
@@ -49,7 +53,7 @@ export default {
} }
}, },
template: ` 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="start">{{ start }}</span>
<span v-if="end">-</span> <span v-if="end">-</span>
<span v-if="end">{{ 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 ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js'; import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js'; import ModeList from './Mode/List.js';
import ModeRange from './Mode/Range.js';
export default { export default {
name: "CalendarLvPlan", name: "CalendarLvPlan",
components: { components: {
FhcCalendar FhcCalendar
}, },
inject: ["isMobile"], inject: {
isMobile: {
default: false,
},
rangeLength: {
default: 30,
}
},
props: { props: {
date: { date: {
type: [Date, String, Number, luxon.DateTime], type: [Date, String, Number, luxon.DateTime],
@@ -28,7 +36,11 @@ export default {
getPromiseFunc: { getPromiseFunc: {
type: Function, type: Function,
required: true required: true
} },
shouldIncludeRangeMode: {
type: Boolean,
default: false,
},
}, },
provide() { provide() {
return { return {
@@ -68,31 +80,37 @@ export default {
backgrounds() { backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone); let now = luxon.DateTime.now().setZone(this.timezone);
if (this.mode == 'Month') if (this.mode == 'Month') {
return [ return [
{ {
class: 'background-past', class: 'background-past',
end: now.startOf('day') end: now.startOf('day')
} }
]; ];
} else if (this.mode == 'Range') {
return [ return [];
{ } else {
class: 'background-past', return [
end: now, {
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false }) class: 'background-past',
} end: now,
]; label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
}, },
modes() { modes() {
let modes = { let modes = {
day: Vue.markRaw(ModeDay), day: Vue.markRaw(ModeDay),
month: Vue.markRaw(ModeMonth), month: Vue.markRaw(ModeMonth),
}; };
if (this.isMobile) { if (!this.isMobile) {
modes.list = Vue.markRaw(ModeList);
} else {
modes.week = Vue.markRaw(ModeWeek); modes.week = Vue.markRaw(ModeWeek);
if (this.$props.shouldIncludeRangeMode) {
modes.range = Vue.markRaw(ModeRange);
}
} else {
modes.list = Vue.markRaw(ModeList);
} }
return modes; return modes;
@@ -163,7 +181,7 @@ export default {
:backgrounds="backgrounds" :backgrounds="backgrounds"
:time-grid="teachingunits" :time-grid="teachingunits"
show-btns 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:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
@update:range="updateRange" @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 ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.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_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List"; export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
@@ -23,9 +24,23 @@ export default {
isMitarbeiter: false, isMitarbeiter: false,
isStudent: false, isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone, timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
semesterRangePresets: {
label: null,
presets: [],
},
}; };
}, },
inject: ["isMobile"], 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:{ computed:{
currentDay() { currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date))) if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
@@ -34,7 +49,11 @@ export default {
}, },
currentMode() { currentMode() {
let validModes = ["day", "month"]; 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 const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE ? DEFAULT_MODE_LVPLAN_MOBILE
@@ -94,10 +113,10 @@ export default {
}, },
}, },
methods: { methods: {
handleChangeDate(day, newMode) { handleChangeDate(day, newMode, rangeLength) {
return this.handleChangeMode(newMode, day); return this.handleChangeMode(newMode, day, rangeLength);
}, },
handleChangeMode(newMode, day) { handleChangeMode(newMode, day, range_length = null) {
const mode = newMode[0].toUpperCase() + newMode.slice(1) const mode = newMode[0].toUpperCase() + newMode.slice(1)
const focus_date = day.toISODate(); const focus_date = day.toISODate();
@@ -105,7 +124,8 @@ export default {
name: "MyLvPlan", name: "MyLvPlan",
params: { params: {
mode, mode,
focus_date focus_date,
range_length,
} }
}); });
}, },
@@ -134,13 +154,32 @@ export default {
this.isMitarbeiter = authInfo.isMitarbeiter; this.isMitarbeiter = authInfo.isMitarbeiter;
this.isStudent = authInfo.isStudent; 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() { async created() {
await this.fetchAuthInfo(); await this.fetchAuthInfo();
await this.fetchSemesters();
}, },
template: /*html*/` template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100"> <div class="cis-lvplan-personal d-flex flex-column h-100">
<h2> <h2 id="cis-lvplan-personal-heading">
{{ $p.t('lehre/stundenplan') }} {{ $p.t('lehre/stundenplan') }}
<span v-if="studiensemester_kurzbz" class="ps-3"> <span v-if="studiensemester_kurzbz" class="ps-3">
{{ studiensemester_kurzbz }} {{ studiensemester_kurzbz }}
@@ -154,6 +193,7 @@ export default {
:get-promise-func="getPromiseFunc" :get-promise-func="getPromiseFunc"
:date="currentDay" :date="currentDay"
:mode="currentMode" :mode="currentMode"
:shouldIncludeRangeMode="true"
@update:date="handleChangeDate" @update:date="handleChangeDate"
@update:mode="handleChangeMode" @update:mode="handleChangeMode"
@update:range="updateRange" @update:range="updateRange"