mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-26 00:54:27 +00:00
Merge branch 'master' into feature-39571/Studierendenverwaltung_CoreNotizcontroller
This commit is contained in:
@@ -16,7 +16,7 @@ export default {
|
||||
:class="this.cModalClass"
|
||||
:id="this.id"
|
||||
:aria-labelledby="this.id + '_label'" aria-hidden="true" tabindex="-1">
|
||||
<div class="modal-dialog" :class="this.cModalDialogClass">
|
||||
<div class="modal-dialog modal-dialog-scrollable" :class="this.cModalDialogClass">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" :id="this.id + '_label'">
|
||||
|
||||
@@ -29,14 +29,24 @@ export default {
|
||||
uid: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
/** List of types to allow for creation */
|
||||
betriebsmittelTypes: {
|
||||
type: Array,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* If true: only show the types specified in 'betriebsmittelTypes'.
|
||||
* If false: show all available types.
|
||||
*/
|
||||
filterByProvidedTypes: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabulatorOptions: {
|
||||
ajaxURL: 'dummy',
|
||||
ajaxRequestFunc: () => this.$api.call(
|
||||
this.endpoint.getAllBetriebsmittel(this.typeId, this.id)
|
||||
this.endpoint.getAllBetriebsmittel(this.typeId, this.id, (this.filterByProvidedTypes ? this.betriebsmittelTypes : null))
|
||||
),
|
||||
ajaxResponse: (url, params, response) => response.data,
|
||||
columns: [
|
||||
@@ -137,8 +147,6 @@ export default {
|
||||
layout: 'fitColumns',
|
||||
layoutColumnsOnNewData: false,
|
||||
height: '550',
|
||||
selectableRangeMode: 'click',
|
||||
selectable: true,
|
||||
persistenceID: 'core-betriebsmittel'
|
||||
},
|
||||
tabulatorEvents: [
|
||||
@@ -296,7 +304,7 @@ export default {
|
||||
},
|
||||
created() {
|
||||
return this.$api
|
||||
.call(this.endpoint.getTypenBetriebsmittel())
|
||||
.call(this.endpoint.getTypenBetriebsmittel(this.betriebsmittelTypes))
|
||||
.then(result => {
|
||||
this.listBetriebsmitteltyp = result.data;
|
||||
})
|
||||
@@ -311,6 +319,7 @@ export default {
|
||||
table-only
|
||||
:side-menu="false"
|
||||
reload
|
||||
:reload-btn-infotext="this.$p.t('table', 'reload')"
|
||||
new-btn-show
|
||||
:new-btn-label="this.$p.t('ui', 'betriebsmittel')"
|
||||
@click:new="actionNewBetriebsmittel"
|
||||
@@ -427,6 +436,7 @@ export default {
|
||||
v-model="formData.ausgegebenam"
|
||||
auto-apply
|
||||
:enable-time-picker="false"
|
||||
text-input
|
||||
format="dd.MM.yyyy"
|
||||
preview-format="dd.MM.yyyy"
|
||||
:teleport="true"
|
||||
@@ -442,6 +452,7 @@ export default {
|
||||
v-model="formData.retouram"
|
||||
auto-apply
|
||||
:enable-time-picker="false"
|
||||
text-input
|
||||
format="dd.MM.yyyy"
|
||||
preview-format="dd.MM.yyyy"
|
||||
:teleport="true"
|
||||
|
||||
@@ -23,9 +23,17 @@ export default {
|
||||
},
|
||||
noCloseBtn: Boolean,
|
||||
dialogClass: [String,Array,Object],
|
||||
headerClass: {
|
||||
type: [String,Array,Object],
|
||||
default: ''
|
||||
},
|
||||
bodyClass: {
|
||||
type: [String,Array,Object],
|
||||
default: 'px-4 py-5'
|
||||
},
|
||||
footerClass: {
|
||||
type: [String,Array,Object],
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
@@ -117,15 +125,16 @@ export default {
|
||||
template: `<div ref="modal" class="bootstrap-modal modal" tabindex="-1" @[\`hide.bs.modal\`]="$emit('hideBsModal')" @[\`hidden.bs.modal\`]="$emit('hiddenBsModal')" @[\`hidePrevented.bs.modal\`]="$emit('hidePreventedBsModal')" @[\`show.bs.modal\`]="$emit('showBsModal')" >
|
||||
<div class="modal-dialog" :class="dialogClass">
|
||||
<div class="modal-content">
|
||||
<div v-if="$slots.title" class="modal-header">
|
||||
<div v-if="$slots.title" class="modal-header" :class="headerClass">
|
||||
<h5 class="modal-title"><slot name="title"/></h5>
|
||||
<slot name="popoutButton"></slot>
|
||||
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<slot name="modal-header-content"></slot>
|
||||
</div>
|
||||
<div class="modal-body" :class="bodyClass">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="modal-footer">
|
||||
<div v-if="$slots.footer" class="modal-footer" :class="footerClass">
|
||||
<slot name="footer"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import CalendarHeader from './Header.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalendarHeader
|
||||
},
|
||||
inject: [
|
||||
'date',
|
||||
'focusDate',
|
||||
'size'
|
||||
],
|
||||
emits: [
|
||||
'updateMode',
|
||||
'change:range',
|
||||
'input'
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import BaseDraganddrop from './Base/DragAndDrop.js';
|
||||
import BaseHeader from './Base/Header.js';
|
||||
import BaseSlider from './Base/Slider.js';
|
||||
import BsModal from '../Bootstrap/Modal.js';
|
||||
|
||||
import CalClick from '../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarBase",
|
||||
components: {
|
||||
BaseDraganddrop,
|
||||
BaseHeader,
|
||||
BaseSlider,
|
||||
BsModal
|
||||
},
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
locale: Vue.computed(() => this.locale),
|
||||
timezone: Vue.computed(() => this.timezone),
|
||||
timeGrid: Vue.computed(() => this.timeGrid),
|
||||
draggableEvents: Vue.computed(() => {
|
||||
if (!this.draggableEvents)
|
||||
return () => false;
|
||||
|
||||
if (Array.isArray(this.draggableEvents))
|
||||
return event => this.draggableEvents.includes(event.type);
|
||||
if (this.draggableEvents instanceof Function)
|
||||
return this.draggableEvents;
|
||||
|
||||
return () => true;
|
||||
}),
|
||||
dropableEvents: Vue.computed(() => {
|
||||
if (!this.onDrop)
|
||||
return () => false;
|
||||
|
||||
if (Array.isArray(this.dropableEvents))
|
||||
return item => this.dropableEvents.includes(item.type);
|
||||
if (this.dropableEvents instanceof Function)
|
||||
return this.dropableEvents;
|
||||
|
||||
return () => true;
|
||||
}),
|
||||
hasDragoverFunc: Vue.computed(() => this.onDragover),
|
||||
mode: Vue.computed(() => this.mode)
|
||||
};
|
||||
},
|
||||
props: {
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'de'
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: [Date, String, Number, luxon.DateTime],
|
||||
default: props => luxon.DateTime.now().setZone(props.timezone).startOf('day')
|
||||
},
|
||||
modes: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: {}
|
||||
// TODO(chris): verfication functions
|
||||
},
|
||||
mode: String,
|
||||
modeOptions: Object,
|
||||
events: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
backgrounds: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
showBtns: Boolean,
|
||||
btnMonth: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnWeek: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnDay: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
btnList: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
timeGrid: Array,
|
||||
draggableEvents: [Boolean, Array, Function],
|
||||
dropableEvents: [Boolean, Array, Function],
|
||||
onDragover: Function,
|
||||
onDrop: Function
|
||||
},
|
||||
emits: [
|
||||
"click:next",
|
||||
"click:prev",
|
||||
"click:mode",
|
||||
"click:event",
|
||||
"click:day",
|
||||
"click:week",
|
||||
"update:date",
|
||||
"update:mode",
|
||||
"update:range",
|
||||
"drop"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
internalView: null,
|
||||
internalDate: null,
|
||||
modalEvent: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
convertedEvents() {
|
||||
return this.events.map(orig => ({
|
||||
id: orig.type + orig[orig.type + '_id'],
|
||||
type: orig.type,
|
||||
start: luxon.DateTime.fromISO(orig.isostart).setZone(this.timezone),
|
||||
end: luxon.DateTime.fromISO(orig.isoend).setZone(this.timezone),
|
||||
orig
|
||||
}));
|
||||
},
|
||||
convertedBackgrounds() {
|
||||
return this.backgrounds.map(bg => {
|
||||
const res = { ...bg };
|
||||
if (res.start) {
|
||||
if (Number.isInteger(res.start))
|
||||
res.start = luxon.DateTime.fromMillis(res.start, { zone: this.timezone, locale: this.locale });
|
||||
else if (res.start instanceof Date)
|
||||
res.start = luxon.DateTime.fromJSDate(res.start, { zone: this.timezone, locale: this.locale });
|
||||
else if (typeof res.start ===
|
||||
'string' || res.start instanceof String)
|
||||
res.start = luxon.DateTime.fromISO(res.start, { zone: this.timezone, locale: this.locale });
|
||||
}
|
||||
if (res.end) {
|
||||
if (Number.isInteger(res.end))
|
||||
res.end = luxon.DateTime.fromMillis(res.end, { zone: this.timezone, locale: this.locale });
|
||||
else if (res.end instanceof Date)
|
||||
res.end = luxon.DateTime.fromJSDate(res.end, { zone: this.timezone, locale: this.locale });
|
||||
else if (typeof res.end ===
|
||||
'string' || res.end instanceof String)
|
||||
res.end = luxon.DateTime.fromISO(res.end, { zone: this.timezone, locale: this.locale });
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
sDate() {
|
||||
if (this.date instanceof luxon.DateTime)
|
||||
return this.date;
|
||||
return luxon.DateTime.fromJSDate(new Date(this.date)).setZone(this.timezone);
|
||||
},
|
||||
cDate: {
|
||||
get() {
|
||||
const date = this.internalDate ? this.internalDate : this.sDate;
|
||||
return date.setLocale(this.locale);
|
||||
},
|
||||
set(value) {
|
||||
this.internalDate = value;
|
||||
this.$emit('update:date', value, this.cMode);
|
||||
}
|
||||
},
|
||||
sMode() {
|
||||
// choose default mode
|
||||
let mode = this.mode;
|
||||
if (mode)
|
||||
mode = mode.toLowerCase();
|
||||
if (!mode || !this.modes[mode])
|
||||
mode = Object.keys(this.modes).find(Boolean); // start with first entry as active mode
|
||||
return mode || '';
|
||||
},
|
||||
cMode: {
|
||||
get() {
|
||||
return this.internalView ? this.internalView : this.sMode;
|
||||
},
|
||||
set(value) {
|
||||
this.internalView = value;
|
||||
this.$emit('update:mode', value, this.cDate);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
sDate(n, o) {
|
||||
if (this.sDate.isValid && !this.sDate.hasSame(this.internalDate, 'day'))
|
||||
this.internalDate = this.sDate;
|
||||
},
|
||||
sMode() {
|
||||
if (this.sMode)
|
||||
this.internalView = this.sMode;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickPrev() {
|
||||
const evt = new Event('click:prev', {cancelable: true});
|
||||
this.$emit('click:prev', evt);
|
||||
if (evt.defaultPrevented)
|
||||
return;
|
||||
|
||||
// default: switch page
|
||||
this.$refs.mode.prevPage();
|
||||
},
|
||||
clickNext() {
|
||||
const evt = new Event('click:next', {cancelable: true});
|
||||
this.$emit('click:next', evt);
|
||||
if (evt.defaultPrevented)
|
||||
return;
|
||||
|
||||
// default: switch page
|
||||
this.$refs.mode.nextPage();
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
// TODO(chris): implement
|
||||
switch (evt.detail.source) {
|
||||
case 'day':
|
||||
if (this.cMode != 'day' && this.modes['day']) {
|
||||
evt.stopPropagation();
|
||||
this.cDate = evt.detail.value;
|
||||
this.cMode = 'day';
|
||||
}
|
||||
break;
|
||||
case 'week':
|
||||
if (this.cMode != 'week' && this.modes['week']) {
|
||||
evt.stopPropagation();
|
||||
this.cDate = luxon.DateTime.fromObject({
|
||||
localWeekNumber: evt.detail.value.number,
|
||||
localWeekYear: evt.detail.value.year
|
||||
}, {
|
||||
zone: this.cDate.zoneName,
|
||||
locale: this.cDate.locale
|
||||
});
|
||||
this.cMode = 'week';
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
onDropItem(evt, start, end) {
|
||||
this.$emit('drop', evt, start, end);
|
||||
},
|
||||
showEventModal(eventObj) {
|
||||
this.modalEvent = eventObj;
|
||||
this.$refs.modal.show();
|
||||
},
|
||||
hideEventModal() {
|
||||
if (this.modalEvent)
|
||||
this.modalEvent.closeFn = undefined;
|
||||
this.$refs.modal.hide();
|
||||
this.modalEvent = null;
|
||||
},
|
||||
onModalHidden() {
|
||||
if (this.modalEvent.closeFn)
|
||||
this.modalEvent.closeFn();
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.hideEventModal();
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-base h-100">
|
||||
<base-draganddrop
|
||||
class="card h-100"
|
||||
:events="convertedEvents"
|
||||
:backgrounds="convertedBackgrounds"
|
||||
@drop="onDropItem"
|
||||
v-cal-click:container
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-header
|
||||
class="card-header"
|
||||
v-model:date="cDate"
|
||||
v-model:mode="cMode"
|
||||
@prev="clickPrev"
|
||||
@next="clickNext"
|
||||
@click:mode="$emit('click:mode', $event)"
|
||||
:btn-day="!!modes['day'] && (btnDay || (showBtns && btnDay !== false))"
|
||||
:btn-week="!!modes['week'] && (btnWeek || (showBtns && btnWeek !== false))"
|
||||
:btn-month="!!modes['month'] && (btnMonth || (showBtns && btnMonth !== false))"
|
||||
:btn-list="!!modes['list'] && (btnList || (showBtns && btnList !== false))"
|
||||
:mode-options="modeOptions ? modeOptions[cMode] : undefined"
|
||||
>
|
||||
<slot name="actions" />
|
||||
</base-header>
|
||||
<component
|
||||
:is="modes ? modes[cMode] : null || 'div'"
|
||||
ref="mode"
|
||||
v-model:current-date="cDate"
|
||||
@update:range="$emit('update:range', $event)"
|
||||
@request-modal-open="showEventModal"
|
||||
@request-modal-close="hideEventModal"
|
||||
v-bind="modeOptions ? modeOptions[cMode] : null || {}"
|
||||
>
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</component>
|
||||
</base-draganddrop>
|
||||
<bs-modal ref="modal" dialog-class="modal-lg" body-class="" @hidden-bs-modal="onModalHidden">
|
||||
<template #title>
|
||||
<slot v-if="modalEvent" v-bind="{mode: 'eventheader', event: modalEvent.event}" />
|
||||
</template>
|
||||
<template #default>
|
||||
<slot v-if="modalEvent" v-bind="{mode: 'event', event: modalEvent.event}" />
|
||||
</template>
|
||||
</bs-modal>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
import DragAndDrop from '../../../helpers/DragAndDrop.js';
|
||||
|
||||
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
|
||||
|
||||
/**
|
||||
* TODO(chris): this needs serious rework!
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "CalendarDragAndDrop",
|
||||
directives: {
|
||||
CalDnd
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
events: Vue.computed(() => this.correctedEvents),
|
||||
backgrounds: Vue.computed(() => this.backgrounds),
|
||||
dropAllowed: Vue.computed(() => this.dragging && this.dropAllowed)
|
||||
};
|
||||
},
|
||||
inject: {
|
||||
mode: "mode",
|
||||
dropableEvents: "dropableEvents"
|
||||
},
|
||||
props: {
|
||||
events: Array,
|
||||
backgrounds: Array
|
||||
},
|
||||
emits: [
|
||||
"drop"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
allowed: false,
|
||||
draggedInternalEvent: null,
|
||||
draggedExternalEvent: null,
|
||||
targetTimestamp: 0,
|
||||
targetGridEnds: null,
|
||||
dropAllowed: false,
|
||||
|
||||
shadowPreview: false // TODO(chris): IMPLEMENT! (use background instead of event as preview)
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
correctedEvents() {
|
||||
if (this.dragging) {
|
||||
if (this.draggedInternalEvent) {
|
||||
const index = this.events.findIndex(e => e.id == this.draggedInternalEvent.id);
|
||||
if (this.previewEvent && !this.shadowPreview)
|
||||
return this.events.toSpliced(index, 1, this.previewEvent);
|
||||
else
|
||||
return this.events.toSpliced(index, 1);
|
||||
}
|
||||
if (this.previewEvent && !this.shadowPreview)
|
||||
return [...this.events, this.previewEvent];
|
||||
}
|
||||
|
||||
return this.events;
|
||||
},
|
||||
correctedBackgrounds() {
|
||||
if (this.dragging) {
|
||||
if (this.shadowPreview) {
|
||||
// TODO(chris): how to get the length
|
||||
return [...this.backgrounds, {
|
||||
start: new Date(this.targetTimestamp),
|
||||
class: 'shadow-preview'
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
return this.backgrounds;
|
||||
},
|
||||
previewEvent() {
|
||||
if (!this.dragging || !this.dropAllowed)
|
||||
return null;
|
||||
if (!this.targetTimestamp)
|
||||
return null;
|
||||
|
||||
const event = this.draggedInternalEvent || this.draggedExternalEvent;
|
||||
|
||||
if (!event)
|
||||
return null;
|
||||
|
||||
// TODO(chris): calculate length correctly from orig
|
||||
let length = event.end - event.start;
|
||||
if (this.targetGridEnds)
|
||||
length = this.targetGridEnds.find(end => end >= this.targetTimestamp + length) - this.targetTimestamp;
|
||||
|
||||
return {
|
||||
orig: event.orig,
|
||||
start: this.targetTimestamp,
|
||||
end: this.targetTimestamp + length
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDragstart(evt) {
|
||||
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
|
||||
this.draggedInternalEvent = evt.detail.item;
|
||||
},
|
||||
onDragend() {
|
||||
this.draggedInternalEvent = null;
|
||||
this.dragging = false;
|
||||
},
|
||||
onDragenter(evt) {
|
||||
this.dragging = true;
|
||||
|
||||
if (!this.draggedInternalEvent) {
|
||||
const event = DragAndDrop.getValidTransferData(evt.detail.originalEvent);
|
||||
if (event) {
|
||||
this.draggedExternalEvent = {
|
||||
id: event.id,
|
||||
type: event.type,
|
||||
start: event.isostart
|
||||
? luxon.DateTime.fromISO(event.isostart).setZone(this.timezone)
|
||||
: luxon.DateTime.local().setZone(this.timezone),
|
||||
end: event.isoend
|
||||
? luxon.DateTime.fromISO(event.isoend).setZone(this.timezone)
|
||||
: luxon.DateTime.local().setZone(this.timezone),
|
||||
orig: event
|
||||
};
|
||||
} else {
|
||||
this.draggedExternalEvent = null;
|
||||
}
|
||||
this.dropAllowed = this.dropableEvents(event, this.mode);
|
||||
} else {
|
||||
this.dropAllowed = this.dropableEvents(this.draggedInternalEvent, this.mode);
|
||||
}
|
||||
},
|
||||
onDragleave() {
|
||||
this.dragging = false;
|
||||
},
|
||||
onDragchange(evt) {
|
||||
this.targetTimestamp = evt.detail.timestamp;
|
||||
|
||||
this.targetGridEnds = evt.detail.ends || null;
|
||||
},
|
||||
onDrop(evt) {
|
||||
if (!this.dragging || !this.dropAllowed)
|
||||
return;
|
||||
|
||||
this.$emit('drop', evt, this.previewEvent.start, this.previewEvent.end);
|
||||
this.dropAllowed = false;
|
||||
this.dragging = false;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-base-draganddrop"
|
||||
@calendar-dragstart="onDragstart"
|
||||
@calendar-dragend="onDragend"
|
||||
v-cal-dnd:dropcage
|
||||
@calendar-dragenter="onDragenter"
|
||||
@calendar-dragleave="onDragleave"
|
||||
@calendar-dragchange="onDragchange"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
import GridLine from './Grid/Line.js';
|
||||
import GridLineEvent from './Grid/Line/Event.js';
|
||||
|
||||
import CalDnd from '../../../directives/Calendar/DragAndDrop.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarGrid",
|
||||
components: {
|
||||
GridLine,
|
||||
GridLineEvent
|
||||
},
|
||||
directives: {
|
||||
CalDnd
|
||||
},
|
||||
inject: {
|
||||
originalEvents: "events",
|
||||
originalBackgrounds: "backgrounds",
|
||||
dropAllowed: "dropAllowed"
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
flipAxis: Vue.computed(() => this.flipAxis),
|
||||
axisRow: Vue.computed(() => this.axisRow)
|
||||
};
|
||||
},
|
||||
props: {
|
||||
axisMain: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value.every(item => item instanceof luxon.DateTime);
|
||||
}
|
||||
},
|
||||
axisParts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return value.every(item =>
|
||||
item instanceof luxon.Duration
|
||||
|| Number.isInteger(item)
|
||||
|| (
|
||||
(
|
||||
item.start instanceof luxon.Duration
|
||||
|| Number.isInteger(item.start)
|
||||
) && (
|
||||
item.end instanceof luxon.Duration
|
||||
|| Number.isInteger(item.end)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
flipAxis: Boolean,
|
||||
allDayEvents: Boolean,
|
||||
axisMainCollapsible: Boolean,
|
||||
snapToGrid: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragging: false,
|
||||
resizeObserver: null,
|
||||
mutationObserver: null,
|
||||
userScroll: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisRow() {
|
||||
return this.flipAxis ? 'column' : 'row';
|
||||
},
|
||||
axisCol() {
|
||||
return this.flipAxis ? 'row' : 'column';
|
||||
},
|
||||
axisPartsWithBreaks() {
|
||||
return this.axisParts.reduce((res, tu, index) => {
|
||||
const start = tu.start || tu;
|
||||
const end = tu.end;
|
||||
|
||||
if (res.length) {
|
||||
const lastTuEnd = res.pop();
|
||||
if (Array.isArray(lastTuEnd)) {
|
||||
res.push({
|
||||
start: lastTuEnd[0],
|
||||
end: start,
|
||||
index: lastTuEnd[1]
|
||||
});
|
||||
} else if (lastTuEnd != start) {
|
||||
// add pause
|
||||
res.push({
|
||||
start: lastTuEnd,
|
||||
end: start
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!end) {
|
||||
res.push([start, index]);
|
||||
} else {
|
||||
res.push({
|
||||
start,
|
||||
end,
|
||||
index
|
||||
});
|
||||
res.push(end);
|
||||
}
|
||||
return res;
|
||||
}, []).slice(0, -1);
|
||||
},
|
||||
axisPartsSave() {
|
||||
if (!this.axisParts[this.axisParts.length - 1].end)
|
||||
return this.axisParts.slice(0, -1);
|
||||
return this.axisParts;
|
||||
},
|
||||
start() {
|
||||
return this.axisPartsWithBreaks[0].start;
|
||||
},
|
||||
end() {
|
||||
return this.axisPartsWithBreaks[this.axisPartsWithBreaks.length - 1].end;
|
||||
},
|
||||
ends() {
|
||||
const ends = [];
|
||||
const partsEnds = this.axisPartsWithBreaks
|
||||
.filter(p => p.index !== undefined)
|
||||
.map(p => p.end);
|
||||
for (var date of this.axisMain)
|
||||
for (var part of partsEnds)
|
||||
ends.push(date.plus(part));
|
||||
|
||||
return ends;
|
||||
},
|
||||
axisMainBorders() {
|
||||
return this.axisMain.reduce(
|
||||
(res, curr) => res.concat([curr.plus(this.start), curr.plus(this.end)]),
|
||||
[]
|
||||
);
|
||||
},
|
||||
eventsAllDay() {
|
||||
if (!this.allDayEvents)
|
||||
return [];
|
||||
return this.mapIntoMainAxis(this.originalEvents.filter(event => event.orig.allDayEvent));
|
||||
},
|
||||
eventsNormal() {
|
||||
if (!this.allDayEvents)
|
||||
return this.events;
|
||||
return this.mapIntoMainAxis(this.originalEvents.filter(event => !event.orig.allDayEvent));
|
||||
},
|
||||
events() {
|
||||
return this.mapIntoMainAxis(this.originalEvents);
|
||||
},
|
||||
backgrounds() {
|
||||
return this.mapIntoMainAxis(this.originalBackgrounds);
|
||||
},
|
||||
hasValidEvents() {
|
||||
return this.events.find(e => e.length);
|
||||
},
|
||||
styleGridCols() {
|
||||
let cols = 'repeat(' + this.axisMain.length + ', 1fr)';
|
||||
if (this.axisMainCollapsible) {
|
||||
if (this.hasValidEvents)
|
||||
cols = this.events
|
||||
.map(e => e.length
|
||||
? '1fr'
|
||||
: 'var(--fhc-calendar-axis-collapsible, .5fr)')
|
||||
.join(' ');
|
||||
}
|
||||
return cols;
|
||||
},
|
||||
styleGridRows() {
|
||||
const gridlines = {};
|
||||
|
||||
this.axisPartsWithBreaks.forEach(part => {
|
||||
let ts = part.start.toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts];
|
||||
if (part.index !== undefined)
|
||||
gridlines[ts].push('ps_' + part.index);
|
||||
ts = part.end.toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts];
|
||||
if (part.index !== undefined)
|
||||
gridlines[ts].push('pe_' + part.index);
|
||||
});
|
||||
|
||||
this.eventsNormal.forEach((events, mainIndex) => {
|
||||
let day = this.axisMain[mainIndex];
|
||||
events.forEach(event => {
|
||||
if (!event.startsHere && !event.endsHere)
|
||||
return;
|
||||
|
||||
if (event.startsHere) {
|
||||
let ts = event.start.diff(day).toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts, 'e_' + ts];
|
||||
}
|
||||
if (event.endsHere) {
|
||||
let ts = event.end.diff(day).toMillis();
|
||||
if (!gridlines[ts])
|
||||
gridlines[ts] = ['t_' + ts, 'e_' + ts];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Object.keys(gridlines).sort((a,b) => parseInt(a)-parseInt(b)).map((start, i, keys) => {
|
||||
let end = keys[i + 1];
|
||||
if (!end) {
|
||||
gridlines[start].push('end');
|
||||
return '[' + gridlines[start].join(' ') + ']';
|
||||
}
|
||||
return '[' + gridlines[start].join(' ') + '] ' + (end - start) + 'fr';
|
||||
}).join(' ');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mapIntoMainAxis(target) {
|
||||
const result = Array.from({length: this.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];
|
||||
if (event.orig?.allDayEvent) {
|
||||
laneStart = laneStart.startOf('day');
|
||||
laneEnd = laneEnd.endOf('day');
|
||||
}
|
||||
if (start < laneEnd && end > laneStart) {
|
||||
const startsHere = start >= laneStart;
|
||||
const endsHere = end <= laneEnd;
|
||||
result[i].push({
|
||||
...event,
|
||||
startsHere,
|
||||
endsHere
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/* DRAG AND DROP */
|
||||
getPageTop(el) {
|
||||
let pageTop = el.offsetTop;
|
||||
if (el.offsetParent)
|
||||
pageTop += this.getPageTop(el.offsetParent);
|
||||
return pageTop;
|
||||
},
|
||||
getPageLeft(el) {
|
||||
let pageLeft = el.offsetLeft;
|
||||
if (el.offsetParent)
|
||||
pageLeft += this.getPageLeft(el.offsetParent);
|
||||
return pageLeft;
|
||||
},
|
||||
getTimestampFromMouse(evt, dayTimestamp) {
|
||||
let mouse, mouseFrac;
|
||||
if (this.flipAxis) {
|
||||
mouse = evt.pageX - this.getPageLeft(this.$refs.body) + this.$refs.main.scrollLeft;
|
||||
mouseFrac = mouse / this.$refs.body.offsetWidth;
|
||||
} else {
|
||||
mouse = evt.pageY - this.getPageTop(this.$refs.body) + this.$refs.main.scrollTop;
|
||||
mouseFrac = mouse / this.$refs.body.offsetHeight;
|
||||
}
|
||||
|
||||
return dayTimestamp + this.start + Math.floor((this.end - this.start) * mouseFrac);
|
||||
},
|
||||
|
||||
/* SCROLLING */
|
||||
enableAutoScroll() {
|
||||
if (!this.resizeObserver)
|
||||
this.resizeObserver = new ResizeObserver(this.scrollToEarliestEvent);
|
||||
this.resizeObserver.observe(this.$refs.body);
|
||||
|
||||
if (!this.mutationObserver)
|
||||
this.mutationObserver = new MutationObserver(mutations => {
|
||||
if (mutations.some(m => m.addedNodes.length && [].some.call(m.addedNodes, el => el.matches && el.matches('.fhc-calendar-base-grid-line-event'))))
|
||||
this.scrollToEarliestEvent();
|
||||
});
|
||||
this.mutationObserver.observe(this.$refs.body, {
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
|
||||
this.scrollToEarliestEvent();
|
||||
},
|
||||
disableAutoScroll() {
|
||||
if (this.resizeObserver)
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
|
||||
if (this.mutationObserver)
|
||||
this.mutationObserver.disconnect();
|
||||
this.mutationObserver = null;
|
||||
},
|
||||
scrollToEarliestEvent() {
|
||||
const eventElements = this.$refs.scroller.querySelectorAll('.fhc-calendar-base-grid-line-event');
|
||||
|
||||
let earliestEventOffset = [0, null];
|
||||
for (var el of eventElements.values()) {
|
||||
const top = el.offsetTop;
|
||||
if (!earliestEventOffset[1] || top < earliestEventOffset[0])
|
||||
earliestEventOffset = [top, el];
|
||||
}
|
||||
|
||||
this.userScroll = false;
|
||||
if (earliestEventOffset[1]) {
|
||||
earliestEventOffset[1].scrollIntoView({ behavior: "smooth" });
|
||||
} else {
|
||||
this.$refs.scroller.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.disableAutoScroll();
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid"
|
||||
style="display:grid;width:100%;height:100%"
|
||||
:style="'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>
|
||||
</div>
|
||||
<div
|
||||
v-if="allDayEvents"
|
||||
class="grid-allday"
|
||||
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-slot="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
</grid-line-event>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="scroller"
|
||||
@scrollend="userScroll ? disableAutoScroll() : userScroll = true"
|
||||
style="display:grid;overflow:auto"
|
||||
:style="'grid-' + axisCol + ':1/-1;grid-template-' + axisCol + 's:subgrid'"
|
||||
>
|
||||
<div
|
||||
ref="main"
|
||||
class="grid-main"
|
||||
style="position:relative;grid-column:1/-1;grid-row:1/-1;display:grid"
|
||||
:style="'grid-template-' + axisCol + 's:subgrid;grid-template-' + axisRow + 's:' + styleGridRows"
|
||||
>
|
||||
<div
|
||||
v-for="(part, index) in axisPartsSave"
|
||||
:key="index"
|
||||
class="part-header"
|
||||
:style="'grid-' + axisCol + ':1;grid-' + axisRow + ': ps_' + index + '/pe_' + index"
|
||||
>
|
||||
<slot name="part-header" v-bind="{ index, part }" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="body"
|
||||
class="grid-body"
|
||||
style="display:grid;grid-template-rows:subgrid;grid-template-columns:subgrid"
|
||||
:style="'grid-' + axisCol + ':2/-1;grid-' + axisRow + ':1/-1'"
|
||||
v-cal-dnd:dropcage
|
||||
@calendar-dragenter="dragging = true"
|
||||
@calendar-dragleave="dragging = false"
|
||||
@dragover="dropAllowed ? $event.preventDefault() : null"
|
||||
>
|
||||
<template
|
||||
v-for="(date, index) in axisMain"
|
||||
: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 }" />
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import LineEvent from './Line/Event.js';
|
||||
import LineBackground from './Line/Background.js';
|
||||
|
||||
/**
|
||||
* TODO(chris):
|
||||
* Event overflow for Month mode (more-button)
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "GridLine",
|
||||
components: {
|
||||
LineEvent,
|
||||
LineBackground
|
||||
},
|
||||
inject: {
|
||||
axisRow: "axisRow"
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
start: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
end: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
events: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
backgrounds: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
eventsWithRowInfo() {
|
||||
const events = [];
|
||||
this.events.forEach(event => {
|
||||
const rows = [1, -1];
|
||||
if (event.startsHere) {
|
||||
rows[0] = 't_' + event.start.diff(this.date).toMillis();
|
||||
}
|
||||
if (event.endsHere) {
|
||||
rows[1] = 't_' + event.end.diff(this.date).toMillis();
|
||||
}
|
||||
|
||||
events.push({
|
||||
...event,
|
||||
rows
|
||||
});
|
||||
});
|
||||
return events;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line"
|
||||
style="position:relative;display:grid;grid-auto-flow:dense"
|
||||
:style="'grid-template-' + axisRow + 's:subgrid'"
|
||||
>
|
||||
<line-background
|
||||
v-for="bg in backgrounds"
|
||||
:start="start"
|
||||
:end="end"
|
||||
:background="bg"
|
||||
></line-background>
|
||||
<line-event
|
||||
v-for="(event, i) in eventsWithRowInfo"
|
||||
:key="i"
|
||||
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
|
||||
:event="event"
|
||||
>
|
||||
<template v-slot="slot">
|
||||
<slot name="event" v-bind="slot" />
|
||||
</template>
|
||||
</line-event>
|
||||
<slot name="dropzone" />
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
export default {
|
||||
name: "GridLineBackground",
|
||||
inject: {
|
||||
flipAxis: "flipAxis"
|
||||
},
|
||||
props: {
|
||||
start: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
end: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
background: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
if (!value.start && !value.end)
|
||||
return false;
|
||||
if (value.start && !(value.start instanceof luxon.DateTime))
|
||||
return false;
|
||||
if (value.end && !(value.end instanceof luxon.DateTime))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
if (!this.background.endsHere && !this.background.startsHere)
|
||||
return this.background.style;
|
||||
|
||||
const perc = (this.end.ts - this.start.ts) / 100;
|
||||
|
||||
let border = {};
|
||||
if (this.background.startsHere)
|
||||
border[this.flipAxis ? 'left' : 'top'] = (this.background.start.diff(this.start)) / perc + '%';
|
||||
if (this.background.endsHere)
|
||||
border[this.flipAxis ? 'right' : 'bottom'] = (this.end.diff(this.background.end)) / perc + '%';
|
||||
|
||||
if (!this.background.style)
|
||||
return border;
|
||||
|
||||
return [this.background.style, border];
|
||||
},
|
||||
classes() {
|
||||
if (!this.background.endsHere && !this.background.startsHere)
|
||||
return this.background.class;
|
||||
|
||||
const result = [];
|
||||
if (this.background.class)
|
||||
result.push(this.background.class);
|
||||
if (this.background.startsHere)
|
||||
result.push('bg-begin');
|
||||
if (this.background.endsHere)
|
||||
result.push('bg-end');
|
||||
return result;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line-background"
|
||||
:class="classes"
|
||||
style="position:absolute;inset:0;z-index:0"
|
||||
:style="styles"
|
||||
:title="background.title"
|
||||
>
|
||||
<span v-if="background.label">{{ background.label }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import CalDnd from '../../../../../directives/Calendar/DragAndDrop.js';
|
||||
import CalClick from '../../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "GridLineEvent",
|
||||
directives: {
|
||||
CalDnd,
|
||||
CalClick
|
||||
},
|
||||
inject: {
|
||||
draggableEvents: "draggableEvents",
|
||||
mode: "mode"
|
||||
},
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return (value.start && value.end && value.orig);
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isHeaderOrFooter() {
|
||||
return ['header', 'footer'].includes(this.event.orig);
|
||||
},
|
||||
draggable() {
|
||||
return !this.isHeaderOrFooter && this.draggableEvents(this.event.orig, this.mode);
|
||||
},
|
||||
classes() {
|
||||
const classes = [];
|
||||
if (this.isHeaderOrFooter) {
|
||||
classes.push('event-' + this.event.orig);
|
||||
} else {
|
||||
if (this.event.startsHere)
|
||||
classes.push('event-begin');
|
||||
if (this.event.endsHere)
|
||||
classes.push('event-end');
|
||||
}
|
||||
return classes
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-grid-line-event event"
|
||||
:class="classes"
|
||||
style="z-index: 1"
|
||||
:draggable="draggable"
|
||||
v-cal-dnd:draggable="event"
|
||||
v-cal-click:event="isHeaderOrFooter ? event : event.orig"
|
||||
>
|
||||
<slot :event="isHeaderOrFooter ? event : event.orig">
|
||||
{{ event.orig }}
|
||||
</slot>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* TODO(chris): use click-directive
|
||||
*/
|
||||
import DatePicker from './Header/Datepicker.js';
|
||||
|
||||
export default {
|
||||
name: "CalendarHeader",
|
||||
components: {
|
||||
DatePicker
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modeOptions: {
|
||||
type: Object,
|
||||
default: {}
|
||||
},
|
||||
btnMonth: Boolean,
|
||||
btnWeek: Boolean,
|
||||
btnDay: Boolean,
|
||||
btnList: Boolean
|
||||
},
|
||||
emits: [
|
||||
"next",
|
||||
"prev",
|
||||
"click:mode",
|
||||
"update:date",
|
||||
"update:mode"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
open: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
clickMode(evt, mode) {
|
||||
this.$emit('click:mode', evt);
|
||||
if (!evt.defaultPrevented)
|
||||
this.$emit('update:mode', mode);
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-base-header">
|
||||
<div class="header-actions">
|
||||
<div class="header-userdefined">
|
||||
<slot />
|
||||
</div>
|
||||
<div class="header-modes">
|
||||
<div class="d-flex gap-1 justify-content-end" role="group">
|
||||
<button
|
||||
v-if="btnMonth"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'month'}"
|
||||
@click="clickMode($event, 'month')"
|
||||
>
|
||||
<i class="fa fa-calendar-days"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnWeek"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'week'}"
|
||||
@click="clickMode($event, 'week')"
|
||||
>
|
||||
<i class="fa fa-calendar-week"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnDay"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'day'}"
|
||||
@click="clickMode($event, 'day')"
|
||||
>
|
||||
<i class="fa fa-calendar-day"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="btnList"
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
:class="{active: mode === 'list'}"
|
||||
@click="clickMode($event, 'list')"
|
||||
>
|
||||
<i class="fa fa-table-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-picker">
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
class="btn btn-outline-secondary border-0"
|
||||
@click="$emit('prev')"
|
||||
:disabled="open"
|
||||
>
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<date-picker
|
||||
:mode="mode"
|
||||
:date="date"
|
||||
@update:date="$emit('update:date', $event)"
|
||||
@open="open = true"
|
||||
@closed="open = false"
|
||||
:list-length="modeOptions.length"
|
||||
/>
|
||||
<button
|
||||
class="btn btn-outline-secondary border-0"
|
||||
@click="$emit('next')"
|
||||
:disabled="open"
|
||||
>
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// TODO(chris): translate aria-labels
|
||||
|
||||
export default {
|
||||
name: "CalendarHeaderDatepicker",
|
||||
components: {
|
||||
VueDatePicker
|
||||
},
|
||||
inject: [
|
||||
"locale",
|
||||
"timezone"
|
||||
],
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
listLength: {
|
||||
type: Number,
|
||||
default: 7
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:date"
|
||||
],
|
||||
computed: {
|
||||
convertedDate() {
|
||||
// convert to target TZ then strip TZ Information
|
||||
// so the datepicker can work with local times
|
||||
return this.date.setZone(this.timezone).setZone('local', { keepLocalTime: true });
|
||||
},
|
||||
current() {
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
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 "week":
|
||||
return [this.convertedDate.startOf('week', { useLocaleWeeks: true }).ts, this.convertedDate.endOf('week', { useLocaleWeeks: true }).ts];
|
||||
case "day":
|
||||
return this.convertedDate;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
title() {
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
return this.date.toLocaleString({ month: 'long', year: 'numeric' });
|
||||
case "week":
|
||||
var year = this.date.localWeekYear;
|
||||
var week = this.date.toFormat('nn');
|
||||
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 "day":
|
||||
return this.date.toLocaleString(luxon.DateTime.DATE_FULL);
|
||||
default:
|
||||
return 'View not Supported';
|
||||
}
|
||||
},
|
||||
weekStart() {
|
||||
return luxon.Info.getStartOfWeek(this.date)%7;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update(value) {
|
||||
let date;
|
||||
switch (this.mode) {
|
||||
case "month":
|
||||
value.month++;
|
||||
date = luxon.DateTime.fromObject(value).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
|
||||
break;
|
||||
case "list":
|
||||
case "week":
|
||||
date = luxon.DateTime.fromJSDate(value[0]).setZone(this.timezone, { keepLocalTime: true }).setLocale(this.locale);
|
||||
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);
|
||||
},
|
||||
weekNumbers(date) {
|
||||
return luxon.DateTime.fromJSDate(date, { locale: this.locale }).localWeekNumber;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<vue-date-picker
|
||||
:model-value="current"
|
||||
@update:model-value="update"
|
||||
:format="() => title"
|
||||
:month-picker="mode == 'month'"
|
||||
:week-picker="mode == 'week'"
|
||||
:range="mode == 'list' ? { autoRange: listLength - 1 } : false"
|
||||
:text-input="mode == 'day'"
|
||||
:week-start="weekStart"
|
||||
:week-numbers="{ type: weekNumbers }"
|
||||
:clearable="false"
|
||||
:enable-time-picker="false"
|
||||
:config="{ keepActionRow: mode != 'month' }"
|
||||
:action-row="{ showSelect: false, showCancel: false, showNow: mode != 'month', showPreview: false }"
|
||||
auto-apply
|
||||
six-weeks
|
||||
teleport
|
||||
:locale="locale"
|
||||
:now-button-label="$p.t('calendar/today')"
|
||||
:week-num-name="$p.t('calendar/kw')"
|
||||
/>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelDay",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleFull() {
|
||||
return this.date.toLocaleString({day: 'numeric', month: 'long', year: 'numeric'});
|
||||
},
|
||||
titleLong() {
|
||||
return this.date.toLocaleString({day: '2-digit', month: '2-digit', year: 'numeric'});
|
||||
},
|
||||
titleShort() {
|
||||
return this.date.toLocaleString({day: 'numeric', month: 'numeric'});
|
||||
},
|
||||
titleNarrow() {
|
||||
return this.date.toLocaleString({day: 'numeric'});
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-label-day"
|
||||
v-cal-click:day="date"
|
||||
>
|
||||
<span class="full">{{ titleFull }}</span>
|
||||
<span class="long">{{ titleLong }}</span>
|
||||
<span class="short">{{ titleShort }}</span>
|
||||
<span class="narrow">{{ titleNarrow }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelDow",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
titleLong() {
|
||||
return this.date.toLocaleString({weekday: 'long'});
|
||||
},
|
||||
titleShort() {
|
||||
return this.date.toLocaleString({weekday: 'short'});
|
||||
},
|
||||
titleNarrow() {
|
||||
return this.date.toLocaleString({weekday: 'narrow'});
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-label-dow"
|
||||
v-cal-click:dow="date"
|
||||
>
|
||||
<b class="long">{{ titleLong }}</b>
|
||||
<b class="short">{{ titleShort }}</b>
|
||||
<b class="narrow">{{ titleNarrow }}</b>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
export default {
|
||||
name: "LabelTime",
|
||||
props: {
|
||||
part: {
|
||||
type: [luxon.Duration, Number, Object],
|
||||
required: true,
|
||||
validator(value) {
|
||||
if (value instanceof Object) {
|
||||
if (value instanceof luxon.Duration)
|
||||
return true;
|
||||
let start_ok = true;
|
||||
let end_ok = true;
|
||||
if (value.start) {
|
||||
start_ok = (
|
||||
value.start instanceof luxon.Duration
|
||||
|| Number.isInteger(value.start)
|
||||
);
|
||||
}
|
||||
if (value.end) {
|
||||
end_ok = (
|
||||
value.end instanceof luxon.Duration
|
||||
|| Number.isInteger(value.end)
|
||||
);
|
||||
}
|
||||
return start_ok && end_ok;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sanitizedTimestamps() {
|
||||
return this.part.start || this.part.end ? this.part : { start: this.part };
|
||||
},
|
||||
start() {
|
||||
if (!this.sanitizedTimestamps.start)
|
||||
return null;
|
||||
return this.formatTime(this.sanitizedTimestamps.start);
|
||||
},
|
||||
end() {
|
||||
if (!this.sanitizedTimestamps.end)
|
||||
return null;
|
||||
return this.formatTime(this.sanitizedTimestamps.end);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatTime(date) {
|
||||
return date.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-base-label-time">
|
||||
<span v-if="start">{{ start }}</span>
|
||||
<span v-if="end">-</span>
|
||||
<span v-if="end">{{ end }}</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
export default {
|
||||
name: "LabelWeek",
|
||||
directives: {
|
||||
CalClick
|
||||
},
|
||||
props: {
|
||||
date: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
weeks() {
|
||||
const firstDay = this.date.startOf('week', { useLocaleWeeks: true });
|
||||
const lastDay = this.date.endOf('week', { useLocaleWeeks: true });
|
||||
|
||||
const weeks = [
|
||||
{ number: firstDay.localWeekNumber, year: firstDay.localWeekYear },
|
||||
{ number: lastDay.localWeekNumber, year: lastDay.localWeekYear }
|
||||
];
|
||||
if (weeks[0].number == weeks[1].number)
|
||||
weeks.pop();
|
||||
return weeks;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-base-label-week">
|
||||
<span
|
||||
v-for="week in weeks"
|
||||
v-cal-click:week="week"
|
||||
>
|
||||
{{ week.number }}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
export default {
|
||||
name: 'CalendarSlider',
|
||||
inject: {
|
||||
time: {
|
||||
from: "sliderTime",
|
||||
default: ".3s"
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'slid'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
target: 0,
|
||||
extrasAfter: 0,
|
||||
extrasBefore: 0,
|
||||
running: false,
|
||||
promiseResolve: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
itemsAfter() {
|
||||
return [...Array(this.extrasAfter)].map((i, k) => 1+k);
|
||||
},
|
||||
itemsBefore() {
|
||||
return [...Array(this.extrasBefore)].map((i, k) => k-this.extrasBefore);
|
||||
},
|
||||
styleSlider() {
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
};
|
||||
if (this.running) {
|
||||
style.left = (-this.target * 100) + '%';
|
||||
style.transition = 'left ' + this.time + ' ease-in-out';
|
||||
}
|
||||
return style;
|
||||
},
|
||||
styleBefore() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
right: '100%',
|
||||
width: (this.extrasBefore * 100) + '%'
|
||||
};
|
||||
},
|
||||
styleAfter() {
|
||||
return {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
left: '100%',
|
||||
width: (this.extrasAfter * 100) + '%'
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
return this.slidePages(-1);
|
||||
},
|
||||
nextPage() {
|
||||
return this.slidePages(1);
|
||||
},
|
||||
slidePages(dir) {
|
||||
return new Promise(resolve => {
|
||||
this.promiseResolve = resolve;
|
||||
this.running = true;
|
||||
const newTarget = this.target + dir;
|
||||
if (newTarget > 0) {
|
||||
if (this.extrasAfter < newTarget)
|
||||
this.extrasAfter = newTarget;
|
||||
} else if (newTarget < 0) {
|
||||
if (-this.extrasBefore > newTarget)
|
||||
this.extrasBefore = -newTarget;
|
||||
}
|
||||
this.target = newTarget;
|
||||
});
|
||||
},
|
||||
endSlide() {
|
||||
if (this.promiseResolve) {
|
||||
this.promiseResolve(this.target);
|
||||
this.promiseResolve = null;
|
||||
}
|
||||
this.$emit('slid', this.target);
|
||||
this.running = false;
|
||||
this.target = 0;
|
||||
this.extrasAfter = this.extrasBefore = 0;
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="fhc-calendar-base-slider h-100"
|
||||
style="position:relative;overflow:hidden"
|
||||
>
|
||||
<div
|
||||
:style="styleSlider"
|
||||
@transitionend="endSlide"
|
||||
>
|
||||
<div :style="styleBefore">
|
||||
<div
|
||||
v-for="i in itemsBefore"
|
||||
:key="i"
|
||||
style="height:100%;width:100%"
|
||||
>
|
||||
<slot :offset="i" />
|
||||
</div>
|
||||
</div>
|
||||
<div :style="styleAfter">
|
||||
<div
|
||||
v-for="i in itemsAfter"
|
||||
:key="i"
|
||||
style="height:100%;width:100%"
|
||||
>
|
||||
<slot :offset="i" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:100%;width:100%">
|
||||
<slot :offset="0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
import CalendarMonth from './Month.js';
|
||||
import CalendarMonths from './Months.js';
|
||||
import CalendarYears from './Years.js';
|
||||
import CalendarWeek from './Week.js';
|
||||
import CalendarWeeks from './Weeks.js';
|
||||
import CalendarDay from './Day.js';
|
||||
import CalendarMinimized from './Minimized.js';
|
||||
import CalendarDate from '../../composables/CalendarDate.js';
|
||||
import CalendarDates from '../../composables/CalendarDates.js';
|
||||
|
||||
const todayDate = new Date(new Date().setHours(0, 0, 0, 0));
|
||||
const today = todayDate.getTime()
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CalendarMonth,
|
||||
CalendarMonths,
|
||||
CalendarYears,
|
||||
CalendarWeek,
|
||||
CalendarWeeks,
|
||||
CalendarDay,
|
||||
CalendarMinimized,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
today,
|
||||
todayDate,
|
||||
date: this.date,
|
||||
focusDate: this.focusDate,
|
||||
size: Vue.computed({ get: () => this.size }),
|
||||
containerHeight: Vue.computed({ get: () => this.containerHeight }),
|
||||
containerWidth: Vue.computed({ get: () => this.containerWidth }),
|
||||
|
||||
events: Vue.computed(() => this.eventsPerDay),
|
||||
filteredEvents: Vue.computed(() => this.filteredEvents),
|
||||
minimized: Vue.computed({ get: () => this.minimized, set: v => this.$emit('update:minimized', v) }),
|
||||
showWeeks: this.showWeeks,
|
||||
noMonthView: this.noMonthView,
|
||||
noWeekView: this.noWeekView,
|
||||
eventsAreNull: Vue.computed(() => this.events === null),
|
||||
mode: Vue.computed(()=>this.mode),
|
||||
selectedEvent: Vue.computed(() => this.selectedEvent),
|
||||
setSelectedEvent: (event)=>{this.selectedEvent = event;},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
events: Array,
|
||||
initialDate: {
|
||||
type: [Date, String],
|
||||
default: new Date()
|
||||
},
|
||||
showWeeks: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
initialMode: {
|
||||
type: String,
|
||||
default: 'month'
|
||||
},
|
||||
minimized: Boolean,
|
||||
noWeekView: Boolean,
|
||||
noMonthView: Boolean
|
||||
},
|
||||
watch:{
|
||||
mode(newVal) {
|
||||
this.$emit('change:mode', newVal)
|
||||
},
|
||||
selectedEvent:{
|
||||
handler(newSelectedEvent) {
|
||||
this.$emit('selectedEvent', newSelectedEvent);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
// scroll to the first event if the html element was found
|
||||
scrollTime({focusDate,scrollTime}){
|
||||
// return early if the scrollTime is not set
|
||||
if(!scrollTime) return;
|
||||
// scroll the Stundenplan to the closest event
|
||||
Vue.nextTick(()=>{
|
||||
let previousScrollAnchor = document.getElementById('scroll' + (scrollTime - 1) + this.focusDate.d + this.focusDate.w)
|
||||
let scrollAnchor = document.getElementById('scroll' + scrollTime + this.focusDate.d + this.focusDate.w);
|
||||
if (previousScrollAnchor) {
|
||||
previousScrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
else {
|
||||
if (scrollAnchor) {
|
||||
scrollAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'select:day',
|
||||
'select:event',
|
||||
'change:range',
|
||||
'update:minimized',
|
||||
'selectedEvent',
|
||||
'change:offset'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
header: '',
|
||||
prevMode: null,
|
||||
currMode: null,
|
||||
date: new CalendarDate(),
|
||||
focusDate: new CalendarDate(),
|
||||
size: 0,
|
||||
containerWidth: 0,
|
||||
containerHeight: 0,
|
||||
selectedEvent:null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeClass() {
|
||||
// mainly determines calendar font-size
|
||||
return 'fhc-calendar-' + ['xs', 'sm', 'md', 'lg'][this.size];
|
||||
},
|
||||
mode: {
|
||||
get() { return this.minimized ? 'minimized' : this.currMode; },
|
||||
set(v) {
|
||||
if (!v && this.prevMode) {
|
||||
this.currMode = this.prevMode;
|
||||
this.prevMode = null;
|
||||
} else {
|
||||
this.prevMode = this.currMode;
|
||||
this.currMode = v;
|
||||
}
|
||||
}
|
||||
},
|
||||
eventsPerDay() {
|
||||
if (!this.events)
|
||||
return {};
|
||||
return this.events.reduce((result, event) => {
|
||||
let days = Math.ceil((event.end - event.start) / 86400000) || 1;
|
||||
while (days-- > 0) {
|
||||
let day = (new Date(event.start.getFullYear(), event.start.getMonth(), event.start.getDate() + days)).toDateString();
|
||||
if (!result[day])
|
||||
result[day] = [];
|
||||
result[day].push(event);
|
||||
}
|
||||
return result;
|
||||
}, {});
|
||||
},
|
||||
// returns the hour of the earliest event, used to scroll to the events in the calendar (week / day view)
|
||||
scrollTime() {
|
||||
// return the first beginning time of the filtered events
|
||||
if(this.filteredEvents && Array.isArray(this.filteredEvents) && this.filteredEvents.length > 0)
|
||||
{
|
||||
let scrollTime = parseInt(this.filteredEvents.sort((a, b) => parseInt(a.beginn) - parseInt(b.beginn))[0].beginn);
|
||||
// to ensure that the scrollTime watcher triggers even if the scrollTime doesn't change, it returns both the scrollTime and the focusDate
|
||||
return { focusDate: this.focusDate, scrollTime };
|
||||
}
|
||||
// there is no event that matches the current view mode constraints
|
||||
else
|
||||
{
|
||||
return { focusDate: this.focusDate, scrollTime: null };
|
||||
}
|
||||
},
|
||||
// filters the events based on the current calendar view mode
|
||||
// week view - filter events based on their week
|
||||
// day view - filter events based on their day and week
|
||||
// month view - does not filter the events
|
||||
filteredEvents: function(){
|
||||
if (this.events && Array.isArray(this.events) && this.events.length > 0) {
|
||||
let filteredEvents = this.events.filter(event => {
|
||||
let eventDate = new CalendarDate(new Date(event.datum));
|
||||
if (this.mode == 'week' || this.mode == 'Week')
|
||||
{
|
||||
// week view filters the elements only for the same week
|
||||
return this.focusDate.w == eventDate.w;
|
||||
}
|
||||
else if (this.mode == 'day' || this.mode == 'Day')
|
||||
{
|
||||
// day view filters the elements for the same day and the same week
|
||||
return this.focusDate.d == eventDate.d && this.focusDate.w == eventDate.w;
|
||||
}
|
||||
else
|
||||
{
|
||||
// returns all the events, does not filter the events
|
||||
return true;
|
||||
}
|
||||
})
|
||||
|
||||
return filteredEvents;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setMode(mode) {
|
||||
this.mode = mode
|
||||
},
|
||||
handleInput(day) {
|
||||
this.$emit(day[0], day[1]);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
const initMode = this.initialMode.toLowerCase()
|
||||
const allowedInitialModes = ['day'];
|
||||
if (!this.noWeekView)
|
||||
allowedInitialModes.push('week');
|
||||
if (!this.noMonthView)
|
||||
allowedInitialModes.push('month');
|
||||
this.mode = allowedInitialModes[allowedInitialModes.indexOf(initMode)] || allowedInitialModes.pop();
|
||||
this.date.set(new Date(this.initialDate));
|
||||
this.focusDate.set(this.date);
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.container) {
|
||||
new ResizeObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
const w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
|
||||
const h = entry.contentBoxSize ? entry.contentBoxSize[0].blockSize : entry.contentRect.height;
|
||||
|
||||
// https://getbootstrap.com/docs/5.0/layout/breakpoints/
|
||||
// bootstrap breakpoints watch window size and this function monitors container size of calendar itself.
|
||||
// calendar is using bootstrap breakpoints which influence layout, which retriggers this function
|
||||
// -> some width constellations will loop so we dont use values around bs5 breakpoints
|
||||
// ['xs', 'sm', 'md', 'lg'][this.size]
|
||||
if (w >= 600)
|
||||
this.size = 3;
|
||||
else if (w >= 350)
|
||||
this.size = 2;
|
||||
else if (w >= 250)
|
||||
this.size = 1;
|
||||
else
|
||||
this.size = 0;
|
||||
|
||||
this.containerWidth = w
|
||||
this.containerHeight = h
|
||||
}
|
||||
}).observe(this.$refs.container);
|
||||
}
|
||||
},
|
||||
unmounted(){
|
||||
CalendarDates.cleanup();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
|
||||
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)"
|
||||
@input="handleInput" @change:offset="$emit('change:offset', $event)">
|
||||
<template #calendarDownloads>
|
||||
<slot name="calendarDownloads" ></slot>
|
||||
</template>
|
||||
<template #monthPage="{event,day}">
|
||||
<slot name="monthPage" :event="event" :day="day" ></slot>
|
||||
</template>
|
||||
<template #weekPage="{event,day}">
|
||||
<slot name="weekPage" :event="event" :day="day" ></slot>
|
||||
</template>
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<slot name="dayPage" :event="event" :day="day" :mobile="mobile"></slot>
|
||||
</template>
|
||||
<template #pageMobilContent="{lvMenu}">
|
||||
<slot name="pageMobilContent" :lvMenu="lvMenu"></slot>
|
||||
</template>
|
||||
<template #pageMobilContentEmpty>
|
||||
<slot name="pageMobilContentEmpty" ></slot>
|
||||
</template>
|
||||
<template #minimizedPage="{event,day}">
|
||||
<slot name="minimizedPage" :event="event" :day="day"></slot>
|
||||
</template>
|
||||
</component>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
import CalendarPane from './Pane.js';
|
||||
import CalendarDayPage from './Day/Page.js';
|
||||
import CalendarDate from '../../composables/CalendarDate.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
components: {
|
||||
CalendarDayPage,
|
||||
CalendarPane
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paneChanged(dir) {
|
||||
let previousDate = new CalendarDate(this.focusDate);
|
||||
this.focusDate.d += dir;
|
||||
this.emitRangeChanged(previousDate);
|
||||
},
|
||||
emitRangeChanged(previousDate) {
|
||||
this.$emit('change:range', { start: previousDate, end:this.focusDate });
|
||||
},
|
||||
prev() {
|
||||
this.$refs.pane.prev();
|
||||
this.$emit('change:offset', { y: 0, m: 0, d: -1 });
|
||||
},
|
||||
next() {
|
||||
this.$refs.pane.next();
|
||||
this.$emit('change:offset', { y: 0, m: 0, d: 1 });
|
||||
},
|
||||
selectEvent(event) {
|
||||
this.$emit('input', ['select:event', event]);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.emitRangeChanged();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="fhc-calendar-day">
|
||||
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'week')">
|
||||
<template #calendarDownloads>
|
||||
<slot name="calendarDownloads"></slot>
|
||||
</template>
|
||||
</calendar-header>
|
||||
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
|
||||
<calendar-day-page :active="slot.active" :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<slot name="dayPage" :event="event" :day="day" :mobile="mobile" ></slot>
|
||||
</template>
|
||||
<template #pageMobilContent="{lvMenu}">
|
||||
<slot name="pageMobilContent" :lvMenu="lvMenu" ></slot>
|
||||
</template>
|
||||
<template #pageMobilContentEmpty>
|
||||
<slot name="pageMobilContentEmpty" ></slot>
|
||||
</template>
|
||||
</calendar-day-page>
|
||||
</calendar-pane>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
import CalendarDate from '../../../composables/CalendarDate.js';
|
||||
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
|
||||
|
||||
import ApiStundenplan from '../../../api/factory/stundenplan.js';
|
||||
import ApiAddons from '../../../api/factory/addons.js';
|
||||
|
||||
function ggt(m, n) {
|
||||
return n == 0 ? m : ggt(n, m % n);
|
||||
}
|
||||
|
||||
function kgv(m, n) {
|
||||
return (m * n) / ggt(m, n);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DayPage',
|
||||
components: {
|
||||
LvModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hourPosition: null,
|
||||
curHourPosition: null,
|
||||
hourPositionTime: null,
|
||||
lvMenu: null,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'today',
|
||||
'todayDate',
|
||||
'date',
|
||||
'focusDate',
|
||||
'size',
|
||||
'events',
|
||||
'noMonthView',
|
||||
'filteredEvents',
|
||||
'isSliding',
|
||||
'calendarScrollTop',
|
||||
'calendarClientHeight',
|
||||
'setSelectedEvent',
|
||||
'selectedEvent',
|
||||
'rowMinHeight'
|
||||
],
|
||||
props: {
|
||||
year: Number,
|
||||
week: Number,
|
||||
active: Boolean,
|
||||
},
|
||||
emits: [
|
||||
'updateMode',
|
||||
'page:back',
|
||||
'page:forward',
|
||||
'input'
|
||||
],
|
||||
watch: {
|
||||
//TODO: on first render non of the day-page components are active and the watcher on selectedEvent does not fetch the lvMenu
|
||||
//TODO: workaround is to watch the active state and refetch in case the lvMenu is empty
|
||||
active: {
|
||||
handler(value) {
|
||||
if (value) {
|
||||
if (!this.lvMenu) {
|
||||
this.fetchLvMenu(this.selectedEvent);
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
eventsPerDayAndHour: {
|
||||
handler(newEvents) {
|
||||
// if no event is selected, select the first event of the day
|
||||
if (this.selectedEvent == null && newEvents[this.day.toDateString()]?.events.length > 0) {
|
||||
let events = newEvents[this.day.toDateString()]?.events;
|
||||
if (Array.isArray(events) && events.length > 0) {
|
||||
this.setSelectedEvent(events[0].orig);
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
selectedEvent: {
|
||||
handler(event) {
|
||||
// return early if the day-page component is not the active carousel item
|
||||
if (!this.active) {
|
||||
return;
|
||||
}
|
||||
this.lvMenu = null;
|
||||
this.fetchLvMenu(event);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
isSliding: {
|
||||
handler(value) {
|
||||
if (value) {
|
||||
this.setSelectedEvent(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
overlayStyle() {
|
||||
return {
|
||||
'background-color': '#F5E9D7',
|
||||
'position': 'absolute',
|
||||
'pointer-events': 'none',
|
||||
'z-index': 2,
|
||||
height: this.getDayTimePercent + '%',
|
||||
opacity: 0.5,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
},
|
||||
pageHeaderStyle() {
|
||||
return {
|
||||
'z-index': 4,
|
||||
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
|
||||
'grid-template-rows': 1,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
}
|
||||
},
|
||||
dayText(){
|
||||
if(!this.day)return {};
|
||||
return {
|
||||
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
|
||||
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
|
||||
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
|
||||
}
|
||||
},
|
||||
noLvStyle() {
|
||||
return {
|
||||
top: (this.calendarScrollTop + 100) + 'px',
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
'text-align': 'center',
|
||||
width: '100%',
|
||||
'z-index': 1,
|
||||
}
|
||||
},
|
||||
indicatorStyle() {
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
'padding-left': '3.5rem',
|
||||
'margin-top': '-1px',
|
||||
'z-index': 2,
|
||||
'border-color': '#00649C!important',
|
||||
top: this.hourPosition + 'px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
}
|
||||
},
|
||||
curTime() {
|
||||
const now = new Date();
|
||||
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
||||
},
|
||||
curIndicatorStyle() {
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
'padding-left': '7rem',
|
||||
'margin-top': '-1px',
|
||||
'z-index': 2,
|
||||
'border-color': '#00649C!important',
|
||||
top: this.getDayTimePercent + '%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
}
|
||||
},
|
||||
noEventsCondition() {
|
||||
return !this.isSliding && (this.filteredEvents?.length === 0 || !this.filteredEvents);
|
||||
},
|
||||
hours() {
|
||||
// returns an array with elements starting at 7 and ending at 24
|
||||
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
|
||||
},
|
||||
day() {
|
||||
return new Date(this.focusDate.y, this.focusDate.m, this.focusDate.d);
|
||||
},
|
||||
eventsPerDayAndHour() {
|
||||
// return early if the calendar pane is sliding
|
||||
if (this.isSliding) return {};
|
||||
|
||||
const res = {};
|
||||
|
||||
let key = this.day.toDateString();
|
||||
|
||||
let nextDay = new Date(this.day);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
nextDay.setMilliseconds(nextDay.getMilliseconds() - 1);
|
||||
let d = {events: [], lanes: 1};
|
||||
if (this.events[key]) {
|
||||
this.events[key].forEach(evt => {
|
||||
let event = {
|
||||
orig: evt,
|
||||
lane: 1,
|
||||
maxLane: 1,
|
||||
start: evt.start < this.day ? this.day : evt.start,
|
||||
end: evt.end > nextDay ? nextDay : evt.end,
|
||||
shared: [],
|
||||
setSharedMaxRecursive(doneItems) {
|
||||
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
|
||||
doneItems.push(this);
|
||||
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
|
||||
}
|
||||
};
|
||||
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
|
||||
event.shared.forEach(other => other.shared.push(event));
|
||||
let occupiedLanes = event.shared.map(other => other.lane);
|
||||
while (occupiedLanes.includes(event.lane))
|
||||
event.lane++;
|
||||
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
|
||||
if (event.maxLane > 1) {
|
||||
event.setSharedMaxRecursive([event]);
|
||||
}
|
||||
d.events.push(event);
|
||||
});
|
||||
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
|
||||
}
|
||||
res[key] = d;
|
||||
|
||||
return res;
|
||||
},
|
||||
smallestTimeFrame() {
|
||||
return [30, 15, 10, 5][this.size];
|
||||
},
|
||||
lookingAtToday() {
|
||||
return this.date.compare(this.todayDate)
|
||||
},
|
||||
getDayTimePercent() {
|
||||
const now = new Date(Date.now())
|
||||
const currentMinutes = now.getMinutes() + now.getHours() * 60
|
||||
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
|
||||
|
||||
return timePercentage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dayGridStyle(day) {
|
||||
const styleObj = {
|
||||
'grid-template-columns': '1 1fr',
|
||||
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
|
||||
}
|
||||
|
||||
if(this.date.compare(this.todayDate)) {
|
||||
styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
|
||||
styleObj['border-color'] = '#E8E8E8';
|
||||
// styleObj.opacity = 0.5; // would opaque the whole column
|
||||
}
|
||||
|
||||
return styleObj
|
||||
},
|
||||
fetchLvMenu(event) {
|
||||
if (event && event.type == 'lehreinheit') {
|
||||
this.$api
|
||||
.call(ApiStundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]))
|
||||
.then(res => res.data)
|
||||
.then(studiensemester_kurzbz => this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
event.lehreinheit_id,
|
||||
studiensemester_kurzbz
|
||||
)
|
||||
))
|
||||
.then(res => {
|
||||
if (res.data) {
|
||||
this.lvMenu = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
hourGridIdentifier(hour) {
|
||||
// this is the id attribute that is responsible to scroll the calender to the first event
|
||||
return 'scroll' + hour + this.focusDate.d + this.week;
|
||||
},
|
||||
hourGridStyle(hour) {
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
top: this.getAbsolutePositionForHour(hour),
|
||||
left: 0,
|
||||
right: 0,
|
||||
'z-index': 0,
|
||||
}
|
||||
},
|
||||
eventGridStyle(day, event) {
|
||||
return {
|
||||
'z-index': 1,
|
||||
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
|
||||
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
|
||||
'grid-row-start': this.dateToMinutesOfDay(event.start),
|
||||
'grid-row-end': this.dateToMinutesOfDay(event.end),
|
||||
'background-color': event.orig.color,
|
||||
'--test': this.dateToMinutesOfDay(event.end),
|
||||
}
|
||||
},
|
||||
eventClick(evt) {
|
||||
let event = evt.orig;
|
||||
this.setSelectedEvent(event);
|
||||
this.$emit('input', event);
|
||||
},
|
||||
calcHourPosition(event) {
|
||||
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
|
||||
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
|
||||
let position = event.clientY - top;
|
||||
// position percentage of total height
|
||||
let timePercentage = (position / height) * 100;
|
||||
// minute percentage of total minutes
|
||||
let result = (this.hours.length * 60) * (timePercentage / 100);
|
||||
// calculate time in float
|
||||
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
|
||||
// get hour part of time
|
||||
let currentHour = Math.floor(currentMinutes);
|
||||
// get float part of time
|
||||
let minutePercentage = currentMinutes % currentHour;
|
||||
// calculate minutes from float part of time
|
||||
let minute = Math.round(60 * minutePercentage);
|
||||
// convert minutes to 5 minute interval
|
||||
if (minute % 5 != 0) {
|
||||
minute = Math.round(minute / 5) * 5;
|
||||
}
|
||||
// in case the rounding made the minutes 60, increase the hour and reset the minutes
|
||||
if (minute == 60) {
|
||||
currentHour++;
|
||||
minute = 0;
|
||||
}
|
||||
|
||||
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
|
||||
// convert current time in minutes
|
||||
currentMinutes = currentHour * 60 + minute;
|
||||
// calculate the minutes percentage of the total minutes
|
||||
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
|
||||
// calculate the relative position of the time percentage
|
||||
position = height * (timePercentage / 100);
|
||||
this.hourPosition = position;
|
||||
|
||||
// add padding to minutes that consist of only one digit
|
||||
minute.toString().length == 1 ? minute = "0" + minute : minute;
|
||||
this.hourPositionTime = currentHour + ":" + minute;
|
||||
},
|
||||
getAbsolutePositionForHour(hour) {
|
||||
// used for the absolute positioning of the gutters of hours
|
||||
return (100 / this.hours.length) * (hour - (24 - this.hours.length)) + '%';
|
||||
},
|
||||
changeToMonth(day) {
|
||||
if (!this.noMonthView) {
|
||||
this.date.set(day);
|
||||
this.focusDate.set(day);
|
||||
this.$emit('updateMode', 'month');
|
||||
}
|
||||
},
|
||||
dateToMinutesOfDay(day) {
|
||||
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
|
||||
}
|
||||
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="fhc-calendar-day-page h-100">
|
||||
<div class="row m-0 h-100">
|
||||
<div class="col-12 col-xl-6 p-0 h-100">
|
||||
<div class="d-flex flex-column h-100">
|
||||
<div ref="header" class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle">
|
||||
<div type="button" class="flex-grow-1" :title="dayText.heading" @click.prevent="changeToMonth(day)">
|
||||
<div class="fw-bold">{{dayText.tag}}</div>
|
||||
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scroll g-0" style="height: 100%; overflow-y: scroll;">
|
||||
|
||||
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
|
||||
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
|
||||
|
||||
<Transition>
|
||||
<div v-if="hourPosition && !noEventsCondition" class="position-absolute border-top small" :style="indicatorStyle">
|
||||
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition>
|
||||
<div v-if="lookingAtToday && !noEventsCondition" class="position-absolute border-top small" :style="curIndicatorStyle">
|
||||
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<div>
|
||||
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">{{ $p.t('lehre/noLvFound') }}</h1>
|
||||
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
|
||||
|
||||
<div class="hours">
|
||||
<div v-for="hour in hours" :style="'min-height:' + rowMinHeight " :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
|
||||
</div>
|
||||
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
|
||||
<div v-if="lookingAtToday && !noEventsCondition" :style="overlayStyle"></div>
|
||||
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" v-contrast
|
||||
:selected="event.orig == selectedEvent" class="fhc-entry mx-2 small rounded overflow-hidden" >
|
||||
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
|
||||
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
|
||||
<slot name="dayPage" :event="event" :day="day" :mobile="false">
|
||||
<p>this is a slot placeholder</p>
|
||||
</slot>
|
||||
</div>
|
||||
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
|
||||
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
|
||||
<slot name="dayPage" :event="event" :day="day" :mobile="true">
|
||||
<p>this is a slot placeholder</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-xl-block col-xl-6 p-4 d-none" style="max-height: 100%">
|
||||
<div style="z-index:0; max-height: 100%" class="sticky-top d-flex justify-content-center align-items-center flex-column">
|
||||
<div style="max-height: 100%; overflow-y:auto;" class="w-100">
|
||||
<template v-if="selectedEvent && lvMenu">
|
||||
<slot name="pageMobilContent" :lvMenu="lvMenu" >
|
||||
<p>this is a slot placeholder</p>
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else-if="noEventsCondition">
|
||||
<slot name="pageMobilContentEmpty" >
|
||||
<h3>This is an slot placeholder</h3>
|
||||
</slot>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="p-4 d-flex w-100 justify-content-center align-items-center">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
export default {
|
||||
data(){
|
||||
return{
|
||||
selected: this.mode,
|
||||
modes:{
|
||||
day: { mode_bezeichnung: "day", icon: "fa-calendar-day" , condition:true},
|
||||
week: { mode_bezeichnung: "week", icon: "fa-calendar-week", condition: !this.noWeekView },
|
||||
month: { mode_bezeichnung: "month", icon: "fa-calendar-days", condition: !this.noMonthView },
|
||||
},
|
||||
headerPadding:null,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'eventsAreNull',
|
||||
'size',
|
||||
'mode',
|
||||
'noWeekView',
|
||||
'noMonthView',
|
||||
'containerWidth'
|
||||
],
|
||||
props: {
|
||||
title: String
|
||||
},
|
||||
emits: [
|
||||
'updateMode',
|
||||
'prev',
|
||||
'next',
|
||||
'click'
|
||||
],
|
||||
computed: {
|
||||
getHeaderClassSide() {
|
||||
return this.containerWidth > 780 ? 'col-3' : 'col-12'
|
||||
},
|
||||
getHeaderClassMiddle() {
|
||||
return this.containerWidth > 780 ? 'col-6' : 'col-12'
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="calendar-header card-header w-100">
|
||||
<div class="row align-items-center ">
|
||||
<div :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-start align-items-center">
|
||||
<slot name="calendarDownloads"></slot>
|
||||
</div>
|
||||
<div :class="getHeaderClassMiddle" :style="{'padding-left':headerPadding}">
|
||||
<div class="row align-items-center justify-content-center">
|
||||
<div class="col-auto p-2">
|
||||
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm':!this.size}" @click="$emit('prev')"><i class="fa fa-chevron-left"></i></button>
|
||||
</div>
|
||||
<div class="justify-content-center text-center col-auto">
|
||||
<div class="d-flex justify-content-center align-items-center">
|
||||
<button class="btn btn-link link-secondary text-decoration-none" :class="{'btn-sm': !this.size}" @click="$emit('click')">
|
||||
{{ title }}
|
||||
<i v-if="eventsAreNull" class="fa fa-spinner fa-pulse"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto p-2">
|
||||
<button class="btn btn-outline-secondary border-0" :class="{'btn-sm': !this.size}" @click="$emit('next')"><i class="fa fa-chevron-right"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="viewButtons" v-if="!noWeekView && !noMonthView" :class="getHeaderClassSide" class="d-flex justify-content-center justify-content-md-end align-items-center" style="pointer-events: none;">
|
||||
<div style="pointer-events: all;">
|
||||
<div role="group" aria-label="Kalender Modus">
|
||||
<button type="button" :class="{'active':mode_kurzbz.toLowerCase() === mode.toLowerCase()}" style="margin-right: 4px;" @click.prevent="$emit('updateMode',mode_kurzbz)" class="btn btn-outline-secondary" v-for="({mode_bezeichnung,icon,condition},mode_kurzbz) in modes">
|
||||
<i v-if="condition" class="fa" :class="icon" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -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="(newDate, newMode) => $emit('update:date', newDate, newMode)"
|
||||
@update:mode="(newMode, newDate) => $emit('update:mode', newMode, newDate)"
|
||||
@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>`
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
inject: [
|
||||
'size',
|
||||
'minimized',
|
||||
'date',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
start: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
maximize() {
|
||||
this.minimized = false;
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-minimized h-100 d-flex flex-column">
|
||||
<slot name="minimizedPage"></slot>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import DayView from './Day/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeDay",
|
||||
components: {
|
||||
BaseSlider,
|
||||
DayView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen",
|
||||
"requestModalClose"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate.startOf('day');
|
||||
let last = this.focusDate.endOf('day');
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
if (this.rangeOffset < 0) {
|
||||
first = first.plus({ days: this.rangeOffset });
|
||||
} else {
|
||||
last = last.plus({ days: this.rangeOffset });
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
|
||||
if (this.rangeOffset) {
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.rangeOffset = this.$refs.slider.target - 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.nextPage().then(this.updatePage);
|
||||
},
|
||||
updatePage(days) {
|
||||
const newFocusDate = this.focusDate.plus({ days });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
viewAttrs(days) {
|
||||
const day = this.focusDate.plus({ days });
|
||||
return { ...this.$attrs, day };
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-mode-day flex-grow-1 position-relative"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<day-view
|
||||
ref="view"
|
||||
v-bind="viewAttrs(slot.offset)"
|
||||
@request-modal-open="$emit('requestModalOpen', $event)"
|
||||
@request-modal-close="$emit('requestModalClose', $event)"
|
||||
>
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</day-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
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';
|
||||
|
||||
import { useResizeObserver } from '../../../../composables/Responsive.js';
|
||||
|
||||
export default {
|
||||
name: "DayView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelDay,
|
||||
LabelDow,
|
||||
LabelTime
|
||||
},
|
||||
inject: {
|
||||
timeGrid: "timeGrid",
|
||||
originalEvents: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
emptyMessage: String,
|
||||
emptyMessageDetails: String
|
||||
},
|
||||
emits: [
|
||||
"requestModalOpen",
|
||||
"requestModalClose"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
chosenEvent: null,
|
||||
gridMainRef: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
axisMain() {
|
||||
return [this.day.startOf('day')];
|
||||
},
|
||||
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 }));
|
||||
}
|
||||
},
|
||||
events() {
|
||||
return this.originalEvents
|
||||
.filter(event => event.start < this.day.plus({ days: 1 }) && event.end > this.day)
|
||||
.sort((a, b) => a.start.ts - b.start.ts)
|
||||
.map(evt => evt.orig);
|
||||
},
|
||||
currentEvent() {
|
||||
if (this.chosenEvent) {
|
||||
if (this.events.find(e => e == this.chosenEvent))
|
||||
return this.chosenEvent;
|
||||
}
|
||||
let first = null;
|
||||
if (this.events)
|
||||
first = this.events.find(Boolean); // undefined => none found
|
||||
|
||||
if (first && first.type == 'loading')
|
||||
return null; // null => loading
|
||||
|
||||
return first;
|
||||
},
|
||||
isToday() {
|
||||
return this.day.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
compact() {
|
||||
if (this.compact) {
|
||||
if (this.chosenEvent) {
|
||||
this.$emit('requestModalOpen', {
|
||||
event: this.chosenEvent,
|
||||
closeFn: () => { this.chosenEvent = null; }
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.$emit('requestModalClose');
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClickDefaults(evt) {
|
||||
if (evt.detail.source == 'event') {
|
||||
this.chosenEvent = evt.detail.value;
|
||||
if (this.compact) {
|
||||
this.$emit('requestModalOpen', {
|
||||
event: this.chosenEvent,
|
||||
closeFn: () => { this.chosenEvent = null; }
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const container = Vue.ref(null); // use useTemplateRef when updating to Vue 3.5
|
||||
const { compact } = useResizeObserver(container, 750);
|
||||
|
||||
return {
|
||||
container, // must be exposed or it won't be set in Vue < 3.5
|
||||
compact
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.gridMainRef = this.$refs.grid.$refs.main;
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
ref="container"
|
||||
class="fhc-calendar-mode-day-view d-flex h-100"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<calendar-grid
|
||||
ref="grid"
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
:snap-to-grid="!!timeGrid"
|
||||
all-day-events
|
||||
>
|
||||
<template #main-header="{ date }">
|
||||
<div :class="{ today: isToday }">
|
||||
<label-dow
|
||||
@cal-click="evt => evt.detail.source = 'day'"
|
||||
v-bind="{ date }"
|
||||
/>
|
||||
<label-day
|
||||
v-bind="{ date }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #part-header="{ part }">
|
||||
<label-time v-bind="{ part }" />
|
||||
</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" mode="day" />
|
||||
</template>
|
||||
</calendar-grid>
|
||||
<Teleport :disabled="!gridMainRef" :to="gridMainRef">
|
||||
<div
|
||||
v-if="emptyMessage && currentEvent !== null && !currentEvent"
|
||||
class="fhc-calendar-no-events-overlay"
|
||||
style="position:absolute;inset:0"
|
||||
>
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
</Teleport>
|
||||
<div class="event-details" v-if="!compact">
|
||||
<div
|
||||
v-if="currentEvent === null"
|
||||
class="p-4 d-flex w-100 justify-content-center align-items-center"
|
||||
>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<h3 v-else-if="!currentEvent">{{ emptyMessageDetails }}</h3>
|
||||
<slot v-else :event="currentEvent" mode="event" />
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import ListView from './List/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeList",
|
||||
components: {
|
||||
BaseSlider,
|
||||
ListView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
default: 7
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate;
|
||||
let last = first.plus({ days: this.length });
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
if (this.rangeOffset < 0) {
|
||||
first = first.plus({ days: this.rangeOffset });
|
||||
} else {
|
||||
last = first.plus({ days: this.rangeOffset + this.length });
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
this.rangeOffset = this.currentDate.startOf('day').diff(this.focusDate.startOf('day'), 'days').days;
|
||||
if (this.rangeOffset) {
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.rangeOffset = this.$refs.slider.target - 1;
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.nextPage().then(this.updatePage);
|
||||
},
|
||||
updatePage(offset) {
|
||||
const newFocusDate = this.focusDate.plus({ days: offset });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
viewAttrs(offset) {
|
||||
const day = this.focusDate.plus({ days: offset });
|
||||
return { day, length: this.length };
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
switch (evt.detail.source) {
|
||||
case 'event':
|
||||
// default: Request Modal
|
||||
this.$emit('requestModalOpen', { event: evt.detail.value });
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-mode-list flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<list-view v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" /></template>
|
||||
</list-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
|
||||
import CalDnd from '../../../../directives/Calendar/DragAndDrop.js';
|
||||
import CalClick from '../../../../directives/Calendar/Click.js';
|
||||
|
||||
// TODO(chris): drag and drop
|
||||
|
||||
export default {
|
||||
name: "ListView",
|
||||
components: {
|
||||
LabelDay,
|
||||
LabelDow
|
||||
},
|
||||
directives: {
|
||||
CalDnd,
|
||||
CalClick
|
||||
},
|
||||
inject: {
|
||||
draggableEvents: "draggableEvents",
|
||||
events: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chosenEvent: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
days() {
|
||||
return Array.from({ length: this.length }, (e, days) => this.day.plus({ days }));
|
||||
},
|
||||
eventsPerDay() {
|
||||
const eventsPerDay = this.days.map(day => {
|
||||
return {
|
||||
day,
|
||||
events: this.events
|
||||
.filter(event => event.start < day.plus({ days: 1 }) && event.end > day)
|
||||
.sort((a, b) => a.start.ts - b.start.ts)
|
||||
};
|
||||
});
|
||||
return eventsPerDay.filter(day => day.events.length);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
draggable(event) {
|
||||
return this.draggableEvents(event.orig, 'list');
|
||||
},
|
||||
isToday(date) {
|
||||
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-mode-list-view h-100 overflow-auto">
|
||||
<div v-if="!eventsPerDay.length" class="h-100">
|
||||
<slot :event="undefined" mode="list" />
|
||||
</div>
|
||||
<div v-for="{ day, events } in eventsPerDay">
|
||||
<div
|
||||
class="text-center"
|
||||
:class="{ today: isToday(day) }"
|
||||
>
|
||||
<label-dow
|
||||
:date="day"
|
||||
class="d-inline"
|
||||
@cal-click="evt => evt.detail.source = 'day'"
|
||||
/>
|
||||
,
|
||||
<label-day :date="day" class="d-inline" />
|
||||
</div>
|
||||
<div v-for="event in events">
|
||||
<div v-if="event.type == 'loading'" class="placeholder-glow opacity-50">
|
||||
<span class="placeholder w-100" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="event"
|
||||
:draggable="draggable(event)"
|
||||
v-cal-dnd:draggable="event"
|
||||
v-cal-click:event="event.orig"
|
||||
>
|
||||
<slot :event="event.orig" mode="list" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import MonthView from './Month/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeMonth",
|
||||
components: {
|
||||
BaseSlider,
|
||||
MonthView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
|
||||
let last = first.plus({ days: 41 }).endOf('day'); // NOTE(chris): 6 weeks minus 1 day
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
const nextFocusDate = this.focusDate.plus({ months: this.rangeOffset});
|
||||
const nextRangeStart = nextFocusDate.startOf('month').startOf('week', { useLocaleWeeks: true });
|
||||
if (this.rangeOffset < 0) {
|
||||
first = nextRangeStart;
|
||||
} else {
|
||||
last = nextRangeStart.plus({ days: 41 }).endOf('day');
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
if (this.currentDate.locale != this.focusDate.locale) {
|
||||
this.focusDate = this.currentDate;
|
||||
this.$emit('update:range', this.range);
|
||||
} else {
|
||||
this.rangeOffset = this.currentDate.startOf('month').diff(this.focusDate.startOf('month'), 'months').months;
|
||||
if (this.rangeOffset) {
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.rangeOffset = this.$refs.slider.target - 1;
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.nextPage().then(this.updatePage);
|
||||
},
|
||||
updatePage(months) {
|
||||
const newFocusDate = this.focusDate.plus({ months });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
},
|
||||
viewAttrs(months) {
|
||||
const day = this.focusDate.plus({ months });
|
||||
return { day };
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
switch (evt.detail.source) {
|
||||
case 'week':
|
||||
// default: Move to week if not in month
|
||||
let dayInWeek = luxon.DateTime.fromObject({
|
||||
localWeekNumber: evt.detail.value.number,
|
||||
localWeekYear: evt.detail.value.year
|
||||
}, {
|
||||
zone: this.currentDate.zoneName,
|
||||
locale: this.currentDate.locale
|
||||
});
|
||||
|
||||
if (!this.focusDate.hasSame(dayInWeek.startOf('week', { useLocaleWeeks: true }), 'month')) {
|
||||
this.$emit('update:currentDate', dayInWeek.startOf('week', { useLocaleWeeks: true }));
|
||||
} else if (!this.focusDate.hasSame(dayInWeek.endOf('week', { useLocaleWeeks: true }), 'month')) {
|
||||
this.$emit('update:currentDate', dayInWeek.endOf('week', { useLocaleWeeks: true }));
|
||||
}
|
||||
break;
|
||||
case 'day':
|
||||
// default: Set current-date
|
||||
this.$emit('update:currentDate', 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: `
|
||||
<div
|
||||
class="fhc-calendar-mode-month flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<month-view v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" mode="month" /></template>
|
||||
</month-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import CalendarGrid from '../../Base/Grid.js';
|
||||
import LabelWeek from '../../Base/Label/Week.js';
|
||||
import LabelDow from '../../Base/Label/Dow.js';
|
||||
import LabelDay from '../../Base/Label/Day.js';
|
||||
|
||||
export default {
|
||||
name: "MonthView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelWeek,
|
||||
LabelDow,
|
||||
LabelDay
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
// NOTE(chris): snap events to day
|
||||
events: Vue.computed(() => {
|
||||
//const events = [];
|
||||
const events = this.events.map(event => {
|
||||
const start = event.start.startOf('day');
|
||||
const end = event.end.plus({ days: 1 }).startOf('day');
|
||||
return {
|
||||
...event,
|
||||
start,
|
||||
end
|
||||
};
|
||||
});
|
||||
for (var w = 5; w > -1; w--) {
|
||||
for (var d = 6; d > -1; d--) {
|
||||
const startdate = this.axisMain[w].plus(this.axisParts[d]);
|
||||
events.unshift({
|
||||
start: startdate,
|
||||
end: startdate.plus({ days: 1 }),
|
||||
orig: 'header'
|
||||
});
|
||||
}
|
||||
}
|
||||
return events;
|
||||
})
|
||||
};
|
||||
},
|
||||
inject: {
|
||||
events: "events",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
axisMain() {
|
||||
const start = this.day.startOf('month').startOf('week', { useLocaleWeeks: true });
|
||||
return Array.from({ length: 6 }, (e, i) => start.plus({ weeks: i }));
|
||||
},
|
||||
axisParts() {
|
||||
return Array.from({ length: 8 }, (e, i) => luxon.Duration.fromObject({ days: i }));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isToday(date) {
|
||||
return date.hasSame(luxon.DateTime.now().setZone(this.timezone), 'day');
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="fhc-calendar-mode-month-view h-100">
|
||||
<calendar-grid
|
||||
flip-axis
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
|
||||
snap-to-grid
|
||||
>
|
||||
<template #main-header="{ date }">
|
||||
<label-week v-bind="{ date }" />
|
||||
</template>
|
||||
<template #part-header="{ part }">
|
||||
<label-dow :date="axisMain[0].plus(part)" class="text-center" />
|
||||
</template>
|
||||
<template #event="slot">
|
||||
<label-day
|
||||
v-if="slot.event.orig == 'header'"
|
||||
:date="slot.event.start"
|
||||
:class="{ disabled: !day.hasSame(slot.event.start, 'month'), today: isToday(slot.event.start) }"
|
||||
/>
|
||||
<div v-else-if="slot.event.type == 'loading'" class="placeholder-glow opacity-50">
|
||||
<span class="placeholder w-100 fs-1" />
|
||||
</div>
|
||||
<slot v-else v-bind="slot" />
|
||||
</template>
|
||||
</calendar-grid>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import BaseSlider from '../Base/Slider.js';
|
||||
import WeekView from './Week/View.js';
|
||||
|
||||
export default {
|
||||
name: "ModeWeek",
|
||||
components: {
|
||||
BaseSlider,
|
||||
WeekView
|
||||
},
|
||||
props: {
|
||||
currentDate: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"update:currentDate",
|
||||
"update:range",
|
||||
"click",
|
||||
"requestModalOpen"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
focusDate: this.currentDate,
|
||||
rangeOffset: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
let first = this.focusDate.startOf('week', { useLocaleWeeks: true });
|
||||
let last = this.focusDate.endOf('week', { useLocaleWeeks: true });
|
||||
|
||||
if (this.rangeOffset != 0) {
|
||||
if (this.rangeOffset < 0) {
|
||||
first = first.plus({ weeks: this.rangeOffset });
|
||||
} else {
|
||||
last = last.plus({ weeks: this.rangeOffset });
|
||||
}
|
||||
}
|
||||
|
||||
return luxon.Interval.fromDateTimes(first, last);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentDate() {
|
||||
if (this.currentDate.locale != this.focusDate.locale) {
|
||||
this.focusDate = this.currentDate;
|
||||
this.$emit('update:range', this.range);
|
||||
} else {
|
||||
this.rangeOffset = this.currentDate.startOf('week', { useLocaleWeeks: true }).diff(this.focusDate.startOf('week', { useLocaleWeeks: true }), 'weeks').weeks;
|
||||
if (this.rangeOffset) {
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.slidePages(this.rangeOffset).then(this.updatePage);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.rangeOffset = this.$refs.slider.target - 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.prevPage().then(this.updatePage);
|
||||
},
|
||||
nextPage() {
|
||||
this.rangeOffset = this.$refs.slider.target + 1;
|
||||
this.$refs.view.$refs.grid.disableAutoScroll();
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.slider.nextPage().then(this.updatePage);
|
||||
},
|
||||
updatePage(weeks) {
|
||||
const newFocusDate = this.focusDate.plus({ weeks });
|
||||
this.focusDate = newFocusDate;
|
||||
this.rangeOffset = 0;
|
||||
this.$emit('update:currentDate', this.focusDate);
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
viewAttrs(weeks) {
|
||||
const day = this.focusDate.plus({ weeks });
|
||||
return { ...this.$attrs, day };
|
||||
},
|
||||
handleClickDefaults(evt) {
|
||||
switch (evt.detail.source) {
|
||||
case 'day':
|
||||
// default: Set current-date
|
||||
this.$emit('update:currentDate', evt.detail.value);
|
||||
break;
|
||||
case 'event':
|
||||
// default: Request Modal
|
||||
this.$emit('requestModalOpen', { event: evt.detail.value });
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$emit('update:range', this.range);
|
||||
this.$refs.view.$refs.grid.enableAutoScroll();
|
||||
},
|
||||
template: `
|
||||
<div
|
||||
class="fhc-calendar-mode-week flex-grow-1 position-relative"
|
||||
@cal-click-default.capture="handleClickDefaults"
|
||||
>
|
||||
<base-slider ref="slider" v-slot="slot">
|
||||
<week-view ref="view" v-bind="viewAttrs(slot.offset)">
|
||||
<template v-slot="slot"><slot v-bind="slot" mode="week" /></template>
|
||||
</week-view>
|
||||
</base-slider>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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: "WeekView",
|
||||
components: {
|
||||
CalendarGrid,
|
||||
LabelDay,
|
||||
LabelDow,
|
||||
LabelTime
|
||||
},
|
||||
inject: {
|
||||
timeGrid: "timeGrid",
|
||||
timezone: "timezone"
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
type: luxon.DateTime,
|
||||
required: true
|
||||
},
|
||||
collapseEmptyDays: Boolean
|
||||
},
|
||||
computed: {
|
||||
start() {
|
||||
return this.day.startOf('week', { useLocaleWeeks: true });
|
||||
},
|
||||
axisMain() {
|
||||
return Array.from({ length: 7 }, (e, i) => this.start.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-week-view h-100">
|
||||
<calendar-grid
|
||||
ref="grid"
|
||||
:axis-main="axisMain"
|
||||
:axis-parts="axisParts"
|
||||
:axis-main-collapsible="collapseEmptyDays"
|
||||
:snap-to-grid="!!timeGrid"
|
||||
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 }" />
|
||||
</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>
|
||||
`
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
import CalendarPane from './Pane.js';
|
||||
import CalendarMonthPage from './Month/Page.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
components: {
|
||||
CalendarMonthPage,
|
||||
CalendarPane
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
syncOnNextChange: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'}, this.$p.user_locale.value);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paneChanged(dir) {
|
||||
if (this.syncOnNextChange) {
|
||||
this.syncOnNextChange = false;
|
||||
this.focusDate.set(this.date);
|
||||
} else {
|
||||
this.focusDate.moveMonthInDirection(dir)
|
||||
}
|
||||
this.$emit('change:range', {
|
||||
start: new Date(this.focusDate.y, this.focusDate.m, 1),
|
||||
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
|
||||
});
|
||||
},
|
||||
prev() {
|
||||
this.$refs.pane.prev();
|
||||
this.$emit('change:offset', { y: 0, m: -1, d: 0 });
|
||||
},
|
||||
next() {
|
||||
this.$refs.pane.next();
|
||||
this.$emit('change:offset', { y: 0, m: 1, d: 0 });
|
||||
},
|
||||
selectDay(day) {
|
||||
let m = day.getMonth();
|
||||
if (this.focusDate.m != m) {
|
||||
this.syncOnNextChange = true;
|
||||
if (this.focusDate.m-1 == m || (m == 11 && !this.focusDate.m))
|
||||
this.$refs.pane.prev();
|
||||
else
|
||||
this.$refs.pane.next();
|
||||
} else {
|
||||
this.focusDate.set(this.date);
|
||||
}
|
||||
this.$emit('input', ['select:day',day])
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$emit('change:range', {
|
||||
start: new Date(this.focusDate.y, this.focusDate.m, 1),
|
||||
end: new Date(this.focusDate.y, this.focusDate.m+1, 0)
|
||||
});
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-month">
|
||||
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'months')" >
|
||||
<template #calendarDownloads>
|
||||
<slot name="calendarDownloads"></slot>
|
||||
</template>
|
||||
</calendar-header>
|
||||
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
|
||||
<calendar-month-page :year="focusDate.y" :month="focusDate.m+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectDay" >
|
||||
<template #monthPage="{event,day}">
|
||||
<slot name="monthPage" :event="event" :day="day" ></slot>
|
||||
</template>
|
||||
</calendar-month-page>
|
||||
</calendar-pane>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import CalendarDate from '../../../composables/CalendarDate.js';
|
||||
|
||||
export default {
|
||||
name: 'MonthPage',
|
||||
data(){
|
||||
return{
|
||||
highlightedWeek: null,
|
||||
highlightedDay: null,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'today',
|
||||
'todayDate',
|
||||
'date',
|
||||
'focusDate',
|
||||
'size',
|
||||
'events',
|
||||
'showWeeks',
|
||||
'noWeekView',
|
||||
'selectedEvent',
|
||||
'setSelectedEvent'
|
||||
],
|
||||
props: {
|
||||
year: Number,
|
||||
month: Number
|
||||
},
|
||||
emits: [
|
||||
'updateMode',
|
||||
'page:back',
|
||||
'page:forward',
|
||||
'input'
|
||||
],
|
||||
computed: {
|
||||
dayText(){
|
||||
if (!this.size || !this.weeks[0]?.days) return {};
|
||||
let dayTextMap ={};
|
||||
this.weeks[0].days.forEach((day)=>{
|
||||
dayTextMap[day] = day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 1 ? 'narrow' : (this.size < 3 ? 'short' : 'long') });
|
||||
});
|
||||
return dayTextMap;
|
||||
},
|
||||
weeks() {
|
||||
let firstDayOfMonth = new CalendarDate(this.year, this.month, 1);
|
||||
let startDay = firstDayOfMonth.firstDayOfCalendarMonth;
|
||||
let endDay = firstDayOfMonth.lastDayOfCalendarMonth;
|
||||
|
||||
let res = [];
|
||||
let week = {no:0,y:0,days:[]};
|
||||
while (startDay <= endDay) {
|
||||
week.days.push(new Date(startDay));
|
||||
|
||||
if (week.days.length == 7) {
|
||||
let d = new CalendarDate(week.days[5]);
|
||||
week.no = d.w;
|
||||
week.y = d.y;
|
||||
res.push(week);
|
||||
week = {no:0,y:0,days:[]};
|
||||
}
|
||||
startDay.setDate(startDay.getDate() + 1);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
getDayClass(week, day) {
|
||||
let classstring = 'fhc-calendar-month-page-day text-decoration-none overflow-hidden'
|
||||
const isHighlightedWeek = this.isHighlightedWeek(week)
|
||||
const isHighlightedDay = this.isHighlightedDay(day)
|
||||
const isThisDate = this.date.compare(day)
|
||||
const isNotThisMonth = day.getMonth() != this.month
|
||||
const isInThePast = day.getTime() < this.today // this.date is just the focusDate but not the initial Date
|
||||
|
||||
if(isHighlightedWeek) classstring += ' fhc-highlight-week'
|
||||
if(isHighlightedDay) classstring += ' fhc-highlight-day'
|
||||
|
||||
if(isNotThisMonth) classstring += ' opacity-25'
|
||||
if(isInThePast) classstring += ' fhc-calendar-past'
|
||||
return classstring
|
||||
},
|
||||
selectDay(day) {
|
||||
this.date.set(day);
|
||||
this.$emit('input', day);
|
||||
},
|
||||
changeToWeek(week) {
|
||||
if (!this.noWeekView) {
|
||||
if (!this.focusDate.isInWeek(week.no, week.y))
|
||||
this.focusDate.set(week.days[0]);
|
||||
this.$emit('updateMode', 'week');
|
||||
}
|
||||
},
|
||||
highlight(week, day){
|
||||
this.highlightedWeek = week.no;
|
||||
this.highlightedDay = day;
|
||||
},
|
||||
isHighlightedDay(day) {
|
||||
return day == this.highlightedDay
|
||||
},
|
||||
isHighlightedWeek(week) {
|
||||
return week.no == this.highlightedWeek
|
||||
},
|
||||
clickEvent(day,week) {
|
||||
if(!this.noWeekView)
|
||||
{
|
||||
this.focusDate.set(day);
|
||||
this.$emit('updateMode', 'day');
|
||||
}
|
||||
this.selectDay(day);
|
||||
},
|
||||
getNumberStyle(day) {
|
||||
|
||||
const styleObj = {}
|
||||
styleObj.display = 'inline-block';
|
||||
styleObj.height = '32px';
|
||||
styleObj['line-height'] = '32px';
|
||||
styleObj['text-align'] = 'center';
|
||||
styleObj['font-weight'] = 'bold';
|
||||
styleObj['font-size'] = '14px';
|
||||
|
||||
if(day.getDate() === this.todayDate.getDate()
|
||||
&& day.getMonth() === this.todayDate.getMonth()
|
||||
&& day.getFullYear() === this.todayDate.getFullYear()) {
|
||||
styleObj['background-color'] = '#00649c'; // fh blau
|
||||
styleObj.color = 'white';
|
||||
}
|
||||
|
||||
return styleObj
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const container = document.getElementById("calendarContainer")
|
||||
if(container) container.style['overflow-y'] = 'auto'
|
||||
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
|
||||
<div v-if="showWeeks" class=" bg-light fw-bold border-top border-bottom text-center"></div>
|
||||
<div v-for="day in weeks[0].days" :key="day" class="bg-light fw-bold border-top border-bottom text-center">
|
||||
{{dayText[day]}}
|
||||
</div>
|
||||
<template v-for="week in weeks"
|
||||
:key="week.no">
|
||||
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25"
|
||||
@click.prevent="changeToWeek(week)">{{week.no}}</a>
|
||||
<a href="#"
|
||||
@click="clickEvent(day,week)"
|
||||
@mouseover="highlight(week,day)"
|
||||
@mouseleave="highlightedWeek = null; highlightedDay = null"
|
||||
v-for="day in week.days"
|
||||
:key="day"
|
||||
:class="getDayClass(week, day)"
|
||||
>
|
||||
<span @click="clickEvent(day,week)" class="no" :style="getNumberStyle(day)">{{day.getDate()}}</span>
|
||||
<span v-if="events[day.toDateString()] && events[day.toDateString()].length" class="events">
|
||||
<div v-for="event in events[day.toDateString()]" :key="event.id"
|
||||
:style="{'background-color': event.color}" class="fhc-entry" :selected="event == selectedEvent" v-contrast >
|
||||
<slot name="monthPage" :event="event" :day="day" >
|
||||
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
|
||||
</slot>
|
||||
</div>
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
inject: [
|
||||
'size'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
monthIndices: [...Array(12).keys()]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.focusDate.format({year: 'numeric'});
|
||||
},
|
||||
months() {
|
||||
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(this.$p.user_locale.value, {month: this.size < 2 ? 'short' : 'long'}));
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-months">
|
||||
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
|
||||
<div class="d-flex flex-wrap">
|
||||
<div v-for="(month, key) in months" :key="key" class="d-grid col-4">
|
||||
<button @click="focusDate.m = key; $emit('updateMode', 'month')" class="btn btn-outline-secondary" :class="{'border-0': key != focusDate.m}">
|
||||
{{month}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
export default {
|
||||
name: 'Pane',
|
||||
emits: [
|
||||
'slid'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
carousel: null,
|
||||
queue: 0,
|
||||
offset: 0,
|
||||
slideAnimation:false,
|
||||
scrollTop:null,
|
||||
clientHeight:null,
|
||||
carouselItems:null,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
isSliding: Vue.computed(() => this.slideAnimation),
|
||||
calendarScrollTop: Vue.computed(() =>this.scrollTop),
|
||||
calendarClientHeight: Vue.computed(() => this.clientHeight),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
offsets() {
|
||||
return [...Array(3).keys()].map(i => (3+i-this.offset)%3-1);
|
||||
},
|
||||
activeCarouselItemIndex() {
|
||||
if (Array.isArray(this.carouselItems) && this.carouselItems.length > 0) {
|
||||
for(let index=0; index < this.carouselItems.length; index++){
|
||||
if (this.carouselItems[index] == true){
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
scrollCalendar(event){
|
||||
this.scrollTop = this.$refs.calendarContainer.scrollTop;
|
||||
this.clientHeight = this.$refs.calendarContainer.clientHeight;
|
||||
},
|
||||
prev() {
|
||||
if (!this.queue--)
|
||||
this.carousel.prev();
|
||||
},
|
||||
next() {
|
||||
if (!this.queue++)
|
||||
this.carousel.next();
|
||||
},
|
||||
slid(evt) {
|
||||
let dir = evt.direction == 'left' ? 1 : -1;
|
||||
this.queue -= dir;
|
||||
this.$emit('slid', dir);
|
||||
this.offset = (3+this.offset+dir)%3;
|
||||
if (this.queue) {
|
||||
if (this.queue > 0)
|
||||
this.carousel.next();
|
||||
else
|
||||
this.carousel.prev();
|
||||
}
|
||||
this.carouselItems = this.$refs.carouselItems.map((item) => { return item.classList.contains('active') });
|
||||
this.slideAnimation = false;
|
||||
},
|
||||
slide(evt) {
|
||||
this.slideAnimation = true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.carousel) {
|
||||
this.$refs.carousel.children[0].children[1].classList.add('active');
|
||||
this.carousel = new window.bootstrap.Carousel(this.$refs.carousel, {
|
||||
interval: false
|
||||
});
|
||||
}
|
||||
this.carouselItems = this.$refs.carouselItems.map((item)=>{return item.classList.contains('active')});
|
||||
this.scrollTop = this.$refs.calendarContainer.scrollTop;
|
||||
this.clientHeight = this.$refs.calendarContainer.clientHeight;
|
||||
},
|
||||
template: `
|
||||
<div ref="carousel" class="calendar-pane carousel slide" @[\`slide.bs.carousel\`]="slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
|
||||
<div id="calendarContainer" @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner " style="height:var(--fhc-calendar-pane-height);">
|
||||
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item" style="height:var(--fhc-calendar-pane-height);">
|
||||
<slot :active="i == activeCarouselItemIndex" :index="i" :offset="offsets[i]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
import CalendarPane from './Pane.js';
|
||||
import CalendarWeekPage from './Week/Page.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
components: {
|
||||
CalendarWeekPage,
|
||||
CalendarPane
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paneChanged(dir) {
|
||||
this.focusDate.d += dir * 7;
|
||||
this.emitRangeChanged();
|
||||
},
|
||||
emitRangeChanged() {
|
||||
let start = this.focusDate.firstDayOfWeek;
|
||||
let end = this.focusDate.lastDayOfWeek;
|
||||
this.$emit('change:range', { start, end });
|
||||
},
|
||||
prev() {
|
||||
this.$refs.pane.prev();
|
||||
this.$emit('change:offset', { y: 0, m: 0, d: -7 });
|
||||
},
|
||||
next() {
|
||||
this.$refs.pane.next();
|
||||
this.$emit('change:offset', { y: 0, m: 0, d: 7 });
|
||||
},
|
||||
selectEvent(event) {
|
||||
this.$emit('input', ['select:event',event]);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.emitRangeChanged();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="fhc-calendar-week">
|
||||
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')">
|
||||
<template #calendarDownloads>
|
||||
<slot name="calendarDownloads"></slot>
|
||||
</template>
|
||||
</calendar-header>
|
||||
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
|
||||
<calendar-week-page :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
|
||||
<template #weekPage="{event,day}">
|
||||
<slot name="weekPage" :event="event" :day="day" ></slot>
|
||||
</template>
|
||||
</calendar-week-page>
|
||||
</calendar-pane>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,368 +0,0 @@
|
||||
import CalendarDate from '../../../composables/CalendarDate.js';
|
||||
|
||||
function ggt(m,n) { return n==0 ? m : ggt(n, m%n); }
|
||||
function kgv(m,n) { return (m*n) / ggt(m,n); }
|
||||
|
||||
export default {
|
||||
name: 'WeekPage',
|
||||
data(){
|
||||
return{
|
||||
hourPosition:null,
|
||||
hourPositionTime:null,
|
||||
resizeObserver: null,
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'today',
|
||||
'todayDate',
|
||||
'date',
|
||||
'focusDate',
|
||||
'size',
|
||||
'events',
|
||||
'noMonthView',
|
||||
'isSliding',
|
||||
'selectedEvent',
|
||||
'setSelectedEvent',
|
||||
'rowMinHeight'
|
||||
],
|
||||
props: {
|
||||
year: Number,
|
||||
week: Number
|
||||
},
|
||||
emits: [
|
||||
'updateMode',
|
||||
'page:back',
|
||||
'page:forward',
|
||||
'input',
|
||||
],
|
||||
computed: {
|
||||
getGridStyle() {
|
||||
return {
|
||||
'min-height': this.rowMinHeight,
|
||||
// this.size is the magic number anyway which directs font-size,
|
||||
// which in turn influences a lot of layout
|
||||
width: '42px'
|
||||
}
|
||||
},
|
||||
laneWidth() {
|
||||
return (this.width - 42) / this.days.length
|
||||
},
|
||||
curTime() {
|
||||
const now = new Date();
|
||||
return String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');
|
||||
},
|
||||
pageHeaderStyle(){
|
||||
return {
|
||||
'z-index': 4,
|
||||
'grid-template-columns': 'repeat(' + this.days.length + ', 1fr)',
|
||||
'grid-template-rows': 1,
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
}
|
||||
},
|
||||
overlayStyle() {
|
||||
return {
|
||||
'background-color': '#F5E9D7',
|
||||
'position': 'absolute',
|
||||
'pointer-events': 'none',
|
||||
'z-index': 2,
|
||||
height: this.getDayTimePercent + '%',
|
||||
width: this.laneWidth + 'px',
|
||||
opacity: 0.5
|
||||
}
|
||||
},
|
||||
indicatorStyle() {
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
'padding-left': '3.5rem',
|
||||
'margin-top': '-1px',
|
||||
'z-index': 2,
|
||||
'border-color': '#00649C!important',
|
||||
top: this.hourPosition + 'px',
|
||||
left: 0,
|
||||
right: 0,
|
||||
}
|
||||
},
|
||||
hours(){
|
||||
// returns an array with elements starting at 7 and ending at 24
|
||||
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
|
||||
},
|
||||
dayText() {
|
||||
if (!this.size || !this.days) return {};
|
||||
let dayTextMap ={};
|
||||
this.days.forEach((day)=>{
|
||||
dayTextMap[day] = {
|
||||
heading: day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
|
||||
tag: day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
|
||||
datum: day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
|
||||
};
|
||||
});
|
||||
return dayTextMap;
|
||||
},
|
||||
days() {
|
||||
|
||||
let tmpDate = new CalendarDate(this.year,1,1); // NOTE(chris): somewhere in the middle of the year
|
||||
tmpDate.w = this.week;
|
||||
let startDay = tmpDate.firstDayOfWeek;
|
||||
let result = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
result.push(new Date(startDay.getFullYear(), startDay.getMonth(), startDay.getDate() + i));
|
||||
}
|
||||
return result;
|
||||
|
||||
},
|
||||
eventsPerDayAndHour() {
|
||||
// return early if the calendar pane is sliding
|
||||
if (this.isSliding) return {};
|
||||
const res = {};
|
||||
this.days.forEach(day => {
|
||||
let key = day.toDateString();
|
||||
|
||||
let nextDay = new Date(day);
|
||||
nextDay.setDate(nextDay.getDate()+1);
|
||||
nextDay.setMilliseconds(nextDay.getMilliseconds()-1);
|
||||
let d = {events:[],lanes:1, isPast: false};
|
||||
d.isPast = nextDay.getTime() < this.today
|
||||
d.isToday = nextDay.getFullYear() === this.todayDate.getFullYear() && nextDay.getMonth() === this.todayDate.getMonth() && nextDay.getDate() === this.todayDate.getDate()
|
||||
if (this.events[key]) {
|
||||
this.events[key].forEach(evt => {
|
||||
let event = {orig:evt,lane:1,maxLane:1,start: evt.start < day ? day : evt.start, end: evt.end > nextDay ? nextDay : evt.end,shared:[],setSharedMaxRecursive(doneItems) {
|
||||
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
|
||||
doneItems.push(this);
|
||||
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
|
||||
}};
|
||||
event.shared = d.events.filter(other => other.start < event.end && other.end > event.start);
|
||||
event.shared.forEach(other => other.shared.push(event));
|
||||
let occupiedLanes = event.shared.map(other => other.lane);
|
||||
while (occupiedLanes.includes(event.lane))
|
||||
event.lane++;
|
||||
event.maxLane = Math.max(...[event.lane], ...occupiedLanes);
|
||||
if (event.maxLane > 1) {
|
||||
event.setSharedMaxRecursive([event]);
|
||||
}
|
||||
d.events.push(event);
|
||||
});
|
||||
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
|
||||
}
|
||||
res[key] = d;
|
||||
});
|
||||
return res;
|
||||
},
|
||||
smallestTimeFrame() {
|
||||
return [30,15,10,5][this.size];
|
||||
},
|
||||
lookingAtToday() {
|
||||
return this.days.some(d =>
|
||||
d.getFullYear() === this.todayDate.getFullYear() &&
|
||||
d.getMonth() === this.todayDate.getMonth() &&
|
||||
d.getDate() === this.todayDate.getDate()
|
||||
)
|
||||
},
|
||||
curIndicatorStyle() {
|
||||
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
'padding-left': '1rem',
|
||||
'margin-top': '-1px',
|
||||
'z-index': 2,
|
||||
'border-color': '#00649C!important',
|
||||
top: this.getDayTimePercent + '%',
|
||||
width: this.laneWidth + 'px'
|
||||
}
|
||||
},
|
||||
getDayTimePercent() {
|
||||
const now = new Date(Date.now())
|
||||
const currentMinutes = now.getMinutes() + now.getHours() * 60
|
||||
let timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
|
||||
|
||||
return timePercentage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
hourGridIdentifier(hour) {
|
||||
// this is the id attribute that is responsible to scroll the calender to the first event
|
||||
return 'scroll' + hour + this.focusDate.d + this.week;
|
||||
},
|
||||
hourGridStyle(hour) {
|
||||
return {
|
||||
'pointer-events': 'none',
|
||||
top: this.getAbsolutePositionForHour(hour),
|
||||
left: 0,
|
||||
right: 0,
|
||||
'z-index': 0
|
||||
}
|
||||
},
|
||||
dayGridStyle(day) {
|
||||
const styleObj = {
|
||||
'grid-template-columns': 'repeat(' + day.lanes + ', 1fr)',
|
||||
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
|
||||
}
|
||||
|
||||
if(day.isPast) {
|
||||
styleObj['background-color'] = '#F5E9D7'
|
||||
styleObj['border-color'] = '#E8E8E8';
|
||||
styleObj.opacity = 0.5;
|
||||
} else if (day.isToday) {
|
||||
|
||||
// styleObj['backgroundImage'] = 'linear-gradient(to bottom, #F5E9D7 '+this.getDayTimePercent+'%, #FFFFFF '+this.getDayTimePercent+'%)'
|
||||
// styleObj['border-color'] = '#E8E8E8';
|
||||
// styleObj.opacity = 0.5;
|
||||
}
|
||||
|
||||
return styleObj
|
||||
},
|
||||
eventGridStyle(day, event) {
|
||||
return {
|
||||
'z-index': 1,
|
||||
'grid-column-start': 1 + (event.lane - 1) * day.lanes / event.maxLane,
|
||||
'grid-column-end': 1 + event.lane * day.lanes / event.maxLane,
|
||||
'grid-row-start': this.dateToMinutesOfDay(event.start),
|
||||
'grid-row-end': this.dateToMinutesOfDay(event.end),
|
||||
'background-color': event.orig.color,
|
||||
'max-height': '75px'
|
||||
}
|
||||
},
|
||||
calcHourPosition(event) {
|
||||
let height = this.$refs.eventcontainer.getBoundingClientRect().height;
|
||||
let top = this.$refs.eventcontainer.getBoundingClientRect().top;
|
||||
let position = event.clientY - top;
|
||||
// position percentage of total height
|
||||
let timePercentage = (position / height) * 100;
|
||||
// minute percentage of total minutes
|
||||
let result = (this.hours.length * 60) * (timePercentage / 100);
|
||||
// calculate time in float
|
||||
let currentMinutes = ((result + (this.hours[0] * 60)) / 60);
|
||||
// get hour part of time
|
||||
let currentHour = Math.floor(currentMinutes);
|
||||
// get float part of time
|
||||
let minutePercentage = currentMinutes % currentHour;
|
||||
// calculate minutes from float part of time
|
||||
let minute = Math.round(60 * minutePercentage);
|
||||
// convert minutes to 5 minute interval
|
||||
if (minute % 5 != 0) {
|
||||
minute = Math.round(minute / 5) * 5;
|
||||
}
|
||||
// in case the rounding made the minutes 60, increase the hour and reset the minutes
|
||||
if (minute == 60) {
|
||||
currentHour++;
|
||||
minute = 0;
|
||||
}
|
||||
|
||||
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
|
||||
// convert current time in minutes
|
||||
currentMinutes = currentHour * 60 + minute;
|
||||
// calculate the minutes percentage of the total minutes
|
||||
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
|
||||
// calculate the relative position of the time percentage
|
||||
position = height * (timePercentage / 100);
|
||||
this.hourPosition = position;
|
||||
|
||||
// add padding to minutes that consist of only one digit
|
||||
minute.toString().length == 1 ? minute = "0" + minute : minute;
|
||||
this.hourPositionTime = currentHour + ":" + minute;
|
||||
},
|
||||
getAbsolutePositionForHour(hour){
|
||||
// used for the absolute positioning of the gutters of hours
|
||||
return (100 / this.hours.length) * (hour - (24-this.hours.length)) + '%';
|
||||
},
|
||||
changeToMonth(day) {
|
||||
if (!this.noMonthView) {
|
||||
this.date.set(day);
|
||||
this.focusDate.set(day);
|
||||
this.$emit('updateMode', 'month');
|
||||
}
|
||||
},
|
||||
changeToDay(day) {
|
||||
this.date.set(day);
|
||||
this.focusDate.set(day);
|
||||
this.$emit('updateMode', 'day');
|
||||
},
|
||||
dateToMinutesOfDay(day) {
|
||||
// subtract 7 from the total hours because the hours range from 7 to 24
|
||||
return Math.floor(((day.getHours()-7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
|
||||
},
|
||||
weekPageClick(event, day) {
|
||||
this.setSelectedEvent(event);
|
||||
this.focusDate.set(new CalendarDate(new Date(event.datum)));
|
||||
this.$emit('input', event)
|
||||
},
|
||||
initResizeObserver() {
|
||||
const events = this.$refs['eventsRef'+this.week];
|
||||
if (!events) return;
|
||||
|
||||
this.resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
const { width, height } = entry.contentRect;
|
||||
if(width > 0) this.width = width
|
||||
}
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(events);
|
||||
},
|
||||
destroyResizeObserver() {
|
||||
if (this.resizeObserver) {
|
||||
this.resizeObserver.disconnect();
|
||||
this.resizeObserver = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
|
||||
|
||||
const container = document.getElementById("calendarContainer")
|
||||
if(container) {
|
||||
container.style['overflow-y'] = 'scroll'
|
||||
container.style['overflow-x'] = 'auto'
|
||||
}
|
||||
|
||||
this.initResizeObserver();
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.destroyResizeObserver();
|
||||
},
|
||||
template: /*html*/`
|
||||
<div ref="page" class="fhc-calendar-week-page" style="min-width: 700px;">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
|
||||
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToDay(day)">
|
||||
<div class="fw-bold">{{dayText[day]?.tag}}</div>
|
||||
<a href="#" class="small text-secondary text-decoration-none" >{{dayText[day]?.datum}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
|
||||
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
|
||||
<Transition>
|
||||
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
|
||||
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<div class="events" :ref="'eventsRef'+week">
|
||||
<div class="hours">
|
||||
<div v-for="hour in hours" :style="getGridStyle" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
|
||||
</div>
|
||||
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
|
||||
<Transition>
|
||||
<div v-if="day.isToday" class="position-absolute border-top small" :style="curIndicatorStyle">
|
||||
<span class="border border-top-0 px-2 bg-white">{{curTime}}</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<div v-if="day.isToday" :style="overlayStyle"></div>
|
||||
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)"
|
||||
:selected="event.orig == selectedEvent"
|
||||
:style="eventGridStyle(day,event)"
|
||||
class="mx-2 small rounded overflow-hidden fhc-entry "
|
||||
v-contrast >
|
||||
<slot name="weekPage" :event="event" :day="day">
|
||||
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
inject: [
|
||||
'size',
|
||||
'focusDate'
|
||||
],
|
||||
computed: {
|
||||
weeks(){
|
||||
return [...Array(this.focusDate.numWeeks).keys()].map(i => i + 1);
|
||||
},
|
||||
title() {
|
||||
return this.focusDate.format({year: 'numeric'});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setWeek(week) {
|
||||
// TODO(chris): test is there a week jump on year select? => yes there is if the same month/day are in different weeks ... should we prevent that?
|
||||
this.focusDate.w = week;
|
||||
this.$emit('updateMode', 'week');
|
||||
},
|
||||
prev(){
|
||||
this.focusDate.y--;
|
||||
this.focusDate._clean();
|
||||
},
|
||||
next() {
|
||||
this.focusDate.y++;
|
||||
this.focusDate._clean();
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-weeks h-100">
|
||||
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
|
||||
<div class="d-flex flex-wrap">
|
||||
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
|
||||
<button @click="setWeek(week)" class="btn btn-outline-secondary" :class="{'border-0': week != focusDate.w}">
|
||||
{{week}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import CalendarAbstract from './Abstract.js';
|
||||
import CalendarPane from './Pane.js';
|
||||
import CalendarYearsPage from './Years/Page.js';
|
||||
|
||||
export default {
|
||||
mixins: [
|
||||
CalendarAbstract
|
||||
],
|
||||
components: {
|
||||
CalendarYearsPage,
|
||||
CalendarPane
|
||||
},
|
||||
inject: [
|
||||
'size'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
start: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
range() {
|
||||
switch (this.size) {
|
||||
case 3:
|
||||
// eslint-disable-next-line
|
||||
case 2:
|
||||
return 24;
|
||||
}
|
||||
return 12;
|
||||
},
|
||||
end() {
|
||||
return this.start + this.range - 1;
|
||||
},
|
||||
title() {
|
||||
return this.start + ' - ' + this.end;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
paneChanged(dir) {
|
||||
this.start += this.range * dir;
|
||||
},
|
||||
prev() {
|
||||
this.$refs.pane.prev();
|
||||
this.$emit('change:offset', { y: -1, m: 0, d: 0 });
|
||||
},
|
||||
next() {
|
||||
this.$refs.pane.next();
|
||||
this.$emit('change:offset', { y: 1, m: 0, d: 0 });
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.start = this.focusDate.y - this.focusDate.y%this.range;
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-years">
|
||||
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" />
|
||||
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
|
||||
<calendar-years-page :data-test="slot.index" :start="start+range*slot.offset" :end="start+range*slot.offset+range" @updateMode="$emit('updateMode')"/>
|
||||
</calendar-pane>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
export default {
|
||||
inject: [
|
||||
'focusDate'
|
||||
],
|
||||
props: {
|
||||
start: Number,
|
||||
end: Number
|
||||
},
|
||||
emits: [
|
||||
'updateMode'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
years() {
|
||||
return [...Array(this.end - this.start).keys()].map(i => i + this.start);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const container = document.getElementById("calendarContainer")
|
||||
if(container) {
|
||||
container.style['overflow-y'] = 'scroll'
|
||||
container.style['overflow-x'] = 'auto'
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="fhc-calendar-years-page d-flex flex-wrap">
|
||||
<div v-for="year in years" :key="year" class="d-grid col-4">
|
||||
<button class="btn btn-outline-secondary" :class="{'border-0': year != focusDate.y}" @click="focusDate.y = year; $emit('updateMode')">
|
||||
{{year}}
|
||||
</button>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
import BsModal from '../../Bootstrap/Modal.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
|
||||
const today = new Date()
|
||||
export const AbgabeMitarbeiterDetail = {
|
||||
name: "AbgabeMitarbeiterDetail",
|
||||
components: {
|
||||
BsModal,
|
||||
InputNumber: primevue.inputnumber,
|
||||
Checkbox: primevue.checkbox,
|
||||
Dropdown: primevue.dropdown,
|
||||
Textarea: primevue.textarea,
|
||||
VueDatePicker
|
||||
},
|
||||
props: {
|
||||
projektarbeit: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
oldPaBeurteilungLink: 'https://moodle.technikum-wien.at/mod/page/view.php?id=1005052', // TODO: inject from app & app provide link from config
|
||||
eidAkzeptiert: false,
|
||||
enduploadTermin: null,
|
||||
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
|
||||
// TODO: fetch types
|
||||
allAbgabeTypes: [
|
||||
{
|
||||
paabgabetyp_kurzbz: 'abstract',
|
||||
bezeichnung: 'Entwurf'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'zwischen',
|
||||
bezeichnung: 'Zwischenabgabe'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'note',
|
||||
bezeichnung: 'Benotung'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'end',
|
||||
bezeichnung: 'Endupload'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'enda',
|
||||
bezeichnung: 'Endabgabe im Sekretariat'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openZusatzdatenModal(termin) {
|
||||
|
||||
},
|
||||
saveTermin(termin) {
|
||||
const paabgabe_id = termin.paabgabe_id
|
||||
this.$fhcApi.factory.lehre.postProjektarbeitAbgabe(termin).then( (res) => {
|
||||
if(res?.meta?.status == 'success') {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui/gespeichert'))
|
||||
|
||||
if(paabgabe_id === -1) { // new abgabe has been inserted
|
||||
termin.paabgabe_id = res?.data?.retval
|
||||
|
||||
this.projektarbeit.abgabetermine.push({ // new abgatermin row
|
||||
|
||||
'paabgabe_id': -1,
|
||||
'projektarbeit_id': this.projektarbeit.projektarbeit_id,
|
||||
'fixtermin': false,
|
||||
'kurzbz': '',
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'paabgabetyp_kurzbz': '',
|
||||
'bezeichnung': '',
|
||||
'abgabedatum': null,
|
||||
'insertvon': this.viewData?.uid ?? '',
|
||||
'allowedToSave': true,
|
||||
'allowedToDelete': true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
} else if(res?.meta?.status == 'error'){
|
||||
this.$fhcAlert.alertError()
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
deleteTermin(termin) {
|
||||
this.$fhcApi.factory.lehre.deleteProjektarbeitAbgabe(termin.paabgabe_id).then( (res) => {
|
||||
if(res?.meta?.status == 'success') {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui/genericDeleted', [this.$p.t('abgabetool/abgabe')]))
|
||||
// this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ])
|
||||
const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id)
|
||||
this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1)
|
||||
|
||||
|
||||
} else if(res?.meta?.status == 'error'){
|
||||
this.$fhcAlert.alertError()
|
||||
}
|
||||
})
|
||||
},
|
||||
validate: function(termin) {
|
||||
if(!termin.file.length) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('global/warningChooseFile'));
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
downloadAbgabe(termin) {
|
||||
this.$fhcApi.factory.lehre.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)
|
||||
},
|
||||
dateDiffInDays(datum, today){
|
||||
const oneDayMs = 1000 * 60 * 60 * 24
|
||||
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
|
||||
},
|
||||
getDateStyle(termin) {
|
||||
const datum = new Date(termin.datum)
|
||||
const abgabedatum = new Date(termin.abgabedatum)
|
||||
|
||||
// todo: rework styling but keep the color pattern logic
|
||||
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
|
||||
let color = 'white'
|
||||
let fontColor = 'black'
|
||||
if (termin.abgabedatum === null) {
|
||||
if(datum < today) {
|
||||
color = 'red'
|
||||
fontColor = 'white'
|
||||
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
|
||||
color = 'yellow'
|
||||
}
|
||||
} else if(abgabedatum > datum) {
|
||||
color = 'pink' // aka "hellrot"
|
||||
fontColor = 'white'
|
||||
} else {
|
||||
color = 'green'
|
||||
}
|
||||
|
||||
return `font-color: ${fontColor} ; background-color: ${color}; border-radius: 50%;`
|
||||
},
|
||||
openBeurteilungLink(link) {
|
||||
window.open(link, '_blank')
|
||||
},
|
||||
getOptionLabelSprache(option) {
|
||||
return option.sprache
|
||||
},
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return option.bezeichnung
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
},
|
||||
openStudentPage() {
|
||||
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
|
||||
+ '/Cis/Abgabetool/Student/' + this.projektarbeit?.student_uid
|
||||
window.open(link, '_blank')
|
||||
},
|
||||
openPlagiatcheck() {
|
||||
// todo: hardcoded turnitin link?
|
||||
const link = "https://technikum-wien.turnitin.com/sso/sp/redwood/saml/5IyfmBr2OcSIaWQTKlFCGj/start"
|
||||
window.open(link, '_blank')
|
||||
},
|
||||
openBenotung() {
|
||||
const path = this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'
|
||||
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/' + path
|
||||
window.open(link, '_blank')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getEid() {
|
||||
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
|
||||
},
|
||||
getEnduploadErlaubt() {
|
||||
return !this.eidAkzeptiert
|
||||
},
|
||||
getSemesterBenotbar(){
|
||||
return this.projektarbeit?.isCurrent ?? false
|
||||
},
|
||||
endUploadVorhanden(){
|
||||
return this.projektarbeit?.abgabetermine.find(abgabe => abgabe.paabgabetyp_kurzbz === 'end' && abgabe.abgabedatum !== null)
|
||||
}
|
||||
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
template: `
|
||||
<div v-if="projektarbeit">
|
||||
|
||||
|
||||
<h5>{{$p.t('abgabetool/c4abgabeMitarbeiterbereich')}}</h5>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<p> {{projektarbeit?.student}}</p>
|
||||
<p> {{projektarbeit?.titel}}</p>
|
||||
<p v-if="projektarbeit?.zweitbegutachter"> {{projektarbeit?.zweitbegutachter}}</p>
|
||||
</div>
|
||||
<div class="col-4 d-flex">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<button :disabled="!getSemesterBenotbar || !endUploadVorhanden" class="btn btn-secondary border-0" @click="openBenotung" style="width: 80%;">
|
||||
benoten
|
||||
<i style="margin-left: 8px" class="fa-solid fa-user-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row" style="width: 90%;">
|
||||
<span v-if="!getSemesterBenotbar" v-html="$p.t('abgabetool/c4aeltereParbeitBenoten', oldPaBeurteilungLink)"></span>
|
||||
<span v-else-if="!endUploadVorhanden">Kein Endupload vorhanden!</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<button v-if="projektarbeit?.betreuerart_kurzbz !== 'Zweitbegutachter'" class="btn btn-secondary border-0" @click="openPlagiatcheck" style="width: 80%;">
|
||||
zur Plagiatsprüfung
|
||||
<i style="margin-left: 8px" class="fa-solid fa-user-check"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<button class="btn btn-secondary border-0" @click="openStudentPage" style="width: 80%;">
|
||||
Studentenansicht
|
||||
<i style="margin-left: 8px" class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploadWrapper">
|
||||
<div class="row" style="margin-bottom: 12px;">
|
||||
<div style="width: 100px">{{$p.t('abgabetool/c4fixtermin')}}</div>
|
||||
<div class="col-2">{{$p.t('abgabetool/c4zieldatum')}}</div>
|
||||
<div class="col-2">{{$p.t('abgabetool/c4abgabetyp')}}</div>
|
||||
<div class="col-4">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
|
||||
<div class="col-1">{{$p.t('abgabetool/c4abgabedatum')}}</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!projektarbeit?.abgabetermine?.length">keine Termine gefunden!</div>
|
||||
<div class="row" v-for="termin in projektarbeit.abgabetermine">
|
||||
<div style="width: 100px" class="d-flex justify-content-center align-items-center">
|
||||
<p class="fhc-bullet" :class="{ 'fhc-bullet-red': termin.fixtermin, 'fhc-bullet-green': !termin.fixtermin }"></p>
|
||||
</div>
|
||||
<div class="col-2 d-flex justify-content-center align-items-center">
|
||||
<div :style="getDateStyle(termin)">
|
||||
<VueDatePicker
|
||||
style="width: 95%;"
|
||||
v-model="termin.datum"
|
||||
:clearable="false"
|
||||
:disabled="!termin.allowedToSave"
|
||||
:enable-time-picker="false"
|
||||
:format="formatDate"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 d-flex justify-content-center align-items-center">
|
||||
<Dropdown
|
||||
:style="{'width': '100%'}"
|
||||
:disabled="!termin.allowedToSave"
|
||||
v-model="termin.bezeichnung"
|
||||
:options="allAbgabeTypes"
|
||||
:optionLabel="getOptionLabelAbgabetyp">
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="col-4 d-flex justify-content-center align-items-center">
|
||||
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="3" cols="60" :disabled="!termin.allowedToSave"></Textarea>
|
||||
</div>
|
||||
<div class="col-1 d-flex justify-content-center align-items-center">
|
||||
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
|
||||
<a v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" style="margin-left:4px; cursor: pointer;">
|
||||
<i class="fa-solid fa-file-pdf"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-2 align-content-center">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button v-if="termin.allowedToSave" class="btn btn-primary border-0" @click="saveTermin(termin)">
|
||||
Speichern
|
||||
<i style="margin-left: 8px" class="fa-solid fa-floppy-disk"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button v-if="termin.allowedToDelete && termin.paabgabe_id > 0" class="btn btn-primary border-0" @click="deleteTermin(termin)">
|
||||
Löschen
|
||||
<i style="margin-left: 8px" class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
export default AbgabeMitarbeiterDetail;
|
||||
@@ -0,0 +1,378 @@
|
||||
import Upload from '../../../components/Form/Upload/Dms.js';
|
||||
import BsModal from '../../Bootstrap/Modal.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
|
||||
const today = new Date()
|
||||
export const AbgabeStudentDetail = {
|
||||
name: "AbgabeStudentDetail",
|
||||
components: {
|
||||
Upload,
|
||||
BsModal,
|
||||
InputNumber: primevue.inputnumber,
|
||||
Checkbox: primevue.checkbox,
|
||||
Dropdown: primevue.dropdown,
|
||||
Textarea: primevue.textarea,
|
||||
VueDatePicker
|
||||
},
|
||||
props: {
|
||||
projektarbeit: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
viewMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
eidAkzeptiert: false,
|
||||
enduploadTermin: null,
|
||||
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
|
||||
form: Vue.reactive({
|
||||
sprache: '',
|
||||
abstract: '',
|
||||
abstract_en: '',
|
||||
schlagwoerter: '',
|
||||
schlagwoerter_en: '',
|
||||
kontrollschlagwoerter: '',
|
||||
seitenanzahl: 1,
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validate: function(termin) {
|
||||
if(!termin.file.length) {
|
||||
this.$fhcAlert.alertWarning(this.$p.t('global/warningChooseFile'));
|
||||
return false
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
triggerEndupload() {
|
||||
if (!this.validate(this.enduploadTermin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// post endabgabe
|
||||
const formData = new FormData();
|
||||
formData.append('paabgabetyp_kurzbz', this.enduploadTermin.paabgabetyp_kurzbz)
|
||||
formData.append('projektarbeit_id', this.enduploadTermin.projektarbeit_id);
|
||||
formData.append('paabgabe_id', this.enduploadTermin.paabgabe_id)
|
||||
formData.append('student_uid', this.projektarbeit.student_uid)
|
||||
formData.append('bperson_id', this.projektarbeit.bperson_id)
|
||||
|
||||
// TODO: validate/check for null etc.
|
||||
formData.append('sprache', this.form['sprache'].sprache)
|
||||
formData.append('abstract', this.form['abstract'])
|
||||
formData.append('abstract_en', this.form['abstract_en'])
|
||||
formData.append('schlagwoerter', this.form['schlagwoerter'])
|
||||
formData.append('schlagwoerter_en', this.form['schlagwoerter_en'])
|
||||
formData.append('seitenanzahl', this.form['seitenanzahl'])
|
||||
|
||||
for (let i = 0; i < this.enduploadTermin.file.length; i++) {
|
||||
formData.append('file', this.enduploadTermin.file[i]);
|
||||
}
|
||||
this.$fhcApi.factory.lehre.postStudentProjektarbeitEndupload(formData)
|
||||
.then(res => {
|
||||
this.handleUploadRes(res)
|
||||
})
|
||||
|
||||
this.$refs.modalContainerEnduploadZusatzdaten.hide()
|
||||
},
|
||||
downloadAbgabe(termin) {
|
||||
this.$fhcApi.factory.lehre.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
},
|
||||
upload(termin) {
|
||||
|
||||
if (!this.validate(termin))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(termin.bezeichnung === 'Endupload') {
|
||||
// open endupload form modal for further inputs
|
||||
this.enduploadTermin = termin
|
||||
this.$refs.modalContainerEnduploadZusatzdaten.show()
|
||||
} else {
|
||||
const formData = new FormData();
|
||||
formData.append('paabgabetyp_kurzbz', termin.paabgabetyp_kurzbz)
|
||||
formData.append('projektarbeit_id', this.projektarbeit.projektarbeit_id)
|
||||
formData.append('paabgabe_id', termin.paabgabe_id)
|
||||
formData.append('student_uid', this.projektarbeit.student_uid)
|
||||
formData.append('bperson_id', this.projektarbeit.bperson_id)
|
||||
|
||||
for (let i = 0; i < termin.file.length; i++) {
|
||||
formData.append('file', termin.file[i]);
|
||||
}
|
||||
this.$fhcApi.factory.lehre.postStudentProjektarbeitZwischenabgabe(formData)
|
||||
.then(res => {
|
||||
this.handleUploadRes(res)
|
||||
})
|
||||
}
|
||||
},
|
||||
handleUploadRes(res) {
|
||||
if(res.meta.status == "success") {
|
||||
this.$fhcAlert.alertSuccess('File erfolgreich hochgeladen')
|
||||
} else {
|
||||
this.$fhcAlert.alertError('File upload error')
|
||||
}
|
||||
|
||||
if(res.meta.signaturInfo) {
|
||||
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
|
||||
}
|
||||
},
|
||||
dateDiffInDays(datum, today){
|
||||
const oneDayMs = 1000 * 60 * 60 * 24
|
||||
return Math.round((new Date(datum) - new Date(today)) / oneDayMs)
|
||||
},
|
||||
getDateStyle(termin, mode) {
|
||||
const datum = new Date(termin.datum)
|
||||
const abgabedatum = new Date(termin.abgabedatum)
|
||||
|
||||
// todo: rework styling but keep the color pattern logic
|
||||
// https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende
|
||||
let color = 'white'
|
||||
let fontColor = 'black'
|
||||
let icon = '';
|
||||
if (termin.abgabedatum === null) {
|
||||
if(datum < today) {
|
||||
color = 'red'
|
||||
fontColor = 'white'
|
||||
icon = 'fa-triangle-exclamation'
|
||||
} else if (datum > today && this.dateDiffInDays(datum, today) <= 12) {
|
||||
color = 'yellow'
|
||||
icon = 'fa-circle-exclamation'
|
||||
}
|
||||
} else if(abgabedatum > datum) {
|
||||
color = 'pink' // aka "hellrot"
|
||||
fontColor = 'white'
|
||||
icon = 'fa-circle-question'
|
||||
} else {
|
||||
color = 'green'
|
||||
icon = 'fa-square-check'
|
||||
}
|
||||
|
||||
//return `font-color: ${fontColor} ; background-color: ${color}; border-radius: 50%;`
|
||||
if( typeof mode !== 'undefined' || mode === 'icon') {
|
||||
return icon;
|
||||
} else {
|
||||
return 'abgabe-zieldatum-border-' + color;
|
||||
}
|
||||
},
|
||||
openBeurteilungLink(link) {
|
||||
window.open(link, '_blank')
|
||||
},
|
||||
getOptionLabel(option) {
|
||||
return option.sprache
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
projektarbeit(newVal) {
|
||||
// default select german if projektarbeit sprache was null
|
||||
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
|
||||
this.form.abstract = newVal.abstract
|
||||
this.form.abstract_en = newVal.abstract_en
|
||||
this.form.schlagwoerter = newVal.schlagwoerter
|
||||
this.form.schlagwoerter_en = newVal.schlagwoerter_en
|
||||
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter
|
||||
this.form.seitenanzahl = newVal.seitenanzahl
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getEid() {
|
||||
return this.$p.t('abgabetool/c4eidesstattlicheErklaerung')
|
||||
},
|
||||
getEnduploadErlaubt() {
|
||||
return !this.eidAkzeptiert
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
template: `
|
||||
<div v-if="projektarbeit">
|
||||
|
||||
<h5>{{$p.t('abgabetool/c4abgabeStudentenbereich')}}</h5>
|
||||
<div class="row">
|
||||
<p> {{projektarbeit?.betreuer}}</p>
|
||||
<p> {{projektarbeit?.titel}}</p>
|
||||
</div>
|
||||
<div id="uploadWrapper">
|
||||
<div class="row" style="margin-bottom: 12px;">
|
||||
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4fixtermin')}}</div>
|
||||
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4zieldatum')}}</div>
|
||||
<div class="col-2 fw-bold">{{$p.t('abgabetool/c4abgabetyp')}}</div>
|
||||
<div class="col-3 fw-bold">{{$p.t('abgabetool/c4abgabekurzbz')}}</div>
|
||||
<div class="col-1 fw-bold text-center">{{$p.t('abgabetool/c4abgabedatum')}}</div>
|
||||
<div class="col-3 fw-bold">
|
||||
{{$p.t('abgabetool/c4fileupload')}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" v-for="termin in projektarbeit.abgabetermine">
|
||||
<div class="col-1 d-flex justify-content-center align-items-start">
|
||||
<i v-if="termin.fixtermin" class="fa-solid fa-2x fa-circle-check fhc-bullet-red"></i>
|
||||
<i v-else="" class="fa-solid fa-2x fa-circle-xmark fhc-bullet-green"></i>
|
||||
<!--
|
||||
<p class="fhc-bullet" :class="{ 'fhc-bullet-red': termin.fixtermin, 'fhc-bullet-green': !termin.fixtermin }"></p>
|
||||
-->
|
||||
</div>
|
||||
<div class="col-2 d-flex justify-content-start align-items-start">
|
||||
<div class="position-relative" :class="getDateStyle(termin)">
|
||||
<VueDatePicker
|
||||
v-model="termin.datum"
|
||||
:clearable="false"
|
||||
:disabled="true"
|
||||
:enable-time-picker="false"
|
||||
:format="formatDate"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
<i class="position-absolute abgabe-zieldatum-overlay fa-solid fa-2x" :class="getDateStyle(termin, 'icon')"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 d-flex justify-content-start align-items-start">{{ termin.bezeichnung }}</div>
|
||||
<div class="col-3 d-flex justify-content-start align-items-start">
|
||||
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="3" cols="45" :disabled="true"></Textarea>
|
||||
</div>
|
||||
<div class="col-1 d-flex flex-column justify-content-start align-items-center">
|
||||
{{ termin.abgabedatum?.split("-").reverse().join(".") }}
|
||||
<a v-if="termin?.abgabedatum" @click="downloadAbgabe(termin)" style="margin-left:4px; cursor: pointer;">
|
||||
<i class="fa-solid fa-3x fa-file-pdf"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-3" v-if="!viewMode">
|
||||
<div class="row">
|
||||
<div class="col-8">
|
||||
<Upload v-if="termin && termin.allowedToUpload" accept=".pdf" v-model="termin.file"></Upload>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-primary border-0" @click="upload(termin)" :disabled="!termin.allowedToUpload">
|
||||
Upload
|
||||
<i style="margin-left: 8px" class="fa-solid fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<bs-modal ref="modalContainerEnduploadZusatzdaten" class="bootstrap-prompt" dialogClass="modal-lg">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{$p.t('abgabetool/c4enduploadZusatzdaten')}}
|
||||
</div>
|
||||
<div class="row mb-3 align-items-start">
|
||||
|
||||
<p class="ml-4 mr-4">Student UID: {{ projektarbeit?.student_uid}}</p>
|
||||
|
||||
</div>
|
||||
<div class="row mb-3 align-items-start">
|
||||
|
||||
<p class="ml-4 mr-4">Titel: {{ projektarbeit?.titel }}</p>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4Sprache')}}</div>
|
||||
<div class="row">
|
||||
<Dropdown
|
||||
:style="{'width': '100%'}"
|
||||
v-model="form.sprache"
|
||||
:options="allActiveLanguages"
|
||||
:optionLabel="getOptionLabel">
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- lektor fills these out-->
|
||||
<!-- <div class="row mb-3 align-items-start">-->
|
||||
<!-- <div class="row">Kontrollierte Schlagwörter</div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <Textarea v-model="form.kontrollschlagwoerter"></Textarea>-->
|
||||
<!-- </div>-->
|
||||
<!-- -->
|
||||
<!-- -->
|
||||
<!-- </div>-->
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4schlagwoerterGer')}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4schlagwoerterEng')}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter_en"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4abstractGer')}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.abstract" rows="10"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4abstractEng')}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.abstract_en" rows="10"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$p.t('abgabetool/c4seitenanzahl')}}</div>
|
||||
<div class="row">
|
||||
<InputNumber
|
||||
v-model="form.seitenanzahl"
|
||||
inputId="seitenanzahlInput" :min="1" :max="100000">
|
||||
</InputNumber>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="projektarbeit">
|
||||
<div v-html="getEid"></div>
|
||||
<div class="row">
|
||||
<div class="col-9"></div>
|
||||
<div class="col-2"><p>{{ $p.t('abgabetool/c4gelesenUndAkzeptiert') }}</p></div>
|
||||
<div class="col-1">
|
||||
|
||||
<Checkbox
|
||||
v-model="eidAkzeptiert"
|
||||
:binary="true"
|
||||
:pt="{ root: { class: 'ml-auto' }}"
|
||||
>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button class="btn btn-primary" :disabled="getEnduploadErlaubt" @click="triggerEndupload">{{$p.t('ui/hochladen')}}</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
export default AbgabeStudentDetail;
|
||||
@@ -0,0 +1,483 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import AbgabeDetail from "./AbgabeMitarbeiterDetail.js";
|
||||
import VerticalSplit from "../../verticalsplit/verticalsplit.js"
|
||||
import BsModal from '../../Bootstrap/Modal.js';
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
|
||||
export const AbgabetoolMitarbeiter = {
|
||||
name: "AbgabetoolMitarbeiter",
|
||||
components: {
|
||||
BsModal,
|
||||
CoreFilterCmpt,
|
||||
AbgabeDetail,
|
||||
VerticalSplit,
|
||||
Dropdown: primevue.dropdown,
|
||||
Textarea: primevue.textarea,
|
||||
VueDatePicker
|
||||
},
|
||||
props: {
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({name: '', uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.name && value.uid
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
loading: false,
|
||||
// TODO: fetch types
|
||||
allAbgabeTypes: [
|
||||
{
|
||||
paabgabetyp_kurzbz: 'abstract',
|
||||
bezeichnung: 'Entwurf'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'zwischen',
|
||||
bezeichnung: 'Zwischenabgabe'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'note',
|
||||
bezeichnung: 'Benotung'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'end',
|
||||
bezeichnung: 'Endupload'
|
||||
},
|
||||
{
|
||||
paabgabetyp_kurzbz: 'enda',
|
||||
bezeichnung: 'Endabgabe im Sekretariat'
|
||||
}
|
||||
],
|
||||
serienTermin: Vue.reactive({
|
||||
datum: new Date(),
|
||||
bezeichnung: {
|
||||
paabgabetyp_kurzbz: 'zwischen',
|
||||
bezeichnung: 'Zwischenabgabe'
|
||||
},
|
||||
kurzbz: ''
|
||||
}),
|
||||
showAll: false,
|
||||
tabulatorUuid: Vue.ref(0),
|
||||
selectedData: [],
|
||||
domain: '',
|
||||
student_uid: null,
|
||||
detail: null,
|
||||
detailOffset: 0,
|
||||
projektarbeiten: null,
|
||||
selectedProjektarbeit: null,
|
||||
tableBuiltResolve: null,
|
||||
tableBuiltPromise: null,
|
||||
abgabeTableOptions: {
|
||||
height: 700,
|
||||
index: 'projektarbeit_id',
|
||||
layout: 'fitDataStretch',
|
||||
placeholder: this.$p.t('global/noDataAvailable'),
|
||||
selectable: true,
|
||||
selectableCheck: this.selectionCheck,
|
||||
columns: [
|
||||
{
|
||||
formatter: 'rowSelection',
|
||||
titleFormatter: 'rowSelection',
|
||||
titleFormatterParams: {
|
||||
rowRange: "active" // Only toggle the values of the active filtered rows
|
||||
},
|
||||
hozAlign:"center",
|
||||
headerSort: false,
|
||||
frozen: true,
|
||||
width: 70
|
||||
},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4personenkennzeichen')), field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4vorname')), field: 'vorname', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4nachname')), field: 'nachname', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter, widthGrow: 2},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'studiensemester_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuerart')), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 8}
|
||||
],
|
||||
persistence: false,
|
||||
},
|
||||
abgabeTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
handler: async () => {
|
||||
this.tableBuiltResolve()
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellClick",
|
||||
handler: async (e, cell) => {
|
||||
if(cell.getColumn().getField() === "details") {
|
||||
this.setDetailComponent(cell.getValue())
|
||||
this.undoSelection(cell)
|
||||
} else if (cell.getColumn().getField() === "mail") {
|
||||
this.undoSelection(cell)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "rowSelectionChanged",
|
||||
handler: async(data) => {
|
||||
this.selectedData = data
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return option.bezeichnung
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
},
|
||||
undoSelection(cell) {
|
||||
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
|
||||
// but actually just revert it after the fact
|
||||
|
||||
const row = cell.getRow()
|
||||
if(row.isSelected()) {
|
||||
row.deselect();
|
||||
}
|
||||
},
|
||||
selectionCheck(row) {
|
||||
const data = row.getData()
|
||||
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
|
||||
return true
|
||||
},
|
||||
showDeadlines(){
|
||||
const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
|
||||
+ '/Cis/Abgabetool/Deadlines'
|
||||
window.open(link, '_blank')
|
||||
},
|
||||
toggleShowAll(showall) {
|
||||
this.showAll = showall
|
||||
this.loading = true
|
||||
this.loadProjektarbeiten(showall, () => {
|
||||
this.$refs.abgabeTable?.tabulator.redraw(true)
|
||||
this.$refs.abgabeTable?.tabulator.setSort([]);
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openAddSeriesModal() {
|
||||
this.$refs.modalContainerAddSeries.show()
|
||||
},
|
||||
addSeries() {
|
||||
this.saving = true
|
||||
this.$fhcApi.factory.lehre.postSerientermin(
|
||||
this.serienTermin.datum.toISOString(),
|
||||
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
|
||||
this.serienTermin.bezeichnung.bezeichnung,
|
||||
this.serienTermin.kurzbz,
|
||||
this.selectedData?.map(projekt => projekt.projektarbeit_id)
|
||||
).then(res => {
|
||||
if (res.meta.status === "success" && res.data) {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert'))
|
||||
// TODO: sticky lifetime erhöhen um sinnvoll lesen zu können?
|
||||
this.$fhcAlert.alertInfo(this.$p.t('abgabetool/serienTerminEmailSentInfo', [this.createInfoString(res.data)]));
|
||||
} else {
|
||||
this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern'))
|
||||
}
|
||||
}).finally(()=>{
|
||||
this.saving = false
|
||||
})
|
||||
|
||||
this.$refs.modalContainerAddSeries.hide()
|
||||
},
|
||||
createInfoString(data) {
|
||||
let str = '';
|
||||
|
||||
data.forEach(name => {
|
||||
str += name
|
||||
str += '; '
|
||||
})
|
||||
|
||||
return str
|
||||
},
|
||||
isPastDate(date) {
|
||||
return new Date(date) < new Date(Date.now())
|
||||
},
|
||||
setDetailComponent(details){
|
||||
this.loadAbgaben(details).then((res)=> {
|
||||
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
|
||||
pa.abgabetermine = res.data[0].retval
|
||||
pa.isCurrent = res.data[1]
|
||||
pa.abgabetermine.push({ // new abgatermin row
|
||||
|
||||
'paabgabe_id': -1,
|
||||
'projektarbeit_id': pa.projektarbeit_id,
|
||||
'fixtermin': false,
|
||||
'kurzbz': '',
|
||||
'datum': new Date().toISOString().split('T')[0],
|
||||
'paabgabetyp_kurzbz': '',
|
||||
'bezeichnung': '',
|
||||
'abgabedatum': null,
|
||||
'insertvon': this.viewData?.uid ?? ''
|
||||
|
||||
})
|
||||
pa.abgabetermine.forEach(termin => {
|
||||
termin.file = []
|
||||
termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter'
|
||||
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
|
||||
|
||||
termin.bezeichnung = {
|
||||
bezeichnung: termin.bezeichnung,
|
||||
paabgabetyp_kurzbz: termin.paabgabetyp_kurzbz
|
||||
}
|
||||
})
|
||||
pa.betreuer = this.buildBetreuer(pa)
|
||||
pa.student_uid = details.student_uid
|
||||
pa.student = `${pa.vorname} ${pa.nachname}`
|
||||
|
||||
this.selectedProjektarbeit = pa
|
||||
|
||||
|
||||
this.$refs.verticalsplit.showBoth()
|
||||
|
||||
|
||||
})
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(!val) return
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal; margin: 0px; text-align: center">'+val+'</p></div>'
|
||||
},
|
||||
detailFormatter(cell) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
mailFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
beurteilungFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(val) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
|
||||
} else return '-'
|
||||
},
|
||||
pkzTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
buildMailToLink(abgabe) {
|
||||
return 'mailto:' + abgabe.uid +'@'+ this.domain
|
||||
},
|
||||
buildPKZ(projekt) {
|
||||
return `${projekt.uid} / ${projekt.matrikelnr}`
|
||||
},
|
||||
buildStg(projekt) {
|
||||
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
|
||||
},
|
||||
buildBetreuer(abgabe) {
|
||||
// TODO: preload and insert own titled name of betreuer somehow
|
||||
return abgabe.betreuerart_beschreibung + ': ' + (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
|
||||
},
|
||||
setupData(data){
|
||||
this.projektarbeiten = data[0]
|
||||
this.domain = data[1]
|
||||
|
||||
const d = data[0]?.retval?.map(projekt => {
|
||||
let mode = 'detailTermine'
|
||||
|
||||
return {
|
||||
...projekt,
|
||||
details: {
|
||||
student_uid: projekt.uid,
|
||||
projektarbeit_id: projekt.projektarbeit_id,
|
||||
},
|
||||
pkz: this.buildPKZ(projekt),
|
||||
beurteilung: projekt.beurteilungLink ?? null,
|
||||
sem: projekt.studiensemester_kurzbz,
|
||||
stg: this.buildStg(projekt),
|
||||
mail: this.buildMailToLink(projekt),
|
||||
typ: projekt.projekttyp_kurzbz,
|
||||
titel: projekt.titel
|
||||
}
|
||||
})
|
||||
|
||||
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
|
||||
this.$refs.abgabeTable.tabulator.setData(d);
|
||||
},
|
||||
loadProjektarbeiten(all = false, callback) {
|
||||
this.$fhcApi.factory.lehre.getMitarbeiterProjektarbeiten(this.viewData?.uid ?? null, all)
|
||||
.then(res => {
|
||||
if(res?.data) this.setupData(res.data)
|
||||
}).finally(() => {
|
||||
if(callback) {
|
||||
callback()
|
||||
}
|
||||
})
|
||||
},
|
||||
loadAbgaben(details) {
|
||||
return new Promise((resolve) => {
|
||||
this.$fhcApi.factory.lehre.getStudentProjektabgaben(details)
|
||||
.then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
},
|
||||
calcMaxTableHeight() {
|
||||
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
|
||||
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
|
||||
if(!tableDataSet) return
|
||||
const rect = tableDataSet.getBoundingClientRect();
|
||||
|
||||
this.abgabeTableOptions.height = window.visualViewport.height - rect.top
|
||||
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
|
||||
},
|
||||
async setupMounted() {
|
||||
this.tableBuiltPromise = new Promise(this.tableResolve)
|
||||
await this.tableBuiltPromise
|
||||
|
||||
this.loadProjektarbeiten()
|
||||
|
||||
|
||||
this.$refs.verticalsplit.collapseBottom()
|
||||
this.calcMaxTableHeight()
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
template: `
|
||||
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
|
||||
dialogClass="modal-lg">
|
||||
<template v-slot:title>
|
||||
<div>
|
||||
{{ $p.t('abgabetool/neueTerminserie') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<div class="row">
|
||||
<div class="col-3 d-flex justify-content-center align-items-center">
|
||||
{{$p.t('abgabetool/c4zieldatum')}}
|
||||
</div>
|
||||
<div class="col-3 d-flex justify-content-center align-items-center">
|
||||
{{$p.t('abgabetool/c4abgabetyp')}}
|
||||
</div>
|
||||
<div class="col-6 d-flex justify-content-center align-items-center">
|
||||
{{$p.t('abgabetool/c4abgabekurzbz')}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-3 d-flex justify-content-center align-items-center">
|
||||
<div>
|
||||
<VueDatePicker
|
||||
style="width: 95%;"
|
||||
v-model="serienTermin.datum"
|
||||
:clearable="false"
|
||||
:enable-time-picker="false"
|
||||
:format="formatDate"
|
||||
:text-input="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex justify-content-center align-items-center">
|
||||
<Dropdown
|
||||
:style="{'width': '100%'}"
|
||||
v-model="serienTermin.bezeichnung"
|
||||
:options="allAbgabeTypes"
|
||||
:optionLabel="getOptionLabelAbgabetyp">
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div class="col-6 d-flex justify-content-center align-items-center">
|
||||
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="3" cols="40"></Textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<button type="button" class="btn btn-primary" @click="addSeries">{{ $p.t('global/speichern') }}</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
|
||||
<vertical-split ref="verticalsplit">
|
||||
|
||||
<template #top>
|
||||
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
|
||||
<hr>
|
||||
<core-filter-cmpt
|
||||
:title="''"
|
||||
@uuidDefined="handleUuidDefined"
|
||||
ref="abgabeTable"
|
||||
:newBtnShow="true"
|
||||
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
|
||||
:newBtnDisabled="!selectedData.length"
|
||||
@click:new=openAddSeriesModal
|
||||
:tabulator-options="abgabeTableOptions"
|
||||
:tabulator-events="abgabeTableEventHandlers"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
:useSelectionSpan="false"
|
||||
>
|
||||
<template #actions>
|
||||
<button @click="toggleShowAll(!showAll)" role="button" class="btn btn-secondary ml-2">
|
||||
<i v-show="!showAll" class="fa fa-eye"></i>
|
||||
<i v-show="showAll" class="fa fa-eye-slash"></i>
|
||||
{{ $p.t('abgabetool/showAll') }}
|
||||
</button>
|
||||
|
||||
<button @click="showDeadlines" role="button" class="btn btn-secondary ml-2">
|
||||
<i class="fa fa-hourglass-end"></i>
|
||||
{{ $p.t('abgabetool/showDeadlines') }}
|
||||
</button>
|
||||
|
||||
<div v-show="saving">
|
||||
{{ $p.t('abgabetool/currentlySaving') }} <i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<div v-show="loading">
|
||||
{{ $p.t('abgabetool/currentlyLoading') }} <i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</core-filter-cmpt>
|
||||
|
||||
</template>
|
||||
<template #bottom>
|
||||
<div v-show="selectedProjektarbeit" ref="selProj">
|
||||
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
|
||||
</div>
|
||||
</template>
|
||||
</vertical-split>
|
||||
|
||||
|
||||
`,
|
||||
};
|
||||
|
||||
export default AbgabetoolMitarbeiter;
|
||||
@@ -0,0 +1,264 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import AbgabeDetail from "./AbgabeStudentDetail.js";
|
||||
import VerticalSplit from "../../verticalsplit/verticalsplit.js";
|
||||
|
||||
export const AbgabetoolStudent = {
|
||||
name: "AbgabetoolStudent",
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
AbgabeDetail,
|
||||
VerticalSplit
|
||||
},
|
||||
props: {
|
||||
student_uid_prop: {
|
||||
default: null
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.uid
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabulatorUuid: Vue.ref(0),
|
||||
domain: '',
|
||||
student_uid: null,
|
||||
detail: null,
|
||||
projektarbeiten: null,
|
||||
selectedProjektarbeit: null,
|
||||
tableBuiltResolve: null,
|
||||
tableBuiltPromise: null,
|
||||
abgabeTableOptions: {
|
||||
minHeight: 250,
|
||||
index: 'projektarbeit_id',
|
||||
layout: 'fitColumns',
|
||||
placeholder: this.$p.t('global/noDataAvailable'),
|
||||
columns: [
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4details')), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4beurteilung')), field: 'beurteilung', formatter: this.beurteilungFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'sem', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4kontakt')), field: 'mail', formatter: this.mailFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4betreuer')), field: 'betreuer', formatter: this.centeredTextFormatter,widthGrow: 2},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4projekttyp')), field: 'typ', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4titel')), field: 'titel', formatter: this.centeredTextFormatter, widthGrow: 8}
|
||||
],
|
||||
persistence: false,
|
||||
},
|
||||
abgabeTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
handler: async () => {
|
||||
this.tableBuiltResolve()
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellClick",
|
||||
handler: async (e, cell) => {
|
||||
|
||||
if(cell.getColumn().getField() === "details") {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val.mode === 'detailTermine') {
|
||||
this.setDetailComponent(cell.getValue())
|
||||
} else if (val.mode === 'beurteilungDownload') {
|
||||
const pdfExportLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
|
||||
// const pdfExportLink2 = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/lehre/projektbeurteilungDocumentExport.php?betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
|
||||
window.open(pdfExportLink, '_blank')
|
||||
}
|
||||
|
||||
} else if (cell.getColumn().getField() === "beurteilung") {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val != '-') window.open(val, '_blank')
|
||||
}
|
||||
e.stopPropagation()
|
||||
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
isPastDate(date) {
|
||||
return new Date(date) < new Date(Date.now())
|
||||
},
|
||||
setDetailComponent(details){
|
||||
this.loadAbgaben(details).then((res)=> {
|
||||
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
|
||||
pa.abgabetermine = res.data[0].retval
|
||||
pa.abgabetermine.forEach(termin => {
|
||||
termin.file = []
|
||||
termin.allowedToUpload = true
|
||||
|
||||
// TODO: fixtermin logic?
|
||||
if(termin.bezeichnung == 'Endupload' && this.isPastDate(termin.datum)) {
|
||||
|
||||
// termin.allowedToUpload = false
|
||||
} else {
|
||||
// termin.allowedToUpload = true
|
||||
}
|
||||
|
||||
})
|
||||
pa.betreuer = this.buildBetreuer(pa)
|
||||
pa.student_uid = this.student_uid
|
||||
|
||||
this.selectedProjektarbeit = pa
|
||||
|
||||
|
||||
this.$refs.verticalsplit.showBoth()
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
|
||||
},
|
||||
detailFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val.mode === 'detailTermine') {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
|
||||
} else if (val.mode === 'beurteilungDownload') {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
|
||||
}
|
||||
},
|
||||
mailFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
beurteilungFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(val) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
|
||||
} else return '-'
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
buildMailToLink(abgabe) {
|
||||
return 'mailto:' + abgabe.mitarbeiter_uid +'@'+ this.domain
|
||||
},
|
||||
buildBetreuer(abgabe) {
|
||||
return abgabe.betreuerart_beschreibung + ': ' + (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
|
||||
},
|
||||
setupData(data){
|
||||
this.projektarbeiten = data[0]
|
||||
this.domain = data[1]
|
||||
this.student_uid = data[2]
|
||||
const d = data[0]?.retval?.map(projekt => {
|
||||
let mode = 'detailTermine'
|
||||
|
||||
if (projekt.babgeschickt || projekt.zweitbetreuer_abgeschickt) {
|
||||
// mode = 'beurteilungDownload' // build dl link for both betreuer documents
|
||||
projekt.beurteilungLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+projekt.betreuerart_kurzbz+'&projektarbeit_id='+projekt.projektarbeit_id+'&person_id=' + projekt.bperson_id
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
details: {
|
||||
student_uid: this.student_uid,
|
||||
projektarbeit_id: projekt.projektarbeit_id,
|
||||
betreuer_person_id: projekt.bperson_id,
|
||||
betreuerart_kurzbz: projekt.betreuerart_kurzbz,
|
||||
mode
|
||||
},
|
||||
beurteilung: projekt.beurteilungLink ?? null,
|
||||
sem: projekt.studiensemester_kurzbz,
|
||||
stg: projekt.kurzbzlang,
|
||||
mail: this.buildMailToLink(projekt),
|
||||
betreuer: this.buildBetreuer(projekt),
|
||||
typ: projekt.projekttypbezeichnung,
|
||||
titel: projekt.titel
|
||||
}
|
||||
})
|
||||
|
||||
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
|
||||
this.$refs.abgabeTable.tabulator.setData(d);
|
||||
},
|
||||
loadProjektarbeiten() {
|
||||
this.$fhcApi.factory.lehre.getStudentProjektarbeiten(this.student_uid_prop || this.viewData?.uid || null)
|
||||
.then(res => {
|
||||
if(res?.data) this.setupData(res.data)
|
||||
})
|
||||
},
|
||||
loadAbgaben(details) {
|
||||
return new Promise((resolve) => {
|
||||
this.$fhcApi.factory.lehre.getStudentProjektabgaben(details)
|
||||
.then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
},
|
||||
calcMaxTableHeight() {
|
||||
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
|
||||
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
|
||||
if(!tableDataSet) return
|
||||
const rect = tableDataSet.getBoundingClientRect();
|
||||
|
||||
this.abgabeTableOptions.height = window.visualViewport.height - rect.top
|
||||
this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height)
|
||||
},
|
||||
async setupMounted() {
|
||||
this.tableBuiltPromise = new Promise(this.tableResolve)
|
||||
await this.tableBuiltPromise
|
||||
|
||||
this.loadProjektarbeiten()
|
||||
|
||||
this.$refs.verticalsplit.collapseBottom()
|
||||
//this.calcMaxTableHeight()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
computed: {
|
||||
isViewMode() {
|
||||
return this.student_uid !== this.viewData.uid
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
template: `
|
||||
<vertical-split ref="verticalsplit">
|
||||
<template #top>
|
||||
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
|
||||
<hr>
|
||||
|
||||
<core-filter-cmpt
|
||||
@uuidDefined="handleUuidDefined"
|
||||
:title="''"
|
||||
ref="abgabeTable"
|
||||
:tabulator-options="abgabeTableOptions"
|
||||
:tabulator-events="abgabeTableEventHandlers"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
/>
|
||||
|
||||
</template>
|
||||
<template #bottom>
|
||||
<div v-show="selectedProjektarbeit">
|
||||
<AbgabeDetail :viewMode="isViewMode" :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
|
||||
</div>
|
||||
</template>
|
||||
</vertical-split>
|
||||
`,
|
||||
};
|
||||
|
||||
export default AbgabetoolStudent;
|
||||
@@ -0,0 +1,151 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
|
||||
export const DeadlineOverview = {
|
||||
name: "DeadlineOverview",
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
},
|
||||
props: {
|
||||
person_uid_prop: {
|
||||
default: null
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({name: '', uid: ''}),
|
||||
validator(value) {
|
||||
return value && value.name && value.uid
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullName: null, // TODO: fetch this somewhere
|
||||
deadlines: null,
|
||||
tabulatorUuid: Vue.ref(0),
|
||||
tableBuiltResolve: null,
|
||||
tableBuiltPromise: null,
|
||||
deadlineTableOptions: {
|
||||
height: 700,
|
||||
index: 'projektarbeit_id',
|
||||
layout: 'fitColumns',
|
||||
placeholder: this.$p.t('global/noDataAvailable'),
|
||||
columns: [
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4zieldatum')), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4fixtermin')), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabetyp')), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4abgabekurzbz')), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
|
||||
{title: Vue.computed(() => this.$p.t('person/studentIn')), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4stg')), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('abgabetool/c4sem')), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
|
||||
],
|
||||
persistence: false,
|
||||
},
|
||||
deadlineTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
handler: async () => {
|
||||
this.tableBuiltResolve()
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellClick",
|
||||
handler: async (e, cell) => {
|
||||
|
||||
if(cell.getColumn().getField() === "details") {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val.mode === 'detailTermine') {
|
||||
this.setDetailComponent(cell.getValue())
|
||||
} else if (val.mode === 'beurteilungDownload') {
|
||||
const pdfExportLink = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/pdfExport.php?xml=projektarbeitsbeurteilung.xml.php&xsl=Projektbeurteilung&betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
|
||||
// const pdfExportLink2 = FHC_JS_DATA_STORAGE_OBJECT.app_root + 'cis/private/lehre/projektbeurteilungDocumentExport.php?betreuerart_kurzbz='+val.betreuerart_kurzbz+'&projektarbeit_id='+val.projektarbeit_id+'&person_id=' + val.betreuer_person_id
|
||||
window.open(pdfExportLink, '_blank')
|
||||
}
|
||||
|
||||
} else if (cell.getColumn().getField() === "beurteilung") {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val != '-') window.open(val, '_blank')
|
||||
}
|
||||
e.stopPropagation()
|
||||
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
centeredTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</p></div>'
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
loadDeadlines() {
|
||||
this.$fhcApi.factory.lehre.fetchDeadlines(this.person_uid_prop ?? null)
|
||||
.then(res => {
|
||||
if(res?.data) this.setupData(res.data)
|
||||
})
|
||||
},
|
||||
setupData(data) {
|
||||
this.deadlines = data
|
||||
|
||||
this.deadlines.forEach(dl => {
|
||||
dl.student = (dl.stud_titelpre ? (dl.stud_titelpre + ' ') :'') + dl.stud_vorname + ' ' + dl.stud_nachname + (dl.stud_titelpost ? (' ' + dl.stud_titelpost) :'')
|
||||
dl.fixterminstring = dl.fixtermin ? this.$p.t('abgabetool/c4yes') : this.$p.t('abgabetool/c4no')
|
||||
})
|
||||
|
||||
this.$refs.deadlineTable.tabulator.setColumns(this.deadlineTableOptions.columns)
|
||||
this.$refs.deadlineTable.tabulator.setData(this.deadlines);
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
},
|
||||
calcMaxTableHeight() {
|
||||
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
|
||||
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
|
||||
if(!tableDataSet) return
|
||||
const rect = tableDataSet.getBoundingClientRect();
|
||||
|
||||
this.deadlineTableOptions.height = window.visualViewport.height - rect.top
|
||||
this.$refs.deadlineTable.tabulator.setHeight(this.deadlineTableOptions.height)
|
||||
},
|
||||
async setupMounted() {
|
||||
this.tableBuiltPromise = new Promise(this.tableResolve)
|
||||
await this.tableBuiltPromise
|
||||
|
||||
this.loadDeadlines()
|
||||
this.calcMaxTableHeight()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
computed: {
|
||||
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
template: `
|
||||
<h2>{{$p.t('abgabetool/deadlinesTitle')}} {{ fullName ? ('-' + fullName) : ''}}</h2>
|
||||
<hr>
|
||||
|
||||
<core-filter-cmpt
|
||||
@uuidDefined="handleUuidDefined"
|
||||
:title="''"
|
||||
ref="deadlineTable"
|
||||
:tabulator-options="deadlineTableOptions"
|
||||
:tabulator-events="deadlineTableEventHandlers"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
/>
|
||||
`,
|
||||
};
|
||||
|
||||
export default DeadlineOverview;
|
||||
@@ -1,7 +1,7 @@
|
||||
import raum_contentmittitel from './Content_types/Raum_contentmittitel.js'
|
||||
import general from './Content_types/General.js'
|
||||
import BsConfirm from "../../Bootstrap/Confirm.js";
|
||||
|
||||
import news_content from './Content_types/News_content.js';
|
||||
import ApiCms from '../../../api/factory/cms.js';
|
||||
|
||||
export default {
|
||||
@@ -22,6 +22,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
raum_contentmittitel,
|
||||
news_content,
|
||||
general,
|
||||
},
|
||||
data() {
|
||||
@@ -80,6 +81,8 @@ export default {
|
||||
switch (this.content_type) {
|
||||
case "raum_contentmittitel":
|
||||
return "raum_contentmittitel";
|
||||
case "news":
|
||||
return "news_content";
|
||||
default:
|
||||
return "general";
|
||||
};
|
||||
@@ -92,7 +95,9 @@ export default {
|
||||
},
|
||||
template: /*html*/ `
|
||||
<!-- div that contains the content -->
|
||||
<component ref="content" :is="computeContentType" v-if="content" :content="content" :content_id="content_id_internal" />
|
||||
<div id="fhc-cms-content" v-if="content">
|
||||
<component ref="content" :is="computeContentType" :content="content" :content_id="content_id_internal" />
|
||||
</div>
|
||||
<p v-else>No content is available to display</p>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -85,26 +85,36 @@ export default {
|
||||
.then(() => {
|
||||
Axios.get(el.href)
|
||||
.then((res) => {
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
})
|
||||
.catch((err) => console.error("ERROR:", err));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
el.href = el.dataset.href.replace(
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[href]").forEach((element) => {
|
||||
let orignal_href = element.getAttribute("href");
|
||||
let new_href = replaceRelativeLegacyLink(orignal_href);
|
||||
element.href = new_href;
|
||||
});
|
||||
document.querySelectorAll("[href]").forEach((element) => {
|
||||
let orignal_href = element.getAttribute("href");
|
||||
let new_href = replaceRelativeLegacyLink(orignal_href);
|
||||
element.href = new_href;
|
||||
});
|
||||
|
||||
document.querySelectorAll("[style*=background-color]").forEach((element) => {
|
||||
if (element.style.backgroundColor == "rgb(255, 255, 255)"){
|
||||
element.style.backgroundColor = "var(--fhc-background)";
|
||||
}
|
||||
if(element.querySelector("*[style*=background-color]")){
|
||||
element.style.backgroundColor = "var(--fhc-tertiary)";
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
template: /*html*/ `
|
||||
<!-- div that contains the content -->
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
|
||||
export default {
|
||||
name: "GeneralComponent",
|
||||
props:{
|
||||
content:{
|
||||
type:String,
|
||||
required:true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
sanitizeLegacyTables(table) {
|
||||
|
||||
// find nested tables and replace with p element
|
||||
const tt = table.querySelectorAll('table')
|
||||
tt.forEach(t => {
|
||||
const textContent = t.textContent.trim();
|
||||
const pElement = document.createElement('p');
|
||||
pElement.textContent = textContent;
|
||||
t.parentNode.replaceChild(pElement, t);
|
||||
})
|
||||
|
||||
// find unordered lists, traverse li childs and replace with p element -> more readable than 1 p tag for ul
|
||||
const ul = table.querySelectorAll('ul')
|
||||
ul.forEach(u => {
|
||||
Array.from(u.children).forEach(li => {
|
||||
const p = document.createElement('p');
|
||||
p.textContent = li.textContent
|
||||
u.parentNode.appendChild(p)
|
||||
})
|
||||
u.parentNode.removeChild(u)
|
||||
|
||||
})
|
||||
|
||||
// find bare text nodes and put into p element
|
||||
const td = Array.from(table.querySelectorAll('td')).filter(el => el.scrollWidth > 100)
|
||||
td.forEach(element => {
|
||||
if (element.firstChild?.nodeType === Node.TEXT_NODE && element.firstChild.length > 10) {
|
||||
const p = document.createElement('p');
|
||||
p.appendChild(element.firstChild)
|
||||
element.appendChild(p);
|
||||
}
|
||||
});
|
||||
|
||||
// flatten nested th elements
|
||||
const ths = Array.from(table.querySelectorAll('th'))
|
||||
ths.forEach(th => {
|
||||
|
||||
if(th.children.length > 1) {
|
||||
th.innerHTML = Array.from(th.childNodes).find(cn => cn.textContent).textContent
|
||||
}
|
||||
})
|
||||
|
||||
// let p elements wrap on overflow
|
||||
const p = table.querySelectorAll('p')
|
||||
p.forEach(p => {
|
||||
p.style.setProperty('word-wrap', 'break-word');
|
||||
p.style.setProperty('white-space', 'normal');
|
||||
p.style.setProperty('max-width', '400px');
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
// replaces the tablesorter with the tabulator
|
||||
let tables = Array.from(document.getElementsByClassName("tablesorter"));
|
||||
|
||||
tables.forEach((table, index) => {
|
||||
this.sanitizeLegacyTables(table)
|
||||
|
||||
new Tabulator(table, {
|
||||
index: index,
|
||||
layout: "fitDataFill",
|
||||
|
||||
columnDefaults: {
|
||||
formatter: "html",
|
||||
resizable: true,
|
||||
minWidth: "100px"
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
|
||||
el.addEventListener("click", (evt) => {
|
||||
evt.preventDefault();
|
||||
BsConfirm.popup(el.dataset.confirm)
|
||||
.then(() => {
|
||||
Axios.get(el.href)
|
||||
.then((res) => {
|
||||
// TODO(chris): check for success then show message and/or reload
|
||||
location = location;
|
||||
})
|
||||
.catch((err) => console.error("ERROR:", err));
|
||||
})
|
||||
.catch(() => {});
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#cms [data-href]").forEach((el) => {
|
||||
el.href = el.dataset.href.replace(
|
||||
/^ROOT\//,
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
);
|
||||
});
|
||||
|
||||
document.querySelectorAll("[href]").forEach((element) => {
|
||||
let orignal_href = element.getAttribute("href");
|
||||
let new_href = replaceRelativeLegacyLink(orignal_href);
|
||||
element.href = new_href;
|
||||
});
|
||||
|
||||
document.querySelectorAll("[style*=background-color]").forEach((element) => {
|
||||
if (element.style.backgroundColor == "rgb(255, 255, 255)"){
|
||||
element.style.backgroundColor = "var(--fhc-background)";
|
||||
}
|
||||
if(element.querySelector("*[style*=background-color]")){
|
||||
element.style.backgroundColor = "var(--fhc-tertiary)";
|
||||
}
|
||||
});
|
||||
|
||||
Vue.nextTick(() => {
|
||||
document.querySelectorAll(".card-header").forEach((el) => {
|
||||
el.classList.add("fhc-primary");
|
||||
});
|
||||
document.querySelectorAll(".row").forEach((el) => {
|
||||
el.classList.add("w-100");
|
||||
el.classList.add("align-items-center");
|
||||
|
||||
});
|
||||
document.querySelectorAll(".row h2").forEach((el) => {
|
||||
el.classList.add("mb-0");
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
template: /*html*/ `
|
||||
<!-- div that contains the content -->
|
||||
<div v-if="content" class="container" style="max-width: 100%;"><div class="row"><div class="col">
|
||||
<div v-html="content" ></div>
|
||||
</div></div></div>
|
||||
<p v-else>Content was not found</p>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -58,6 +58,20 @@ export default {
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
);
|
||||
});
|
||||
Vue.nextTick(()=>{
|
||||
document.querySelectorAll(".card-header").forEach((el) => {
|
||||
el.classList.add("fhc-primary");
|
||||
});
|
||||
document.querySelectorAll(".row").forEach((el) => {
|
||||
el.classList.add("w-100");
|
||||
el.classList.add("align-items-center");
|
||||
|
||||
});
|
||||
document.querySelectorAll(".row h2").forEach((el) => {
|
||||
el.classList.add("mb-0");
|
||||
});
|
||||
|
||||
})
|
||||
});
|
||||
},
|
||||
loadNewPageContent(data) {
|
||||
@@ -66,6 +80,7 @@ export default {
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.content = result;
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -80,11 +95,11 @@ export default {
|
||||
});
|
||||
},
|
||||
template: /*html*/ `
|
||||
<h2 >News</h2>
|
||||
<h2 class="fhc-primary-color">News</h2>
|
||||
<hr/>
|
||||
<pagination v-show="content?true:false" :page_size="page_size" @page="page=$event.page; loadNewPageContent($event)" :maxPageCount="maxPageCount">
|
||||
</pagination>
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col" v-html="content">
|
||||
</div>
|
||||
|
||||
@@ -28,16 +28,16 @@ components:{
|
||||
StudiengangVertretung,
|
||||
},
|
||||
template:/*html*/`
|
||||
|
||||
<div id="fhc-studiengang-informationen">
|
||||
<template v-if="studiengang?.bezeichnung && semester">
|
||||
<div class="card card-body mb-3">
|
||||
<div class="card card-body mb-3 border-0">
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
|
||||
<span class="mb-1">{{studiengang?.bezeichnung}}</span>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<h2 class="h4 mb-1 pb-0">Moodle:</h2>
|
||||
<a class="mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
|
||||
<a class="fhc-link-color mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
|
||||
</div>
|
||||
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
|
||||
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
|
||||
@@ -74,6 +74,7 @@ template:/*html*/`
|
||||
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
`,
|
||||
computed:{
|
||||
@@ -118,4 +119,4 @@ computed:{
|
||||
Object.assign(this, studiengangInformationen);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,37 +15,27 @@ export default {
|
||||
}
|
||||
},
|
||||
template:/*html*/`
|
||||
<div class="card" :style="{'width':displayWidget?'12rem':'15rem'}">
|
||||
<div class="bg-dark d-flex justify-content-center">
|
||||
<div class="card border-0" :style="{'width':displayWidget?'12rem':'15rem'}">
|
||||
<div class="d-flex justify-content-center">
|
||||
<img :src="base64Image" alt="mitarbeiter_foto" style="width: 110px; height: auto; object-fir:scale-down;" class="card-img-top" >
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="text-center card-title mb-0">{{fullname}} <a v-if="profilViewLink" :href="profilViewLink"><i class="ms-2 fa fa-arrow-up-right-from-square" style="color:#00649C"></i></a></h6>
|
||||
<div class="card-body p-2 flex-grow-0" style="min-height: 50px;">
|
||||
<h6 class="text-center card-title mb-0">{{fullname}} <a v-if="profilViewLink" :href="profilViewLink" :aria-label="$p.t('profil','profil')" :title="$p.t('profil','profil')"><i class="ms-2 fa fa-arrow-up-right-from-square fhc-primary-color" aria-hidden="true"></i></a></h6>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="card-body">
|
||||
|
||||
<div class="flex flex-column gap-3">
|
||||
<div class="mb-3">
|
||||
<span>
|
||||
<i class="fa fa-phone me-2"></i>
|
||||
<a :href="phone.link">{{phone.number}}</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<span>
|
||||
<i class="fa fa-home me-2"></i>
|
||||
{{ort}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
<i class="fa-regular fa-envelope me-2"></i>
|
||||
<a :href="email_link" v-html="formattedEmail"></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
|
||||
<dl class="stgkontaktinfo">
|
||||
<dt><i class="fa fa-phone me-2"></i></dt>
|
||||
<dd class="mb-3"><a class="fhc-link-color" :href="phone.link">{{phone.number}}</a></dd>
|
||||
|
||||
<dt><i class="fa fa-home me-2"></i></dt>
|
||||
<dd class="mb-3">{{ort}}</dd>
|
||||
|
||||
<dt><i class="fa-regular fa-envelope me-2"></i></dt>
|
||||
<dd class="mb-3"><a class="fhc-link-color" :href="email_link" v-html="formattedEmail"></a></dd>
|
||||
</dl>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -5,14 +5,14 @@ export default {
|
||||
showBezeichnung:Boolean,
|
||||
},
|
||||
template:/*html*/`
|
||||
<div class="card mb-3">
|
||||
<div class="card mb-3 border-0">
|
||||
<div class="card-header">
|
||||
<span>{{title}}</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p v-for="vertretung in vertretungsList">
|
||||
<a v-if="profilViewLink(vertretung.uid)" :href="profilViewLink(vertretung.uid)">
|
||||
<i class="me-2 fa fa-arrow-up-right-from-square" style="color:#00649C"></i>
|
||||
<a v-if="profilViewLink(vertretung.uid)" :href="profilViewLink(vertretung.uid)" :aria-label="$p.t('profil','profil')" :title="$p.t('profil','profil')">
|
||||
<i class="me-2 fa fa-arrow-up-right-from-square fhc-primary-color" aria-hidden="true"></i>
|
||||
</a>
|
||||
{{vertretungFormatedName(vertretung,false)}}
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
|
||||
export default {
|
||||
name: 'LvPlanLehrveranstaltung',
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lv: null
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return DEFAULT_MODE_LVPLAN;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
currentLv() {
|
||||
if (isNaN(parseInt(this.propsViewData?.lv_id)))
|
||||
return null;
|
||||
return this.propsViewData.lv_id;
|
||||
},
|
||||
lvTitle() {
|
||||
if (this.currentLv === null)
|
||||
return '';
|
||||
if (!this.lv)
|
||||
return '';
|
||||
|
||||
if (this.$p.user_language.value === 'English')
|
||||
return this.lv.bezeichnung_english;
|
||||
|
||||
return this.lv.bezeichnung;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "LvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
lv_id: this.currentLv
|
||||
}
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(ApiLvPlan.eventsLv(this.propsViewData.lv_id, start.toISODate(), end.toISODate())),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.currentLv === null)
|
||||
return;
|
||||
this.$api
|
||||
.call(ApiLvPlan.getLv(this.propsViewData?.lv_id))
|
||||
.then(res => {
|
||||
this.lv = res.data;
|
||||
});
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="cis-lvplan-personal d-flex flex-column h-100">
|
||||
<h2>
|
||||
{{ $p.t('lehre/stundenplan') }}
|
||||
<span v-if="lvTitle" class="ps-3">
|
||||
{{ lvTitle }}
|
||||
</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<div v-if="currentLv === null || lv === false">
|
||||
{{ $p.t('lehre/noLvFound') }}
|
||||
</div>
|
||||
<fhc-calendar
|
||||
v-else-if="lv"
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
class="responsive-calendar"
|
||||
/>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
|
||||
export default {
|
||||
name: 'LvPlan',
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
|
||||
return {
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
lv: null
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
currentDay() {
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
},
|
||||
currentMode() {
|
||||
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN;
|
||||
},
|
||||
downloadLinks() {
|
||||
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
|
||||
return false;
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
const ende = luxon.DateTime
|
||||
.fromISO(this.studiensemester_ende, opts)
|
||||
.toUnixInteger();
|
||||
|
||||
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/lvplan/stpl_kalender.php'
|
||||
+ '?type=student'
|
||||
+ '&pers_uid=' + this.uid
|
||||
+ '&begin=' + start
|
||||
+ '&ende=' + ende;
|
||||
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "LvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
lv_id: this.propsViewData?.lv_id ?? null
|
||||
}
|
||||
});
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.$api
|
||||
.call(ApiLvPlan.studiensemesterDateInterval(
|
||||
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;
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(ApiLvPlan.LvPlanEvents(start.toISODate(), end.toISODate(), this.propsViewData.lv_id)),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthUID())
|
||||
.then(res => {
|
||||
this.uid = res.data.uid;
|
||||
});
|
||||
},
|
||||
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 }}
|
||||
</span>
|
||||
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv">
|
||||
{{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english }}
|
||||
</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
v-model:lv="lv"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
class="responsive-calendar"
|
||||
>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 btn btn-outline-secondary"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span style="font-size:.5rem">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,155 @@
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week'
|
||||
|
||||
export default {
|
||||
name: 'LvPlanPersonal',
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
props: {
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
isMitarbeiter: false,
|
||||
isStudent: false
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return DEFAULT_MODE_LVPLAN;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
|
||||
return false;
|
||||
|
||||
let type = false;
|
||||
type = this.isStudent ? 'student' : type;
|
||||
type = this.isMitarbeiter ? 'lektor' : type;
|
||||
if (false === type)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
const ende = luxon.DateTime
|
||||
.fromISO(this.studiensemester_ende, opts)
|
||||
.toUnixInteger();
|
||||
|
||||
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/lvplan/stpl_kalender.php'
|
||||
+ '?type=' + type
|
||||
+ '&pers_uid=' + this.uid
|
||||
+ '&begin=' + start
|
||||
+ '&ende=' + ende;
|
||||
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "MyLvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date
|
||||
}
|
||||
});
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.$api
|
||||
.call(ApiLvPlan.studiensemesterDateInterval(
|
||||
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;
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(ApiLvPlan.eventsPersonal(start.toISODate(), end.toISODate())),
|
||||
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
|
||||
];
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthInfo())
|
||||
.then(res => {
|
||||
this.uid = res.data.uid;
|
||||
this.isMitarbeiter = res.data.isMitarbeiter;
|
||||
this.isStudent = res.data.isStudent;
|
||||
});
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="cis-lvplan-personal d-flex flex-column h-100">
|
||||
<h2>
|
||||
{{ $p.t('lehre/stundenplan') }}
|
||||
<span v-if="studiensemester_kurzbz" class="ps-3">
|
||||
{{ studiensemester_kurzbz }}
|
||||
</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
class="responsive-calendar"
|
||||
>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 btn btn-outline-secondary"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span style="font-size:.5rem">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import CisMenuEntry from "./Menu/Entry.js";
|
||||
import FhcSearchbar from "../searchbar/searchbar.js";
|
||||
import CisSprachen from "./Sprachen.js";
|
||||
|
||||
import CisSprachen from "./Sprachen.js"
|
||||
import ThemeSwitch from "./ThemeSwitch.js";
|
||||
import ApiCisMenu from '../../api/factory/cis/menu.js';
|
||||
|
||||
export default {
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
CisMenuEntry,
|
||||
FhcSearchbar,
|
||||
CisSprachen,
|
||||
ThemeSwitch,
|
||||
},
|
||||
props: {
|
||||
rootUrl: String,
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
url:null,
|
||||
urlMatchRankings:[],
|
||||
navUserDropdown:null,
|
||||
menuOpen:true,
|
||||
};
|
||||
},
|
||||
provide(){
|
||||
@@ -36,6 +38,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
menuCollapseAriaLabel(){
|
||||
if(this.menuOpen){
|
||||
return this.$p.t('global', 'collapseMenu');
|
||||
}else{
|
||||
return this.$p.t('global', 'extendMenu');
|
||||
}
|
||||
},
|
||||
highestMatchingUrlCount(){
|
||||
// gets the hightest ranking inside the array
|
||||
let highestMatch = Math.max(...this.urlMatchRankings);
|
||||
@@ -97,7 +106,7 @@ export default {
|
||||
this.fetchMenu();
|
||||
},
|
||||
mounted(){
|
||||
this.$p.loadCategory(['ui', 'global'])
|
||||
this.$p.loadCategory(['ui', 'global', 'profilUpdate'])
|
||||
this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown,{
|
||||
toggle: false
|
||||
});
|
||||
@@ -106,31 +115,36 @@ export default {
|
||||
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<fhc-searchbar ref="searchbar" id="nav-search" style="background-color:var(--fhc-cis-primary)" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
|
||||
<a id="nav-logo" class="d-none d-lg-block" :href="rootUrl">
|
||||
<img :src="logoUrl" alt="Logo">
|
||||
</a>
|
||||
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
|
||||
<div id="nav-logo" class="d-none d-lg-block">
|
||||
<div class="d-flex h-100 justify-content-between">
|
||||
<a :href="rootUrl">
|
||||
<img :src="logoUrl" alt="Corporate Identity Logo">
|
||||
</a>
|
||||
<theme-switch></theme-switch>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nav-user">
|
||||
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
|
||||
<img :src="avatarUrl" class="bg-dark avatar rounded-circle border border-dark"/>
|
||||
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
|
||||
</button>
|
||||
<ul ref="navUserDropdown"
|
||||
@[\`shown.bs.collapse\`]="handleShowNavUser"
|
||||
@[\`hide.bs.collapse\`]="handleHideNavUser"
|
||||
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
|
||||
<li class="btn-level-2"><a class="btn btn-level-2 rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
|
||||
<li class="btn-level-2">
|
||||
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
|
||||
<li >
|
||||
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
|
||||
</li>
|
||||
<li class="btn-level-2"><hr class="dropdown-divider m-0 "></li>
|
||||
<li><a class="btn btn-level-2 rounded-0 d-block" :href="logoutUrl">Logout</a></li>
|
||||
<li><hr class="dropdown-divider m-0 "></li>
|
||||
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<nav id="nav-main" class="offcanvas offcanvas-start bg-dark" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
|
||||
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
|
||||
<div id="nav-main-sticky">
|
||||
<div id="nav-main-toggle" class="position-static d-none d-lg-block bg-dark">
|
||||
<button type="button" class="btn bg-dark text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
|
||||
<i class="fa fa-arrow-circle-left"></i>
|
||||
<div id="nav-main-toggle" class="position-static d-none d-lg-block ">
|
||||
<button :aria-label="menuCollapseAriaLabel" type="button" @click="menuOpen = !menuOpen" class="btn text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
|
||||
<i aria-hidden="true" class="fa fa-arrow-circle-left fhc-text"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="offcanvas-body p-0">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { numberPadding, formatDate } from "../../../helpers/DateHelpers.js"
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
event: Object,
|
||||
@@ -11,17 +10,16 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lektorenLinks: function(){
|
||||
lektorenLinks: function () {
|
||||
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
|
||||
|
||||
let lektorenLinks ={};
|
||||
this.event.lektor.forEach((lektor)=>{
|
||||
let lektorenLinks = {};
|
||||
this.event.lektor.forEach((lektor) => {
|
||||
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
|
||||
})
|
||||
return lektorenLinks;
|
||||
},
|
||||
getOrtContentLink: function()
|
||||
{
|
||||
getOrtContentLink: function () {
|
||||
if (!this.event || !this.event.ort_content_id) return "a";
|
||||
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
|
||||
@@ -51,64 +49,64 @@ export default {
|
||||
},
|
||||
template:/*html*/`
|
||||
<table class="table table-hover mb-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','datum')?
|
||||
$p.t('global','datum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{methodFormatDate(event.datum)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','raum')?
|
||||
$p.t('global','raum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<a v-if="event.ort_content_id" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
|
||||
{{event.ort_kurzbz}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lehrveranstaltung')?
|
||||
$p.t('lehre','lehrveranstaltung')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{'('+event.lehrform+') ' + event.lehrfach_bez}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lektor')?
|
||||
$p.t('lehre','lektor')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<div v-for="lektor in event.lektor" class="d-block">
|
||||
<a v-if="lektorenLinks[lektor.kurzbz]" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
|
||||
{{lektor.kurzbz}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('ui','zeitraum')?
|
||||
$p.t('ui','zeitraum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{start_time + ' - ' + end_time}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','organisationseinheit')?
|
||||
$p.t('lehre','organisationseinheit')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{event.organisationseinheit}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr v-if="event?.datum">
|
||||
<th>{{
|
||||
$p.t('global','datum')?
|
||||
$p.t('global','datum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{methodFormatDate(event.datum)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','raum')?
|
||||
$p.t('global','raum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<a v-if="event.ort_content_id" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1 fhc-primary-color" ></i></a>
|
||||
{{event.ort_kurzbz}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lehrveranstaltung')?
|
||||
$p.t('lehre','lehrveranstaltung')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{'('+event.lehrform+') ' + event.lehrfach_bez}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lektor')?
|
||||
$p.t('lehre','lektor')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<div v-for="lektor in event.lektor" class="d-block">
|
||||
<a v-if="lektorenLinks[lektor.kurzbz]" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1 fhc-primary-color" ></i></a>
|
||||
{{lektor.kurzbz}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('ui','zeitraum')?
|
||||
$p.t('ui','zeitraum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{start_time + ' - ' + end_time}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','organisationseinheit')?
|
||||
$p.t('lehre','organisationseinheit')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{event.organisationseinheit}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ export default {
|
||||
},
|
||||
containerStyles: Array,
|
||||
rowStyles: Array,
|
||||
hasLvPlanEintraege: {
|
||||
required:false,
|
||||
default:true,
|
||||
type:Boolean,
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
@@ -18,30 +23,14 @@ export default {
|
||||
if (!this.c4_link(menuItem) && !menuItem.c4_moodle_links?.length) {
|
||||
return true;
|
||||
}
|
||||
if (menuItem.id == "addon_fhtw_menu_lvplan_lva" && !this.hasLvPlanEintraege){
|
||||
return true;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
c4_target: function (menuItem) {
|
||||
if (menuItem.c4_moodle_links?.length > 0) return null;
|
||||
return menuItem.c4_target ?? null;
|
||||
},
|
||||
selectMenu: function (menuItem, index = null) {
|
||||
|
||||
// early return if link is #
|
||||
if (index != null && menuItem.c4_linkList[index][1] == '#') return;
|
||||
|
||||
switch (menuItem.id) {
|
||||
case "core_menu_mailanstudierende": window.location.href = menuItem.c4_link; break;
|
||||
default:
|
||||
this.selectedMenu = { ...menuItem };
|
||||
this.$emit("update:isMenuSelected", true);
|
||||
}
|
||||
|
||||
if (this.selectedMenu && index != null && menuItem.c4_linkList[index][1] != '#') {
|
||||
this.selectedMenu.c4_link = menuItem.c4_linkList[index][1];
|
||||
this.selectedMenu.name += ' - ' + menuItem.c4_linkList[index][0];
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
c4_link(menuItem) {
|
||||
if (!menuItem) return null;
|
||||
@@ -57,16 +46,16 @@ export default {
|
||||
},
|
||||
template:/*html*/`
|
||||
<div v-if="!menu">{{$p.t('lehre','lehrveranstaltungsUnavailable')}}</div>
|
||||
<div v-else>
|
||||
<div id="cis-menu" v-else>
|
||||
<div class="container" :class="containerStyles">
|
||||
<div class="row g-2 justify-content-center" :class="rowStyles">
|
||||
<div style="min-height:150px; min-width:150px;" class="col-12 col-lg-6 col-xl-4" v-for="(menuItem, index) in menu" :key="index">
|
||||
<a :id="menuItem.name" :class="{'dropdown-toggle':menuItem.c4_moodle_links?.length }" role="button" :href="c4_link(menuItem)"
|
||||
:disabled="c4_disabled(menuItem)" :data-bs-toggle="menuItem.c4_moodle_links?.length?'dropdown':null"
|
||||
class="fhc-entry p-2 w-100 text-wrap border border-1 rounded-3 d-flex flex-column align-items-center justify-content-center text-center text-decoration-none link-dark h-100">
|
||||
class="menu-entry p-2 w-100 text-wrap border border-1 rounded-3 d-flex flex-column align-items-center justify-content-center text-center text-decoration-none link h-100">
|
||||
<img :src="menuItem.c4_icon" :alt="menuItem.name" />
|
||||
<p @click="selectMenu(menuItem)" class="w-100 mt-2">{{menuItem.name}}</p>
|
||||
<p v-for="([text,link],index) in menuItem.c4_linkList" @click.stop="selectMenu(menuItem,index)" class="mt-1 w-100" :index="index">{{text}}</p>
|
||||
<p class="w-100 mt-2 mb-0">{{menuItem.name}}</p>
|
||||
<a v-for="([text,link],index) in menuItem.c4_linkList" target="_blank" :href="link" class="my-1 w-100 submenu text-decoration-none" :index="index">{{text}}</a>
|
||||
</a>
|
||||
<ul v-if="menuItem.c4_moodle_links?.length" class="dropdown-menu p-0" :aria-labelledby="menuItem.name">
|
||||
<li v-for="item in menuItem.c4_moodle_links"><a class="dropdown-item border-bottom" :href="item.url">{{item.lehrform}}</a></li>
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import Alert from "../../Bootstrap/Alert.js";
|
||||
import LvMenu from "./LvMenu.js"
|
||||
import LvInfo from "./LvInfo.js"
|
||||
|
||||
import ApiStundenplan from '../../../api/factory/stundenplan.js';
|
||||
import ApiAddons from '../../../api/factory/addons.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BsModal,
|
||||
Alert,
|
||||
LvMenu,
|
||||
LvInfo,
|
||||
},
|
||||
mixins: [BsModal],
|
||||
props: {
|
||||
event:Object,
|
||||
title:{
|
||||
type:String,
|
||||
default:"title"
|
||||
},
|
||||
showMenu:{
|
||||
type:Boolean,
|
||||
default:true,
|
||||
},
|
||||
/*
|
||||
* NOTE(chris):
|
||||
* Hack to expose in "emits" declared events to $props which we use
|
||||
* in the v-bind directive to forward all events.
|
||||
* @see: https://github.com/vuejs/core/issues/3432
|
||||
*/
|
||||
onHideBsModal: Function,
|
||||
onHiddenBsModal: Function,
|
||||
onHidePreventedBsModal: Function,
|
||||
onShowBsModal: Function,
|
||||
onShownBsModal: Function,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: [],
|
||||
result: false,
|
||||
info: null,
|
||||
};
|
||||
},
|
||||
methods:{
|
||||
onModalShow: function()
|
||||
{
|
||||
// do not load the menu if the menu is not getting rendered
|
||||
if(!this.showMenu) return;
|
||||
|
||||
if (this.event.type == 'lehreinheit') {
|
||||
this.$api
|
||||
.call(ApiStundenplan.getLehreinheitStudiensemester(this.event.lehreinheit_id[0]))
|
||||
.then(res => res.data)
|
||||
.then(studiensemester_kurzbz => this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
this.event.lehrveranstaltung_id,
|
||||
studiensemester_kurzbz
|
||||
)
|
||||
))
|
||||
.then(res => {
|
||||
if (res.data) {
|
||||
this.menu = res.data;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.modal = this.$refs.modalContainer.modal;
|
||||
},
|
||||
popup(options) {
|
||||
return BsModal.popup.bind(this)(null, options);
|
||||
},
|
||||
template: /*html*/ `
|
||||
<bs-modal ref="modalContainer" @showBsModal="onModalShow" @hideBsModal="onModalHide" v-bind="$props" :bodyClass="''" dialogClass='modal-lg' class="bootstrap-alert" :backdrop="false" >
|
||||
<template v-slot:title>
|
||||
<template v-if="event.titel">{{ event.titel + ' - ' + event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</template>
|
||||
<template v-else>{{ event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</template>
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
|
||||
<lv-info :event="event"></lv-info>
|
||||
<template v-if="showMenu">
|
||||
<h3>{{$p.t('lehre','lehrveranstaltungsmenue')}}</h3>
|
||||
<lv-menu :menu="menu"></lv-menu>
|
||||
</template>
|
||||
</template>
|
||||
<!-- optional footer -->
|
||||
<template v-slot:footer >
|
||||
<button class="btn btn-outline-secondary " @click="hide">{{$p.t('ui','cancel')}}</button>
|
||||
</template>
|
||||
<!-- end of optional footer -->
|
||||
</bs-modal>`,
|
||||
};
|
||||
@@ -11,6 +11,16 @@ export default {
|
||||
required:true,
|
||||
default:null,
|
||||
},
|
||||
studiensemester: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
titel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
// prop used to preselect a menu item and skip the grid overview
|
||||
preselectedMenu: {
|
||||
type: Object,
|
||||
@@ -23,7 +33,8 @@ export default {
|
||||
result: false,
|
||||
menu: [],
|
||||
isMenuSelected:false,
|
||||
|
||||
hasLvPlanEintraege: true,
|
||||
lvEvaluierungMessage: "",
|
||||
}
|
||||
},
|
||||
mixins:[BsModal],
|
||||
@@ -31,6 +42,7 @@ export default {
|
||||
BsModal,
|
||||
LvMenu,
|
||||
},
|
||||
inject: ["studium_studiensemester"],
|
||||
methods:{
|
||||
|
||||
hiddenModal: function(){
|
||||
@@ -39,7 +51,7 @@ export default {
|
||||
showModal: function(){
|
||||
if (!this.preselectedMenu) {
|
||||
this.$api
|
||||
.call(ApiAddons.getLvMenu(this.event.lehrveranstaltung_id, this.event.studiensemester_kurzbz))
|
||||
.call(ApiAddons.getLvMenu(this.event.lehrveranstaltung_id, (this.studiensemester ?? this.event.studiensemester_kurzbz)))
|
||||
.then(res => {
|
||||
if (res.data) {
|
||||
this.menu = res.data;
|
||||
@@ -48,20 +60,54 @@ export default {
|
||||
} else {
|
||||
this.isMenuSelected = true;
|
||||
}
|
||||
|
||||
// check lv evaluierung info
|
||||
if (this.studium_studiensemester) {
|
||||
this.$fhcApi.factory.studium.getLvEvaluierungInfo(this.studium_studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.lvEvaluierungMessage = res.message;
|
||||
})
|
||||
}
|
||||
|
||||
// check if the lv has lvplan entries for this studiensemester
|
||||
if (this.studiensemester && this.event) {
|
||||
return this.$fhcApi.factory.studium.getLvPlanForStudiensemester(this.studiensemester, this.event.lehreinheit_id ?? this.event.lehrveranstaltung_id)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
if (Array.isArray(res) && res.length > 0) {
|
||||
this.hasLvPlanEintraege = true;
|
||||
} else {
|
||||
this.hasLvPlanEintraege = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
mounted(){
|
||||
this.modal = this.$refs.modalContainer;
|
||||
},
|
||||
beforeUnmount(){
|
||||
this.$refs.modalContainer.hide();
|
||||
},
|
||||
template:/*html*/`
|
||||
<bs-modal :bodyClass="isMenuSelected ? '' : 'px-4 py-5'" @showBsModal="showModal" @hiddenBsModal="hiddenModal" ref="modalContainer" :dialogClass="{'modal-lg': !isMenuSelected, 'modal-fullscreen':isMenuSelected}">
|
||||
<template #title>
|
||||
<span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span>
|
||||
<span v-else>Lehrveranstaltungs Übersicht</span>
|
||||
|
||||
<template #title>
|
||||
<template v-if="titel">
|
||||
<span>{{titel}}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span>
|
||||
<span v-else>Lehrveranstaltungs Übersicht</span>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
<template #default>
|
||||
<lv-menu v-model:isMenuSelected="isMenuSelected" :preselectedMenu="preselectedMenu" :menu="menu" @hideModal="hide"></lv-menu>
|
||||
<div class="mb-4" v-if="lvEvaluierungMessage" v-html="lvEvaluierungMessage"></div>
|
||||
<slot name="content"></slot>
|
||||
<lv-menu v-model:isMenuSelected="isMenuSelected" :hasLvPlanEintraege="hasLvPlanEintraege" :preselectedMenu="preselectedMenu" :menu="menu" @hideModal="hide"></lv-menu>
|
||||
</template>
|
||||
|
||||
</bs-modal>
|
||||
|
||||
@@ -1,259 +1,63 @@
|
||||
import FhcCalendar from "../../Calendar/Calendar.js";
|
||||
import CalendarDate from "../../../composables/CalendarDate.js";
|
||||
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
|
||||
import LvInfo from "../../../components/Cis/Mylv/LvInfo.js"
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiStudenplan from '../../../api/factory/stundenplan.js';
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
|
||||
export const DEFAULT_MODE_RAUMINFO = 'Week'
|
||||
|
||||
const RoomInformation = {
|
||||
export default {
|
||||
name: "RoomInformation",
|
||||
props:{
|
||||
propsViewData: {
|
||||
type: Object
|
||||
},
|
||||
rowMinHeight: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
eventMaxHeight: {
|
||||
type: String,
|
||||
default: '125px'
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FhcCalendar,
|
||||
LvModal,
|
||||
LvInfo,
|
||||
FhcCalendar
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
rowMinHeight: this.rowMinHeight,
|
||||
eventMaxHeight: this.eventMaxHeight
|
||||
props:{
|
||||
viewData: Object, // NOTE(chris): this is inherited from router-view
|
||||
propsViewData: Object
|
||||
},
|
||||
computed: {
|
||||
currentDay() {
|
||||
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
},
|
||||
currentMode() {
|
||||
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO;
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
events: null,
|
||||
calendarMode: DEFAULT_MODE_RAUMINFO,
|
||||
calendarDate: new CalendarDate(new Date()),
|
||||
currentlySelectedEvent: null,
|
||||
currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
|
||||
minimized: false,
|
||||
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
currentDate: function(){
|
||||
return new Date(this.calendarWeek.y, this.calendarWeek.m, this.calendarWeek.d);
|
||||
},
|
||||
weekFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
|
||||
methods:{
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
weekLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
|
||||
},
|
||||
monthFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfCalendarMonth);
|
||||
},
|
||||
monthLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfCalendarMonth);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'propsViewData.ort_kurzbz'(newVal) {
|
||||
// relevant if ort_kurzbz can be changed from within this component
|
||||
},
|
||||
'propsViewData.mode'(newVal) {
|
||||
if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
|
||||
},
|
||||
'propsViewData.focus_date'(newVal) {
|
||||
this.currentDate = new Date(newVal)
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
setSelectedEvent: function(event){
|
||||
this.currentlySelectedEvent = event;
|
||||
},
|
||||
getLvID: function () {
|
||||
this.lv_id = window.location.pathname
|
||||
},
|
||||
selectDay: function(day){
|
||||
const date = day.getFullYear() + "-" +
|
||||
String(day.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(day.getDate()).padStart(2, "0");
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "RoomInformation",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
mode,
|
||||
focus_date,
|
||||
ort_kurzbz: this.propsViewData.ort_kurzbz
|
||||
}
|
||||
})
|
||||
|
||||
this.currentDay = day;
|
||||
},
|
||||
handleOffset: function(offset) {
|
||||
this.currentDay = new Date(
|
||||
this.currentDay.getFullYear() + offset.y,
|
||||
this.currentDay.getMonth() + offset.m,
|
||||
this.currentDay.getDate() + offset.d
|
||||
)
|
||||
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "Stundenplan",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
lv_id: this.propsViewData?.lv_id || null
|
||||
}
|
||||
})
|
||||
},
|
||||
handleChangeMode(mode) {
|
||||
const modeCapitalized = mode.charAt(0).toUpperCase() + mode.slice(1)
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "RoomInformation",
|
||||
params: {
|
||||
mode: modeCapitalized,
|
||||
focus_date: date,
|
||||
ort_kurzbz: this.propsViewData.ort_kurzbz
|
||||
}
|
||||
})
|
||||
|
||||
this.calendarMode = mode
|
||||
},
|
||||
showModal: function (event) {
|
||||
this.currentlySelectedEvent = event;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.lvmodal.show();
|
||||
});
|
||||
|
||||
},
|
||||
updateRange: function ({ start, end }) {
|
||||
|
||||
let checkDate = (date) => {
|
||||
return date.m != this.calendarDate.m || date.y != this.calendarDate.y;
|
||||
}
|
||||
|
||||
// only load month data if the month or year has changed
|
||||
if (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end))) {
|
||||
// reset the events before querying the new events to activate the loading spinner
|
||||
this.events = null;
|
||||
this.calendarDate = new CalendarDate(end);
|
||||
Vue.nextTick(() => {
|
||||
this.loadEvents();
|
||||
});
|
||||
}
|
||||
},
|
||||
calendarDateToString: function (calendarDate) {
|
||||
return calendarDate instanceof CalendarDate ?
|
||||
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
|
||||
null;
|
||||
|
||||
},
|
||||
loadEvents: function(){
|
||||
|
||||
// bundles the room_events and the reservierungen together into the this.events array
|
||||
Promise.allSettled([
|
||||
this.$api.call(ApiStudenplan.getRoomInfo(this.propsViewData.ort_kurzbz, this.monthFirstDay, this.monthLastDay)),
|
||||
this.$api.call(ApiStudenplan.getOrtReservierungen(this.propsViewData.ort_kurzbz, this.monthFirstDay, this.monthLastDay))
|
||||
]).then((result) => {
|
||||
let promise_events = [];
|
||||
result.forEach((promise_result) => {
|
||||
if(promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success"){
|
||||
|
||||
let data = promise_result.value.data;
|
||||
// adding additional information to the events
|
||||
if (data && data.forEach) {
|
||||
data.forEach((el, i) => {
|
||||
el.id = i;
|
||||
if (el.type === 'reservierung') {
|
||||
el.color = '#' + (el.farbe || 'FFFFFF');
|
||||
} else {
|
||||
el.color = '#' + (el.farbe || 'CCCCCC');
|
||||
}
|
||||
|
||||
el.start = new Date(el.datum + ' ' + el.beginn);
|
||||
el.end = new Date(el.datum + ' ' + el.ende);
|
||||
|
||||
});
|
||||
}
|
||||
promise_events = promise_events.concat(data);
|
||||
}
|
||||
})
|
||||
this.events = promise_events;
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.loadEvents();
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
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()))
|
||||
];
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
template: /*html*/`
|
||||
<div class="fhc-roominformation d-flex flex-column h-100">
|
||||
<h2>{{ $p.t('rauminfo/rauminfo') }} {{ propsViewData.ort_kurzbz }}</h2>
|
||||
<hr>
|
||||
<lv-modal v-if="currentlySelectedEvent" :showMenu="false" :event="currentlySelectedEvent" ref="lvmodal" />
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
@selectedEvent="setSelectedEvent"
|
||||
:initial-date="currentDay"
|
||||
@change:range="updateRange"
|
||||
@change:offset="handleOffset"
|
||||
:events="events"
|
||||
:initial-mode="propsViewData.mode"
|
||||
show-weeks
|
||||
@select:day="selectDay"
|
||||
@change:mode="handleChangeMode"
|
||||
v-model:minimized="minimized"
|
||||
>
|
||||
<template #monthPage="{event,day}">
|
||||
<span >
|
||||
{{event.topic}}
|
||||
</span>
|
||||
</template>
|
||||
<template #weekPage="{event,day}">
|
||||
<div @click="showModal(event?.orig)" type="button" class=" border border-secondary border d-flex flex-column align-items-center justify-content-evenly h-100">
|
||||
<span>{{event?.orig.topic}}</span>
|
||||
<span v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</span>
|
||||
<span>{{event?.orig.ort_kurzbz}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<div @click="mobile? showModal(event?.orig):null" type="button" class="fhc-entry border border-secondary border row h-100 justify-content-center align-items-center text-center">
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('lehre/lehrveranstaltung') }}:</p>
|
||||
<p class="m-0">{{event?.orig.topic}}</p>
|
||||
</div>
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('lehre/lektor') }}:</p>
|
||||
<p class="m-0" v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</p>
|
||||
</div>
|
||||
<div class="col ">
|
||||
<p>{{ $p.t('profil/Ort') }}: </p>
|
||||
<p class="m-0">{{event?.orig.ort_kurzbz}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContent>
|
||||
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
|
||||
<div class="w-100">
|
||||
<lv-info :event="currentlySelectedEvent" />
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContentEmpty >
|
||||
<h3>{{$p.t('rauminfo','keineRaumReservierung')}}</h3>
|
||||
</template>
|
||||
</fhc-calendar>
|
||||
`,
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
class="responsive-calendar"
|
||||
></fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
|
||||
export default RoomInformation
|
||||
@@ -22,7 +22,8 @@ export default {
|
||||
lv.studiengang_kz + '#' + lv.semester,
|
||||
{
|
||||
studiengang_kz: lv.studiengang_kz,
|
||||
bezeichnung: lv.sg_bezeichnung,
|
||||
bezeichnung: lv.sg_bezeichnung,
|
||||
sg_bezeichnung_eng: lv.sg_bezeichnung_eng,
|
||||
kuerzel: lv.studiengang_kuerzel,
|
||||
semester: lv.semester
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ export default {
|
||||
bezeichnung: String,
|
||||
kuerzel: String,
|
||||
semester: [String,Number],
|
||||
lvs: Array
|
||||
lvs: Array,
|
||||
sg_bezeichnung_eng: String
|
||||
},
|
||||
computed: {
|
||||
lehrveranstaltungen() {
|
||||
@@ -32,7 +33,7 @@ export default {
|
||||
},
|
||||
template: `<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title mb-3">{{bezeichnung}} - {{kuerzel}}
|
||||
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
|
||||
<small>{{semester}}.{{$p.t('lehre/semester')}}</small>
|
||||
</h4>
|
||||
<div class="row">
|
||||
|
||||
@@ -24,7 +24,9 @@ export default {
|
||||
lvinfo: Boolean,
|
||||
benotung: Boolean,
|
||||
lvnote: String,
|
||||
lvnotebez: Array,
|
||||
znote: String,
|
||||
znotebez: Array,
|
||||
studiengang_kuerzel: String,
|
||||
semester: [String, Number],
|
||||
orgform_kurzbz: String,
|
||||
@@ -32,7 +34,7 @@ export default {
|
||||
ects: String,
|
||||
incoming: Number,
|
||||
positiv: Boolean,
|
||||
note_index: String,
|
||||
note_index: String
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
@@ -49,11 +51,11 @@ export default {
|
||||
// returns a suitable color for the given grade
|
||||
if (this.positiv)
|
||||
{
|
||||
return 'var(--fhc-cis-grade-positive)';
|
||||
return 'var(--fhc-success)';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'var(--fhc-cis-grade-negative)';
|
||||
return 'var(--fhc-danger)';
|
||||
}
|
||||
},
|
||||
is_organisatorische_einheit(){
|
||||
@@ -63,14 +65,18 @@ export default {
|
||||
return !this.menu || !Array.isArray(this.menu) || Array.isArray(this.menu) && this.menu.length == 0;
|
||||
},
|
||||
bodyStyle() {return {};
|
||||
const bodyStyle = {};
|
||||
/*const bodyStyle = {};
|
||||
if (this.farbe)
|
||||
bodyStyle['background-color'] = '#' + this.farbe;
|
||||
return bodyStyle;
|
||||
return bodyStyle;*/
|
||||
},
|
||||
grade() {
|
||||
// TODO: noten phrasen
|
||||
return this.benotung ? this.znote || this.lvnote || null : null;
|
||||
const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0
|
||||
if(this.benotung && this.znotebez?.length) {
|
||||
return this.znotebez[languageIndex]
|
||||
} else if(this.benotung && this.lvnotebez?.length) {
|
||||
return this.lvnotebez[languageIndex]
|
||||
} else return null
|
||||
},
|
||||
LvHasPruefungenInformation(){
|
||||
return this.pruefungenData && this.pruefungenData.length > 0;
|
||||
@@ -127,6 +133,7 @@ export default {
|
||||
LvInfo.popup({
|
||||
lehrveranstaltung_id: this.lehrveranstaltung_id,
|
||||
bezeichnung: this.bezeichnung,
|
||||
bezeichnung_eng: this.bezeichnung_eng,
|
||||
studiengang_kuerzel: this.studiengang_kuerzel,
|
||||
semester: this.semester,
|
||||
studien_semester: this.studien_semester,
|
||||
@@ -165,7 +172,7 @@ export default {
|
||||
<div class="p-2" :class="is_organisatorische_einheit?'':'card-header'">
|
||||
<!-- {{module}} if the module of the lv is important then query the module from the api endpoint for LV-->
|
||||
<h6 class="fw-bold" v-if="is_organisatorische_einheit" >{{ $p.t('lehre/organisationseinheit') }}:</h6>
|
||||
<h6 class="mb-0">{{bezeichnung}}</h6>
|
||||
<h6 class="mb-0">{{$p.user_language.value === 'English' ? bezeichnung_eng : bezeichnung}}</h6>
|
||||
</div>
|
||||
<div v-if="!emptyMenu" class="card-body " :style="bodyStyle">
|
||||
<template v-if="menu">
|
||||
@@ -176,9 +183,9 @@ export default {
|
||||
<i :class="[menuItem.c4_icon2 ? menuItem.c4_icon2 : 'fa-solid fa-pen-to-square', !menuItem.c4_link ? 'unavailable' : null ]"></i>
|
||||
</div>
|
||||
<a
|
||||
class="text-decoration-none text-truncate"
|
||||
class="fhc-body text-decoration-none text-truncate"
|
||||
:id="'moodle_links_'+lehrveranstaltung_id"
|
||||
:class="{'link-dark':menuItem.c4_link, 'unavailable':!menuItem.c4_link, 'dropdown-toggle':menuItem.c4_moodle_links?.length }"
|
||||
:class="{ 'unavailable':!menuItem.c4_link, 'dropdown-toggle':menuItem.c4_moodle_links?.length }"
|
||||
:target="menuItem.c4_target"
|
||||
:href="c4_link(menuItem) ? c4_link(menuItem) : null">
|
||||
{{ menuItem.phrase ? $p.t(menuItem.phrase) : menuItem.name}}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
<td>
|
||||
<ul v-if="lektorNames.length" class="list-unstyled mb-0">
|
||||
<li v-for="name in new Set(lektorNames)" :key="name">
|
||||
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
|
||||
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1 fhc-primary-color" ></i></a>
|
||||
{{name}}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li v-for="name in lvLeitung" :key="name">
|
||||
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C"></i></a>
|
||||
<a :href="lektorNamesLinks[name]?lektorNamesLinks[name]:null"><i class="fa fa-arrow-up-right-from-square me-1 fhc-primary-color" ></i></a>
|
||||
{{name}}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -76,6 +76,7 @@ export default {
|
||||
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
},
|
||||
setHash(val) {
|
||||
// TODO: make this a router param to enable history
|
||||
location.hash = val;
|
||||
}
|
||||
},
|
||||
@@ -100,13 +101,13 @@ export default {
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
|
||||
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="currentIsLast" @click="nextSem">
|
||||
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -27,13 +27,13 @@ export default {
|
||||
ProfilInformation,
|
||||
},
|
||||
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language"],
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
editDataFilter: null,
|
||||
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaProfilFunktionen",
|
||||
@@ -58,21 +58,21 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('ui/bezeichnung')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.bezeichnungPhrase),
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.organisationseinheitPhrase),
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigVon')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase),
|
||||
field: "Gültig_von",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigBis')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase),
|
||||
field: "Gültig_bis",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/wochenstunden')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase),
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -121,14 +121,14 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/entlehnteBetriebsmittel')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/inventarnummer')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.inventarnummerPhrase),
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -136,7 +136,7 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/ausgabedatum')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase),
|
||||
field: "Ausgegeben_am",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -215,8 +215,8 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
editable() {
|
||||
return this.data?.editAllowed ?? false;
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
getTelefonValue() {
|
||||
if(this.data.standort_telefon?.kontakt) {
|
||||
@@ -280,6 +280,18 @@ export default {
|
||||
},
|
||||
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => {
|
||||
this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung');
|
||||
this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit');
|
||||
this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon');
|
||||
this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis');
|
||||
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden');
|
||||
this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel');
|
||||
this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer');
|
||||
this.preloadedPhrasen.ausgabedatumPhrase = this.$p.t('profil/ausgabedatum');
|
||||
this.preloadedPhrasen.loaded=true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
|
||||
@@ -295,21 +307,21 @@ export default {
|
||||
this.setTableColumnTitles()
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :title="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<div class="row">
|
||||
<div class="d-md-none col-12 ">
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row mb-3">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row mb-3 ">
|
||||
<div v-if="isEditable" class="row mb-3 ">
|
||||
<div class="col">
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start card w-100 btn btn-outline-secondary" >
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<i class="fa fa-edit"></i>
|
||||
@@ -321,7 +333,7 @@ export default {
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates" ></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
@@ -331,13 +343,13 @@ export default {
|
||||
<div class="col-sm-12 col-md-8 col-xxl-9 ">
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
|
||||
<!-- INFORMATION CONTENT START -->
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
@@ -408,18 +420,20 @@ export default {
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- FUNKTIONEN TABELLE -->
|
||||
<core-filter-cmpt
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="funktionenTableBuilt"
|
||||
:title="$p.t('person','funktionen')"
|
||||
ref="funktionenTable"
|
||||
:tabulator-options="funktionen_table_options"
|
||||
tableOnly
|
||||
:title="$p.t('person','funktionen')"
|
||||
ref="funktionenTable"
|
||||
:tabulator-options="funktionen_table_options"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- BETRIEBSMITTEL TABELLE -->
|
||||
<core-filter-cmpt
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
@@ -446,7 +460,7 @@ export default {
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block ">
|
||||
<div class="col mb-3">
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start card w-100 btn btn-outline-secondary" >
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<i class="fa fa-edit"></i>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
collapseIconFunktionen: true,
|
||||
|
||||
preloadedPhrasen:{},
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaViewProfilFunktionen",
|
||||
persistence: {
|
||||
@@ -110,8 +110,8 @@ export default {
|
||||
return this.data.telefonklappe
|
||||
}
|
||||
},
|
||||
editable() {
|
||||
return this.data?.editAllowed ?? false;
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
|
||||
personEmails() {
|
||||
@@ -163,6 +163,16 @@ export default {
|
||||
};
|
||||
},
|
||||
},
|
||||
created(){
|
||||
this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => {
|
||||
this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung');
|
||||
this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit');
|
||||
this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon');
|
||||
this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis');
|
||||
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden');
|
||||
this.preloadedPhrasen.loaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
|
||||
@@ -172,9 +182,9 @@ export default {
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<!-- TODO: uncomment when implemented
|
||||
<div class="d-md-none col-12 ">
|
||||
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true" ></quick-links>
|
||||
|
||||
|
||||
</div>
|
||||
-->
|
||||
<!-- END OF HIDDEN QUCK LINKS -->
|
||||
@@ -189,7 +199,7 @@ export default {
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- Profil Informationen -->
|
||||
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
|
||||
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
@@ -220,7 +230,7 @@ export default {
|
||||
<div class="row">
|
||||
<!-- FIRST TABLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
<core-filter-cmpt v-if="preloadedPhrasen.loaded" @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
</div>
|
||||
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,10 @@ export const Profil = {
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
default: 'Profil'
|
||||
required:false,
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -59,10 +62,12 @@ export const Profil = {
|
||||
data: null,
|
||||
// notfound is null by default, but contains an UID if no user exists with that UID
|
||||
notFoundUID: null,
|
||||
isEditable: this.viewData.editable ?? false,
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
isEditable: Vue.computed(()=>this.isEditable),
|
||||
profilUpdateStates: Vue.computed(() =>
|
||||
this.profilUpdateStates ? this.profilUpdateStates : false
|
||||
),
|
||||
@@ -153,18 +158,19 @@ export const Profil = {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
let uid = this.uid ?? location.pathname.split("/").pop();
|
||||
|
||||
|
||||
this.$api
|
||||
.call(ApiProfil.getView(uid))
|
||||
.then((res) => {
|
||||
if (!res.data) {
|
||||
this.notFoundUID = uid;
|
||||
} else {
|
||||
this.view = res.data?.view;
|
||||
this.data = res.data?.data;
|
||||
}
|
||||
});
|
||||
.call(ApiProfil.profilViewData(this.$route.params.uid??null))
|
||||
.then((response) => response.data).then(data=>{
|
||||
this.view = data?.profil_data.view;
|
||||
this.data = data?.profil_data.data;
|
||||
this.isEditable = data?.editable ?? false;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
|
||||
},
|
||||
zustellAdressenCount() {
|
||||
if (!this.data || !this.data.adressen) {
|
||||
@@ -254,6 +260,7 @@ export const Profil = {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
filteredEditData() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
@@ -365,7 +372,7 @@ export const Profil = {
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
template: `
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
|
||||
<!-- column 1 in the address row -->
|
||||
<div class="col-1 text-center">
|
||||
<i class="fa fa-location-dot fa-lg" style="color:#00649C "></i>
|
||||
<i class="fa fa-location-dot fa-lg fhc-link-color" ></i>
|
||||
</div>
|
||||
|
||||
<div class="col-11 col-sm-8 col-xl-11 col-xxl-8 order-1">
|
||||
|
||||
@@ -155,7 +155,7 @@ export default {
|
||||
|
||||
template: /*html*/ `
|
||||
<div class="card">
|
||||
<edit-profil v-if="showUpdateModal" ref="updateEditModal" @hideBsModal="hideEditProfilModal" :value="content" :title="editProfilTitle"></edit-profil>
|
||||
<edit-profil v-if="showUpdateModal" ref="updateEditModal" @hideBsModal="hideEditProfilModal" :value="content" :titel="editProfilTitle"></edit-profil>
|
||||
<div class="card-header">{{$p.t('profilUpdate','profilUpdates')}}</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive text-nowrap">
|
||||
@@ -180,9 +180,9 @@ export default {
|
||||
<div class="align-middle text-center"><i role="button" @click="showEditProfilModal(item)" class="fa fa-eye"></i></div>
|
||||
</template>
|
||||
<template v-else >
|
||||
<div class="align-middle text-center" ><i style="color:#00639c" @click="showEditProfilModal(item)" role="button" class="fa fa-edit"></i></div>
|
||||
<div class="align-middle text-center" ><i @click="showEditProfilModal(item)" role="button" class="fa fa-edit fhc-primary-color"></i></div>
|
||||
</template>
|
||||
<div class="align-middle text-center"><i style="color:red" role="button" @click="deleteRequest(item)" class="fa fa-trash"></i></div>
|
||||
<div class="align-middle text-center"><i role="button" @click="deleteRequest(item)" class="text-danger fa fa-trash"></i></div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
@@ -12,9 +12,10 @@ export default{
|
||||
|
||||
},
|
||||
template:/*html*/`
|
||||
<template v-if="data.kontakt">
|
||||
<div class="gy-2 row align-items-center justify-content-center">
|
||||
<div class="col-1 text-center" >
|
||||
<i class="fa-solid " :class="{...(data.kontakt.includes('@')?{'fa-envelope':true}:{'fa-phone':true})}" style="color:rgb(0, 100, 156)"></i>
|
||||
<i class="fa-solid fhc-link-color" :class="{...(data.kontakt.includes('@')?{'fa-envelope':true}:{'fa-phone':true})}" ></i>
|
||||
</div>
|
||||
<div :class="{...(data.anmerkung? {'col-11':true, 'col-md-6':true, 'col-xl-11':true, 'col-xxl-6':true} : {'col-10':true, 'col-xl-9':true, 'col-xxl-10':true})}">
|
||||
<!-- rendering KONTAKT emails -->
|
||||
@@ -35,5 +36,6 @@ export default{
|
||||
<i v-else="data.zustellung" class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
`,
|
||||
};
|
||||
@@ -10,25 +10,27 @@ export default {
|
||||
|
||||
},
|
||||
template: /*html*/`
|
||||
<template v-if="Array.isArray(data) && data.length >0">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{$p.t('profil','mailverteilerMitglied')}}</h6>
|
||||
<h4 class="card-title">{{$p.t('profil','mailverteilerMitglied')}}</h4>
|
||||
<div class="card-text row text-break mb-2" v-for="verteiler in data">
|
||||
<div class="col-12 ">
|
||||
<div class="row">
|
||||
<div class="row">
|
||||
<div class="col-1 ">
|
||||
<i class="fa-solid fa-envelope" style="color: #00649C;"></i>
|
||||
<i class="fa-solid fa-envelope fhc-link-color" ></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a :href="verteiler.mailto"><b>{{verteiler.gruppe_kurzbz}}</b></a>
|
||||
<a class="fhc-link-color" :href="verteiler.mailto"><b>{{verteiler.gruppe_kurzbz}}</b></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-11 offset-1 ">{{verteiler.beschreibung}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`,
|
||||
</div>
|
||||
</template>`,
|
||||
};
|
||||
|
||||
@@ -20,17 +20,19 @@ export default {
|
||||
<!-- HIER SIND DIE EMAILS -->
|
||||
<div class="gy-3 row justify-content-center ">
|
||||
<div v-for="email in data" class="col-12 ">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-1 text-center">
|
||||
<i class="fa-solid fa-envelope" style="color:rgb(0, 100, 156)"></i>
|
||||
</div>
|
||||
<div class="col-11">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{email.type}}</div>
|
||||
<a :href="'mailto:'+email.email" class="form-underline-content">{{email.email}} </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="email.email">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-1 text-center">
|
||||
<i class="fa-solid fa-envelope fhc-link-color" ></i>
|
||||
</div>
|
||||
<div class="col-11">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{email?.type}}</div>
|
||||
<a :href="'mailto:'+email?.email" class="form-underline-content">{{email?.email}} </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ApiProfil from '../../../../api/factory/profil.js';
|
||||
import ImageUpload from '../../Profil/ProfilModal/EditProfilComponents/ImageUpload.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -8,19 +9,26 @@ export default {
|
||||
data: {
|
||||
type: Object,
|
||||
},
|
||||
editable: {
|
||||
fotoStatus:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
default: true
|
||||
}
|
||||
},
|
||||
components:{
|
||||
ImageUpload,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
FotoSperre: this.data.foto_sperre,
|
||||
};
|
||||
},
|
||||
emits: ["showEditProfilModal"],
|
||||
inject:["isEditable"],
|
||||
|
||||
methods: {
|
||||
showModal(){
|
||||
this.$refs.imageUpload.show();
|
||||
},
|
||||
sperre_foto_function() {
|
||||
//TODO: refactor
|
||||
if (!this.data) {
|
||||
@@ -62,9 +70,10 @@ export default {
|
||||
template: /*html*/ `
|
||||
|
||||
<div class="card h-100">
|
||||
<image-upload ref="imageUpload" :titel="$p.t('profilUpdate','profilBild')"></image-upload>
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div v-if="editable" @click="$emit('showEditProfilModal','Personen_Informationen')" class="col-auto" type="button">
|
||||
<div v-if="isEditable" @click="$emit('showEditProfilModal','Personen_Informationen')" class="col-auto" type="button" :title="$p.t('profilUpdate','profilBearbeiten')">
|
||||
<i class="fa fa-edit"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
@@ -78,11 +87,14 @@ export default {
|
||||
<!-- START OF THE FIRST ROW WITH THE PROFIL IMAGE -->
|
||||
<div class="col-12 col-sm-6 mb-2">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-auto " style="position:relative">
|
||||
<img class=" img-thumbnail " style=" max-height:150px; " :src="get_image_base64_src"/>
|
||||
<div class="col-auto profil-image" style="position:relative">
|
||||
<img alt="profile picture" class=" img-thumbnail " style=" max-height:150px; " :src="get_image_base64_src"/>
|
||||
<!-- LOCKING IMAGE FUNCTIONALITY -->
|
||||
<div v-if="editable" role="button" @click.prevent="sperre_foto_function" class="image-lock">
|
||||
<div v-if="isEditable" role="button" type="button" :title="FotoSperre?$p.t('profil','fotoSperren'):$p.t('profil','fotoEntsperren')" @click.prevent="sperre_foto_function" class="image-lock">
|
||||
<i :class="{'fa':true, ...(FotoSperre?{'fa-lock':true}:{'fa-lock-open':true})} "></i>
|
||||
</div>
|
||||
<div v-if="fotoStatus != null && !fotoStatus" role="button" type="button" :title="$p.t('profil','fotoHochladen')" @click.prevent="showModal" class="image-upload">
|
||||
<i class="fa fa-upload"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,24 +48,24 @@ export default {
|
||||
<!-- print semester link -->
|
||||
<span v-else-if="key == 'semester' && entry.value" class="form-underline-content">
|
||||
{{ entry.value }}
|
||||
<a class="ms-auto mb-2" target="_blank" :href="getLinkSemesterListe">
|
||||
<i class="fa fa-arrow-up-right-from-square me-1"></i>
|
||||
<a :aria-label="$p.t('profil','semesterLink')" class="ms-auto mb-2" target="_blank" :href="getLinkSemesterListe">
|
||||
<i aria-hidden="true" class="fa fa-arrow-up-right-from-square me-1 fhc-link-color"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- print verband link -->
|
||||
<span v-else-if="key =='verband' && entry.value" class="form-underline-content">
|
||||
{{ entry.value }}
|
||||
<a class="ms-auto mb-2" target="_blank" :href="getLinkVerbandListe">
|
||||
<i class="fa fa-arrow-up-right-from-square me-1"></i>
|
||||
<a :aria-label="$p.t('profil','verbandLink')" class="ms-auto mb-2" target="_blank" :href="getLinkVerbandListe">
|
||||
<i aria-hidden="true" class="fa fa-arrow-up-right-from-square me-1 fhc-link-color"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<!-- print gruppe link -->
|
||||
<span v-else-if="key == 'gruppe' && entry.value" class="form-underline-content">
|
||||
{{ entry.value }}
|
||||
<a class="ms-auto mb-2" target="_blank" :href="getLinkGruppeListe">
|
||||
<i class="fa fa-arrow-up-right-from-square me-1"></i>
|
||||
<a :aria-label="$p.t('profil','gruppenLink')" class="ms-auto mb-2" target="_blank" :href="getLinkGruppeListe">
|
||||
<i aria-hidden="true" class="fa fa-arrow-up-right-from-square me-1 fhc-link-color"></i>
|
||||
</a>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export default {
|
||||
mixins: [BsModal],
|
||||
props: {
|
||||
value: Object,
|
||||
title: String,
|
||||
titel: String,
|
||||
zustelladressenCount: Function,
|
||||
zustellkontakteCount: Function,
|
||||
/*
|
||||
@@ -189,8 +189,8 @@ export default {
|
||||
return BsModal.popup(null, options);
|
||||
},
|
||||
template: /*html*/ `
|
||||
<bs-modal v-show="!loading" ref="modalContainer" v-bind="$props" body-class="" dialog-class="modal-lg" class="bootstrap-alert" :backdrop="false">
|
||||
<template v-if="title" v-slot:title>{{title}}</template>
|
||||
<bs-modal v-show="!loading" ref="modalContainer" body-class="" v-bind="$props" dialog-class="modal-lg" class="bootstrap-alert" :backdrop="false">
|
||||
<template v-if="titel" v-slot:title>{{titel}}</template>
|
||||
<template v-slot:default>
|
||||
<div>
|
||||
<nav aria-label="breadcrumb" class="ps-2">
|
||||
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
{{$p.t('person','zustelladresse')}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NATION -->
|
||||
@@ -162,7 +162,7 @@ export default {
|
||||
<select :value="data.nation" @change="updateValue($event,'nation')" @change="getGemeinde" class="form-select" aria-label="Select Kontakttyp">
|
||||
<option selected></option>
|
||||
<option :value="nation.code" v-for="nation in nationenList">{{nation.langtext}}</option>
|
||||
</select>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
<div class=" col-4">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('person','plz')}}*</div>
|
||||
<input class="form-control" :value="data.plz" @input="updateValue($event,'plz')" @input="getGemeinde" :placeholder="data.plz">
|
||||
<input class="form-control" :value="data.plz" :aria-label="$p.t('person','plz')" :title="$p.t('person','plz')" @input="updateValue($event,'plz')" @input="getGemeinde" :placeholder="data.plz">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@ export default {
|
||||
<div class="col-lg-6">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','gemeinde')}}*</div>
|
||||
<auto-complete class="w-100" v-model="data.gemeinde" dropdown :forceSelection="data.nation ==='A'?true:false" :suggestions="gemeinden" @complete="autocompleteSearchGemeinden" ></auto-complete>
|
||||
<auto-complete :aria-label="$p.t('person','gemeinde')" class="w-100" v-model="data.gemeinde" dropdown :forceSelection="data.nation ==='A'?true:false" :suggestions="gemeinden" @complete="autocompleteSearchGemeinden" ></auto-complete>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,7 +186,7 @@ export default {
|
||||
<div class="col-lg-6" >
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','ort')}}*</div>
|
||||
<auto-complete class="w-100" v-model="data.ort" dropdown :forceSelection="data.nation ==='A'?true:false" :suggestions="ortschaftnamen" @complete="autocompleteSearchOrtschaftsnamen" ></auto-complete>
|
||||
<auto-complete :aria-label="$p.t('person','ort')" class="w-100" v-model="data.ort" dropdown :forceSelection="data.nation ==='A'?true:false" :suggestions="ortschaftnamen" @complete="autocompleteSearchOrtschaftsnamen" ></auto-complete>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,7 @@ export default {
|
||||
<div class="col-lg-8">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','strasse')}}*</div>
|
||||
<input class="form-control" :value="data.strasse" @input="updateValue($event,'strasse')" :placeholder="data.strasse">
|
||||
<input :aria-label="$p.t('person','strasse')" class="form-control" :value="data.strasse" @input="updateValue($event,'strasse')" :placeholder="data.strasse">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -68,11 +68,11 @@ export default {
|
||||
<!-- End of warning -->
|
||||
|
||||
<div v-if="!data.kontakt_id" class="col-12">
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','kontaktTyp')}}</div>
|
||||
|
||||
|
||||
<select :value="data.kontakttyp" @change="updateValue($event,'kontakttyp')" class="form-select" aria-label="Select Kontakttyp">
|
||||
<option selected></option>
|
||||
<option value="email">{{$p.t('person','email')}}</option>
|
||||
@@ -81,37 +81,37 @@ export default {
|
||||
<option value="mobil">{{$p.t('profilUpdate','mobiltelefonnummer')}}</option>
|
||||
<option value="homepage">{{$p.t('profilUpdate','homepage')}}</option>
|
||||
<option value="fax">{{$p.t('profilUpdate','faxnummer')}}</option>
|
||||
|
||||
</select>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
|
||||
<!-- rendering KONTAKT emails -->
|
||||
|
||||
|
||||
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{data.kontakttyp?data.kontakttyp:$p.t('global','kontakt')}}</div>
|
||||
|
||||
<input :disabled="data.kontakttyp?false:true" class="form-control" :value="data.kontakt" @input="updateValue($event,'kontakt')" :placeholder="data.kontakt">
|
||||
|
||||
<input :aria-label="data.kontakttyp" :title="data.kontakttyp" :disabled="data.kontakttyp?false:true" class="form-control" :value="data.kontakt" @input="updateValue($event,'kontakt')" :placeholder="data.kontakt">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('global','anmerkung')}}</div>
|
||||
|
||||
<input class="form-control" :value="data.anmerkung" @input="updateValue($event,'anmerkung')" :placeholder="data.anmerkung">
|
||||
<input :aria-label="$p.t('global','anmerkung')" :title="$p.t('global','anmerkung')" class="form-control" :value="data.anmerkung" @input="updateValue($event,'anmerkung')" :placeholder="data.anmerkung">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="d-flex flex-row justify-content-start col-12 allign-middle">
|
||||
<span style="opacity: 0.65; font-size: .85rem; " class="px-2">{{$p.t('profilUpdate','zustellungsKontakt')}}</span>
|
||||
|
||||
<input class="form-check-input " type="checkbox" :checked="data.zustellung" @change="updateValue($event,'zustellung')" id="flexCheckDefault">
|
||||
<input :aria-label="$p.t('profilUpdate','zustellungsKontakt')" :title="$p.t('profilUpdate','zustellungsKontakt')" class="form-check-input " type="checkbox" :checked="data.zustellung" @change="updateValue($event,'zustellung')" id="flexCheckDefault">
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import Dms from "../../../../Form/Upload/Dms.js";
|
||||
import BsModal from "../../../../Bootstrap/Modal.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dmsData: [],
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Dms,
|
||||
BsModal,
|
||||
},
|
||||
mixins: [BsModal],
|
||||
props: {
|
||||
titel: {
|
||||
type: Object,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
},
|
||||
updateID: {
|
||||
type: Boolean,
|
||||
},
|
||||
onHideBsModal: Function,
|
||||
onHiddenBsModal: Function,
|
||||
onHidePreventedBsModal: Function,
|
||||
onShowBsModal: Function,
|
||||
onShownBsModal: Function,
|
||||
},
|
||||
methods:{
|
||||
async uploadImage(){
|
||||
if(this.dmsData){
|
||||
let formData = new FormData();
|
||||
formData.append("files[]", this.dmsData[0]);
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.updateProfilbild(formData))
|
||||
.then((res) => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('global','hochgeladen'));
|
||||
this.modal.hide();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.modal = this.$refs.modalContainer.modal;
|
||||
if (this.files) {
|
||||
this.dmsData = this.files;
|
||||
}
|
||||
},
|
||||
popup(options) {
|
||||
BsModal.popup.bind(this);
|
||||
return BsModal.popup(null, options);
|
||||
},
|
||||
template: /*html*/`
|
||||
|
||||
<bs-modal v-show="!loading" ref="modalContainer" v-bind="$props" body-class="" dialog-class="modal-lg" class="bootstrap-alert" :backdrop="false">
|
||||
<template #title>
|
||||
<p style="opacity:0.8" class="ms-2" v-if="!updateID">{{$p.t('profilUpdate','profilBildUpdateMessage',[titel])}}</p>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{titel?titel:$p.t('global','titel')}}</div>
|
||||
</div>
|
||||
<div class="row gx-2">
|
||||
<div class="col">
|
||||
<dms ref="update" id="files" name="files" :multiple="false" v-model="dmsData" ></dms>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button @click="dmsData=[]" class="btn btn-danger"><i style="color:white" class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex" >
|
||||
<button @click="uploadImage" class="btn fhc-primary-bg mt-4 text-light" style="margin-left:auto;">{{$p.t('global','upload')}}</button>
|
||||
</div>
|
||||
</template>
|
||||
</bs-modal>
|
||||
`,
|
||||
};
|
||||
+5
-5
@@ -93,20 +93,20 @@ export default {
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
|
||||
|
||||
<p style="opacity:0.8" class="ms-2" v-if="withFiles && !updateID">{{$p.t('profilUpdate','profilUpdateInformationMessage',[data.titel])}}</p>
|
||||
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{data.titel?data.titel:$p.t('global','titel')}}</div>
|
||||
|
||||
<input class="mb-2 form-control" @input="emitChanges" v-model="data.value" :placeholder="data.value">
|
||||
|
||||
|
||||
<input class="mb-2 form-control" @input="emitChanges" :aria-label="data.titel" :tooltip="data.titel" v-model="data.value" :placeholder="data.value">
|
||||
|
||||
<div class="row gx-2">
|
||||
<div class="col">
|
||||
<dms ref="update" v-if="withFiles" id="files" name="files" :multiple="false" v-model="dmsData" @update:model-value="didFilesChange" ></dms>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button @click="dmsData=[]" class="btn btn-danger"><i style="color:white" class="fa fa-trash"></i></button>
|
||||
<button @click="dmsData=[]" class="btn btn-danger" :aria-label="$p.t('profilUpdate','deleteAttachment')" :title="$p.t('profilUpdate','deleteAttachment')" ><i style="color:white" class="fa fa-trash" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,12 +156,12 @@ export default {
|
||||
<component :is="item.listview" v-bind="item"></component>
|
||||
</div>
|
||||
</button>
|
||||
<button v-if="item.listview" @click="deleteItem(item)" type="button" class="mx-3 btn btn-danger btn-circle"><i class="fa fa-trash"></i></button>
|
||||
<button v-if="item.listview" @click="deleteItem(item)" type="button" class="mx-3 btn btn-danger btn-circle" :aria-label="$p.t('profilUpdate','deleteItem')" :title="$p.t('profilUpdate','deleteItem')" ><i class="fa fa-trash" aria-hide="true"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="Array.isArray(data)" class="mt-4 d-flex justify-content-center">
|
||||
<button @click="addItem" type="button" class="btn btn-primary btn-circle"><i class="fa fa-plus"></i></button>
|
||||
<button @click="addItem" type="button" class="btn btn-primary btn-circle" :aria-label="$p.t('profilUpdate','addItem')" :title="$p.t('profilUpdate','addItem')"><i class="fa fa-plus" aria-hide="true"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
||||
@@ -26,13 +26,13 @@ export default {
|
||||
FetchProfilUpdates,
|
||||
EditProfil,
|
||||
},
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language"],
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
collapseIconBetriebsmittel: true,
|
||||
editDataFilter: null,
|
||||
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
zutrittsgruppen_table_options: {
|
||||
persistenceID: "filterTableStudentProfilZutrittsgruppen",
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
height: 200,
|
||||
layout: "fitColumns",
|
||||
columns: [{
|
||||
title: Vue.computed(() => this.$p.t('profil/zutrittsGruppen')),
|
||||
title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase),
|
||||
field: "bezeichnung"
|
||||
}],
|
||||
},
|
||||
@@ -68,14 +68,14 @@ export default {
|
||||
headerClick: this.collapseFunction,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/entlehnteBetriebsmittel')),
|
||||
title: Vue.computed(()=>this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/inventarnummer')),
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.inventarnummerPhrase) ,
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -83,7 +83,7 @@ export default {
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/ausgabedatum')),
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) ,
|
||||
field: "Ausgegeben_am",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -163,8 +163,9 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
editable() {
|
||||
return this.data?.editAllowed ?? false;
|
||||
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
|
||||
filteredEditData() {
|
||||
@@ -228,6 +229,14 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory('profil').then(() => {
|
||||
this.preloadedPhrasen.zutrittsGruppenPhrase = this.$p.t('profil/zutrittsGruppen');
|
||||
this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel');
|
||||
this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer');
|
||||
this.preloadedPhrasen.ausgabedatum = this.$p.t('profil/ausgabedatum');
|
||||
this.preloadedPhrasen.loaded = true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
},
|
||||
@@ -239,9 +248,9 @@ export default {
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form">
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal"
|
||||
:value="JSON.parse(JSON.stringify(filteredEditData))" :title="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<!-- ROW -->
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal"
|
||||
:value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<div class="d-md-none col-12 ">
|
||||
@@ -251,11 +260,11 @@ export default {
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
|
||||
<!-- Bearbeiten Button -->
|
||||
<div v-if="editable" class="row ">
|
||||
<div v-if="isEditable" class="row ">
|
||||
<div class="col mb-3">
|
||||
<button @click="showEditProfilModal" type="button" class="text-start w-100 btn btn-outline-secondary" >
|
||||
<button @click="showEditProfilModal" type="button" class="card text-start w-100 btn btn-outline-secondary" >
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-edit"></i>
|
||||
@@ -267,31 +276,31 @@ export default {
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<!-- MOBILE PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF HIDDEN QUCK LINKS -->
|
||||
|
||||
|
||||
<!-- MAIN PANNEL -->
|
||||
<div class="col-sm-12 col-md-8 col-xxl-9 ">
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
|
||||
<!-- INFORMATION CONTENT START -->
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<!-- ROW WITH THE PROFIL INFORMATION -->
|
||||
<div class="row mb-4 ">
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PROFIL INFORMATION -->
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','studentIn')" :data="profilInformation" :editable="editable"></profil-information>
|
||||
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','studentIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- STUDENT INFO -->
|
||||
<role-information :title="$p.t('profil','studentInformation')" :data="roleInformation"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
</div>
|
||||
@@ -326,7 +335,7 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
@@ -344,7 +353,7 @@ export default {
|
||||
<div class="card-body">
|
||||
<div class="gy-3 row ">
|
||||
<div v-for="element in data.adressen" class="col-12">
|
||||
<Adresse :data="element"></Adresse>
|
||||
<Adresse :data="element"></Adresse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -356,16 +365,18 @@ export default {
|
||||
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
:tabulator-options="betriebsmittel_table_options"
|
||||
tableOnly
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
:tabulator-options="betriebsmittel_table_options"
|
||||
tableOnly
|
||||
:sideMenu="false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="zutrittsgruppenTableBuilt"
|
||||
:title="$p.t('profil','zutrittsGruppen')"
|
||||
ref="zutrittsgruppenTable"
|
||||
@@ -388,7 +399,7 @@ export default {
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block">
|
||||
<div class="col mb-3">
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
|
||||
<button @click="()=>showEditProfilModal()" type="button" class="card text-start w-100 btn btn-outline-secondary" >
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-edit"></i>
|
||||
|
||||
@@ -26,8 +26,8 @@ export default {
|
||||
methods: {},
|
||||
|
||||
computed: {
|
||||
editable() {
|
||||
return this.data?.editAllowed ?? false;
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
@@ -115,7 +115,7 @@ export default {
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :editable="editable"></profil-information>
|
||||
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
|
||||
@@ -252,7 +252,7 @@ export default {
|
||||
this.profilUpdateStates["Pending"];
|
||||
|
||||
let html = `<div class="d-flex justify-content-evenly align-items-center">
|
||||
<button class="btn border-primary border-2" id="showButton"><i class="fa-solid fa-eye" style="color: #0d6efd;"></i></button>
|
||||
<button class="btn border-primary border-2" id="showButton"><i class="fa-solid fa-eye fhc-primary-color"></i></button>
|
||||
${
|
||||
STATUS_PENDING ?
|
||||
`<button class="btn border-success border-2" id="acceptButton"><i class='fa fa-lg fa-circle-check text-success'></i></button>
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
|
||||
export const Raumsuche = {
|
||||
name: "Raumsuche",
|
||||
props: {
|
||||
|
||||
},
|
||||
components: {
|
||||
VueDatePicker,
|
||||
CoreFilterCmpt,
|
||||
InputNumber: primevue.inputnumber,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabulatorUuid: Vue.ref(0),
|
||||
tableBuiltResolve: null,
|
||||
tableBuiltPromise: null,
|
||||
roomtypes: null,
|
||||
defaultType: {
|
||||
raumtyp_kurzbz: '',
|
||||
beschreibung: Vue.computed(() => this.$p.t('global/alle'))
|
||||
},
|
||||
anzahl: 1,
|
||||
selectedType: null,
|
||||
datum: new Date(),
|
||||
von: Vue.ref({
|
||||
hours: new Date().getHours(),
|
||||
minutes: new Date().getMinutes()
|
||||
}),
|
||||
bis: Vue.ref({
|
||||
hours: new Date().getHours() + 1,
|
||||
minutes: new Date().getMinutes()
|
||||
}),
|
||||
datepickerTextInputOptions: {
|
||||
enterSubmit: true,
|
||||
tabSubmit: true,
|
||||
selectOnFocus: true,
|
||||
format: 'dd.MM.yyyy',
|
||||
escClose: true
|
||||
},
|
||||
timepickerTextInputOptions: {
|
||||
enterSubmit: true,
|
||||
tabSubmit: true,
|
||||
selectOnFocus: true,
|
||||
format: 'HH:mm',
|
||||
escClose: true
|
||||
},
|
||||
raumsucheTableOptions: {
|
||||
height: Vue.ref(400),
|
||||
index: 'ort_kurzbz',
|
||||
layout: 'fitColumns',
|
||||
placeholder: this.$p.t('global/noDataAvailable'),
|
||||
columns: [
|
||||
{title: Vue.computed(() => this.$p.t('rauminfo/raum_kurzbz')), field: 'ort_kurzbz', widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('global/bezeichnung')), field: 'bezeichnung', widthGrow: 2},
|
||||
{title: Vue.computed(() => this.$p.t('global/nummer')), field: 'nummer', widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('global/personen')), field: 'personen', widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('rauminfo/rauminfo')),
|
||||
field: 'linkInfo', formatter: this.linkFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('rauminfo/roomReservations')),
|
||||
field: 'linkRes', formatter: this.linkFormatter, widthGrow: 1}
|
||||
],
|
||||
persistence: false,
|
||||
},
|
||||
raumsucheTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
handler: async () => {
|
||||
this.tableBuiltResolve()
|
||||
}
|
||||
},
|
||||
{
|
||||
event: "cellClick",
|
||||
handler: async (e, cell) => {
|
||||
|
||||
if((cell.column.field === 'linkInfo' || cell.column.field === 'linkRes') && cell.value){
|
||||
window.open(cell.value, '_blank');
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
linkFormatter(cell) {
|
||||
const val = cell.getValue();
|
||||
const field = cell.getField();
|
||||
const arialabel = (field === 'linkInfo')
|
||||
? this.$p.t('rauminfo/rauminfo')
|
||||
: this.$p.t('rauminfo/roomReservations');
|
||||
if(val) {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'<a href="'+val+'" aria-label="' + arialabel + '">' +
|
||||
'<i class="fa fa-arrow-up-right-from-square me-1 fhc-primary-color" ></i>' +
|
||||
'</a></div>'
|
||||
} else {
|
||||
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
|
||||
'-</div>'
|
||||
}
|
||||
},
|
||||
roomPlanLink(room) {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
|
||||
+ '/CisVue/Cms/getRoomInformation/' + room.ort_kurzbz
|
||||
},
|
||||
roomInfoLink(room) {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router
|
||||
+ '/CisVue/Cms/content/' + room.content_id
|
||||
},
|
||||
getTimeString(time) {
|
||||
const hours = String(time.hours).padStart(2, '0');
|
||||
const minutes = String(time.minutes).padStart(2, '0');
|
||||
return `${hours}:${minutes}`
|
||||
},
|
||||
setupData(data){
|
||||
const d = data.map(room => {
|
||||
return {
|
||||
ort_kurzbz: room.ort_kurzbz,
|
||||
bezeichnung: room.bezeichnung.replace('&', '&'),
|
||||
nummer: room.planbezeichnung,
|
||||
personen: room.max_person,
|
||||
linkInfo: room.content_id ? this.roomInfoLink(room) : null,
|
||||
linkRes: this.roomPlanLink(room)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
this.$refs.raumsucheTable.tabulator.setData(d);
|
||||
},
|
||||
loadRoomTypes() {
|
||||
this.$fhcApi.factory.ort.getRoomTypes().then(res => {
|
||||
res?.data?.forEach(type => {
|
||||
type.beschreibung = type.beschreibung.replace('&', '&')
|
||||
})
|
||||
this.selectedType = this.defaultType
|
||||
this.roomtypes = res?.data ?? []
|
||||
})
|
||||
},
|
||||
loadRooms() {
|
||||
this.$fhcApi.factory.ort.getRooms(this.datum.toISOString(), this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl)
|
||||
.then(res => {
|
||||
if(res?.data?.retval) this.setupData(res.data.retval)
|
||||
})
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
},
|
||||
search(){
|
||||
this.loadRooms()
|
||||
},
|
||||
setRoute(val) {
|
||||
// TODO: router push
|
||||
},
|
||||
dateFormat(date) {
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}.${month}.${year}`
|
||||
},
|
||||
timeFormat(date) {
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
},
|
||||
async setupMounted() {
|
||||
this.tableBuiltPromise = new Promise(this.tableResolve)
|
||||
await this.tableBuiltPromise
|
||||
|
||||
this.loadRoomTypes()
|
||||
this.loadRooms()
|
||||
|
||||
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
|
||||
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
|
||||
if(!tableDataSet) return
|
||||
const rect = tableDataSet.getBoundingClientRect();
|
||||
|
||||
const h = window.visualViewport.height - rect.top - 100
|
||||
if(this.$refs.raumsucheTable) {
|
||||
this.$refs.raumsucheTable.$refs.table.style.setProperty('height', h+'px')
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isDarkMode(){
|
||||
return this.$theme.theme_name.value == 'dark';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
},
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
template: `
|
||||
<h1 class="h3">{{$p.t('rauminfo/roomSearch')}}</h1>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-2">
|
||||
<VueDatePicker
|
||||
:dark="isDarkMode"
|
||||
v-model="datum"
|
||||
:clearable="false"
|
||||
date-picker
|
||||
:enable-time-picker="false"
|
||||
:format="dateFormat"
|
||||
:text-input="datepickerTextInputOptions"
|
||||
:min-date="new Date()"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
<div class="col-12 col-lg-1">
|
||||
<VueDatePicker
|
||||
:dark="isDarkMode"
|
||||
v-model="von"
|
||||
:clearable="false"
|
||||
time-picker
|
||||
:format="timeFormat"
|
||||
:text-input="timepickerTextInputOptions"
|
||||
:is-24="true"
|
||||
auto-apply
|
||||
>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
<div class="col-12 col-lg-1">
|
||||
<VueDatePicker
|
||||
:dark="isDarkMode"
|
||||
v-model="bis"
|
||||
:clearable="false"
|
||||
time-picker
|
||||
:format="timeFormat"
|
||||
:text-input="timepickerTextInputOptions"
|
||||
:is-24="true"
|
||||
auto-apply>
|
||||
</VueDatePicker>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-auto">
|
||||
<select ref="raumtyp" id="raumtypSelect" v-model="selectedType" class="form-select"
|
||||
:aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setRoute($event.target.value)">
|
||||
<option :key="defaultType" selected :value="defaultType">{{defaultType.beschreibung}}</option>
|
||||
<option v-for="typ in roomtypes" :key="typ" :value="typ">{{typ.beschreibung}}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-4 col-lg-2">
|
||||
<InputNumber v-model="anzahl" :prefix="$p.t('rauminfo/anzahlPersonen') + ': '" inputId="anzahlInput" :min="1" :max="100" />
|
||||
</div>
|
||||
<div class="col-8 col-lg-2 d-flex justify-content-center align-items-center">
|
||||
<button class="btn btn-primary border-0" @click="search">{{ $p.t('rauminfo/roomSearch') }} <i class="fa fa-magnifying-glass"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<core-filter-cmpt
|
||||
@uuidDefined="handleUuidDefined"
|
||||
:title="''"
|
||||
ref="raumsucheTable"
|
||||
:tabulator-options="raumsucheTableOptions"
|
||||
:tabulator-events="raumsucheTableEventHandlers"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
/>
|
||||
`,
|
||||
};
|
||||
|
||||
export default Raumsuche;
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
export default {
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="cis-renderer-feiertage-calendar-event">
|
||||
<i class="event-icon fa-regular fa-calendar"></i>
|
||||
<span class="event-title fw-bold text-center">{{ event.titel }}</span>
|
||||
</div>`,
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import { formatDate } from "../../../../helpers/DateHelpers.js"
|
||||
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
},
|
||||
methods:{
|
||||
methodFormatDate: function (d) {
|
||||
return formatDate(d);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h2>{{event.titel ? event.titel : event.topic}}</h2>
|
||||
<p><i id="ferienEventIcon" class="fa-regular fa-calendar me-2"></i>{{methodFormatDate(event?.datum)}}</p>
|
||||
</div>`,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
template:`
|
||||
<div>{{ $p.t('global','feiertag') }}</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
tooltipString() {
|
||||
const tooltipArray = [];
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('global/uhrzeit'),
|
||||
[this.start, this.end].join(' - ')
|
||||
].join(": "));
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('profilUpdate/topic'),
|
||||
this.event.topic
|
||||
].join(": "));
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('person/ort'),
|
||||
this.event.ort_kurzbz
|
||||
].join(": "));
|
||||
|
||||
this.event.lektor = [
|
||||
this.event.lektor[0],
|
||||
this.event.lektor[0],
|
||||
this.event.lektor[0],
|
||||
this.event.lektor[0],
|
||||
this.event.lektor[0],
|
||||
];
|
||||
|
||||
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
|
||||
if (this.event.lektor.length > 3) {
|
||||
tooltipArray.push([
|
||||
this.$p.t('lehre/lektor'),
|
||||
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
|
||||
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
|
||||
].join(": "));
|
||||
} else {console.log(this.event.lektor);
|
||||
tooltipArray.push([
|
||||
this.$p.t('lehre/lektor'),
|
||||
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
|
||||
].join(": "));
|
||||
}
|
||||
}
|
||||
|
||||
return tooltipArray.join("\n");
|
||||
},
|
||||
start() {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.beginn)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
},
|
||||
end() {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.ende)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
<div
|
||||
class="cis-renderer-lehreinheit-calendar-event calendar-event-default h-100 w-100 p-1"
|
||||
@wheel.stop
|
||||
>
|
||||
<div
|
||||
v-if="!event.allDayEvent && event?.beginn && event?.ende"
|
||||
class="event-time d-none d-xl-grid h-100"
|
||||
>
|
||||
<span>{{ start }}</span>
|
||||
<span>{{ end }}</span>
|
||||
</div>
|
||||
<div class="event-text" v-tooltip="tooltipString">
|
||||
<span class="event-topic">{{ event.topic }}</span>
|
||||
<span class="event-place">{{ event.ort_kurzbz }}</span>
|
||||
<span
|
||||
v-for="(lektor,index) in event.lektor.slice(0, 3)"
|
||||
class="event-lectors"
|
||||
>
|
||||
{{ lektor.kurzbz }}
|
||||
</span>
|
||||
<span
|
||||
v-if="event.lektor.length > 3"
|
||||
class="event-lectors-plus"
|
||||
>
|
||||
... +{{ event.lektor.length - 3 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
|
||||
import LvMenu from "../../Mylv/LvMenu.js";
|
||||
|
||||
import ApiLvPlan from '../../../../api/factory/lvPlan.js';
|
||||
import ApiAddons from '../../../../api/factory/addons.js';
|
||||
|
||||
export default {
|
||||
components:{
|
||||
LvMenu,
|
||||
},
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lvMenu: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lektorenLinks: function () {
|
||||
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
|
||||
|
||||
let lektorenLinks = {};
|
||||
this.event.lektor.forEach((lektor) => {
|
||||
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
|
||||
})
|
||||
return lektorenLinks;
|
||||
},
|
||||
getOrtContentLink: function () {
|
||||
if (!this.event || !this.event.ort_content_id) return "a";
|
||||
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
|
||||
},
|
||||
start_time: function () {
|
||||
if (!this.event.beginn)
|
||||
return 'N/A';
|
||||
if (!(this.event.beginn instanceof Date)) {
|
||||
return this.event.beginn;
|
||||
}
|
||||
return numberPadding(this.event.beginn.getHours()) + ":" + numberPadding(this.event.beginn.getMinutes());
|
||||
},
|
||||
end_time: function () {
|
||||
if (!this.event.ende)
|
||||
return 'N/A';
|
||||
if (!(this.event.ende instanceof Date)) {
|
||||
return this.event.ende;
|
||||
}
|
||||
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mehtodNumberPadding: function (number) {
|
||||
return numberPadding(number);
|
||||
},
|
||||
methodFormatDate: function (d) {
|
||||
return formatDate(d);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.event.type == 'lehreinheit') {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getLehreinheitStudiensemester(Array.isArray(this.event.lehreinheit_id) ? this.event.lehreinheit_id[0] : this.event.lehreinheit_id))
|
||||
.then(res => res.data)
|
||||
.then(studiensemester_kurzbz => this.$api.call(
|
||||
ApiAddons.getLvMenu(
|
||||
this.event.lehrveranstaltung_id,
|
||||
studiensemester_kurzbz
|
||||
)
|
||||
))
|
||||
.then(res => {
|
||||
this.lvMenu = res.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h5>
|
||||
{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}
|
||||
</h5>
|
||||
<table class="table table-hover mb-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','datum')?
|
||||
$p.t('global','datum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{methodFormatDate(event.datum)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('ui','zeitraum')?
|
||||
$p.t('ui','zeitraum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{start_time + ' - ' + end_time}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','raum')?
|
||||
$p.t('global','raum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<a v-if="event.ort_content_id" :aria-label="$p.t('global','raum')" :title="$p.t('global','raum')" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true" style="color:#00649C"></i></a>
|
||||
{{event.ort_kurzbz}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lehrveranstaltung')?
|
||||
$p.t('lehre','lehrveranstaltung')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{'('+event.lehrform+') ' + event.lehrfach_bez}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lektor')?
|
||||
$p.t('lehre','lektor')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<div id="lektorenContainer">
|
||||
<div v-for="lektor in event.lektor" class="d-block">
|
||||
<a v-if="lektorenLinks[lektor.kurzbz]" :aria-label="$p.t('lehre','lektor')" :title="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
|
||||
{{lektor.kurzbz}}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','organisationseinheit')?
|
||||
$p.t('lehre','organisationseinheit')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{event.organisationseinheit}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu.length" :menu="lvMenu" />
|
||||
</div>`,
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
template:`
|
||||
<div v-if="event.titel">{{ event.titel + ' - ' + event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</div>
|
||||
<div v-else>{{ event.lehrfach_bez + ' [' + event.ort_kurzbz+']'}}</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
export default {
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tooltipString() {
|
||||
const tooltipArray = [];
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('global/uhrzeit'),
|
||||
[this.start, this.end].join(' - ')
|
||||
].join(": "));
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('profilUpdate/topic'),
|
||||
this.event.topic
|
||||
].join(": "));
|
||||
|
||||
tooltipArray.push([
|
||||
this.$p.t('person/ort'),
|
||||
this.event.ort_kurzbz
|
||||
].join(": "));
|
||||
|
||||
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
|
||||
if (this.event.lektor.length > 3) {
|
||||
tooltipArray.push([
|
||||
this.$p.t('lehre/lektor'),
|
||||
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
|
||||
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
|
||||
].join(": "));
|
||||
} else {
|
||||
tooltipArray.push([
|
||||
this.$p.t('lehre/lektor'),
|
||||
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
|
||||
].join(": "));
|
||||
}
|
||||
}
|
||||
|
||||
return tooltipArray.join("\n");
|
||||
},
|
||||
start() {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.beginn)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
},
|
||||
end() {
|
||||
return luxon.Duration
|
||||
.fromISOTime(this.event.ende)
|
||||
.toISOTime({ suppressSeconds: true });
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div
|
||||
class="cis-renderer-reservierungen-calendar-event calendar-event-default h-100 w-100 p-1"
|
||||
>
|
||||
<div
|
||||
v-if="!event.allDayEvent && event?.beginn && event?.ende"
|
||||
class="event-time d-grid h-100"
|
||||
>
|
||||
<span>{{ start }}</span>
|
||||
<span>{{ end }}</span>
|
||||
</div>
|
||||
<div class="event-text" v-tooltip="tooltipString">
|
||||
<span class="event-topic">{{ event.topic }}</span>
|
||||
<span
|
||||
v-for="lektor in event.lektor.slice(0, 3)"
|
||||
class="event-lectors"
|
||||
>
|
||||
{{ lektor.kurzbz }}
|
||||
</span>
|
||||
<span
|
||||
v-if="event.lektor.length > 3"
|
||||
class="event-lectors-plus"
|
||||
>
|
||||
... +{{ event.lektor.length - 3 }}
|
||||
</span>
|
||||
<span class="event-place">{{ event.ort_kurzbz }}</span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { numberPadding, formatDate } from "../../../../helpers/DateHelpers.js"
|
||||
import LvMenu from "../../Mylv/LvMenu.js";
|
||||
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
lvMenu:{
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
components:{
|
||||
LvMenu,
|
||||
},
|
||||
computed: {
|
||||
lektorenLinks: function () {
|
||||
if (!this.event || !Array.isArray(this.event.lektor) || !this.event.lektor.length) return "a";
|
||||
|
||||
let lektorenLinks = {};
|
||||
this.event.lektor.forEach((lektor) => {
|
||||
lektorenLinks[lektor.kurzbz] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${lektor.mitarbeiter_uid}`;
|
||||
})
|
||||
return lektorenLinks;
|
||||
},
|
||||
getOrtContentLink: function () {
|
||||
if (!this.event || !this.event.ort_content_id) return "a";
|
||||
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/CisVue/Cms/content/${this.event.ort_content_id}`
|
||||
},
|
||||
start_time: function () {
|
||||
if (!this.event.beginn)
|
||||
return 'N/A';
|
||||
if (!(this.event.beginn instanceof Date)) {
|
||||
return this.event.beginn;
|
||||
}
|
||||
return numberPadding(this.event.beginn.getHours()) + ":" + numberPadding(this.event.beginn.getMinutes());
|
||||
},
|
||||
end_time: function () {
|
||||
if (!this.event.ende)
|
||||
return 'N/A';
|
||||
if (!(this.event.ende instanceof Date)) {
|
||||
return this.event.ende;
|
||||
}
|
||||
return numberPadding(this.event.ende.getHours()) + ":" + numberPadding(this.event.ende.getMinutes());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mehtodNumberPadding: function (number) {
|
||||
return numberPadding(number);
|
||||
},
|
||||
methodFormatDate: function (d) {
|
||||
return formatDate(d);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h3>
|
||||
{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}
|
||||
</h3>
|
||||
<table class="table table-hover mb-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','datum')?
|
||||
$p.t('global','datum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{methodFormatDate(event.datum)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('ui','zeitraum')?
|
||||
$p.t('ui','zeitraum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>{{start_time + ' - ' + end_time}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('global','raum')?
|
||||
$p.t('global','raum')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<a v-if="event.ort_content_id" :aria-label="$p.t('global','raum')" :title="$p.t('global','raum')" :href="getOrtContentLink"><i class="fa fa-arrow-up-right-from-square me-1" style="color:#00649C" aria-hidden="true"></i></a>
|
||||
{{event.ort_kurzbz}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{
|
||||
$p.t('lehre','lektor')?
|
||||
$p.t('lehre','lektor')+':'
|
||||
:''
|
||||
}}</th>
|
||||
<td>
|
||||
<div v-for="lektor in event.lektor" class="d-block">
|
||||
<a v-if="lektorenLinks[lektor.kurzbz]" :aria-label="$p.t('lehre','lektor')" :tooltip="$p.t('lehre','lektor')" :href="lektorenLinks[lektor.kurzbz]"><i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true" style="color:#00649C"></i></a>
|
||||
{{lektor.kurzbz}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>`,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
props:{
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
template:`
|
||||
<div >{{ event.topic + ' [' + event.ort_kurzbz+']'}}</div>
|
||||
`
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default {
|
||||
template:/*html*/`
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center flex-nowrap overflow-hidden">
|
||||
<button v-for="lang in allActiveLanguages" @click.prevent="changeLanguage(lang.sprache)" class="col text-white sprachen-entry btn text-center w-100" :selected="$p.user_language.value==lang.sprache">{{lang.bezeichnung}}</button>
|
||||
<button v-for="lang in allActiveLanguages" @click.prevent="changeLanguage(lang.sprache)" class="col fhc-text-light sprachen-entry btn text-center w-100" :selected="$p.user_language.value==lang.sprache">{{lang.bezeichnung}}</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
import LvUebersicht from "../Mylv/LvUebersicht.js";
|
||||
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
studienSemester :[],
|
||||
selectedStudiensemester: null,
|
||||
studiengaenge:[],
|
||||
selectedStudiengang:null,
|
||||
studienOrdnung: [],
|
||||
selectedStudienordnung: null,
|
||||
semester:[],
|
||||
selectedSemester:null,
|
||||
lehrveranstaltungen: [],
|
||||
selectedLehrveranstaltung: null,
|
||||
menu:null,
|
||||
}
|
||||
},
|
||||
provide(){
|
||||
return {
|
||||
studium_studiengang : Vue.computed(()=> this.selectedStudiengang),
|
||||
studium_studiensemester: Vue.computed(() => this.selectedStudiensemester),
|
||||
studium_semester: Vue.computed(() => this.selectedSemester),
|
||||
studium_studienordnung: Vue.computed(() => this.selectedStudienordnung),
|
||||
|
||||
}
|
||||
},
|
||||
components: {
|
||||
LvUebersicht,
|
||||
},
|
||||
watch:{
|
||||
selectedStudiensemester: function(newVal, oldVal){
|
||||
if(newVal && newVal != oldVal){
|
||||
const studiensemester =this.getDataFromLocalStorage("sudiensemester");
|
||||
if (newVal && (!studiensemester || (studiensemester && studiensemester != newVal))){
|
||||
this.storeDataToLocalStorage("sudiensemester", newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedSemester: function (newVal, oldVal) {
|
||||
if (newVal && newVal != oldVal) {
|
||||
const semester = this.getDataFromLocalStorage("semester");
|
||||
if (!semester || (semester && semester != newVal)) {
|
||||
this.storeDataToLocalStorage("semester", newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedStudiengang: function (newVal, oldVal) {
|
||||
if (newVal && newVal != oldVal) {
|
||||
const studiengang = this.getDataFromLocalStorage("studiengang");
|
||||
if (!studiengang || (studiengang && studiengang != newVal)) {
|
||||
this.storeDataToLocalStorage("studiengang", JSON.stringify(newVal));
|
||||
}
|
||||
}
|
||||
},
|
||||
selectedStudienordnung: function (newVal, oldVal) {
|
||||
if (newVal && newVal != oldVal) {
|
||||
const studienordnung = this.getDataFromLocalStorage("studienordnung");
|
||||
if (!studienordnung || (studienordnung && studienordnung != newVal)) {
|
||||
this.storeDataToLocalStorage("studienordnung", JSON.stringify(newVal));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods:{
|
||||
changeStudiensemester(value){
|
||||
let studiensemester = this.$refs.studiensemester;
|
||||
studiensemester.selectedIndex = (studiensemester.selectedIndex + value + studiensemester.options.length) % studiensemester.options.length;
|
||||
this.changeSelectedStudienSemester(studiensemester.value);
|
||||
},
|
||||
changeStudiengang(value) {
|
||||
let studiengang = this.$refs.studiengaenge;
|
||||
studiengang.selectedIndex = (studiengang.selectedIndex + value + studiengang.options.length) % studiengang.options.length;
|
||||
this.changeSelectedStudienGang(studiengang.value);
|
||||
},
|
||||
changeSemester(value) {
|
||||
let semester = this.$refs.semester;
|
||||
semester.selectedIndex = (semester.selectedIndex + value + semester.options.length) % semester.options.length;
|
||||
this.changeSelectedSemester(semester.value);
|
||||
},
|
||||
changeStudienordnung(value) {
|
||||
let studienordnung = this.$refs.studienordnung;
|
||||
let newSelectIndex = (studienordnung.selectedIndex + value + studienordnung.options.length) % studienordnung.options.length;
|
||||
if(studienordnung.options[newSelectIndex].disabled){
|
||||
newSelectIndex = (newSelectIndex + value + studienordnung.options.length) % studienordnung.options.length;
|
||||
}
|
||||
studienordnung.selectedIndex = newSelectIndex;
|
||||
this.changeSelectedStudienPlan(studienordnung.value);
|
||||
},
|
||||
|
||||
storeDataToLocalStorage(key,value){
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
getDataFromLocalStorage(key){
|
||||
const value = localStorage.getItem(key);
|
||||
return value;
|
||||
},
|
||||
changeSelectedStudienSemester(studiensemester_kurzbz) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedStudienGang(studiengang_kz) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedSemester(semester) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
changeSelectedStudienPlan(studienplan_id) {
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
},
|
||||
openLvUebersicht(lehrveranstaltung) {
|
||||
this.selectedLehrveranstaltung = lehrveranstaltung;
|
||||
//convert lehrveranstaltung properties for compatibility with LvPlan LvModal
|
||||
this.selectedLehrveranstaltung.type ="lehreinheit";
|
||||
this.selectedLehrveranstaltung.lehreinheit_id = this.selectedLehrveranstaltung.lehrveranstaltung_id;
|
||||
if(this.selectedLehrveranstaltung){
|
||||
Vue.nextTick(()=>{
|
||||
this.$refs.lvUebersicht.show();
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
sortStudienSemester(studienSemester){
|
||||
let regex = new RegExp(/^(WS|SS)([0-9]{4})/);
|
||||
studienSemester.sort((sem1,sem2)=>{
|
||||
let [sem1Match, sem1Semester, sem1Year] = sem1.studiensemester_kurzbz.match(regex);
|
||||
let [sem2Match, sem2Semester, sem2Year] = sem2.studiensemester_kurzbz.match(regex);
|
||||
if(sem1Year == sem2Year){
|
||||
return sem1Semester > sem2Semester? -1:1;
|
||||
}
|
||||
return sem1Year > sem2Year? -1:1;
|
||||
})
|
||||
},
|
||||
setHash(val) {
|
||||
// TODO: make this a router param to enable history
|
||||
location.hash = val;
|
||||
},
|
||||
extractPropertyValues(res){
|
||||
let { studienSemester, studiengang, semester, studienplan, lehrveranstaltungen } = res;
|
||||
this.sortStudienSemester(studienSemester.all);
|
||||
this.studienSemester = studienSemester.all;
|
||||
this.selectedStudiensemester = studienSemester.preselected.studiensemester_kurzbz;
|
||||
|
||||
this.studiengaenge = studiengang.all;
|
||||
this.selectedStudiengang = studiengang.preselected?.studiengang_kz;
|
||||
|
||||
this.semester = semester.all;
|
||||
this.selectedSemester = semester?.preselected;
|
||||
|
||||
this.studienOrdnung = studienplan.all;
|
||||
this.selectedStudienordnung = studienplan.preselected?.studienplan_id;
|
||||
|
||||
this.lehrveranstaltungen = lehrveranstaltungen;
|
||||
this.lehrveranstaltungen.sort((lv1, lv2) => {
|
||||
if (lv1.bezeichnung.toLowerCase() > lv2.bezeichnung.toLowerCase()) {
|
||||
return 1;
|
||||
} else if (lv1.bezeichnung.toLowerCase() < lv2.bezeichnung.toLowerCase()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
this.lehrveranstaltungen.forEach((lehrveranstaltung)=>{
|
||||
lehrveranstaltung.lehrveranstaltungen.sort((lv1,lv2)=>{
|
||||
if (lv1.bezeichnung.toLowerCase() > lv2.bezeichnung.toLowerCase()) {
|
||||
return 1;
|
||||
} else if (lv1.bezeichnung.toLowerCase() < lv2.bezeichnung.toLowerCase()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
studienordnungTitel(studienordnung){
|
||||
if(!studienordnung) return "";
|
||||
return `${studienordnung?.bezeichnung}-${studienordnung?.orgform_kurzbz} ( ${studienordnung?.orgform_bezeichnung}, ${studienordnung?.sprache} )`;
|
||||
},
|
||||
studiengangTitel(studiengang) {
|
||||
if (!studiengang) return "";
|
||||
return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`;
|
||||
},
|
||||
studiensemesterTitel(studiensemester){
|
||||
if (!studiensemester) return "";
|
||||
let studiensemester_regex = new RegExp(/^(WS|SS)([0-9]{4})/);
|
||||
let match = studiensemester.match(studiensemester_regex);
|
||||
switch(match[1]){
|
||||
case "WS":
|
||||
return `Wintersemester ${match[2]}`;
|
||||
case "SS":
|
||||
return `Sommersemester ${match[2]}`;
|
||||
default:
|
||||
return `${studiensemester}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed:{
|
||||
selectedLehrveranstaltungTitel(){
|
||||
const studiengang = this.studiengaenge.find((studiengang) => studiengang.studiengang_kz == this.selectedStudiengang);
|
||||
return `${this.selectedLehrveranstaltung?.bezeichnung} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`;
|
||||
},
|
||||
computedStudienOrdnung(){
|
||||
if(!this.studienOrdnung) return null;
|
||||
return Object.values(this.studienOrdnung).reduce((carry, item)=>{
|
||||
if(!carry[item.bezeichnung]){
|
||||
carry[item.bezeichnung] = [];
|
||||
}
|
||||
carry[item.bezeichnung].push(item);
|
||||
return carry;
|
||||
},{});
|
||||
},
|
||||
computedStudienOrdnungSelectValues() {
|
||||
if (!this.computedStudienOrdnung) return null;
|
||||
let result = [];
|
||||
Object.entries(this.computedStudienOrdnung).forEach(([key,value])=>{
|
||||
result.push({
|
||||
bezeichnung: `Studienordnung: ${key}`,
|
||||
disabled: true,
|
||||
});
|
||||
value.forEach((studienplan)=>{
|
||||
result.push({
|
||||
studienplan:studienplan,
|
||||
diabled: false,
|
||||
bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${studienplan?.orgform_bezeichnung}, ${studienplan?.sprache} )`
|
||||
|
||||
});
|
||||
})
|
||||
});
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
created(){
|
||||
|
||||
const studiensemester = this.getDataFromLocalStorage("sudiensemester") ?? undefined;
|
||||
const studiengang = JSON.parse(this.getDataFromLocalStorage("studiengang")) ?? undefined;
|
||||
const semester = this.getDataFromLocalStorage("semester") ?? undefined;
|
||||
const studienordnung = JSON.parse(this.getDataFromLocalStorage("studienordnung")) ?? undefined;
|
||||
|
||||
// only fetch default data if no data is stored in the local storage
|
||||
|
||||
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung)
|
||||
.then(data => data.data)
|
||||
.then(res => {
|
||||
this.extractPropertyValues(res);
|
||||
})
|
||||
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<h2>Studium</h2>
|
||||
<hr>
|
||||
<lv-uebersicht ref="lvUebersicht" :titel="selectedLehrveranstaltungTitel" :event="selectedLehrveranstaltung" :studiensemester="selectedStudiensemester" v-if="selectedLehrveranstaltung">
|
||||
<template #content>
|
||||
<div v-if="Array.isArray(selectedLehrveranstaltung.lektoren) && selectedLehrveranstaltung.lektoren.length>0" class="mb-4">
|
||||
<h4>Lektoren:</h4>
|
||||
<a :href="'mailto:'+lektor?.email" class="fhc-link-color mx-2" v-for="lektor in selectedLehrveranstaltung.lektoren">{{lektor.name}}</a>
|
||||
</div>
|
||||
<h4>Menu:</h4>
|
||||
</template>
|
||||
</lv-uebersicht>
|
||||
<div class="lvOptions">
|
||||
<div>
|
||||
<h6>Studiensemester:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiensemester" v-model="selectedStudiensemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="semester in studienSemester" @click="changeSelectedStudienSemester(semester.studiensemester_kurzbz)" :key="semester" :value="semester.studiensemester_kurzbz">{{studiensemesterTitel(semester.studiensemester_kurzbz) }}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(-1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Studiengang:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studiengaenge" v-model="selectedStudiengang" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="studiengang in studiengaenge" @click="changeSelectedStudienGang(studiengang.studiengang_kz)" :key="studiengang.studiengang_kz" :value="studiengang.studiengang_kz" >{{studiengangTitel(studiengang)}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Semester:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="semester" v-model="selectedSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="sem in semester" @click="changeSelectedSemester(sem)" :key="semester" :value="sem">{{sem}}. Semester</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Studienordnung:</h6>
|
||||
<div class="input-group">
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
|
||||
<i class="fa fa-caret-left" aria-hidden="true"></i>
|
||||
</button>
|
||||
<select ref="studienordnung" v-model="selectedStudienordnung" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
|
||||
<option v-for="ordnung in computedStudienOrdnungSelectValues" :disabled="ordnung.disabled" @click="changeSelectedStudienPlan(ordnung?.studienplan?.studienplan_id)" :key="ordnung?.studienplan?.bezeichnung " :value="ordnung?.studienplan?.studienplan_id">{{ordnung.bezeichnung}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
|
||||
<i class="fa fa-caret-right" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="lvUebersicht " >
|
||||
<template v-for="lehrveranstaltung in lehrveranstaltungen" :key="lehrveranstaltung.lehrveranstaltung_id">
|
||||
<div class="card" v-if="Array.isArray(lehrveranstaltung.lehrveranstaltungen) && lehrveranstaltung.lehrveranstaltungen.length >0" >
|
||||
<div class="card-header">
|
||||
<h5 class=" card-title">{{lehrveranstaltung.bezeichnung}}</h5>
|
||||
<h6 class=" card-subtitle">{{lehrveranstaltung.lehrform_kurzbz}}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="d-flex list-group-item" v-for="lv in lehrveranstaltung.lehrveranstaltungen">
|
||||
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lv)">{{lv.bezeichnung}}</a>
|
||||
<p>{{lv.lehrform_kurzbz}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
`
|
||||
};
|
||||
@@ -1,343 +0,0 @@
|
||||
import FhcCalendar from "../../Calendar/Calendar.js";
|
||||
import CalendarDate from "../../../composables/CalendarDate.js";
|
||||
import LvModal from "../Mylv/LvModal.js";
|
||||
import LvInfo from "../Mylv/LvInfo.js"
|
||||
import LvMenu from "../Mylv/LvMenu.js"
|
||||
|
||||
import ApiStundenplan from '../../../api/factory/stundenplan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_STUNDENPLAN = 'Week'
|
||||
|
||||
const Stundenplan = {
|
||||
name: 'Stundenplan',
|
||||
data() {
|
||||
return {
|
||||
events: null,
|
||||
calendarMode: DEFAULT_MODE_STUNDENPLAN,
|
||||
calendarDate: new CalendarDate(new Date()),
|
||||
eventCalendarDate: new CalendarDate(new Date()),
|
||||
currentlySelectedEvent: null,
|
||||
currentDay: this.propsViewData?.focus_date ? new Date(this.propsViewData.focus_date) : new Date(),
|
||||
lv: null,
|
||||
minimized: false,
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
propsViewData: Object,
|
||||
rowMinHeight: {
|
||||
type: String,
|
||||
default: '100px'
|
||||
},
|
||||
eventMaxHeight: {
|
||||
type: String,
|
||||
default: '125px'
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
rowMinHeight: this.rowMinHeight,
|
||||
eventMaxHeight: this.eventMaxHeight
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
weekFirstDay: {
|
||||
handler: async function (newValue) {
|
||||
let data = await this.fetchStudiensemesterDetails(newValue);
|
||||
let { studiensemester_kurzbz, start, ende } = data.data;
|
||||
this.studiensemester_kurzbz = studiensemester_kurzbz;
|
||||
this.studiensemester_start = start;
|
||||
this.studiensemester_ende = ende;
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
// forward/backward on history entries happening in stundenplan
|
||||
'propsViewData.lv_id'(newVal) {
|
||||
// relevant if lv_id can be changed from within this component
|
||||
},
|
||||
'propsViewData.mode'(newVal) {
|
||||
if(this.$refs.calendar) this.$refs.calendar.setMode(newVal)
|
||||
},
|
||||
'propsViewData.focus_date'(newVal) {
|
||||
this.currentDate = new Date(newVal)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
FhcCalendar, LvModal, LvMenu, LvInfo
|
||||
},
|
||||
computed:{
|
||||
downloadLinks: function(){
|
||||
if(!this.studiensemester_start || !this.studiensemester_ende || !this.uid )return;
|
||||
let start = new Date(this.studiensemester_start);
|
||||
start = Math.floor(start.getTime()/1000);
|
||||
let ende = new Date(this.studiensemester_ende);
|
||||
ende = Math.floor(ende.getTime() / 1000);
|
||||
|
||||
let download_link =
|
||||
(format, version = "", target = "") =>
|
||||
`${FHC_JS_DATA_STORAGE_OBJECT.app_root}cis/private/lvplan/stpl_kalender.php?type=student&pers_uid=
|
||||
${this.uid}&begin=${start}&ende=${ende}&format=${format}
|
||||
${version ? '&version=' + version : ''}${target ? '&target=' + target : ''}`;
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link('excel') },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link('csv') },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link('ical', '1', 'ical') },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link('ical', '2', 'ical') }
|
||||
];
|
||||
},
|
||||
weekFirstDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
|
||||
},
|
||||
weekLastDay: function () {
|
||||
return this.calendarDateToString(this.calendarDate.cdLastDayOfWeek);
|
||||
},
|
||||
monthFirstDay: function () {
|
||||
return this.calendarDateToString(this.eventCalendarDate.cdFirstDayOfCalendarMonth);
|
||||
},
|
||||
monthLastDay: function () {
|
||||
return this.calendarDateToString(this.eventCalendarDate.cdLastDayOfCalendarMonth);
|
||||
},
|
||||
},
|
||||
methods:{
|
||||
fetchStudiensemesterDetails: async function (date) {
|
||||
return this.$api.call(ApiStundenplan.studiensemesterDateInterval(date));
|
||||
},
|
||||
convertTime: function([hour,minute]){
|
||||
let date = new Date();
|
||||
date.setHours(hour);
|
||||
date.setMinutes(minute);
|
||||
// returns date string as hh:mm
|
||||
return date.toLocaleTimeString(this.$p.user_locale, { hour: '2-digit', minute: '2-digit', hour12:false});
|
||||
|
||||
},
|
||||
setSelectedEvent: function (event) {
|
||||
this.currentlySelectedEvent = event;
|
||||
},
|
||||
selectDay: function(day){
|
||||
const date = day.getFullYear() + "-" +
|
||||
String(day.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(day.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "Stundenplan",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
lv_id: this.propsViewData?.lv_id || null
|
||||
}
|
||||
})
|
||||
|
||||
this.currentDay = day;
|
||||
},
|
||||
handleOffset: function(offset) {
|
||||
this.currentDay = new Date(
|
||||
this.currentDay.getFullYear() + offset.y,
|
||||
this.currentDay.getMonth() + offset.m,
|
||||
this.currentDay.getDate() + offset.d
|
||||
)
|
||||
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "Stundenplan",
|
||||
params: {
|
||||
mode: this.calendarMode,
|
||||
focus_date: date,
|
||||
lv_id: this.propsViewData?.lv_id || null
|
||||
}
|
||||
})
|
||||
},
|
||||
handleChangeMode(mode) {
|
||||
const modeCapitalized = mode.charAt(0).toUpperCase() + mode.slice(1)
|
||||
const date = this.currentDay.getFullYear() + "-" +
|
||||
String(this.currentDay.getMonth() + 1).padStart(2, "0") + "-" +
|
||||
String(this.currentDay.getDate()).padStart(2, "0");
|
||||
|
||||
this.$router.push({
|
||||
name: "Stundenplan",
|
||||
params: {
|
||||
mode: modeCapitalized,
|
||||
focus_date: date,
|
||||
lv_id: this.propsViewData?.lv_id ?? null
|
||||
}
|
||||
})
|
||||
|
||||
this.calendarMode = mode
|
||||
},
|
||||
showModal: function(e, event){
|
||||
e.stopPropagation()
|
||||
this.currentlySelectedEvent = event;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.lvmodal.show();
|
||||
});
|
||||
},
|
||||
updateRange: function ({start,end}) {
|
||||
|
||||
let checkDate = (date) => {
|
||||
return date.m != this.eventCalendarDate.m || date.y != this.eventCalendarDate.y;
|
||||
}
|
||||
this.calendarDate = new CalendarDate(end);
|
||||
|
||||
// only load month data if the month or year has changed
|
||||
if (checkDate(new CalendarDate(start)) && checkDate(new CalendarDate(end))){
|
||||
// reset the events before querying the new events to activate the loading spinner
|
||||
this.events = null;
|
||||
this.eventCalendarDate = new CalendarDate(end);
|
||||
Vue.nextTick(() => {
|
||||
this.loadEvents();
|
||||
});
|
||||
}
|
||||
},
|
||||
calendarDateToString: function (calendarDate) {
|
||||
return calendarDate instanceof CalendarDate ?
|
||||
[calendarDate.y, calendarDate.m + 1, calendarDate.d].join('-') :
|
||||
null;
|
||||
|
||||
},
|
||||
loadEvents: function(){
|
||||
Promise.allSettled([
|
||||
this.$api.call(ApiStundenplan.getStundenplan(this.monthFirstDay, this.monthLastDay, this.propsViewData.lv_id)),
|
||||
this.$api.call(ApiStundenplan.getStundenplanReservierungen(this.monthFirstDay, this.monthLastDay))
|
||||
]).then((result) => {
|
||||
let promise_events = [];
|
||||
result.forEach((promise_result) => {
|
||||
if (promise_result.status === 'fulfilled' && promise_result.value.meta.status === "success") {
|
||||
|
||||
if(promise_result.value.meta?.lv) this.lv = promise_result.value.meta.lv
|
||||
|
||||
let data = promise_result.value.data;
|
||||
// adding additional information to the events
|
||||
if (data && data.forEach) {
|
||||
|
||||
data.forEach((el, i) => {
|
||||
el.id = i;
|
||||
if (el.type === 'reservierung') {
|
||||
el.color = '#' + (el.farbe || 'FFFFFF');
|
||||
} else {
|
||||
el.color = '#' + (el.farbe || 'CCCCCC');
|
||||
}
|
||||
|
||||
el.start = new Date(el.datum + ' ' + el.beginn);
|
||||
el.end = new Date(el.datum + ' ' + el.ende);
|
||||
|
||||
});
|
||||
}
|
||||
promise_events = promise_events.concat(data);
|
||||
}
|
||||
})
|
||||
this.events = promise_events;
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthUID())
|
||||
.then(res => res.data)
|
||||
.then(data => {
|
||||
this.uid = data.uid;
|
||||
});
|
||||
this.loadEvents();
|
||||
},
|
||||
beforeUnmount() {
|
||||
if(this.$refs.lvmodal) this.$refs.lvmodal.hide()
|
||||
},
|
||||
template:/*html*/`
|
||||
<h2>
|
||||
{{$p.t('lehre/stundenplan')}}
|
||||
<span style="padding-left: 0.4em;" v-show="studiensemester_kurzbz">{{studiensemester_kurzbz}}</span>
|
||||
<span style="padding-left: 0.5em;" v-show="propsViewData?.lv_id && lv"> {{ $p.user_language.value === 'German' ? lv?.bezeichnung : lv?.bezeichnung_english}}</span>
|
||||
</h2>
|
||||
<hr>
|
||||
<lv-modal v-if="currentlySelectedEvent" :event="currentlySelectedEvent" ref="lvmodal" />
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
@selectedEvent="setSelectedEvent"
|
||||
:initial-date="currentDay"
|
||||
@change:range="updateRange"
|
||||
@change:offset="handleOffset"
|
||||
:events="events"
|
||||
:initial-mode="propsViewData.mode"
|
||||
show-weeks
|
||||
@select:day="selectDay"
|
||||
@change:mode="handleChangeMode"
|
||||
v-model:minimized="minimized"
|
||||
>
|
||||
<template #calendarDownloads>
|
||||
<div v-for="{title,icon,link} in downloadLinks">
|
||||
<a :href="link" :title="title" class="py-1 px-2 m-1 btn btn-outline-secondary">
|
||||
<div class="d-flex flex-column">
|
||||
<i :class="icon"></i>
|
||||
<span class="small">{{title}}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template #monthPage="{event,day}">
|
||||
<div @click="showModal($event, event); ">
|
||||
<span class="fhc-entry">
|
||||
{{event.topic}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<template #weekPage="{event,day}">
|
||||
<div @click="showModal($event, event?.orig);" type="button"
|
||||
class=" position-relative border border-secondary border d-flex flex-col align-items-center
|
||||
justify-content-evenly h-100" style="overflow: auto;">
|
||||
|
||||
<div v-if="event?.orig?.beginn && event?.orig?.ende" class="d-none d-xl-block" >
|
||||
<div class="d-flex flex-column p-4 p-xl-2 border-end border-secondary">
|
||||
<span class="small">{{convertTime(event.orig.beginn.split(":"))}}</span>
|
||||
<span class="small">{{convertTime(event.orig.ende.split(":"))}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-grow-1 align-items-center" style="font-size: 0.75rem">
|
||||
<span>{{event?.orig.topic}}</span>
|
||||
<span v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</span>
|
||||
<span>{{event?.orig.ort_kurzbz}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #dayPage="{event,day,mobile}">
|
||||
<div @click="mobile? showModal($event, event?.orig):null" type="button" class="fhc-entry border border-secondary border row m-0 h-100 justify-content-center align-items-center text-center">
|
||||
<div class="col-auto" v-if="event?.orig?.beginn && event?.orig?.ende" >
|
||||
<div class="d-flex flex-column p-4 border-end border-secondary">
|
||||
<span class="small">{{convertTime(event.orig.beginn.split(":"))}}</span>
|
||||
<span class="small">{{convertTime(event.orig.ende.split(":"))}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>{{ $p.t('lehre/lehrveranstaltung') }}:</p>
|
||||
<p class="m-0">{{event?.orig.topic}}</p>
|
||||
</div>
|
||||
<div class="col" :style="'max-height: ' + eventMaxHeight + '; overflow: auto;'">
|
||||
<p>{{ $p.t('lehre/lektor') }}:</p>
|
||||
<p class="m-0" v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</p>
|
||||
</div>
|
||||
<div class="col">
|
||||
<p>{{ $p.t('profil/Ort') }}: </p>
|
||||
<p class="m-0">{{event?.orig.ort_kurzbz}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #pageMobilContent="{lvMenu}">
|
||||
<h3 >{{$p.t('lvinfo','lehrveranstaltungsinformationen')}}</h3>
|
||||
<div class="w-100">
|
||||
<lv-info :event="currentlySelectedEvent" />
|
||||
</div>
|
||||
<h3 >{{$p.t('lehre','lehrveranstaltungsmenue')}}</h3>
|
||||
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-show="lvMenu" :menu="lvMenu" />
|
||||
</template>
|
||||
<template #pageMobilContentEmpty >
|
||||
<h3>{{ $p.t('lehre/noLvFound') }}</h3>
|
||||
</template>
|
||||
</fhc-calendar>`
|
||||
}
|
||||
|
||||
export default Stundenplan
|
||||
@@ -0,0 +1,33 @@
|
||||
export default {
|
||||
data:()=>{
|
||||
return{
|
||||
theme: null,
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
switchTheme(nextTheme){
|
||||
this.theme = nextTheme;
|
||||
this.$theme.switchTheme(this.theme);
|
||||
},
|
||||
|
||||
},
|
||||
computed:{
|
||||
nextTheme(){
|
||||
return this.$theme.theme_modes[(this.$theme.theme_modes.indexOf(this.theme) + 1) % this.$theme.theme_modes.length];
|
||||
},
|
||||
},
|
||||
created(){
|
||||
this.theme = localStorage.getItem('theme');
|
||||
if (!this.theme || !this.$theme.theme_modes.includes(this.theme)) {
|
||||
this.theme = this.$theme.theme_modes[0];
|
||||
}
|
||||
},
|
||||
template:/*html*/`
|
||||
|
||||
<button id="themeSwitch" :aria-label="$p.t('global','switchTheme',[nextTheme])" @click="switchTheme(nextTheme)" class="fhc-primary-highlight-bg align-self-center btn btn-secondary rounded-5">
|
||||
<i v-if="theme == 'light'" class="fa-solid fa-sun " aria-hidden="true"></i>
|
||||
<i v-else-if="theme == 'dark'" class="fa-solid fa-moon " aria-hidden="true"></i>
|
||||
<!--<i v-else-if="theme == 'purple'" class="fa-solid fa-wine-bottle" aria-hidden="true"></i>-->
|
||||
</button>
|
||||
`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user