Shareable calendar components

This commit is contained in:
chfhtw
2025-08-01 15:22:34 +02:00
parent e326c9483d
commit 219507ffa3
5 changed files with 302 additions and 323 deletions
+164
View File
@@ -0,0 +1,164 @@
import FhcCalendar from "./Base.js";
import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
},
mode: {
type: String,
default: 'Week'
},
getPromiseFunc: {
type: Function,
required: true
}
},
emits: [
"update:date",
"update:mode",
"update:range"
],
data() {
return {
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
},
week: {
collapseEmptyDays: false
}
},
teachingunits: null
};
},
computed: {
backgrounds() {
let now = luxon.DateTime.now().setZone(this.timezone);
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 })
}
];
}
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
}
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
});
return {
rangeInterval,
events,
lv
};
},
created() {
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
},
template: /* html */`
<fhc-calendar
ref="calendar"
class="fhc-calendar-lvplan"
:date="date"
:modes="modes"
:mode-options="modeOptions"
:mode="mode"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
show-btns
@update:date="$emit('update:date', $event)"
@update:mode="$emit('update:mode', $event)"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<slot />
</template>
</fhc-calendar>`
}
+106
View File
@@ -0,0 +1,106 @@
import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeList from '../Calendar/Mode/List.js';
export default {
name: "CalendarWidget",
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
getPromiseFunc: {
type: Function,
required: true
}
},
data() {
return {
now: luxon.DateTime.now().setZone(this.timezone),
modes: {
list: Vue.markRaw(ModeList)
},
modeOptions: {
list: {
length: 7
}
}
};
},
methods: {
eventStyle(event) {
const styles = {};
if (event.farbe)
styles['--event-bg'] = '#' + event.farbe;
else if (event.type == 'reservierung')
styles['--event-bg'] = '#ffffff';
else
styles['--event-bg'] = '#cccccc';
const eventEnd = luxon.DateTime.fromISO(event.isoend, { zone: this.timezone });
if (eventEnd < this.now)
styles['opacity'] = .5;
return styles;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
}
},
setup(props) {
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
return {
rangeInterval,
events
};
},
template: /* html */`
<fhc-calendar
:modes="modes"
:mode-options="modeOptions"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events || []"
@update:range="updateRange"
>
<template v-slot="{ event, mode }">
<div
v-if="!event"
class="h-100 d-flex justify-content-center align-items-center"
>
{{ $p.t('lehre/noLvFound') }}
</div>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<div
v-else
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:style="eventStyle(event)"
>
<component
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
</fhc-calendar>`
}
+16 -113
View File
@@ -1,14 +1,8 @@
import FhcCalendar from "../../Calendar/Base.js";
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import { useEventLoader } from '../../../composables/EventLoader.js';
import ModeDay from '../../Calendar/Mode/Day.js';
import ModeWeek from '../../Calendar/Mode/Week.js';
import ModeMonth from '../../Calendar/Mode/Month.js';
export const DEFAULT_MODE_LVPLAN = 'Week'
export default {
@@ -16,9 +10,6 @@ export default {
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
@@ -26,25 +17,11 @@ export default {
data() {
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
return {
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
emptyMessageDetails: Vue.computed(() => this.$p.t('lehre/noLvFound'))
},
week: {
collapseEmptyDays: false
}
},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
teachingunits: null
lv: null
};
},
computed:{
@@ -54,25 +31,6 @@ export default {
currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN;
},
backgrounds() {
let now = luxon.DateTime.now().setZone(this.viewData.timezone);
if (this.currentMode == 'Month')
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
@@ -101,11 +59,6 @@ export default {
}
},
methods: {
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
@@ -123,35 +76,22 @@ export default {
});
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
this.rangeInterval.end.startOf('week').toISODate()
rangeInterval.end.startOf('week').toISODate()
))
.then(res => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
}
},
setup(props) {
const $api = Vue.inject('$api');
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, (start, end) => {
},
getPromiseFunc(start, end) {
return [
$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), props.propsViewData.lv_id)),
$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
this.$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), this.propsViewData.lv_id)),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
];
});
return {
rangeInterval,
events,
lv
};
}
},
created() {
this.$api
@@ -159,19 +99,10 @@ export default {
.then(res => {
this.uid = res.data.uid;
});
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
},
template:/*html*/`
<div class="fhc-lvplan d-flex flex-column h-100" v-if="renderers">
<h2 @click="modeOptions.week.collapseEmptyDays = !modeOptions.week.collapseEmptyDays">
template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100">
<h2>
{{ $p.t('lehre/stundenplan') }}
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">
{{ studiensemester_kurzbz }}
@@ -183,45 +114,17 @@ export default {
<hr>
<fhc-calendar
ref="calendar"
class="responsive-calendar"
v-model:lv="lv"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:modes="modes"
:mode-options="modeOptions"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
:timezone="viewData.timezone"
:locale="$p.user_locale.value"
show-btns
:events="events || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
class="responsive-calendar"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
<template #actions>
<template>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
@@ -1,13 +1,7 @@
import FhcCalendar from "../../Calendar/Base.js";
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import { useEventLoader } from '../../../composables/EventLoader.js';
import ModeDay from '../../Calendar/Mode/Day.js';
import ModeWeek from '../../Calendar/Mode/Week.js';
import ModeMonth from '../../Calendar/Mode/Month.js';
export const DEFAULT_MODE_RAUMINFO = 'Week'
export default {
@@ -15,65 +9,19 @@ export default {
components: {
FhcCalendar
},
inject: [
"renderers"
],
props:{
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
},
data() {
return {
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung')),
emptyMessageDetails: Vue.computed(() => this.$p.t('rauminfo/keineRaumReservierung'))
},
week: {
collapseEmptyDays: false
}
},
teachingunits: null
}
},
computed: {
currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
},
currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO;
},
backgrounds() {
let now = luxon.DateTime.now().setZone(this.viewData.timezone);
if (this.currentMode == 'Month')
return [
{
class: 'background-past',
end: now.startOf('day')
}
];
return [
{
class: 'background-past',
end: now,
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
}
},
methods:{
eventStyle(event) {
if (!event.farbe)
return undefined;
return '--event-bg:#' + event.farbe;
},
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
@@ -90,37 +38,12 @@ export default {
}
});
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
}
},
setup(props) {
const $api = Vue.inject('$api');
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, (start, end) => {
getPromiseFunc(start, end) {
return [
$api.call(ApiLvPlan.getRoomInfo(props.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())),
$api.call(ApiLvPlan.getOrtReservierungen(props.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate()))
this.$api.call(ApiLvPlan.getRoomInfo(this.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate())),
this.$api.call(ApiLvPlan.getOrtReservierungen(this.propsViewData.ort_kurzbz, start.toISODate(), end.toISODate()))
];
});
return {
rangeInterval,
events
};
},
created() {
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
}
},
template: /*html*/`
<div class="fhc-roominformation d-flex flex-column h-100">
@@ -128,44 +51,13 @@ export default {
<hr>
<fhc-calendar
ref="calendar"
class="responsive-calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:modes="modes"
:mode-options="modeOptions"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
:timezone="viewData.timezone"
:locale="$p.user_locale.value"
show-btns
:events="events || []"
:backgrounds="backgrounds"
:time-grid="teachingunits"
>
<template v-slot="{ event, mode }">
<div
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
>
<component
v-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
</fhc-calendar>
class="responsive-calendar"
></fhc-calendar>
</div>`
};
+7 -93
View File
@@ -1,12 +1,8 @@
import AbstractWidget from './Abstract.js';
import FhcCalendar from '../Calendar/Base.js';
import FhcCalendar from '../Calendar/Widget.js';
import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import ModeList from '../Calendar/Mode/List.js';
export default {
name: "LvPlanWidget",
components: {
@@ -16,103 +12,21 @@ export default {
AbstractWidget
],
inject: [
"renderers",
"timezone"
],
data() {
return {
now: luxon.DateTime.now().setZone(this.timezone),
modes: {
list: Vue.markRaw(ModeList)
},
modeOptions: {
list: {
length: 7
}
},
currentDay: luxon.DateTime.now().setZone(this.timezone).startOf('day')
}
},
methods: {
eventStyle(event) {
const styles = {};
if (event.farbe)
styles['--event-bg'] = '#' + event.farbe;
else if (event.type == 'reservierung')
styles['--event-bg'] = '#ffffff';
else
styles['--event-bg'] = '#cccccc';
const eventEnd = luxon.DateTime.fromISO(event.isoend, { zone: this.timezone });
if (eventEnd < this.now)
styles['opacity'] = .5;
return styles;
},
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
}
},
setup() {
const $api = Vue.inject('$api');
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, (start, end) => {
getPromiseFunc(start, end) {
return [
$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate())),
$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
this.$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate())),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
];
});
return {
rangeInterval,
events
};
}
},
created() {
this.$emit('setConfig', false);
},
template: /*html*/`
<div class="dashboard-widget-lvplan d-flex flex-column h-100">
<fhc-calendar
v-model:date="currentDay"
:modes="modes"
:mode-options="modeOptions"
@update:range="updateRange"
:timezone="timezone"
:locale="$p.user_locale.value"
:events="events"
>
<template v-slot="{ event, mode }">
<div
v-if="!event"
class="h-100 d-flex justify-content-center align-items-center"
>
{{ $p.t('lehre/noLvFound') }}
</div>
<component
v-else-if="mode == 'eventheader'"
:is="renderers[event.type]?.modalTitle"
:event="event"
></component>
<component
v-else-if="mode == 'event'"
:is="renderers[event.type]?.modalContent"
:event="event"
></component>
<div
v-else
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:style="eventStyle(event)"
>
<component
:is="renderers[event.type]?.calendarEvent"
:event="event"
></component>
</div>
</template>
</fhc-calendar>
</div>
`
<fhc-calendar :timezone="timezone" :get-promise-func="getPromiseFunc" />
</div>`
}