mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-26 00:54:27 +00:00
Merge branch 'master' into feature-40314/Electronic_Onboarding_Anbindung_IDA
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
export default {
|
||||
name: "BaseModal",
|
||||
props: [
|
||||
"id",
|
||||
"cModalClass", // optional: add custom classes to modal class
|
||||
"cModalDialogClass" // optional: add custom classes to modal-dialog class
|
||||
],
|
||||
onMounted()
|
||||
{
|
||||
const modal = new bootstrap.Modal(this.$refs.modal, {})
|
||||
modal.show();
|
||||
},
|
||||
template: `
|
||||
<div class="modal fade"
|
||||
ref="modal"
|
||||
:class="this.cModalClass"
|
||||
:id="this.id"
|
||||
:aria-labelledby="this.id + '_label'" aria-hidden="true" tabindex="-1">
|
||||
<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'">
|
||||
<slot name="title"></slot>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button><!-- default -->
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export default {
|
||||
name: "BaseOffcanvas",
|
||||
props: [
|
||||
"id",
|
||||
"cOffcanvasClass", // optional: add custom classes to offcanvas class
|
||||
"closeFunc"
|
||||
],
|
||||
computed: {
|
||||
OffcanvasClass() {
|
||||
return this.cOffcanvasClass || 'offcanvas-end'; // default: slide in from right to left
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="offcanvas"
|
||||
:class="this.OffcanvasClass"
|
||||
:id="this.id"
|
||||
:aria-labelledby="this.id + '_label'"
|
||||
tabindex="-1"
|
||||
data-bs-backdrop="false">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" :id="this.id + '_label'">
|
||||
<slot name="title"></slot>
|
||||
</h5>
|
||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" @click="closeFunc" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<div>
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -29,30 +29,69 @@ 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.endpoint.getAllBetriebsmittel,
|
||||
ajaxParams: () => {
|
||||
return {
|
||||
type: this.typeId,
|
||||
id: this.id
|
||||
};
|
||||
},
|
||||
ajaxRequestFunc: () => this.$api.call(
|
||||
this.endpoint.getAllBetriebsmittel(this.typeId, this.id, (this.filterByProvidedTypes ? this.betriebsmittelTypes : null))
|
||||
),
|
||||
ajaxResponse: (url, params, response) => response.data,
|
||||
columns: [
|
||||
{title: "Nummer", field: "nummer", width: 150},
|
||||
{title: "PersonId", field: "person_id", visible: false},
|
||||
{title: "Typ", field: "betriebsmitteltyp", width: 125},
|
||||
{title: "Anmerkung", field: "anmerkung", visible: false},
|
||||
{title: "Retourdatum", field: "format_retour", width: 128},
|
||||
{
|
||||
title: "Retourdatum",
|
||||
field: "retouram",
|
||||
width: 128,
|
||||
formatter: function (cell) {
|
||||
const dateStr = cell.getValue();
|
||||
if (!dateStr) return "";
|
||||
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
},
|
||||
{title: "Beschreibung", field: "beschreibung"},
|
||||
{title: "UID", field: "uid", width: 87},
|
||||
{title: "Kaution", field: "kaution", visible: false},
|
||||
{title: "Ausgabedatum", field: "format_ausgabe", width: 144, visible: false},
|
||||
{
|
||||
title: "Ausgabedatum",
|
||||
field: "ausgegebenam",
|
||||
width: 144,
|
||||
visible: false,
|
||||
formatter: function (cell) {
|
||||
const dateStr = cell.getValue();
|
||||
if (!dateStr) return "";
|
||||
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
},
|
||||
{title: "Betriebsmittel ID", field: "betriebsmittel_id", visible: false},
|
||||
{title: "Betriebsmittelperson ID", field: "betriebsmittelperson_id", visible: false},
|
||||
{
|
||||
@@ -66,7 +105,7 @@ export default {
|
||||
let button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary btn-action';
|
||||
button.innerHTML = '<i class="fa fa-print"></i>';
|
||||
button.title = 'Übernahmebestätigung drucken';
|
||||
button.title = this.$p.t('betriebsmittel', 'btn_printUebernahmebestaetigung');
|
||||
let cellData = cell.getData();
|
||||
button.addEventListener(
|
||||
'click',
|
||||
@@ -82,7 +121,7 @@ export default {
|
||||
button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary btn-action';
|
||||
button.innerHTML = '<i class="fa fa-edit"></i>';
|
||||
button.title = 'Betriebsmittel bearbeiten';
|
||||
button.title = this.$p.t('betriebsmittel', 'btn_editBetriebsmittel');
|
||||
button.addEventListener(
|
||||
'click',
|
||||
(event) =>
|
||||
@@ -93,7 +132,7 @@ export default {
|
||||
button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary btn-action';
|
||||
button.innerHTML = '<i class="fa fa-xmark"></i>';
|
||||
button.title = 'Betriebsmittel löschen';
|
||||
button.title = this.$p.t('betriebsmittel', 'btn_deleteBetriebsmittel');
|
||||
button.addEventListener(
|
||||
'click',
|
||||
() =>
|
||||
@@ -108,8 +147,6 @@ export default {
|
||||
layout: 'fitColumns',
|
||||
layoutColumnsOnNewData: false,
|
||||
height: '550',
|
||||
selectableRangeMode: 'click',
|
||||
selectable: true,
|
||||
persistenceID: 'core-betriebsmittel'
|
||||
},
|
||||
tabulatorEvents: [
|
||||
@@ -117,26 +154,43 @@ export default {
|
||||
event: 'tableBuilt',
|
||||
handler: async() => {
|
||||
|
||||
await this.$p.loadCategory(['wawi', 'global', 'infocenter']);
|
||||
await this.$p.loadCategory(['wawi', 'global', 'infocenter', 'betriebsmittel', 'person']);
|
||||
|
||||
let cm = this.$refs.table.tabulator.columnManager;
|
||||
|
||||
cm.getColumnByField('nummer').component.updateDefinition({
|
||||
title: this.$p.t('wawi', 'nummer')
|
||||
});
|
||||
cm.getColumnByField('betriebsmitteltyp').component.updateDefinition({
|
||||
title: this.$p.t('global', 'typ')
|
||||
});
|
||||
cm.getColumnByField('anmerkung').component.updateDefinition({
|
||||
title: this.$p.t('global', 'anmerkung')
|
||||
});
|
||||
cm.getColumnByField('format_retour').component.updateDefinition({
|
||||
cm.getColumnByField('retouram').component.updateDefinition({
|
||||
title: this.$p.t('wawi', 'retourdatum')
|
||||
});
|
||||
cm.getColumnByField('beschreibung').component.updateDefinition({
|
||||
title: this.$p.t('global', 'beschreibung')
|
||||
});
|
||||
cm.getColumnByField('kaution').component.updateDefinition({
|
||||
title: this.$p.t('infocenter', 'kaution')
|
||||
});
|
||||
cm.getColumnByField('format_ausgabe').component.updateDefinition({
|
||||
cm.getColumnByField('ausgegebenam').component.updateDefinition({
|
||||
title: this.$p.t('wawi', 'ausgabedatum')
|
||||
});
|
||||
|
||||
cm.getColumnByField('betriebsmittel_id').component.updateDefinition({
|
||||
title: this.$p.t('ui', 'betriebsmittel_id')
|
||||
});
|
||||
cm.getColumnByField('betriebsmittelperson_id').component.updateDefinition({
|
||||
title: this.$p.t('ui', 'betriebsmittelperson_id')
|
||||
});
|
||||
cm.getColumnByField('person_id').component.updateDefinition({
|
||||
title: this.$p.t('person', 'person_id')
|
||||
});
|
||||
cm.getColumnByField('uid').component.updateDefinition({
|
||||
title: this.$p.t('person', 'uid')
|
||||
});
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -171,7 +225,9 @@ export default {
|
||||
.then(result => result
|
||||
? betriebsmittelperson_id
|
||||
: Promise.reject({handled: true}))
|
||||
.then(this.endpoint.deleteBetriebsmittel)
|
||||
.then(betriebsmittelperson_id => this.$api.call(
|
||||
this.endpoint.deleteBetriebsmittel(betriebsmittelperson_id))
|
||||
)
|
||||
.then(result => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
|
||||
window.scrollTo(0, 0);
|
||||
@@ -184,8 +240,8 @@ export default {
|
||||
this.formData.uid = this.uid;
|
||||
if (this.formData.betriebsmitteltyp == 'Inventar')
|
||||
this.formData.betriebsmittel_id = this.formData.inventarData?.betriebsmittel_id;
|
||||
return this.endpoint
|
||||
.addNewBetriebsmittel(this.$refs.betriebsmittelData, this.id, this.formData)
|
||||
return this.$refs.betriebsmittelData
|
||||
.call(this.endpoint.addNewBetriebsmittel(this.id, this.formData))
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
|
||||
this.$refs.betriebsmittelModal.hide();
|
||||
@@ -198,8 +254,11 @@ export default {
|
||||
updateBetriebsmittel(betriebsmittelperson_id) {
|
||||
if (this.formData.betriebsmitteltyp == 'Inventar')
|
||||
this.formData.betriebsmittel_id = this.formData.inventarData?.betriebsmittel_id;
|
||||
return this.endpoint
|
||||
.updateBetriebsmittel(this.$refs.betriebsmittelData, betriebsmittelperson_id, this.formData)
|
||||
return this.$refs.betriebsmittelData
|
||||
.call(this.endpoint.updateBetriebsmittel(
|
||||
betriebsmittelperson_id,
|
||||
this.formData
|
||||
))
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
|
||||
this.$refs.betriebsmittelModal.hide();
|
||||
@@ -212,8 +271,8 @@ export default {
|
||||
loadBetriebsmittel(betriebsmittelperson_id) {
|
||||
this.resetModal();
|
||||
this.statusNew = false;
|
||||
return this.endpoint
|
||||
.loadBetriebsmittel(betriebsmittelperson_id)
|
||||
return this.$api
|
||||
.call(this.endpoint.loadBetriebsmittel(betriebsmittelperson_id))
|
||||
.then(result => {
|
||||
this.formData = result.data;
|
||||
})
|
||||
@@ -221,8 +280,8 @@ export default {
|
||||
},
|
||||
searchInventar(event) {
|
||||
const encodedQuery = encodeURIComponent(event.query);
|
||||
return this.endpoint
|
||||
.loadInventarliste(encodedQuery)
|
||||
return this.$api
|
||||
.call(this.endpoint.loadInventarliste(encodedQuery))
|
||||
.then(result => {
|
||||
this.filteredInventar = result.data;
|
||||
});
|
||||
@@ -244,8 +303,8 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
return this.endpoint
|
||||
.getTypenBetriebsmittel()
|
||||
return this.$api
|
||||
.call(this.endpoint.getTypenBetriebsmittel(this.betriebsmittelTypes))
|
||||
.then(result => {
|
||||
this.listBetriebsmitteltyp = result.data;
|
||||
})
|
||||
@@ -260,8 +319,9 @@ export default {
|
||||
table-only
|
||||
:side-menu="false"
|
||||
reload
|
||||
:reload-btn-infotext="this.$p.t('table', 'reload')"
|
||||
new-btn-show
|
||||
new-btn-label="Betriebsmittel"
|
||||
:new-btn-label="this.$p.t('ui', 'betriebsmittel')"
|
||||
@click:new="actionNewBetriebsmittel"
|
||||
>
|
||||
</core-filter-cmpt>
|
||||
@@ -278,7 +338,7 @@ export default {
|
||||
<div class="row mb-3">
|
||||
<form-input
|
||||
type="select"
|
||||
:label="$p.t('global/typ')"
|
||||
:label="$p.t('global/typ') + ' *'"
|
||||
name="betriebsmitteltyp"
|
||||
v-model="formData.betriebsmitteltyp"
|
||||
:disabled="!statusNew"
|
||||
@@ -376,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"
|
||||
@@ -391,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"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BsModal from './Modal.js';
|
||||
|
||||
export default {
|
||||
name: 'BootstrapAlert',
|
||||
components: {
|
||||
BsModal
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import BsAlert from './Alert.js';
|
||||
|
||||
export default {
|
||||
name: 'BootstrapConfirm',
|
||||
mixins: [
|
||||
BsAlert
|
||||
],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Phrasen from '../../plugin/Phrasen.js';
|
||||
//import Phrasen from '../../plugin/Phrasen.js';
|
||||
|
||||
export default {
|
||||
name: 'BootstrapModal',
|
||||
data: () => ({
|
||||
modal: null
|
||||
modal: null,
|
||||
fullscreen: false
|
||||
}),
|
||||
props: {
|
||||
backdrop: {
|
||||
@@ -21,7 +23,23 @@ export default {
|
||||
default: true
|
||||
},
|
||||
noCloseBtn: Boolean,
|
||||
dialogClass: [String,Array,Object]
|
||||
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: ''
|
||||
},
|
||||
allowFullscreenExpand: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
"hideBsModal",
|
||||
@@ -45,17 +63,18 @@ export default {
|
||||
},
|
||||
toggle() {
|
||||
return this.modal.toggle();
|
||||
},
|
||||
toggleFullscreen() {
|
||||
this.fullscreen = !this.fullscreen
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if(this.$refs.modal)
|
||||
{
|
||||
if (this.$refs.modal)
|
||||
this.modal = new bootstrap.Modal(this.$refs.modal, {
|
||||
backdrop: this.backdrop,
|
||||
focus: this.focus,
|
||||
keyboard: this.keyboard
|
||||
});
|
||||
}
|
||||
},
|
||||
popup(body, options, title, footer) {
|
||||
const BsModal = this,
|
||||
@@ -66,8 +85,16 @@ export default {
|
||||
slots.title = () => title;
|
||||
if (footer !== undefined)
|
||||
slots.footer = () => footer;
|
||||
|
||||
// little hack to check whether primevue is included in the app or not
|
||||
let includedPrimevue = false;
|
||||
if(typeof primevue !== 'undefined'){
|
||||
includedPrimevue = true;
|
||||
}
|
||||
|
||||
return new Promise((resolve,reject) => {
|
||||
const instance = Vue.createApp({
|
||||
name: 'ModalTmpApp',
|
||||
setup() {
|
||||
return () => Vue.h(BsModal, {...{
|
||||
class: 'fade'
|
||||
@@ -78,6 +105,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.$refs.modal.show();
|
||||
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.$refs.modal)
|
||||
@@ -88,22 +116,38 @@ export default {
|
||||
}
|
||||
});
|
||||
const wrapper = document.createElement("div");
|
||||
instance.use(Phrasen); // TODO(chris): find a more dynamic way
|
||||
instance.mount(wrapper);
|
||||
document.body.appendChild(wrapper);
|
||||
|
||||
// if(primevue) --> won't work because primevue is not defined in this scope and promise would be rejected
|
||||
if (includedPrimevue){
|
||||
instance.use(primevue.config.default, {zIndex: {overlay: 9999}})
|
||||
}
|
||||
|
||||
//instance.use(Phrasen); // TODO(chris): find a more dynamic way
|
||||
import('../../plugins/Phrasen.js').then((Phrasen) => {
|
||||
instance.use(Phrasen.default);
|
||||
instance.mount(wrapper);
|
||||
document.body.appendChild(wrapper);
|
||||
});
|
||||
});
|
||||
},
|
||||
template: `<div ref="modal" class="bootstrap-modal modal" tabindex="-1" @[\`hide.bs.modal\`]="$emit('hideBsModal', $event)" @[\`hidden.bs.modal\`]="$emit('hiddenBsModal', $event)" @[\`hidePrevented.bs.modal\`]="$emit('hidePreventedBsModal', $event)" @[\`show.bs.modal\`]="$emit('showBsModal', $event)" @[\`shown.bs.modal\`]="$emit('shownBsModal', $event)">
|
||||
<div class="modal-dialog" :class="dialogClass">
|
||||
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')" @[\`shown.bs.modal\`]="$emit('shownBsModal')">
|
||||
<div class="modal-dialog" :class="fullscreen ? 'modal-fullscreen' : 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>
|
||||
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
<div class="d-flex align-items-center ms-auto">
|
||||
<button type="button" class="btn ms-auto" style="filter: invert(1)" v-if="allowFullscreenExpand" @click="toggleFullscreen">
|
||||
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
|
||||
<i v-else class="fa-solid fa-compress"></i>
|
||||
</button>
|
||||
<button v-if="!noCloseBtn" type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<slot name="modal-header-content"></slot>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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,7 +1,7 @@
|
||||
|
||||
import BsAlert from './Alert.js';
|
||||
|
||||
export default {
|
||||
name: 'BootstrapPrompt',
|
||||
mixins: [
|
||||
BsAlert
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
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() {
|
||||
if (this.currentDate.locale != this.focusDate.locale) {
|
||||
this.focusDate = this.currentDate;
|
||||
this.$emit('update:range', this.range);
|
||||
} else {
|
||||
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,108 @@
|
||||
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() {
|
||||
if (this.currentDate.locale != this.focusDate.locale) {
|
||||
this.focusDate = this.currentDate;
|
||||
this.$emit('update:range', this.range);
|
||||
} else {
|
||||
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>
|
||||
`
|
||||
}
|
||||
@@ -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>`
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
export const FhcChart = {
|
||||
name: 'FhcChart',
|
||||
props: {
|
||||
chartOptions: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div style="width:100%;height:100%;overflow:auto">
|
||||
<div role="group" aria-label="Chart Modus">
|
||||
<Button :class="(chartOptions.chart.type === 'pie' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='pie'"><i class="fa-solid fa-chart-pie"></i></Button>
|
||||
<Button :class="(chartOptions.chart.type === 'bar' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='bar'"><i class="fa-solid fa-chart-bar"></i></Button>
|
||||
<Button :class="(chartOptions.chart.type === 'column' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='column'"><i class="fa-solid fa-chart-simple"></i></Button>
|
||||
<Button :class="(chartOptions.chart.type === 'line' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='line'"><i class="fa-solid fa-chart-line"></i></Button>
|
||||
</div>
|
||||
<figure>
|
||||
<highcharts class="chart" :options="chartOptions"></highcharts>
|
||||
</figure>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
export default FhcChart
|
||||
@@ -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;
|
||||
@@ -0,0 +1,103 @@
|
||||
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 {
|
||||
name: "ContentComponent",
|
||||
props: {
|
||||
content_id: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
},
|
||||
sichtbar: {
|
||||
type: [String, Number],
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
raum_contentmittitel,
|
||||
news_content,
|
||||
general,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
content_id_internal: this.content_id
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
fetchContent(){
|
||||
return this.$api
|
||||
.call(ApiCms.content(this.content_id_internal, this.version, this.sprache, this.sichtbar))
|
||||
.then(res => {
|
||||
this.content = res.data.content;
|
||||
this.content_type = res.data.type;
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
sprache: function(sprache){
|
||||
this.fetchContent();
|
||||
},
|
||||
'$route.params.content_id'(newVal) {
|
||||
this.content_id_internal = newVal
|
||||
this.fetchContent();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sprache(){
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
computeContentType: function () {
|
||||
switch (this.content_type) {
|
||||
case "raum_contentmittitel":
|
||||
return "raum_contentmittitel";
|
||||
case "news":
|
||||
return "news_content";
|
||||
default:
|
||||
return "general";
|
||||
};
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchContent();
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
template: /*html*/ `
|
||||
<!-- div that contains the content -->
|
||||
<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>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import RaumContent from "./Content_types/Raum_contentmittitel.js";
|
||||
|
||||
import ApiCms from '../../../api/factory/cms.js';
|
||||
|
||||
export default {
|
||||
|
||||
|
||||
mixins:[BsModal],
|
||||
|
||||
components:{
|
||||
BsModal,
|
||||
RaumContent,
|
||||
},
|
||||
props:{
|
||||
content_id:{
|
||||
type: Number
|
||||
},
|
||||
ort_kurzbz:{
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
result: false,
|
||||
content: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods:{
|
||||
modalHidden: function(){
|
||||
// reseting the content of the modal
|
||||
this.content = null;
|
||||
},
|
||||
// this method is always called when the modal is shown
|
||||
modalShown: function(){
|
||||
|
||||
if (this.content_id) {
|
||||
this.$api
|
||||
.call(ApiCms.content(this.content_id))
|
||||
.then(res => {
|
||||
this.content = res.data.content;
|
||||
this.type = res.data.type;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted(){
|
||||
this.modal = this.$refs.modalContainer;
|
||||
|
||||
},
|
||||
|
||||
template:/*html*/`
|
||||
<bs-modal @hideBsModal="modalHidden" dialogClass="modal-xl" @showBsModal="modalShown" ref="modalContainer">
|
||||
<template #title>
|
||||
<span v-if="ort_kurzbz">{{ort_kurzbz}}</span>
|
||||
<span v-else>Raum Informationen</span>
|
||||
</template>
|
||||
<template #default>
|
||||
<RaumContent v-if="content" :content="content" :content_id="content_id"></RaumContent>
|
||||
<div v-else>Der Content für diesen Raum konnte nicht geladen werden</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</template>
|
||||
</bs-modal>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
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');
|
||||
})
|
||||
},
|
||||
prepareContent() {
|
||||
// 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)";
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
this.prepareContent();
|
||||
},
|
||||
mounted(){
|
||||
this.prepareContent();
|
||||
},
|
||||
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>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
|
||||
export default {
|
||||
name: "NewsContentType",
|
||||
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>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
|
||||
export default {
|
||||
name: "RaumComponent",
|
||||
data() {
|
||||
return {
|
||||
imgContent: null
|
||||
}
|
||||
},
|
||||
props:{
|
||||
content:{
|
||||
type:String,
|
||||
required:true,
|
||||
},
|
||||
content_id:{
|
||||
type: [Number, String],
|
||||
}
|
||||
},
|
||||
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 = document.getElementsByClassName("tablesorter");
|
||||
|
||||
for (let table of tables) {
|
||||
this.sanitizeLegacyTables(table)
|
||||
new Tabulator(table, {
|
||||
layout: "fitDataStretch",
|
||||
|
||||
columnDefaults: {
|
||||
formatter: "html",
|
||||
resizable: false,
|
||||
minWidth: "100px",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let title = document.getElementsByTagName("h1");
|
||||
title = title.length ? title[0] : null;
|
||||
// tries to wrap the Raum titel with a link tag that redirects to the Reservierungen of that Raum
|
||||
if (title && title.innerText)
|
||||
{
|
||||
let room_name = title.innerText;
|
||||
let room_name_reg_exp = new RegExp("\\w*\\s([a-zA-Z][0-9\\.]+)$");
|
||||
let room_name_reg_exp_result = room_name.match(room_name_reg_exp);
|
||||
if(room_name_reg_exp_result)
|
||||
{
|
||||
room_name = room_name_reg_exp_result[0];
|
||||
room_name = room_name.replace(" ","_");
|
||||
let link_element = document.createElement("a");
|
||||
link_element.href = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/CisVue/Cms/getRoomInformation/" + room_name;
|
||||
link_element.appendChild(title.cloneNode(true));
|
||||
title.replaceWith(link_element);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error(`the regular expression did not match the room name: ${room_name}`);
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(`<div>${this.content}</div>`, "text/html");
|
||||
|
||||
const img = doc.querySelector("img")
|
||||
if(img && img.title)
|
||||
{
|
||||
const imgAttributes = {}
|
||||
for (let attr of img.attributes) {
|
||||
imgAttributes[attr.name] = attr.value
|
||||
}
|
||||
|
||||
this.imgContent = imgAttributes
|
||||
}
|
||||
|
||||
console.error(`was not able to get the title of the raum_contentmittitel`);
|
||||
|
||||
},
|
||||
template: /*html*/ `
|
||||
<!-- div that contains the content -->
|
||||
<!-- TODO: test with more img content from cms-->
|
||||
<div v-if="imgContent"><img v-bind="imgContent"/></div>
|
||||
<div v-html="content" v-else-if="content" ></div>
|
||||
<p v-else>Content was not found</p>
|
||||
`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import Pagination from "../../Pagination/Pagination.js";
|
||||
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
|
||||
import BsConfirm from "../../Bootstrap/Confirm.js";
|
||||
|
||||
import ApiCms from '../../../api/factory/cms.js';
|
||||
|
||||
export default {
|
||||
name: "NewsComponent",
|
||||
components: {
|
||||
Pagination,
|
||||
StudiengangInformation,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
content: null,
|
||||
maxPageCount: 0,
|
||||
page_size: 10,
|
||||
page:1,
|
||||
};
|
||||
},
|
||||
watch:{
|
||||
'$p.user_language.value':function(sprache){
|
||||
this.fetchNews();
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
sprache: function(){
|
||||
return this.$p.user_language.value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchNews() {
|
||||
return this.$api
|
||||
.call(ApiCms.getNews(this.page, this.page_size, this.sprache))
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.content = result;
|
||||
|
||||
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
|
||||
);
|
||||
});
|
||||
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) {
|
||||
this.$api
|
||||
.call(ApiCms.getNews(data.page, data.rows))
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.content = result;
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchNews();
|
||||
|
||||
this.$api
|
||||
.call(ApiCms.getNewsRowCount())
|
||||
.then(res => res.data)
|
||||
.then(result => {
|
||||
this.maxPageCount = result;
|
||||
});
|
||||
},
|
||||
template: /*html*/ `
|
||||
<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 mt-4">
|
||||
<div class="row">
|
||||
<div class="col" v-html="content">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div style="width:15rem">
|
||||
<studiengang-information></studiengang-information>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<pagination v-show="content?true:false" :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
|
||||
</pagination>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,122 @@
|
||||
import StudiengangPerson from "./StudiengangPerson.js";
|
||||
import StudiengangVertretung from "./StudiengangVertretung.js";
|
||||
|
||||
import ApiStudiengang from '../../../../api/factory/studiengang.js';
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return{
|
||||
studiengang:null,
|
||||
semester: null,
|
||||
stg_ltg: null,
|
||||
gf_ltg: null,
|
||||
stv_ltg: null,
|
||||
ass: null,
|
||||
hochschulvertr: null,
|
||||
stdv: null,
|
||||
jahrgangsvertr: null,
|
||||
}
|
||||
},
|
||||
props:{
|
||||
displayWidget:{
|
||||
type:Boolean,
|
||||
default:false,
|
||||
}
|
||||
},
|
||||
components:{
|
||||
StudiengangPerson,
|
||||
StudiengangVertretung,
|
||||
},
|
||||
template:/*html*/`
|
||||
<div id="fhc-studiengang-informationen">
|
||||
<template v-if="studiengang?.bezeichnung && semester">
|
||||
<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="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>
|
||||
<span class="mb-1">{{semester}}</span>
|
||||
</div>
|
||||
<div class="zusatzinfo" v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-for="{title, collection} in collection_array">
|
||||
<template v-if="Array.isArray(collection) && collection.length !==0">
|
||||
<h2 class="h5 text-truncate">{{title}}</h2>
|
||||
<template v-if="displayWidget">
|
||||
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
|
||||
<template v-for="person in collection">
|
||||
<studiengang-person displayWidget v-bind="person"></studiengang-person>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="person in collection">
|
||||
<div class="mb-3">
|
||||
<studiengang-person v-bind="person"></studiengang-person>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
|
||||
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
|
||||
</template>
|
||||
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
|
||||
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
`,
|
||||
computed:{
|
||||
// this function concatenates the studiengangsleitung and the assistenz or the
|
||||
// geschaeftsfuehrende-Stellvertretende Leitung if both collections only contain one person
|
||||
collection_array: function(){
|
||||
let returnData = [];
|
||||
|
||||
if (Array.isArray(this.stg_ltg) && this.stg_ltg.length == 1 && Array.isArray(this.ass) && this.ass.length == 1)
|
||||
{
|
||||
returnData.push({ title: `${this.$p.t('global', 'studiengangsleitung')}/${this.$p.t('studiengangInformation', 'assistenz')}` , collection: [...this.stg_ltg, ...this.ass] });
|
||||
}
|
||||
else
|
||||
{
|
||||
returnData.push({ title: this.$p.t('global', 'studiengangsleitung'), collection: this.stg_ltg });
|
||||
returnData.push({ title: this.$p.t('studiengangInformation', 'assistenz'), collection: this.ass });
|
||||
}
|
||||
if (Array.isArray(this.gf_ltg) && this.gf_ltg.length == 1 && Array.isArray(this.stv_ltg) && this.stv_ltg.length == 1)
|
||||
{
|
||||
returnData.push({ title: this.$p.t('studiengangInformation', 'geschaeftsfuehrende_stellvertretende_leitung'), collection: [...this.gf_ltg, ...this.stv_ltg] });
|
||||
}
|
||||
else
|
||||
{
|
||||
returnData.push({ title: this.$p.t('studiengangInformation', 'geschaeftsfuehrende_leitung'), collection: this.gf_ltg });
|
||||
returnData.push({ title: this.$p.t('studiengangInformation', 'stellvertretende_leitung'), collection: this.stv_ltg });
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
moodleLink: function(){
|
||||
// early return if the studiengang information is not available
|
||||
if(!this.studiengang || !this.studiengang.studiengang_kz) return;
|
||||
|
||||
return `https://moodle.technikum-wien.at/course/view.php?idnumber=dl` + this.studiengang.studiengang_kz;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$api
|
||||
.call(ApiStudiengang.studiengangInformation())
|
||||
.then(res => res.data)
|
||||
.then(studiengangInformationen => {
|
||||
Object.assign(this, studiengangInformationen);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
export default {
|
||||
props:{
|
||||
uid:String,
|
||||
vorname:String,
|
||||
nachname:String,
|
||||
titelpre:String,
|
||||
kontakt:String,
|
||||
telefoneklappe:String,
|
||||
email:String,
|
||||
planbezeichnung:String,
|
||||
foto:String,
|
||||
displayWidget:{
|
||||
type:Boolean,
|
||||
default:false,
|
||||
}
|
||||
},
|
||||
template:/*html*/`
|
||||
<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 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 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>
|
||||
`,
|
||||
computed:{
|
||||
formattedEmail: function(){
|
||||
if(!this.email ) return null;
|
||||
let emailString= this.email.replace("mailto:", "");
|
||||
// when splitting a string, the letter that is used to split the string will be removed from the result
|
||||
let emailArray = emailString.split('@');
|
||||
// returns both parts of the splitted string in combination with the removed letter and a word break
|
||||
return emailArray[0] + '@<wbr>' + emailArray[1];
|
||||
},
|
||||
fullname: function () {
|
||||
if (this.titelpre && this.vorname && this.nachname) {
|
||||
return `${this.titelpre} ${this.vorname} ${this.nachname}`;
|
||||
}
|
||||
else if (this.vorname && this.nachname) {
|
||||
return `${this.vorname} ${this.nachname}`;
|
||||
}
|
||||
else if (this.nachname) {
|
||||
return this.vorname;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
phone: function () {
|
||||
if (this.kontakt && this.telefoneklappe) {
|
||||
return {
|
||||
link: "tel:".concat(this.kontakt).concat(" " + this.telefoneklappe),
|
||||
number: this.kontakt.concat(" " + this.telefoneklappe),
|
||||
}
|
||||
}
|
||||
else {
|
||||
return this.kontakt ? {
|
||||
link: "tel:".concat(this.kontakt),
|
||||
number: this.kontakt,
|
||||
} : null;
|
||||
}
|
||||
},
|
||||
email_link: function () {
|
||||
return this.email ? "mailto:".concat(this.email) : null;
|
||||
},
|
||||
base64Image:function(){
|
||||
return this.foto ? 'data:image/png;base64,'.concat(this.foto) : null;
|
||||
},
|
||||
ort:function(){
|
||||
return this.planbezeichnung ?? null;
|
||||
},
|
||||
profilViewLink: function(){
|
||||
return this.uid ? FHC_JS_DATA_STORAGE_OBJECT.app_root.concat(FHC_JS_DATA_STORAGE_OBJECT.ci_router).concat("/Cis/Profil/View/").concat(this.uid): null;
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export default {
|
||||
props: {
|
||||
title:String,
|
||||
vertretungsList:Array,
|
||||
showBezeichnung:Boolean,
|
||||
},
|
||||
template:/*html*/`
|
||||
<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)" :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>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
methods: {
|
||||
profilViewLink: function (uid) {
|
||||
return uid ? FHC_JS_DATA_STORAGE_OBJECT.app_root.concat(FHC_JS_DATA_STORAGE_OBJECT.ci_router).concat("/Cis/Profil/View/").concat(uid) : null;
|
||||
},
|
||||
vertretungFormatedName: function (vertretung) {
|
||||
if (!vertretung) return null;
|
||||
return `${vertretung.vorname ?? ''} ${vertretung.nachname ?? ''} ${vertretung.bezeichnung && this.showBezeichnung ? '('.concat(vertretung.bezeichnung.replace("(", "").replace(")", "")).concat(")") : ''}`
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
@@ -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>`
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
import CisMenuEntry from "./Menu/Entry.js";
|
||||
import FhcSearchbar from "../searchbar/searchbar.js";
|
||||
import CisSprachen from "./Sprachen.js"
|
||||
import ThemeSwitch from "./ThemeSwitch.js";
|
||||
import ApiCisMenu from '../../api/factory/cis/menu.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CisMenuEntry,
|
||||
FhcSearchbar,
|
||||
CisSprachen,
|
||||
ThemeSwitch,
|
||||
},
|
||||
props: {
|
||||
rootUrl: String,
|
||||
logoUrl: String,
|
||||
avatarUrl: String,
|
||||
logoutUrl: String,
|
||||
selectedtypes: Array,
|
||||
searchbaroptions: Object,
|
||||
searchfunction: Function
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
entries: [],
|
||||
activeEntry:null,
|
||||
url:null,
|
||||
urlMatchRankings:[],
|
||||
navUserDropdown:null,
|
||||
menuOpen:true,
|
||||
};
|
||||
},
|
||||
provide(){
|
||||
return{
|
||||
setActiveEntry: this.setActiveEntry,
|
||||
addUrlCount: this.addUrlCount,
|
||||
makeParentContentActive: this.makeParentContentActive,
|
||||
}
|
||||
},
|
||||
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);
|
||||
|
||||
if(this.urlMatchRankings.length > 0){
|
||||
// if more than one entry has the same ranking, none should be active
|
||||
return this.urlMatchRankings.filter((value)=>value == highestMatch).length > 1 ? null : highestMatch;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
site_url(){
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchMenu() {
|
||||
return this.$api
|
||||
.call(ApiCisMenu.getMenu())
|
||||
.then(res => res.data)
|
||||
.then(menu => {
|
||||
this.entries = menu;
|
||||
});
|
||||
},
|
||||
checkSettingsVisibility: function (event) {
|
||||
// hides the settings collapsible if the user clicks somewhere else
|
||||
if (!this.$refs.navUserDropdown.contains(event.target)) {
|
||||
this.navUserDropdown.hide();
|
||||
}
|
||||
},
|
||||
handleShowNavUser(){
|
||||
document.addEventListener("click", this.checkSettingsVisibility);
|
||||
},
|
||||
handleHideNavUser(){
|
||||
document.removeEventListener("click", this.checkSettingsVisibility);
|
||||
},
|
||||
makeParentContentActive(content_id, collection=this.entries, parent=null){
|
||||
if(!collection) return;
|
||||
if (typeof collection == 'object' && !Array.isArray(collection) && Object.entries(collection).length > 0) {
|
||||
collection = Object.values(collection);
|
||||
}
|
||||
for(let entry of collection){
|
||||
if(entry.content_id == content_id){
|
||||
this.activeEntry = parent;
|
||||
}
|
||||
this.makeParentContentActive(content_id, entry.childs, entry.content_id);
|
||||
}
|
||||
|
||||
},
|
||||
addUrlCount(count){
|
||||
this.urlMatchRankings.push(count);
|
||||
},
|
||||
|
||||
setActiveEntry(content_id){
|
||||
this.activeEntry = content_id;
|
||||
},
|
||||
},
|
||||
created(){
|
||||
this.fetchMenu();
|
||||
},
|
||||
mounted(){
|
||||
this.$p.loadCategory(['ui', 'global', 'profilUpdate'])
|
||||
this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown,{
|
||||
toggle: false
|
||||
});
|
||||
},
|
||||
template: /*html*/`
|
||||
<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" 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" :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><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><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" 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 ">
|
||||
<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">
|
||||
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
|
||||
<div>
|
||||
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>`
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
export default {
|
||||
name: 'CisMenuEntry',
|
||||
props: {
|
||||
entry: Object,
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
activeContent: [String, Number],
|
||||
highestMatchingUrlCount: Number,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
collapse: null,
|
||||
urlCount:0,
|
||||
}
|
||||
},
|
||||
inject: ['makeParentContentActive', 'setActiveEntry','addUrlCount'],
|
||||
watch:{
|
||||
highestMatchingUrlCount: function(newValue)
|
||||
{
|
||||
// if this entry has the most matching url parts then it should be active
|
||||
if (this.activeContent == null && newValue == this.urlCount)
|
||||
{
|
||||
this.setActiveEntry(this.entry.content_id);
|
||||
}
|
||||
},
|
||||
activeContent: function(newValue){
|
||||
if(newValue == this.entry.content_id){
|
||||
// wenn der Menupunkt nicht bereits offen ist
|
||||
if (!this.entry.menu_open){
|
||||
this.entry.menu_open = true;
|
||||
}
|
||||
|
||||
}else{
|
||||
if (this.searchRecursiveChild(this.entry, 'content_id',newValue)) {
|
||||
this.entry.menu_open = true;
|
||||
} else {
|
||||
this.entry.menu_open = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
'entry.menu_open': function (newValue,oldValue) {
|
||||
if (newValue)
|
||||
{
|
||||
// only invokes .show if this.collapse is not null
|
||||
this.collapse && this.collapse.show();
|
||||
}
|
||||
else
|
||||
{
|
||||
// only invokes .hide if this.collapse is not null
|
||||
this.collapse && this.collapse.hide();
|
||||
if (this.activeContent == this.entry.content_id)
|
||||
{
|
||||
this.makeParentContentActive(this.entry.content_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
active: function () {
|
||||
if (this.entry.menu_open){
|
||||
return true;
|
||||
}
|
||||
else if (this.activeContent) {
|
||||
return this.activeContent == this.entry.content_id;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
link() {
|
||||
if (this.entry.template_kurzbz == 'redirect') {
|
||||
if (!this.entry.content)
|
||||
return '';
|
||||
let xmlDoc = (new DOMParser()).parseFromString(this.entry.content,"text/xml");
|
||||
let url = xmlDoc.getElementsByTagName('url')[0];
|
||||
|
||||
if (!url)
|
||||
return '';
|
||||
// TODO(chris): replace get params
|
||||
url = url.childNodes[0].nodeValue + "";
|
||||
if (url.includes("../cms/news.php")) {
|
||||
let news_regex = new RegExp("^\.\./cms/news\.php");
|
||||
url = url.replace(news_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/news');
|
||||
}
|
||||
else if (url.includes("../cms/content.php?")) {
|
||||
let content_regex = new RegExp("^\.\./cms/content.php\\?content_id=([0-9]+)");
|
||||
let content_regex_result = content_regex.exec(url);
|
||||
// content_regex_result[1] will be the first matched group
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + content_regex_result[1];
|
||||
}
|
||||
else if(url.includes("../index.ci.php")){
|
||||
let index_regex = new RegExp("^\.\./index\.ci\.php");
|
||||
url = url.replace(index_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router);
|
||||
}
|
||||
else if (url.includes("../")) {
|
||||
let relative_regex = new RegExp("^\.\./");
|
||||
url = url.replace(relative_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + this.entry.content_id;
|
||||
},
|
||||
hasFullLink() {
|
||||
return this.link.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
|
||||
},
|
||||
target() {
|
||||
if (this.entry.template_kurzbz == 'redirect') {
|
||||
if (!this.entry.content)
|
||||
return '';
|
||||
let xmlDoc = (new DOMParser()).parseFromString(this.entry.content,"text/xml");
|
||||
let target = xmlDoc.getElementsByTagName('target')[0];
|
||||
if (!target)
|
||||
return '';
|
||||
|
||||
target = target.childNodes[0].nodeValue + "";
|
||||
if (target == 'content' || target == '_self')
|
||||
target = "";
|
||||
return target;
|
||||
}
|
||||
return ''
|
||||
},
|
||||
hasChilds() {
|
||||
return this.entry.childs && this.entry.childs.length !== 0;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getUrlMatchPoints(url,link){
|
||||
let splitted_link = link.split('/');
|
||||
let splitted_url = url.href.split('/');
|
||||
|
||||
let count = 0;
|
||||
|
||||
for(let part_url of splitted_url)
|
||||
{
|
||||
for (let part_link of splitted_link)
|
||||
{
|
||||
if(part_url == part_link)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.urlCount = count;
|
||||
this.addUrlCount(count);
|
||||
},
|
||||
checkActiveUrl(url){
|
||||
this.getUrlMatchPoints(url,this.link);
|
||||
|
||||
let url_hash_spaceSymbol_regex = new RegExp("%20","gi");
|
||||
let url_hash_sharpSymbol_regex = new RegExp("^#");
|
||||
let url_hash = url.hash;
|
||||
url_hash = url_hash.replace(url_hash_spaceSymbol_regex, " ").replace(url_hash_sharpSymbol_regex,"");
|
||||
|
||||
// if the url hash contains the titel of the menu
|
||||
// or if the url equals the link of a menu
|
||||
// then set the menu active
|
||||
if (url_hash == this.entry.titel || url.href == this.link) {
|
||||
this.setActiveEntry(this.entry.content_id);
|
||||
}
|
||||
},
|
||||
// searches the childs of an entry recursively based on the value of a property
|
||||
searchRecursiveChild(entry,property,value){
|
||||
if (typeof entry.childs == 'object' && !Array.isArray(entry.childs) && Object.entries(entry.childs).length > 0){
|
||||
entry.childs = Object.values(entry.childs);
|
||||
}
|
||||
for (let child of entry.childs) {
|
||||
if (child[property] == value) {
|
||||
return true;
|
||||
}
|
||||
if ((child.childs instanceof Array && child.childs.length > 0) || Object.values(child.childs).length > 0) {
|
||||
if (this.searchRecursiveChild(child, property, value)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
toggleCollapse(evt) {
|
||||
if (this.active)
|
||||
{
|
||||
this.makeParentContentActive(this.entry.content_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.setActiveEntry(this.entry.content_id);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.children) {
|
||||
if (this.entry.menu_open)
|
||||
this.$refs.children.className += ' show';
|
||||
this.collapse = new bootstrap.Collapse(this.$refs.children, { toggle: false });
|
||||
}
|
||||
|
||||
this.checkActiveUrl(new URL(window.location.href));
|
||||
},
|
||||
template: /*html*/`
|
||||
<div v-if="entry.template_kurzbz == 'include'">
|
||||
INCLUDE
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-if="hasChilds">
|
||||
<div class="btn-group w-100">
|
||||
<a :target="target"
|
||||
:href="(entry.menu_open && hasFullLink)?link:null"
|
||||
@click="toggleCollapse"
|
||||
:class="{
|
||||
'btn btn-default rounded-0 text-start': true,
|
||||
['btn-level-' + level]: true,
|
||||
'fw-bold':active
|
||||
}">
|
||||
{{ entry.titel }}
|
||||
</a>
|
||||
<button @click.prevent="toggleCollapse" :aria-expanded="entry.menu_open"
|
||||
:class="{
|
||||
'btn btn-default rounded-0 dropdown-toggle dropdown-toggle-split flex-grow-0': true,
|
||||
collapsed: !entry.menu_open
|
||||
}">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
</div>
|
||||
<ul ref="children"
|
||||
class="nav w-100 collapse">
|
||||
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
|
||||
</ul>
|
||||
</template>
|
||||
<a v-else
|
||||
:href="link"
|
||||
:target="target"
|
||||
:class="{
|
||||
'btn btn-default rounded-0 w-100 text-start': true,
|
||||
['btn-level-' + level]: true,
|
||||
'fw-bold':active
|
||||
}"
|
||||
@mouseup="setActiveEntry(entry.content_id)">
|
||||
{{ entry.titel }}
|
||||
</a>
|
||||
</template>`
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import { numberPadding, formatDate } from "../../../helpers/DateHelpers.js"
|
||||
|
||||
export default {
|
||||
props: {
|
||||
event: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
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.start) return 'N/A';
|
||||
if (!this.event.start instanceof Date) {
|
||||
return this.event.start;
|
||||
}
|
||||
return numberPadding(this.event.start.getHours()) + ":" + numberPadding(this.event.start.getMinutes());
|
||||
},
|
||||
end_time: function () {
|
||||
if (!this.event.end) return 'N/A';
|
||||
if (!this.event.end instanceof Date) {
|
||||
return this.event.end;
|
||||
}
|
||||
return numberPadding(this.event.end.getHours()) + ":" + numberPadding(this.event.end.getMinutes());
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
mehtodNumberPadding: function (number) {
|
||||
return numberPadding(number);
|
||||
},
|
||||
methodFormatDate: function (d) {
|
||||
return formatDate(d);
|
||||
},
|
||||
},
|
||||
template:/*html*/`
|
||||
<table class="table table-hover mb-4">
|
||||
<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>
|
||||
`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
export default {
|
||||
props:{
|
||||
menu:{
|
||||
type:Array,
|
||||
default:null,
|
||||
},
|
||||
containerStyles: Array,
|
||||
rowStyles: Array,
|
||||
hasLvPlanEintraege: {
|
||||
required:false,
|
||||
default:true,
|
||||
type:Boolean,
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
|
||||
}
|
||||
},
|
||||
methods:{
|
||||
c4_disabled: function (menuItem) {
|
||||
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;
|
||||
},
|
||||
c4_link(menuItem) {
|
||||
if (!menuItem) return null;
|
||||
if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return menuItem.c4_link ?? null;
|
||||
}
|
||||
},
|
||||
getMenuName(menuItem) {
|
||||
if(menuItem.phrase) {
|
||||
return this.$p.t(menuItem.phrase)
|
||||
} else {
|
||||
return menuItem.name
|
||||
}
|
||||
}
|
||||
},
|
||||
template:/*html*/`
|
||||
<div v-if="!menu">{{$p.t('lehre','lehrveranstaltungsUnavailable')}}</div>
|
||||
<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="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 class="w-100 mt-2 mb-0">{{ getMenuName(menuItem) }}</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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import LvMenu from "./LvMenu.js";
|
||||
|
||||
import ApiAddons from '../../../api/factory/addons.js';
|
||||
|
||||
export default {
|
||||
|
||||
props:{
|
||||
event:{
|
||||
type:Object,
|
||||
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,
|
||||
required: false,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
result: false,
|
||||
menu: [],
|
||||
isMenuSelected:false,
|
||||
hasLvPlanEintraege: true,
|
||||
lvEvaluierungMessage: "",
|
||||
}
|
||||
},
|
||||
mixins:[BsModal],
|
||||
components:{
|
||||
BsModal,
|
||||
LvMenu,
|
||||
},
|
||||
inject: ["studium_studiensemester"],
|
||||
methods:{
|
||||
|
||||
hiddenModal: function(){
|
||||
this.isMenuSelected = false;
|
||||
},
|
||||
showModal: function(){
|
||||
if (!this.preselectedMenu) {
|
||||
this.$api
|
||||
.call(ApiAddons.getLvMenu(this.event.lehrveranstaltung_id, (this.studiensemester ?? this.event.studiensemester_kurzbz)))
|
||||
.then(res => {
|
||||
if (res.data) {
|
||||
this.menu = res.data;
|
||||
}
|
||||
});
|
||||
} 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(){
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../../../api/factory/lvPlan.js';
|
||||
|
||||
export const DEFAULT_MODE_RAUMINFO = 'Week'
|
||||
|
||||
export default {
|
||||
name: "RoomInformation",
|
||||
components: {
|
||||
FhcCalendar
|
||||
},
|
||||
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;
|
||||
}
|
||||
},
|
||||
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: "RoomInformation",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
ort_kurzbz: this.propsViewData.ort_kurzbz
|
||||
}
|
||||
});
|
||||
},
|
||||
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*/`
|
||||
<div class="fhc-roominformation d-flex flex-column h-100">
|
||||
<h2>{{ $p.t('rauminfo/rauminfo') }} {{ propsViewData.ort_kurzbz }}</h2>
|
||||
<hr>
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
class="responsive-calendar"
|
||||
></fhc-calendar>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import MylvSemesterStudiengang from "./Semester/Studiengang.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MylvSemesterStudiengang
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
studien_semester: Vue.computed(() => this.semester)
|
||||
}
|
||||
},
|
||||
props: {
|
||||
semester: [String, Number],
|
||||
lvs: Array
|
||||
},
|
||||
computed: {
|
||||
ready() { return this.lvs !== null; },
|
||||
studiengaenge() {
|
||||
return [... new Map(
|
||||
this.lvs
|
||||
.map(lv => [
|
||||
lv.studiengang_kz + '#' + lv.semester,
|
||||
{
|
||||
studiengang_kz: lv.studiengang_kz,
|
||||
bezeichnung: lv.sg_bezeichnung,
|
||||
sg_bezeichnung_eng: lv.sg_bezeichnung_eng,
|
||||
kuerzel: lv.studiengang_kuerzel,
|
||||
semester: lv.semester
|
||||
}
|
||||
])
|
||||
).values()].sort((a, b) => a.bezeichnung.toLowerCase() == b.bezeichnung.toLowerCase() ? a.semester > b.semester : a.bezeichnung.toLowerCase() > b.bezeichnung.toLowerCase());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
lvsForStudiengang(studiengang) {
|
||||
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
|
||||
}
|
||||
},
|
||||
template: `<div class="mylv-semester" v-if="ready">
|
||||
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)"/>
|
||||
</div>
|
||||
<div class="mylv-semester text-center" v-else>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
|
||||
import Phrasen from "../../../../mixins/Phrasen.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MylvSemesterStudiengangLv
|
||||
},
|
||||
mixins: [
|
||||
Phrasen
|
||||
],
|
||||
props: {
|
||||
bezeichnung: String,
|
||||
kuerzel: String,
|
||||
semester: [String,Number],
|
||||
lvs: Array,
|
||||
sg_bezeichnung_eng: String
|
||||
},
|
||||
computed: {
|
||||
lehrveranstaltungen() {
|
||||
return [... new Map(
|
||||
this.lvs
|
||||
.map(lv => [
|
||||
lv.lehrveranstaltung_id,
|
||||
lv
|
||||
])
|
||||
).values()]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
note(lv) {
|
||||
return lv.benotung ? lv.znote || lv.lvnote || null : null;
|
||||
}
|
||||
},
|
||||
template: `<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<div v-for="lv in lehrveranstaltungen" :key="lv.lehrveranstaltung_id" class="col-12 col-md-6 col-xl-4 col-xxl-3 mb-3">
|
||||
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
import LvPruefungen from "./Lv/Pruefungen.js";
|
||||
import LvInfo from "./Lv/Info.js";
|
||||
import Phrasen from "../../../../../mixins/Phrasen.js";
|
||||
import LvUebersicht from "../../LvUebersicht.js";
|
||||
|
||||
import ApiLehre from '../../../../../api/factory/lehre.js';
|
||||
import ApiAddons from '../../../../../api/factory/addons.js';
|
||||
|
||||
// TODO(chris): L10n
|
||||
|
||||
export default {
|
||||
components:{
|
||||
LvUebersicht,
|
||||
},
|
||||
mixins: [
|
||||
Phrasen
|
||||
],
|
||||
inject: ['studien_semester'],
|
||||
props: {
|
||||
lehrveranstaltung_id: Number,
|
||||
bezeichnung: String,
|
||||
bezeichnung_eng: String,
|
||||
module: String,
|
||||
farbe: String,
|
||||
lvinfo: Boolean,
|
||||
benotung: Boolean,
|
||||
lvnote: String,
|
||||
lvnotebez: Array,
|
||||
znote: String,
|
||||
znotebez: Array,
|
||||
studiengang_kuerzel: String,
|
||||
semester: [String, Number],
|
||||
orgform_kurzbz: String,
|
||||
sprache: String,
|
||||
ects: String,
|
||||
incoming: Number,
|
||||
positiv: Boolean,
|
||||
note_index: String
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
pruefungenData: null,
|
||||
info: null,
|
||||
menu: null,
|
||||
preselectedMenuItem: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
gradeColor() {
|
||||
// early return if value is null or undefined
|
||||
if (this.positiv == null) return;
|
||||
// returns a suitable color for the given grade
|
||||
if (this.positiv)
|
||||
{
|
||||
return 'var(--fhc-success)';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'var(--fhc-danger)';
|
||||
}
|
||||
},
|
||||
is_organisatorische_einheit(){
|
||||
return this.menu == "organisatorische_einheit";
|
||||
},
|
||||
emptyMenu(){
|
||||
return !this.menu || !Array.isArray(this.menu) || Array.isArray(this.menu) && this.menu.length == 0;
|
||||
},
|
||||
bodyStyle() {return {};
|
||||
/*const bodyStyle = {};
|
||||
if (this.farbe)
|
||||
bodyStyle['background-color'] = '#' + this.farbe;
|
||||
return bodyStyle;*/
|
||||
},
|
||||
grade() {
|
||||
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;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
fetchMenu(lehrveranstaltung_id = this.lehrveranstaltung_id, studien_semester = this.studien_semester) {
|
||||
return this.$api
|
||||
.call(ApiAddons.getLvMenu(lehrveranstaltung_id, studien_semester))
|
||||
.then(res => {
|
||||
this.menu = res.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$fhcAlert.handleSystemError(error);
|
||||
this.menu = [];
|
||||
});
|
||||
},
|
||||
|
||||
c4_link(menuItem) {
|
||||
if (!menuItem) return null;
|
||||
if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) {
|
||||
return '#';
|
||||
}
|
||||
else {
|
||||
return menuItem.c4_link ?? null;
|
||||
}
|
||||
},
|
||||
openLvOption(menuItem){
|
||||
if (menuItem.id == "core_menu_mailanstudierende"){
|
||||
window.location.href = menuItem.c4_link;
|
||||
} else if (menuItem.id == "core_menu_digitale_anwesenheitslisten") {
|
||||
window.location.href = menuItem.c4_link;
|
||||
} else{
|
||||
this.preselectedMenuItem = menuItem;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.lvUebersicht.show();
|
||||
});
|
||||
}
|
||||
},
|
||||
openPruefungen() {
|
||||
// early return if the pruefungenData is empty or not set
|
||||
if (!this.LvHasPruefungenInformation) return;
|
||||
|
||||
LvPruefungen.popup({
|
||||
pruefungenData: this.pruefungenData,
|
||||
bezeichnung: this.bezeichnung
|
||||
});
|
||||
},
|
||||
openInfos() {
|
||||
if (!this.info) {
|
||||
this.info = true;
|
||||
// TODO(chris): load all this params on ajax?
|
||||
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,
|
||||
orgform_kurzbz: this.orgform_kurzbz,
|
||||
sprache: this.sprache,
|
||||
ects: this.ects,
|
||||
incoming: this.incoming
|
||||
}).then(() => this.info = false).catch(() => this.info = false);
|
||||
}
|
||||
}
|
||||
},
|
||||
watch:{
|
||||
studien_semester(newValue){
|
||||
this.fetchMenu(this.lehrveranstaltung_id, newValue);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiLehre.getStudentPruefungen(this.lehrveranstaltung_id))
|
||||
.then(res => res.data)
|
||||
.then(pruefungen => {
|
||||
this.pruefungenData = pruefungen;
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.fetchMenu(this.lehrveranstaltung_id, this.studien_semester);
|
||||
},
|
||||
template: /*html*/`<div class="mylv-semester-studiengang-lv card">
|
||||
<lv-uebersicht ref="lvUebersicht" :preselectedMenu="preselectedMenuItem" :event="{
|
||||
lehrveranstaltung_id: lehrveranstaltung_id,
|
||||
studiensemester_kurzbz:studien_semester,
|
||||
lehrfach_bez:studien_semester,
|
||||
stg_kurzbzlang:studien_semester,
|
||||
}"/>
|
||||
|
||||
<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">{{$p.user_language.value === 'English' ? bezeichnung_eng : bezeichnung}}</h6>
|
||||
</div>
|
||||
<div v-if="!emptyMenu" class="card-body " :style="bodyStyle">
|
||||
<template v-if="menu">
|
||||
<ul class="list-group border-top-0 border-bottom-0 rounded-0">
|
||||
<li :type="menuItem.c4_link ? 'button' : null" v-for="menuItem in menu" class="list-group-item border-0 " >
|
||||
<div class="d-flex flex-row" :data-bs-toggle="menuItem.c4_moodle_links?.length ? 'dropdown' : null">
|
||||
<div class="mx-4">
|
||||
<i :class="[menuItem.c4_icon2 ? menuItem.c4_icon2 : 'fa-solid fa-pen-to-square', !menuItem.c4_link ? 'unavailable' : null ]"></i>
|
||||
</div>
|
||||
<a
|
||||
class="fhc-body text-decoration-none text-truncate"
|
||||
:id="'moodle_links_'+lehrveranstaltung_id"
|
||||
: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}}
|
||||
</a>
|
||||
</div>
|
||||
<ul v-if="menuItem.c4_moodle_links?.length" class="dropdown-menu p-0" :aria-labelledby="'moodle_links_'+lehrveranstaltung_id">
|
||||
<li v-for="item in menuItem.c4_moodle_links"><a class="dropdown-item border-bottom" :href="item.url">{{item.lehrform}}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-center d-flex justify-content-center align-items-center h-100" >
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="!emptyMenu" class="card-footer">
|
||||
<div class="row">
|
||||
<!-- template for the LV if there are multiple pruefungen -->
|
||||
<template v-if="LvHasPruefungenInformation">
|
||||
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
|
||||
<i class="fa fa-check text-success" v-if="positiv"></i>
|
||||
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<!-- template for the LV with no pruefungen -->
|
||||
<template v-else>
|
||||
<span class="col-auto text-start text-decoration-none" >
|
||||
<i class="fa fa-check text-success" v-if="positiv"></i>
|
||||
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import ApiLehre from '../../../../../../api/factory/lehre.js';
|
||||
|
||||
const infos = {};
|
||||
|
||||
export default {
|
||||
props:{
|
||||
studien_semester: String,
|
||||
lehrveranstaltung_id: Number,
|
||||
},
|
||||
data: () => ({
|
||||
bezeichnung: null,
|
||||
studiengang_kuerzel: null,
|
||||
semester: null,
|
||||
orgform_kurzbz: null,
|
||||
sprache: null,
|
||||
ects: null,
|
||||
incoming: null,
|
||||
result: true,
|
||||
info: null,
|
||||
}),
|
||||
computed: {
|
||||
lektorNamesLinks(){
|
||||
let lektorenLinks = {};
|
||||
this.info.lektoren.forEach(e => {
|
||||
let name = ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim();
|
||||
lektorenLinks[name] = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + `/Cis/Profil/View/${e.uid}`;
|
||||
});
|
||||
return lektorenLinks;
|
||||
},
|
||||
lektorNames(){
|
||||
return this.info.lektoren.map((e)=>((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
|
||||
},
|
||||
lvLeitung() {
|
||||
return this.info.lvLeitung && this.info.lvLeitung.length ? this.info.lvLeitung.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim()) : null;
|
||||
},
|
||||
oe() {
|
||||
return this.info.oe.organisationseinheittyp ? (this.info.oe.organisationseinheittyp + ' ' + this.info.oe.bezeichnung) : '';
|
||||
},
|
||||
oeLeitung() {
|
||||
if (!this.info.oeLeitung || !this.info.oeLeitung.length)
|
||||
return ['-'];
|
||||
return this.info.oeLeitung.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
|
||||
},
|
||||
koordinator() {
|
||||
if (!this.info.koordinator || !this.info.koordinator.length)
|
||||
return null;
|
||||
return this.info.koordinator.map(e => ((e.titelpre || '') + ' ' + (e.vorname || '') + ' ' + (e.nachname || '') + ' ' + (e.titelpost || '')).trim());
|
||||
},
|
||||
currentLang() {
|
||||
if (!this.info)
|
||||
return null;
|
||||
if (this.info.lastLang)
|
||||
return this.info.lastLang;
|
||||
if (!this.info.lvinfo)
|
||||
return null;
|
||||
return this.info.lvinfoDefaultLang && this.info.lvinfo[this.info.lvinfoDefaultLang] ? this.info.lvinfoDefaultLang : Object.keys(this.info.lvinfo).shift();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$api.call(ApiLehre.getLvInfo(this.studien_semester, this.lehrveranstaltung_id))
|
||||
.then(
|
||||
res => res.data
|
||||
).then(data =>{
|
||||
Object.assign(this,
|
||||
{
|
||||
bezeichnung : data.bezeichnung,
|
||||
studiengang_kuerzel: data.studiengang_kuerzel,
|
||||
semester: data.semester,
|
||||
orgform_kurzbz: data.orgform_kurzbz,
|
||||
sprache: data.sprache,
|
||||
ects: data.ects,
|
||||
incoming: data.incoming ?? '-',
|
||||
});
|
||||
})
|
||||
|
||||
if (infos[this.lehrveranstaltung_id]) {
|
||||
this.info = infos[this.lehrveranstaltung_id];
|
||||
} else {
|
||||
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Info/' + this.studien_semester + '/' + this.lehrveranstaltung_id).then(res => {
|
||||
this.info = infos[this.lehrveranstaltung_id] = res.data.retval || [];
|
||||
}).catch(() => this.info = {});
|
||||
}
|
||||
},
|
||||
template: /*html*/`
|
||||
<h1>{{$p.t('lvinfo/lehrveranstaltungsinformationen')}}</h1>
|
||||
<hr>
|
||||
<div v-if="!info" class="text-center">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<table v-else class="table table-hover mb-4">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/lehrveranstaltung')}}</th>
|
||||
<td>{{bezeichnung}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/studiengang')}}</th>
|
||||
<td>{{studiengang_kuerzel}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/semester')}}</th>
|
||||
<td>{{semester}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/studiensemester')}}</th>
|
||||
<td>{{studien_semester}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/organisationsform')}}</th>
|
||||
<td>{{orgform_kurzbz}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/lehrbeauftragter')}}</th>
|
||||
<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 fhc-primary-color" ></i></a>
|
||||
{{name}}
|
||||
</li>
|
||||
</ul>
|
||||
<template v-else>
|
||||
{{$p.t('lehre/keinLektorZugeordnet')}}
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="lvLeitung">
|
||||
<th>{{$p.t('lehre/lvleitung')}}</th>
|
||||
<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 fhc-primary-color" ></i></a>
|
||||
{{name}}
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('global/sprache')}}</th>
|
||||
<td>{{sprache}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/ects')}}</th>
|
||||
<td>{{ects}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/incomingplaetze')}}</th>
|
||||
<td>{{incoming}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{$p.t('lehre/organisationseinheit')}}</th>
|
||||
<td>
|
||||
{{oe}} <br>
|
||||
(
|
||||
<i>{{$p.t('global/leitung')}}: </i>{{oeLeitung.join(', ')}}
|
||||
<template v-if="koordinator">
|
||||
<i>{{$p.t('global/koordination')}}: </i>{{koordinator.join(', ')}}
|
||||
</template>
|
||||
)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div v-if="info && info.lvinfo">
|
||||
<div v-if="Object.keys(info.lvinfo).length > 1" class="text-end">
|
||||
<div class="btn-group" role="group" :title="$p.t('global/verfuegbareSprachen')" :aria-label="$p.t('global/verfuegbareSprachen')">
|
||||
<template v-for="lang in info.sprachen" :key="lang.index">
|
||||
<button v-if="info.lvinfo[lang.sprache]" type="button" class="btn btn-outline-primary" :class="lang.sprache == currentLang ? 'active' : ''" @click.prevent="info.lastLang = lang.sprache">{{lang.bezeichnung[lang.index-1]}}</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="i in info.lvinfo[currentLang]" :key="info">
|
||||
<h4>{{i.header}}</h4>
|
||||
<h6 v-if="i.subheader">{{i.subheader}}</h6>
|
||||
<ul v-if="Array.isArray(i.body)">
|
||||
<li v-for="e in i.body" :key="e">{{e}}</li>
|
||||
</ul>
|
||||
<p v-else>
|
||||
{{i.body}}
|
||||
</p>
|
||||
</template>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import BsModal from '../../../../../Bootstrap/Modal.js';
|
||||
|
||||
const pruefungen = {};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BsModal
|
||||
},
|
||||
mixins: [
|
||||
BsModal
|
||||
],
|
||||
props: {
|
||||
pruefungenData: Array|null,
|
||||
bezeichnung: String,
|
||||
/*
|
||||
* 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: () => ({
|
||||
result: true,
|
||||
}),
|
||||
mounted() {
|
||||
this.modal = this.$refs.modalContainer.modal;
|
||||
},
|
||||
popup(options) {
|
||||
return BsModal.popup.bind(this)(null, options);
|
||||
},
|
||||
template: `<bs-modal ref="modalContainer" class="bootstrap-alert" v-bind="$props" body-class="">
|
||||
<template v-slot:title>
|
||||
Prüfungen: {{bezeichnung}}
|
||||
</template>
|
||||
<template v-slot:default>
|
||||
<div v-if="!pruefungenData" class="text-center">
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>
|
||||
<p v-else-if="!pruefungenData.length" class="alert alert-info mb-0">
|
||||
Keine Prüfungen vorhanden!
|
||||
</p>
|
||||
<table v-else class="table table-hover">
|
||||
<thead>
|
||||
<td> </td>
|
||||
<td>Datum</td>
|
||||
<td class="text-end">Note</td>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="pruefung in pruefungenData" :key="pruefung.pruefung_id">
|
||||
<th>{{pruefung.pruefungstyp_kurzbz}}</th>
|
||||
<td>{{pruefung.datum}}</td>
|
||||
<td class="text-end">{{pruefung.note}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
</bs-modal>`
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import MylvSemester from "./Semester.js";
|
||||
import Phrasen from "../../../mixins/Phrasen.js";
|
||||
|
||||
// TODO(chris): phrase: global/studiensemester_auswaehlen
|
||||
// TODO(chris): phrase: next & prev +aria-label
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MylvSemester
|
||||
},
|
||||
mixins: [
|
||||
Phrasen
|
||||
],
|
||||
data: () => {
|
||||
return {
|
||||
firstLoad: true,
|
||||
studiensemester: null,
|
||||
lvs: {},
|
||||
currentSemester: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
ready() {
|
||||
return this.studiensemester !== null && (!this.firstLoad || this.current.lvs !== null);
|
||||
},
|
||||
current() {
|
||||
if (this.currentSemester === null)
|
||||
return { semester: null, lvs: [] };
|
||||
if (this.lvs[this.currentSemester] === undefined) {
|
||||
this.lvs[this.currentSemester] = {
|
||||
semester: this.currentSemester,
|
||||
lvs: null
|
||||
};
|
||||
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Lvs/' + this.currentSemester).then(res => {
|
||||
this.lvs[this.currentSemester].lvs = res.data.retval || [];
|
||||
this.firstLoad = false;
|
||||
});
|
||||
}
|
||||
return this.lvs[this.currentSemester];
|
||||
},
|
||||
nearestSem() {
|
||||
let now = Date.now();
|
||||
let nearestSem = null;
|
||||
let nearestSemDiff = 0;
|
||||
this.studiensemester.forEach(sem => {
|
||||
let start = new Date(sem.start);
|
||||
let end = new Date(sem.ende);
|
||||
if (now >= start && now <= end) {
|
||||
nearestSem = sem.studiensemester_kurzbz;
|
||||
nearestSemDiff = 0;
|
||||
return;
|
||||
}
|
||||
let diff = Math.min(Math.abs(now - start), Math.abs(now - end));
|
||||
if (nearestSem === null || diff < nearestSemDiff) {
|
||||
nearestSem = sem.studiensemester_kurzbz;
|
||||
nearestSemDiff = diff;
|
||||
}
|
||||
|
||||
});
|
||||
return nearestSem;
|
||||
},
|
||||
currentIsFirst() {
|
||||
return this.studiensemester[0].studiensemester_kurzbz == this.currentSemester;
|
||||
},
|
||||
currentIsLast() {
|
||||
return this.studiensemester[this.studiensemester.length-1].studiensemester_kurzbz == this.currentSemester;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevSem() {
|
||||
this.$refs.studiensemester.selectedIndex--;
|
||||
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
},
|
||||
nextSem() {
|
||||
this.$refs.studiensemester.selectedIndex++;
|
||||
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
},
|
||||
updateRouter(val) {
|
||||
this.$router.push(`/Cis/MyLv/${val}`);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Studiensemester').then(res => {
|
||||
this.studiensemester = res.data.retval || [];
|
||||
const routerStudiensemester = this.$route.params.studiensemester;
|
||||
if (routerStudiensemester && this.studiensemester.filter(s => s.studiensemester_kurzbz == routerStudiensemester).length)
|
||||
this.currentSemester = routerStudiensemester;
|
||||
else
|
||||
this.currentSemester = this.nearestSem;
|
||||
});
|
||||
},
|
||||
beforeRouteUpdate(to, from, next){
|
||||
if (to.params.studiensemester && this.studiensemester.filter(s => s.studiensemester_kurzbz == to.params.studiensemester).length && to.params.studiensemester != this.currentSemester)
|
||||
this.currentSemester = to.params.studiensemester;
|
||||
next();
|
||||
|
||||
},
|
||||
template: `
|
||||
|
||||
<h2>{{$p.t('lehre/myLV')}}</h2>
|
||||
<hr>
|
||||
<div class="mylv-student" v-if="ready">
|
||||
<div v-if="currentSemester" class="row justify-content-center mb-3">
|
||||
<div class="col-auto d-none">
|
||||
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="input-group">
|
||||
<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="updateRouter($event.target.value)">
|
||||
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
|
||||
</select>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger" role="alert" v-else>
|
||||
{{$p.t('lehre/noLvFound')}}
|
||||
</div>
|
||||
<mylv-semester v-bind="current"/>
|
||||
</div>
|
||||
<div class="mylv-student text-center" v-else>
|
||||
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,497 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import EditProfil from "./ProfilModal/EditProfil.js";
|
||||
import Adresse from "./ProfilComponents/Adresse.js";
|
||||
import Kontakt from "./ProfilComponents/Kontakt.js";
|
||||
import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import AusweisStatus from "./ProfilComponents/FhAusweisStatus.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
EditProfil,
|
||||
Adresse,
|
||||
Kontakt,
|
||||
FetchProfilUpdates,
|
||||
AusweisStatus,
|
||||
Mailverteiler,
|
||||
QuickLinks,
|
||||
ProfilEmails,
|
||||
RoleInformation,
|
||||
ProfilInformation,
|
||||
},
|
||||
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
editDataFilter: null,
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaProfilFunktionen",
|
||||
persistence: {
|
||||
columns: false
|
||||
},
|
||||
height: 300,
|
||||
layout: "fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
field: "collapse",
|
||||
headerSort: false,
|
||||
headerFilter: false,
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.bezeichnungPhrase),
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.organisationseinheitPhrase),
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase),
|
||||
field: "Gültig_von",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase),
|
||||
field: "Gültig_bis",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase),
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
betriebsmittel_table_options: {
|
||||
persistenceID: "filterTableMaProfilBetriebsmittel",
|
||||
persistence: {
|
||||
columns: false
|
||||
},
|
||||
height: 300,
|
||||
layout: "fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}],
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
"<i id='collapseIconBetriebsmittel' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
field: "collapse",
|
||||
headerSort: false,
|
||||
headerFilter: false,
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.inventarnummerPhrase),
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase),
|
||||
field: "Ausgegeben_am",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
data: Object,
|
||||
editData: Object,
|
||||
},
|
||||
|
||||
methods: {
|
||||
betriebsmittelTableBuilt: function () {
|
||||
this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
|
||||
},
|
||||
funktionenTableBuilt: function () {
|
||||
this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
|
||||
},
|
||||
hideEditProfilModal: function () {
|
||||
//? checks the editModal component property result, if the user made a successful request or not
|
||||
if (this.$refs.editModal.result) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.selectProfilRequest())
|
||||
.then((request) => {
|
||||
if (!request.error && request.data) {
|
||||
this.data.profilUpdates = request.data;
|
||||
this.data.profilUpdates.sort(this.sortProfilUpdates);
|
||||
} else {
|
||||
console.error("Error when fetching profile updates: " + request);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
// when modal was closed without submitting request
|
||||
}
|
||||
this.showModal = false;
|
||||
this.editDataFilter = null;
|
||||
},
|
||||
|
||||
showEditProfilModal(view) {
|
||||
if (view) {
|
||||
this.editDataFilter = view;
|
||||
}
|
||||
|
||||
this.showModal = true;
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.editModal.show();
|
||||
});
|
||||
|
||||
// after a state change, wait for the DOM updates to complete
|
||||
},
|
||||
|
||||
fetchProfilUpdates: function () {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.selectProfilRequest())
|
||||
.then((res) => {
|
||||
if (!res.error && res) {
|
||||
this.data.profilUpdates = res.data?.length
|
||||
? res.data.sort(this.sortProfilUpdates)
|
||||
: null;
|
||||
}
|
||||
});
|
||||
},
|
||||
setTableColumnTitles() { // reevaluates computed phrasen
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
getTelefonValue() {
|
||||
if(this.data.standort_telefon?.kontakt) {
|
||||
return this.data.standort_telefon.kontakt + " " + this.data.telefonklappe
|
||||
} else if(this.data.standort_telefon) {
|
||||
return this.data.standort_telefon + " " + this.data.telefonklappe
|
||||
} else {
|
||||
return this.data.telefonklappe
|
||||
}
|
||||
},
|
||||
filteredEditData() {
|
||||
return this.editDataFilter
|
||||
? this.editData.data[this.editDataFilter]
|
||||
: this.editData;
|
||||
},
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Vorname: this.data.vorname,
|
||||
Nachname: this.data.nachname,
|
||||
Username: this.data.username,
|
||||
Anrede: this.data.anrede,
|
||||
Titel: this.data.titel,
|
||||
Postnomen: this.data.postnomen,
|
||||
foto_sperre: this.data.foto_sperre,
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('profil','Kurzzeichen')}`,
|
||||
value: this.data.kurzbz
|
||||
},
|
||||
telefon: {
|
||||
label: `${this.$p.t('profil','Telefon')}`,
|
||||
value: this.getTelefonValue
|
||||
},
|
||||
office: {
|
||||
label: `${this.$p.t('profil','Büro')}`,
|
||||
value: this.data.ort_kurzbz
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
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);
|
||||
|
||||
},
|
||||
watch: {
|
||||
'data.funktionen'(newVal) {
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal);
|
||||
},
|
||||
'data.mittel'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal);
|
||||
},
|
||||
'language.value'(newVal) {
|
||||
this.setTableColumnTitles()
|
||||
}
|
||||
},
|
||||
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))" :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>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div v-if="isEditable" class="row mb-3 ">
|
||||
<div class="col">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- 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 ROW (HIDDEN IN VIEWPORTS GREATER THEN-EQUAL MD) -->
|
||||
<!-- 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 -->
|
||||
<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" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- MITARBEITER INFO -->
|
||||
<role-information :title="$p.t('profil','mitarbeiterInformation')" :data="roleInformation"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4 ">
|
||||
<div class="col">
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div @click="showEditProfilModal('Private_Kontakte')" class="col-auto" type="button">
|
||||
<i class="fa fa-edit"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span>{{$p.t('profil','privateKontakte')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body ">
|
||||
<div class="gy-3 row ">
|
||||
<div v-for="element in data.kontakte" class="col-12">
|
||||
<Kontakt :data="element"></Kontakt>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div @click="showEditProfilModal('Private_Adressen')" class="col-auto" type="button">
|
||||
<i class="fa fa-edit"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span>{{$p.t('profil','privateAdressen')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gy-3 row ">
|
||||
<div v-for="element in data.adressen" class="col-12">
|
||||
<Adresse :data="element"></Adresse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- FUNKTIONEN TABELLE -->
|
||||
<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>
|
||||
<div class="col-12 mb-4" >
|
||||
<!-- BETRIEBSMITTEL TABELLE -->
|
||||
<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>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block ">
|
||||
<div class="col mb-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
|
||||
<div class="col mb-3">
|
||||
<!-- PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3" >
|
||||
<div class="col-12">
|
||||
<!-- AUSWEIS STATUS -->
|
||||
<ausweis-status :data="data.zutrittsdatum"></ausweis-status>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- MAILVERTEILER -->
|
||||
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,264 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
Mailverteiler,
|
||||
QuickLinks,
|
||||
RoleInformation,
|
||||
ProfilEmails,
|
||||
ProfilInformation,
|
||||
},
|
||||
inject: ["collapseFunction", "language"],
|
||||
data() {
|
||||
return {
|
||||
collapseIconFunktionen: true,
|
||||
preloadedPhrasen:{},
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaViewProfilFunktionen",
|
||||
persistence: {
|
||||
columns: false
|
||||
},
|
||||
height: 300,
|
||||
layout: "fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
columns: [
|
||||
//? option when wanting to hide the collapsed list
|
||||
|
||||
{
|
||||
title:
|
||||
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
field: "collapse",
|
||||
headerSort: false,
|
||||
headerFilter: false,
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('ui/bezeichnung')),
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')),
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigVon')),
|
||||
field: "Gültig_von",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigBis')),
|
||||
field: "Gültig_bis",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/wochenstunden')),
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
//? this is the prop passed to the dynamic component with the custom data of the view
|
||||
props: ["data"],
|
||||
methods: {
|
||||
funktionenTableBuilt: function () {
|
||||
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'data.funktionen'(newVal) {
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal);
|
||||
},
|
||||
'language.value'(newVal) { // reevaluates computed phrasen
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
getTelefonValue() {
|
||||
if(this.data.standort_telefon?.kontakt) {
|
||||
return this.data.standort_telefon.kontakt + " " + this.data.telefonklappe
|
||||
} else if(this.data.standort_telefon) {
|
||||
return this.data.standort_telefon + " " + this.data.telefonklappe
|
||||
} else {
|
||||
return this.data.telefonklappe
|
||||
}
|
||||
},
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
|
||||
personEmails() {
|
||||
return this.data?.emails ? this.data.emails : [];
|
||||
},
|
||||
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Vorname: this.data.vorname,
|
||||
Nachname: this.data.nachname,
|
||||
Username: this.data.username,
|
||||
Anrede: this.data.anrede,
|
||||
Titel: this.data.titel,
|
||||
Postnomen: this.data.postnomen,
|
||||
foto_sperre: this.data.foto_sperre,
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('profil','Kurzzeichen')}`,
|
||||
value: this.data.kurzbz
|
||||
},
|
||||
telefon: {
|
||||
label: `${this.$p.t('profil','Telefon')}`,
|
||||
value: this.getTelefonValue
|
||||
},
|
||||
office: {
|
||||
label: `${this.$p.t('profil','Büro')}`,
|
||||
value: this.data.ort_kurzbz
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
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*/ `
|
||||
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- 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 -->
|
||||
<!-- 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 -->
|
||||
<div class="row mb-4">
|
||||
<!-- FIRST KAESTCHEN -->
|
||||
<div class="col-lg-12 col-xl-6 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- Profil Informationen -->
|
||||
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
<!-- END OF PROFIL INFORMATION ROW -->
|
||||
<!-- INFORMATION CONTENT END -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
|
||||
<div class="row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<!-- roleInformation -->
|
||||
<role-information :data="roleInformation" :title="$p.t('profil','mitarbeiterInformation')"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
|
||||
<!-- END OF THE SECOND INFORMATION COLUMN -->
|
||||
</div>
|
||||
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
|
||||
</div >
|
||||
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
|
||||
<div class="row">
|
||||
<!-- FIRST TABLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<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>
|
||||
<!-- END OF MAIN CONTENT COL -->
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!-- VISIBLE UNTIL VIEWPORT MD -->
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')" ></quick-links>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- MAILVERTEILER -->
|
||||
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER ROW-->
|
||||
</div>
|
||||
<!-- END OF CONTAINER -->
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,390 @@
|
||||
import StudentProfil from "./StudentProfil.js";
|
||||
import MitarbeiterProfil from "./MitarbeiterProfil.js";
|
||||
import ViewStudentProfil from "./StudentViewProfil.js";
|
||||
import ViewMitarbeiterProfil from "./MitarbeiterViewProfil.js";
|
||||
import Loading from "../../Loader.js";
|
||||
|
||||
import ApiProfil from '../../../api/factory/profil.js';
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
|
||||
Vue.$collapseFormatter = function (data) {
|
||||
//data - an array of objects containing the column title and value for each cell
|
||||
var container = document.createElement("div");
|
||||
container.classList.add("tabulator-collapsed-row");
|
||||
container.classList.add("text-break");
|
||||
|
||||
var list = document.createElement("div");
|
||||
list.classList.add("row");
|
||||
|
||||
container.appendChild(list);
|
||||
|
||||
data.forEach(function (col) {
|
||||
let item = document.createElement("div");
|
||||
item.classList.add("col-6");
|
||||
let item2 = document.createElement("div");
|
||||
item2.classList.add("col-6");
|
||||
|
||||
item.innerHTML = "<strong>" + col.title + "</strong>";
|
||||
item2.innerHTML = col.value ? col.value : "-";
|
||||
|
||||
list.appendChild(item);
|
||||
list.appendChild(item2);
|
||||
});
|
||||
|
||||
return Object.keys(data).length ? container : "";
|
||||
};
|
||||
|
||||
export const Profil = {
|
||||
name: 'Profil',
|
||||
components: {
|
||||
StudentProfil,
|
||||
MitarbeiterProfil,
|
||||
ViewStudentProfil,
|
||||
ViewMitarbeiterProfil,
|
||||
Loading,
|
||||
},
|
||||
props: {
|
||||
uid: {
|
||||
type: String,
|
||||
required:false,
|
||||
},
|
||||
viewData: {
|
||||
type: Object,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
//? loading property is used for showing/hiding the loading modal
|
||||
loading: false,
|
||||
profilUpdateStates: null,
|
||||
profilUpdateTopic: null,
|
||||
view: null,
|
||||
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
|
||||
),
|
||||
profilUpdateTopic: Vue.computed(() =>
|
||||
this.profilUpdateTopic ? this.profilUpdateTopic : false
|
||||
),
|
||||
setLoading: (newValue) => {
|
||||
this.loading = newValue;
|
||||
},
|
||||
getZustellkontakteCount: this.zustellKontakteCount,
|
||||
getZustelladressenCount: this.zustellAdressenCount,
|
||||
collapseFunction: (e, column) => {
|
||||
//* check if property doesn't exist already and add it to the reactive this properties
|
||||
if (this[e.target.id] === undefined) {
|
||||
this[e.target.id] = true;
|
||||
}
|
||||
this[e.target.id] = !this[e.target.id];
|
||||
|
||||
//* gets all event icons of the different rows to use the onClick event later
|
||||
let allClickableIcons = column._column.cells.map((row) => {
|
||||
return row.element.children[0];
|
||||
});
|
||||
|
||||
//* changes the icon that shows or hides all the collapsed columns
|
||||
//* if the replace function does not find the class to replace, it just simply returns false
|
||||
if (this[e.target.id]) {
|
||||
e.target.classList.replace("fa-angle-up", "fa-angle-down");
|
||||
} else {
|
||||
e.target.classList.replace("fa-angle-down", "fa-angle-up");
|
||||
}
|
||||
|
||||
//* changes the icon for every collapsed column to open or closed
|
||||
if (this[e.target.id]) {
|
||||
allClickableIcons
|
||||
.filter((column) => {
|
||||
return !column.classList.contains("open");
|
||||
})
|
||||
.forEach((col) => {
|
||||
col.click();
|
||||
});
|
||||
} else {
|
||||
allClickableIcons
|
||||
.filter((column) => {
|
||||
return column.classList.contains("open");
|
||||
})
|
||||
.forEach((col) => {
|
||||
col.click();
|
||||
});
|
||||
}
|
||||
},
|
||||
sortProfilUpdates: (ele1, ele2) => {
|
||||
let result = 0;
|
||||
if (ele1.status === "pending") {
|
||||
result = -1;
|
||||
} else if (ele1.status === "accepted") {
|
||||
result = ele2.status === "rejected" ? -1 : 1;
|
||||
} else {
|
||||
result = 1;
|
||||
}
|
||||
//? if they have the same status the insert date is used for ordering
|
||||
if (ele1.status === ele2.status) {
|
||||
result =
|
||||
new Date(ele2.insertamum.split(".").reverse().join("-")) -
|
||||
new Date(ele1.insertamum.split(".").reverse().join("-"));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
// fetch profilUpdateStates to provide them to children components
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.getStatus())
|
||||
.then((response) => {
|
||||
this.profilUpdateStates = response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.getTopic())
|
||||
.then((response) => {
|
||||
this.profilUpdateTopic = response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
|
||||
this.$api
|
||||
.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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let adressenArray = [];
|
||||
if (this.data.profilUpdates?.length) {
|
||||
adressenArray = adressenArray.concat(
|
||||
this.data.profilUpdates
|
||||
.filter((update) => {
|
||||
return update.requested_change.zustelladresse;
|
||||
})
|
||||
.map((adresse) => {
|
||||
return adresse.requested_change.adresse_id;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.data.profilUpdates?.length ||
|
||||
!this.data.adressen
|
||||
.filter((adresse) => adresse.zustelladresse)
|
||||
.every((adresse) =>
|
||||
this.data.profilUpdates.some(
|
||||
(update) =>
|
||||
update.requested_change.adresse_id == adresse.adresse_id
|
||||
)
|
||||
)
|
||||
) {
|
||||
adressenArray = adressenArray.concat(
|
||||
this.data.adressen
|
||||
.filter((adresse) => {
|
||||
return adresse.zustelladresse;
|
||||
})
|
||||
.map((adr) => {
|
||||
return adr.adresse_id;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(adressenArray)];
|
||||
|
||||
},
|
||||
zustellKontakteCount() {
|
||||
if (!this.data || !this.data.kontakte) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kontakteArray = [];
|
||||
|
||||
if (this.data.profilUpdates?.length) {
|
||||
kontakteArray = kontakteArray.concat(
|
||||
this.data.profilUpdates
|
||||
.filter((update) => {
|
||||
return update.requested_change.zustellung;
|
||||
})
|
||||
.map((kontant) => {
|
||||
return kontant.requested_change.kontakt_id;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.data.profilUpdates?.length ||
|
||||
!this.data.kontakte
|
||||
.filter((kontakt) => kontakt.zustellung)
|
||||
.every((kontakt) =>
|
||||
this.data.profilUpdates.some(
|
||||
(update) =>
|
||||
update.requested_change.kontakt_id == kontakt.kontakt_id
|
||||
)
|
||||
)
|
||||
) {
|
||||
kontakteArray = kontakteArray.concat(
|
||||
this.data.kontakte
|
||||
.filter((kontakt) => {
|
||||
return kontakt.zustellung;
|
||||
})
|
||||
.map((kon) => {
|
||||
return kon.kontakt_id;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return [...new Set(kontakteArray)];
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
||||
filteredEditData() {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
view: null,
|
||||
data: {
|
||||
Personen_Informationen: {
|
||||
title: this.$p.t("profil", "personenInformationen"),
|
||||
topic: "Personen_informationen",
|
||||
view: null,
|
||||
data: {
|
||||
vorname: {
|
||||
title: this.$p.t("person", "vorname"),
|
||||
topic: this.profilUpdateTopic?.["Vorname"],
|
||||
view: "TextInputDokument",
|
||||
withFiles: true,
|
||||
data: {
|
||||
titel: "vorname",
|
||||
value: this.data.vorname,
|
||||
},
|
||||
},
|
||||
nachname: {
|
||||
title: this.$p.t("person", "nachname"),
|
||||
topic: this.profilUpdateTopic?.["Nachname"],
|
||||
view: "TextInputDokument",
|
||||
withFiles: true,
|
||||
data: {
|
||||
titel: "nachname",
|
||||
value: this.data.nachname,
|
||||
},
|
||||
},
|
||||
titel: {
|
||||
title: this.$p.t("global", "titel"),
|
||||
topic: this.profilUpdateTopic?.["Titel"],
|
||||
view: "TextInputDokument",
|
||||
withFiles: true,
|
||||
data: {
|
||||
titel: "titel",
|
||||
value: this.data.titel,
|
||||
},
|
||||
},
|
||||
postnomen: {
|
||||
title: this.$p.t("profil", "postnomen"),
|
||||
topic: this.profilUpdateTopic?.["Postnomen"],
|
||||
view: "TextInputDokument",
|
||||
withFiles: true,
|
||||
data: {
|
||||
titel: "postnomen",
|
||||
value: this.data.postnomen,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Private_Kontakte: {
|
||||
title: this.$p.t("profil", "privateKontakte"),
|
||||
topic: this.profilUpdateTopic?.["Private Kontakte"],
|
||||
data: this.data.kontakte
|
||||
?.filter((item) => {
|
||||
// excludes all contacts that are already used in pending profil update requests
|
||||
return !this.data.profilUpdates?.some(
|
||||
(update) =>
|
||||
update.status === this.profilUpdateStates["Pending"] &&
|
||||
update.requested_change?.kontakt_id === item.kontakt_id
|
||||
);
|
||||
})
|
||||
.map((kontakt) => {
|
||||
return {
|
||||
listview: "Kontakt",
|
||||
view: "EditKontakt",
|
||||
data: kontakt,
|
||||
};
|
||||
}),
|
||||
},
|
||||
Private_Adressen: {
|
||||
title: this.$p.t("profil", "privateAdressen"),
|
||||
topic: this.profilUpdateTopic?.["Private Adressen"],
|
||||
data: this.data.adressen
|
||||
?.filter((item) => {
|
||||
return !this.data.profilUpdates?.some((update) => {
|
||||
return (
|
||||
update.status === this.profilUpdateStates["Pending"] &&
|
||||
update.requested_change?.adresse_id == item.adresse_id
|
||||
);
|
||||
});
|
||||
})
|
||||
.map((adresse) => {
|
||||
return {
|
||||
listview: "Adresse",
|
||||
view: "EditAdresse",
|
||||
data: adresse,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
loading: function (newValue) {
|
||||
if (newValue) {
|
||||
this.$refs.loadingModalRef.show();
|
||||
} else {
|
||||
this.$refs.loadingModalRef.hide();
|
||||
}
|
||||
},
|
||||
uid (newVal, oldVal) {
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div v-if="notFoundUID">
|
||||
<h3>Es wurde keine Person mit der UID {{this.notFoundUID}} gefunden</h3>
|
||||
</div>
|
||||
<div v-else>
|
||||
<loading ref="loadingModalRef" :timeout="0"></loading>
|
||||
<component :is="view" :data="data" :editData="filteredEditData" ></component>
|
||||
</div>
|
||||
</div>`,
|
||||
}
|
||||
|
||||
export default Profil
|
||||
@@ -0,0 +1,63 @@
|
||||
export default {
|
||||
props:{
|
||||
data:Object,
|
||||
view:String,
|
||||
withZustelladresse:{
|
||||
type:Boolean,
|
||||
default:false,
|
||||
},
|
||||
},
|
||||
data(){
|
||||
return{}
|
||||
},
|
||||
created(){
|
||||
|
||||
},
|
||||
template:/*html*/`
|
||||
|
||||
<div class="gy-2 row justify-content-center align-items-center">
|
||||
|
||||
<!-- column 1 in the address row -->
|
||||
<div class="col-1 text-center">
|
||||
<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">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','strasse')}}</div>
|
||||
<span class="form-underline-content">{{data.strasse}} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- column 2 in the address row -->
|
||||
<div class="offset-1 offset-sm-0 offset-xl-1 offset-xxl-0 order-2 order-sm-4 order-xl-2 order-xxl-4 col-11 col-sm-5 col-xl-11 col-xxl-5">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('global','typ')}}</div>
|
||||
<span class="form-underline-content">{{data.typ}} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="offset-1 order-3 order-sm-3 col-11 col-sm-6 col-xl-7 col-xxl-6">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','ort')}}</div>
|
||||
<span class="form-underline-content">{{data.ort}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" offset-1 offset-sm-0 order-4 order-sm-2 order-xl-4 order-xxl-2 col-11 col-sm-3 col-xl-4 col-xxl-3">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('person','plz')}}</div>
|
||||
<span class="form-underline-content">{{data.plz}} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="withZustelladresse" class="order-5 offset-1 col-11">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('person','zustelladresse')}}</div>
|
||||
<div class="ms-2 form-check ">
|
||||
<input class="form-check-input" type="checkbox" @click.prevent :checked="data.zustelladresse" >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
import EditProfil from "../ProfilModal/EditProfil.js";
|
||||
|
||||
import ApiProfil from '../../../../api/factory/profil.js';
|
||||
import ApiProfilUpdate from '../../../../api/factory/profilUpdate.js';
|
||||
|
||||
//? EditProfil is the modal used to edit the profil updates
|
||||
export default {
|
||||
components: {EditProfil},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
|
||||
inject: [
|
||||
"getZustellkontakteCount",
|
||||
"getZustelladressenCount",
|
||||
"profilUpdateStates",
|
||||
"profilUpdateTopic",
|
||||
],
|
||||
|
||||
emits: ["fetchUpdates"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
showUpdateModal: false,
|
||||
content: null,
|
||||
editProfilTitle: this.$p.t("profil", "profilBearbeiten"),
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
hideEditProfilModal: function () {
|
||||
//? checks the editModal component property result, if the user made a successful request or not
|
||||
if (this.$refs.updateEditModal.result) {
|
||||
this.$emit("fetchUpdates");
|
||||
} else {
|
||||
// when modal was closed without submitting request
|
||||
}
|
||||
this.showUpdateModal = false;
|
||||
},
|
||||
|
||||
async showEditProfilModal(updateRequest) {
|
||||
|
||||
let view = this.getView(updateRequest.topic, updateRequest.status);
|
||||
|
||||
let data = null;
|
||||
let content = null;
|
||||
let files = null;
|
||||
let withFiles = false;
|
||||
|
||||
if (view === "TextInputDokument") {
|
||||
data = {
|
||||
titel: updateRequest.topic,
|
||||
value: updateRequest.requested_change.value,
|
||||
};
|
||||
|
||||
const filesFromDatabase =
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.getProfilRequestFiles(
|
||||
updateRequest.profil_update_id
|
||||
))
|
||||
.then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
|
||||
files = filesFromDatabase;
|
||||
if (files) {
|
||||
withFiles = true;
|
||||
}
|
||||
} else {
|
||||
data = updateRequest.requested_change;
|
||||
}
|
||||
|
||||
content = {
|
||||
updateID: updateRequest.profil_update_id,
|
||||
view: view,
|
||||
data: data,
|
||||
withFiles: withFiles,
|
||||
topic: updateRequest.topic,
|
||||
files: files,
|
||||
};
|
||||
|
||||
if (view === "EditAdresse") {
|
||||
|
||||
const isMitarbeiter = await this.$api.call(ApiProfil.isMitarbeiter(updateRequest.uid)).then((res) => res.data);
|
||||
|
||||
if (isMitarbeiter) {
|
||||
content["isMitarbeiter"] = isMitarbeiter;
|
||||
}
|
||||
|
||||
const filesFromDatabase =
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.getProfilRequestFiles(
|
||||
updateRequest.profil_update_id
|
||||
))
|
||||
.then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
|
||||
files = filesFromDatabase;
|
||||
content["files"] = files;
|
||||
}
|
||||
|
||||
//? adds the status information if the profil update request was rejected or accepted
|
||||
if (updateRequest.status !== this.profilUpdateStates["Pending"]) {
|
||||
content["status"] = updateRequest.status;
|
||||
content["status_message"] = updateRequest.status_message;
|
||||
content["status_timestamp"] = updateRequest.status_timestamp;
|
||||
}
|
||||
|
||||
//? update data of the reactive content
|
||||
this.content = content;
|
||||
this.editProfilTitle = updateRequest.topic;
|
||||
|
||||
//? only show the popup if also the right content is available
|
||||
if (content) {
|
||||
this.showUpdateModal = true;
|
||||
// after a state change, wait for the DOM updates to complete
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.updateEditModal.show();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteRequest: function (item) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.deleteProfilRequest(item.profil_update_id))
|
||||
.then((res) => {
|
||||
if (res.data.error) {
|
||||
//? open alert
|
||||
console.error("error happened", res.data);
|
||||
} else {
|
||||
this.$emit("fetchUpdates");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getView: function (topic, status) {
|
||||
if (!(status === this.profilUpdateStates["Pending"])) {
|
||||
return "Status";
|
||||
}
|
||||
|
||||
switch (topic) {
|
||||
case this.profilUpdateTopic["Private Kontakte"]:
|
||||
return "EditKontakt";
|
||||
case this.profilUpdateTopic["Add Kontakt"]:
|
||||
return "EditKontakt";
|
||||
case this.profilUpdateTopic["Delete Kontakt"]:
|
||||
return "Kontakt";
|
||||
case this.profilUpdateTopic["Private Adressen"]:
|
||||
return "EditAdresse";
|
||||
case this.profilUpdateTopic["Add Adresse"]:
|
||||
return "EditAdresse";
|
||||
case this.profilUpdateTopic["Delete Adresse"]:
|
||||
return "Adresse";
|
||||
default:
|
||||
return "TextInputDokument";
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
created() {
|
||||
},
|
||||
|
||||
computed: {},
|
||||
|
||||
template: /*html*/ `
|
||||
<div class="card">
|
||||
<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">
|
||||
<table class="m-0 table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{$p.t('profilUpdate','topic')}}</th>
|
||||
<th scope="col">{{$p.t('global','status')}}</th>
|
||||
<th scope="col">{{$p.t('global','datum')}}</th>
|
||||
<th class="text-center" scope="col">{{$p.t('ui','aktion')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in data" :style="item.status==profilUpdateStates['Accepted']?'background-color:lightgreen':item.status===profilUpdateStates['Rejected']?'background-color:lightcoral':''">
|
||||
<td class="align-middle text-wrap ">{{item.topic}}</td>
|
||||
<td class="align-middle " >{{item.status}}</td>
|
||||
<td class="align-middle">{{item.status_timestamp?item.status_timestamp:item.insertamum}}</td>
|
||||
<template v-if="item.status === profilUpdateStates['Pending']">
|
||||
<td>
|
||||
<div class="d-flex flex-row justify-content-evenly">
|
||||
<template v-if="item.topic.toLowerCase().includes('delete')">
|
||||
<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 @click="showEditProfilModal(item)" role="button" class="fa fa-edit fhc-primary-color"></i></div>
|
||||
</template>
|
||||
<div class="align-middle text-center"><i role="button" @click="deleteRequest(item)" class="text-danger fa fa-trash"></i></div>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
<template v-else>
|
||||
<td class="align-middle text-center">
|
||||
<div class="d-flex flex-row justify-content-evenly">
|
||||
<i role="button" @click="showEditProfilModal(item)" class="fa fa-eye"></i>
|
||||
</div>
|
||||
</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
template: /*html*/`
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<span >{{$p.t('profil','fhAusweisStatus',[data])}}</span>
|
||||
</div>
|
||||
</div>`,
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
export default{
|
||||
props:{
|
||||
view:String,
|
||||
data:Object,
|
||||
},
|
||||
data(){
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
created(){
|
||||
|
||||
},
|
||||
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 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 -->
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('profil',data.kontakttyp.toUpperCase())}}</div>
|
||||
<a v-if="data.kontakt.includes('@')" role="link" :aria-disabled="view?true:false" :href="!view?('mailto:'+data.kontakt):null" class="form-underline-content">{{data.kontakt}} </a>
|
||||
<a v-else role="link" :aria-disabled="view?true:false" :href="!view?('tel:'+data.kontakt):null" class="form-underline-content">{{data.kontakt}} </a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data?.anmerkung" class="offset-1 offset-md-0 offset-xl-1 offset-xxl-0 order-2 order-sm-1 col-10 col-md-4 col-xl-9 col-xxl-4 ">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('global','anmerkung')}}</div>
|
||||
<span class="form-underline-content">{{data.anmerkung}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center col-1 col-sm-1 order-2 order-lg-1 col-xl-2 col-xxl-1 allign-middle">
|
||||
<i v-if="data.zustellung" class="fa-solid fa-check"></i>
|
||||
<i v-else="data.zustellung" class="fa-solid fa-xmark"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
props: {
|
||||
data: Object,
|
||||
title: { type: String },
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
created(){
|
||||
|
||||
},
|
||||
template: /*html*/`
|
||||
<template v-if="Array.isArray(data) && data.length >0">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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="col-1 ">
|
||||
<i class="fa-solid fa-envelope fhc-link-color" ></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<a class="fhc-link-color" :href="verteiler.mailto"><b>{{verteiler.gruppe_kurzbz}}</b></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-11 offset-1 ">{{verteiler.beschreibung}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>`,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
},
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="card ">
|
||||
<div class="card-header">
|
||||
{{title}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- HIER SIND DIE EMAILS -->
|
||||
<div class="gy-3 row justify-content-center ">
|
||||
<div v-for="email in data" class="col-12 ">
|
||||
<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>
|
||||
</div>`,
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import ApiProfil from '../../../../api/factory/profil.js';
|
||||
import ImageUpload from '../../Profil/ProfilModal/EditProfilComponents/ImageUpload.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
},
|
||||
fotoStatus:{
|
||||
type: Boolean,
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
this.$api
|
||||
.call(ApiProfil.fotoSperre(!this.FotoSperre))
|
||||
.then(res => {
|
||||
this.FotoSperre = res.data.foto_sperre;
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
get_image_base64_src: function () {
|
||||
if (!this.data.foto) {
|
||||
return "";
|
||||
}
|
||||
return "data:image/jpeg;base64," + this.data.foto;
|
||||
},
|
||||
name: function () {
|
||||
return {vorname: this.data.Vorname, nachname: this.data.Nachname};
|
||||
},
|
||||
profilInfo: function () {
|
||||
let res = {};
|
||||
let notIncludedProperties = [
|
||||
"Vorname",
|
||||
"Nachname",
|
||||
"foto_sperre",
|
||||
"foto",
|
||||
];
|
||||
Object.keys(this.data).forEach((key) => {
|
||||
if (!notIncludedProperties.includes(key)) {
|
||||
res[key] = this.data[key];
|
||||
}
|
||||
});
|
||||
return res;
|
||||
},
|
||||
},
|
||||
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="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">
|
||||
<span>{{title}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gy-3 row justify-content-center align-items-center">
|
||||
<!-- SQUEEZING THE IMAGE INSIDE THE FIRST INFORMATION COLUMN -->
|
||||
<!-- 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 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="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>
|
||||
<!-- END OF THE ROW WITH THE IMAGE -->
|
||||
</div>
|
||||
<!-- END OF SQUEEZE -->
|
||||
<!-- COLUMNS WITH MULTIPLE ROWS NEXT TO PROFIL PICTURE -->
|
||||
<div class="col-12 col-sm-6">
|
||||
<div class="row gy-4">
|
||||
<div class="col-12">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','vorname')}}</div>
|
||||
<span class="form-underline-content">{{name.vorname}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','nachname')}}</div>
|
||||
<span class="form-underline-content">{{name.nachname}} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(wert,bez) in profilInfo" class="col-md-6 col-sm-12">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('profil',bez)}}</div>
|
||||
<span class="form-underline-content">{{wert?wert:'-'}} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
export default {
|
||||
//TODO: To be implemented
|
||||
props: {
|
||||
data: {
|
||||
type: String,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
mobile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideCollapse: function () {
|
||||
this.collapseOpen = false;
|
||||
},
|
||||
showCollapse: function () {
|
||||
this.collapseOpen = true;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collapseOpen: false,
|
||||
};
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="card">
|
||||
<template v-if="mobile">
|
||||
<button class="btn btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#quickLinks" :aria-expanded="collapseOpen" aria-controls="quickLinks" >
|
||||
{{title}}
|
||||
<i class="fa " :class="collapseOpen?'fa-chevron-up':'fa-chevron-down'"></i>
|
||||
</button>
|
||||
<div @[\`show.bs.collapse\`]="collapseOpen=true;" @[\`hide.bs.collapse\`]="collapseOpen=false;" class="mt-1 collapse" id="quickLinks">
|
||||
<div class="list-group">
|
||||
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','zeitwuensche')}}</a>
|
||||
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','lehrveranstaltungen')}}</a>
|
||||
<a href="#" class="list-group-item list-group-item-action ">{{$p.t('profil','zeitsperren')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card-header">{{title}}</div>
|
||||
<div class="card-body">
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitwuensche')}}</a>
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','lehrveranstaltungen')}}</a>
|
||||
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitsperren')}}</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>`,
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
}
|
||||
},
|
||||
inject: [
|
||||
'studiengang_kz', // inject info that should not be displayed
|
||||
],
|
||||
computed: {
|
||||
getLinkGruppeListe() {
|
||||
return this.data.gruppe?.value && this.data.verband?.value && this.data.semester?.value ? FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/stud_in_grp.php?kz='+this.studiengang_kz+'&sem=' + this.data.semester.value
|
||||
+ '&verband=' + this.data.verband.value + '&grp=' + this.data.gruppe.value : ''
|
||||
},
|
||||
getLinkVerbandListe() {
|
||||
return this.data.verband?.value && this.data.semester?.value ? FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/stud_in_grp.php?kz='+this.studiengang_kz+'&sem=' + this.data.semester.value
|
||||
+ '&verband=' + this.data.verband.value : ''
|
||||
},
|
||||
getLinkSemesterListe() {
|
||||
return this.data.semester?.value ? FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/stud_in_grp.php?kz='+this.studiengang_kz+'&sem=' + this.data.semester.value : ''
|
||||
}
|
||||
},
|
||||
created() {
|
||||
//TODO: check if data.Telefon is a valid telefon number to call before using it as a tel: link
|
||||
},
|
||||
template: `
|
||||
<div class="card">
|
||||
<div class="card-header">{{title}}</div>
|
||||
<div class="card-body">
|
||||
<div class="gy-3 row">
|
||||
<div v-for="(entry, key) in data" class="col-md-6 col-sm-12 ">
|
||||
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{entry.label }}</div>
|
||||
|
||||
<!-- print Telefon link -->
|
||||
<a v-if="key == 'telefon'" :href="entry.value ?'tel:'+entry.value:null" :class="{'form-underline-content':true,'text-decoration-none':!entry.value,'text-body':!entry.value}">{{entry.value ?? '-'}}</a>
|
||||
|
||||
<!-- print semester link -->
|
||||
<span v-else-if="key == 'semester' && entry.value" class="form-underline-content">
|
||||
{{ entry.value }}
|
||||
<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 :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 :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>
|
||||
|
||||
<!-- else print information -->
|
||||
<span v-else class="form-underline-content">{{ entry.value ?? '-'}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
import BsModal from "../../../Bootstrap/Modal.js";
|
||||
import Alert from "../../../Bootstrap/Alert.js";
|
||||
import EditProfilSelect from "./EditProfilSelect.js";
|
||||
import Loader from "../../../Loader.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BsModal,
|
||||
Alert,
|
||||
EditProfilSelect,
|
||||
Loader,
|
||||
},
|
||||
mixins: [BsModal],
|
||||
props: {
|
||||
value: Object,
|
||||
titel: String,
|
||||
zustelladressenCount: Function,
|
||||
zustellkontakteCount: Function,
|
||||
/*
|
||||
* 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 {
|
||||
topic: null,
|
||||
profilUpdate: null,
|
||||
editData: this.value,
|
||||
fileID: null,
|
||||
breadcrumb: null,
|
||||
loading: false,
|
||||
result: false,
|
||||
info: null,
|
||||
};
|
||||
},
|
||||
inject: ["setLoading"],
|
||||
provide() {
|
||||
return {
|
||||
updateFileID: this.updateFileIDFunction,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateFileIDFunction: function (newFileID) {
|
||||
this.fileID = newFileID;
|
||||
},
|
||||
|
||||
handleFailedError: function (err) {
|
||||
console.error(err);
|
||||
this.loading = false;
|
||||
this.setLoading(false);
|
||||
this.result = false;
|
||||
this.hide();
|
||||
},
|
||||
|
||||
async submitProfilChange() {
|
||||
|
||||
//? check if data is valid before making a request
|
||||
if (this.topic && this.profilUpdate) {
|
||||
//? if profil update contains any attachment
|
||||
if (this.fileID) {
|
||||
const fileData = await this.uploadFiles(this.fileID);
|
||||
|
||||
this.fileID = fileData ? fileData : null;
|
||||
}
|
||||
|
||||
//? inserts new row in public.tbl_cis_profil_update
|
||||
//* calls the update api call if an update field is present in the data that was passed to the modal
|
||||
const handleApiResponse = (res) => {
|
||||
//? toggles the loading to false and closes the loading modal
|
||||
if (res.data.error) {
|
||||
this.result = false;
|
||||
Alert.popup(
|
||||
"Ein Fehler ist aufgetreten: " + JSON.stringify(res.data.retval)
|
||||
);
|
||||
} else {
|
||||
this.result = true;
|
||||
Alert.popup(
|
||||
"Ihre Anfrage wurde erfolgreich gesendet. Bitte warten Sie, während sich das Team um Ihre Anfrage kümmert."
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
//* v-show on EditProfil modal binded to this.loading
|
||||
//? hides the EditProfil modal and shows the loading modal by calling a callback that was passed as prop from the parent component
|
||||
this.loading = true;
|
||||
this.setLoading(true);
|
||||
|
||||
//? if an updateID is present, updateProfilRequest is called, else insertProfilRequest is called
|
||||
this.editData.updateID ?
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.updateProfilRequest(
|
||||
this.topic,
|
||||
this.profilUpdate,
|
||||
this.editData.updateID,
|
||||
this.fileID ? this.fileID[0] : null
|
||||
))
|
||||
.then((res) => {
|
||||
handleApiResponse(res);
|
||||
})
|
||||
.catch((err) => this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.setLoading(false);
|
||||
this.hide();
|
||||
})
|
||||
:
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.insertProfilRequest(
|
||||
this.topic,
|
||||
this.profilUpdate,
|
||||
this.fileID ? this.fileID[0] : null
|
||||
))
|
||||
.then((res) => {
|
||||
handleApiResponse(res);
|
||||
})
|
||||
.catch((err) => this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
this.setLoading(false);
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
uploadFiles: async function (files) {
|
||||
if (files[0].type !== "application/x.fhc-dms+json") {
|
||||
let formData = new FormData();
|
||||
formData.append("files[]", files[0]);
|
||||
const result = this.editData.updateID
|
||||
? //? updating old attachment by replacing
|
||||
//* second parameter of api request insertFile checks if the file has to be replaced or not
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.insertFile(
|
||||
formData,
|
||||
this.editData.updateID
|
||||
))
|
||||
.then((res) => {
|
||||
return res.data?.map((file) => file.dms_id);
|
||||
})
|
||||
: //? fresh insert of new attachment
|
||||
await this.$api
|
||||
.call(ApiProfilUpdate.insertFile(formData))
|
||||
.then((res) => {
|
||||
return res.data?.map((file) => file.dms_id);
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
//? attachment hasn't been replaced
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showFooter: function () {
|
||||
switch (this.value.view) {
|
||||
case 'Status':
|
||||
return false;
|
||||
case 'Kontakt':
|
||||
return false;
|
||||
case 'Adresse':
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.editData.topic) {
|
||||
//? if the topic was passed through the prop add it to the component
|
||||
this.topic = this.editData.topic;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.modal = this.$refs.modalContainer.modal;
|
||||
},
|
||||
popup(options) {
|
||||
BsModal.popup.bind(this);
|
||||
return BsModal.popup(null, options);
|
||||
},
|
||||
template: /*html*/ `
|
||||
<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">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item" v-for="element in breadcrumb">{{element}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<edit-profil-select @submit="submitProfilChange" v-model:breadcrumb="breadcrumb" v-model:topic="topic" v-model:profilUpdate="profilUpdate" ariaLabel="select" :list="editData"></edit-profil-select>
|
||||
</div>
|
||||
</template>
|
||||
<!-- optional footer -->
|
||||
<template v-slot:footer v-if="showFooter">
|
||||
<loader ref="loaderRef" :timeout="0"></loader>
|
||||
<button class="btn btn-outline-danger " @click="hide">{{$p.t('ui','cancel')}}</button>
|
||||
<button :disabled="!profilUpdate" @click="submitProfilChange" role="button" class="btn btn-primary">{{$p.t('ui','senden')}}</button>
|
||||
</template>
|
||||
<!-- end of optional footer -->
|
||||
</bs-modal>`,
|
||||
};
|
||||
@@ -0,0 +1,267 @@
|
||||
import Dms from "../../../../Form/Upload/Dms.js";
|
||||
|
||||
import ApiProfil from '../../../../../api/factory/profil.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AutoComplete: primevue.autocomplete,
|
||||
Dms: Dms
|
||||
},
|
||||
|
||||
props: {
|
||||
data: Object,
|
||||
isMitarbeiter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
},
|
||||
|
||||
inject: ["getZustelladressenCount", "updateFileID"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
gemeinden: [],
|
||||
ortschaftnamen: [],
|
||||
selectedNation: null,
|
||||
nationenList: [],
|
||||
originalValue: null,
|
||||
zustellAdressenCount: null,
|
||||
dmsData: [],
|
||||
fileschanged: false
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
"data.gemeinde": function (newValue, oldValue) {
|
||||
this.$emit("profilUpdate", this.isChanged ? this.data : null);
|
||||
},
|
||||
"data.ort": function (newValue, oldValue) {
|
||||
this.$emit("profilUpdate", this.isChanged ? this.data : null);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
autocompleteSearchGemeinden: function (event) {
|
||||
this.gemeinden = this.gemeinden.map((gemeinde) => gemeinde);
|
||||
},
|
||||
|
||||
autocompleteSearchOrtschaftsnamen: function (event) {
|
||||
this.ortschaftnamen = this.ortschaftnamen.map((ortschaft) => ortschaft);
|
||||
},
|
||||
|
||||
getGemeinde: function () {
|
||||
//? only query the gemeinde is the nation is Austria and the PLZ is greater than 999 and less than 32000
|
||||
if (
|
||||
this.data.nation &&
|
||||
this.data.nation === "A" &&
|
||||
this.data.plz &&
|
||||
this.data.plz > 999 &&
|
||||
this.data.plz < 32000
|
||||
) {
|
||||
this.$api
|
||||
.call(ApiProfil.getGemeinden(this.data.nation, this.data.plz))
|
||||
.then((res) => {
|
||||
if (res.data.length) {
|
||||
this.gemeinden = [
|
||||
...new Set(
|
||||
res.data.map((element) => {
|
||||
return element.name;
|
||||
})
|
||||
),
|
||||
];
|
||||
this.ortschaftnamen = [
|
||||
...new Set(
|
||||
res.data.map((element) => {
|
||||
return element.ortschaftsname;
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.gemeinden = [];
|
||||
}
|
||||
},
|
||||
|
||||
updateValue: function (event, bind) {
|
||||
//? sets the value of a property to null when an empty string is entered to keep the isChanged function valid
|
||||
if (bind === "zustelladresse") {
|
||||
this.data[bind] = event.target.checked;
|
||||
} else if(bind === 'files') {
|
||||
if(this.dmsData.length > 0 && this.dmsData[0].type !== 'application/x.fhc-dms+json') {
|
||||
this.fileschanged = true;
|
||||
}
|
||||
this.updateFileID(this.dmsData);
|
||||
} else {
|
||||
this.data[bind] = event.target.value === "" ? null : event.target.value;
|
||||
}
|
||||
|
||||
this.$emit("profilUpdate", this.isChanged ? this.data : null);
|
||||
// update the zustellAdressen count
|
||||
this.zustellAdressenCount = this.getZustelladressenCount();
|
||||
},
|
||||
|
||||
deleteDmsData: function() {
|
||||
this.dmsData = [];
|
||||
this.updateValue(null, 'files');
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
showZustellAdressenWarning: function () {
|
||||
|
||||
// if the address was already a zustellungsadresse when editing the address, then the warning will not be shown and the zustellungsadresse will just be overwritten
|
||||
if (JSON.parse(this.originalValue).zustelladresse){
|
||||
return false;
|
||||
}
|
||||
// if zustellAdressenCount is not 0 and the own kontakt has the flag zustellung set to true
|
||||
if (!this.zustellAdressenCount.includes(this.data.adresse_id)) {
|
||||
return this.data.zustelladresse && this.zustellAdressenCount.length;
|
||||
}
|
||||
return this.zustellAdressenCount.length >= 2 && this.data.zustelladresse;
|
||||
},
|
||||
isChanged: function () {
|
||||
if (
|
||||
!this.data.strasse ||
|
||||
!this.data.plz ||
|
||||
!this.data.ort ||
|
||||
!this.data.typ ||
|
||||
this.dmsData.length === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datachanged = this.originalValue !== JSON.stringify(this.data);
|
||||
return datachanged || this.fileschanged;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
// get all available nationen
|
||||
this.$api
|
||||
.call(ApiProfil.getAllNationen())
|
||||
.then(res => {
|
||||
this.nationenList = res.data;
|
||||
this.getGemeinde();
|
||||
});
|
||||
|
||||
this.originalValue = JSON.stringify(this.data);
|
||||
this.zustellAdressenCount = this.getZustelladressenCount();
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.files) {
|
||||
this.dmsData = this.files;
|
||||
}
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
<div class="gy-3 row justify-content-center align-items-center">
|
||||
<!-- warning message for too many zustellungs Adressen -->
|
||||
<div v-if="showZustellAdressenWarning" class="col-12 ">
|
||||
<div class="card bg-danger mx-2">
|
||||
<div class="card-body text-white ">
|
||||
<span>{{$p.t('profilUpdate','zustell_adressen_warning')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End of warning -->
|
||||
|
||||
|
||||
<div class="col-12 ">
|
||||
<div class="form-check mb-2">
|
||||
<input class="form-check-input" type="checkbox" @change="updateValue($event,'zustelladresse')" :checked="data.zustelladresse" id="flexCheckDefault">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
{{$p.t('person','zustelladresse')}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NATION -->
|
||||
<div class="col-8">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','nation')}}*</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PLZ -->
|
||||
<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" :aria-label="$p.t('person','plz')" :title="$p.t('person','plz')" @input="updateValue($event,'plz')" @input="getGemeinde" :placeholder="data.plz">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GEMEINDE -->
|
||||
<div class="col-lg-6">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','gemeinde')}}*</div>
|
||||
<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>
|
||||
|
||||
<!-- ORT -->
|
||||
<div class="col-lg-6" >
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','ort')}}*</div>
|
||||
<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>
|
||||
|
||||
<!-- STRASSE -->
|
||||
<div class="col-lg-8">
|
||||
<div class="form-underline ">
|
||||
<div class="form-underline-titel">{{$p.t('person','strasse')}}*</div>
|
||||
<input :aria-label="$p.t('person','strasse')" class="form-control" :value="data.strasse" @input="updateValue($event,'strasse')" :placeholder="data.strasse">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ADRESSEN TYP -->
|
||||
<div class="col-lg-4">
|
||||
<div class="form-underline">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','kontaktTyp')}}*</div>
|
||||
<select :value="data.typ" @change="updateValue($event,'typ')" class="form-select" aria-label="Select Kontakttyp">
|
||||
<option selected></option>
|
||||
<option value="Nebenwohnsitz">{{$p.t('profilUpdate','nebenwohnsitz')}}</option>
|
||||
<option value="Hauptwohnsitz">{{$p.t('profilUpdate','hauptwohnsitz')}}</option>
|
||||
<option v-if="isMitarbeiter" value="Homeoffice">{{$p.t('profilUpdate','homeoffice')}}</option>
|
||||
<option v-if="isMitarbeiter" value="Rechnungsadresse">{{$p.t('profilUpdate','rechnungsadresse')}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','meldebestaetigung')}}*</div>
|
||||
<dms
|
||||
ref="update"
|
||||
id="files"
|
||||
name="files"
|
||||
:multiple="false"
|
||||
v-model="dmsData"
|
||||
@update:model-value="updateValue($event,'files')"
|
||||
></dms>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div> </div>
|
||||
<button
|
||||
@click="deleteDmsData"
|
||||
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>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
export default {
|
||||
props: {
|
||||
data: Object,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
originalValue: null,
|
||||
zustellKontakteCount: null,
|
||||
};
|
||||
},
|
||||
|
||||
inject: ["getZustellkontakteCount"],
|
||||
|
||||
methods: {
|
||||
updateValue: function (event, bind) {
|
||||
if (bind === "zustellung") {
|
||||
this.data[bind] = event.target.checked;
|
||||
} else {
|
||||
//? sets the value of a property to null when an empty string is entered to keep the isChanged function valid
|
||||
this.data[bind] = event.target.value === "" ? null : event.target.value;
|
||||
}
|
||||
this.$emit("profilUpdate", this.isChanged ? this.data : null);
|
||||
this.zustellKontakteCount = this.getZustellkontakteCount();
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
showZustellKontakteWarning: function () {
|
||||
// if the kontakt is already a zustellungskontakt when the user is editing the kontakt, then no warning is shown and the zustellung will be overwritten
|
||||
if (JSON.parse(this.originalValue).zustellung) {
|
||||
return false;
|
||||
}
|
||||
// if zustellKontakteCount is not 0 and the own kontakt has the flag zustellung set to true
|
||||
if (!this.zustellKontakteCount.includes(this.data.kontakt_id)) {
|
||||
return this.data.zustellung && this.zustellKontakteCount.length;
|
||||
}
|
||||
return this.zustellKontakteCount.length >= 2 && this.data.zustellung;
|
||||
},
|
||||
isChanged: function () {
|
||||
//? returns true if the original passed data object was changed
|
||||
if (!this.data.kontakt || !this.data.kontakttyp) {
|
||||
return false;
|
||||
}
|
||||
return JSON.stringify(this.data) !== this.originalValue;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.originalValue = JSON.stringify(this.data);
|
||||
this.zustellKontakteCount = this.getZustellkontakteCount();
|
||||
},
|
||||
|
||||
template:
|
||||
/*html*/
|
||||
`
|
||||
|
||||
<div class="gy-3 row align-items-center justify-content-center">
|
||||
|
||||
<!-- warning message for too many zustellungs Kontakte -->
|
||||
<div v-if="showZustellKontakteWarning" class="col-12 ">
|
||||
<div class="card bg-danger mx-2">
|
||||
<div class="card-body text-white ">
|
||||
<span>{{$p.t('profilUpdate','zustell_kontakte_warning')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 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>
|
||||
<option value="telefon">{{$p.t('person','telefon')}}</option>
|
||||
<option value="notfallkontakt">{{$p.t('profilUpdate','notfallkontakt')}}</option>
|
||||
<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>
|
||||
</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 :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 :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 :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>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import Adresse from "../../ProfilComponents/Adresse.js";
|
||||
import Kontakt from "../../ProfilComponents/Kontakt.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Adresse,
|
||||
Kontakt,
|
||||
},
|
||||
inject: ["profilUpdateTopic"],
|
||||
data() {
|
||||
return {
|
||||
files: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getDocumentLink: function (dms_id) {
|
||||
return (
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
`/Cis/ProfilUpdate/show/${dms_id}`
|
||||
);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
getComponentView: function () {
|
||||
if (
|
||||
this.topic == this.profilUpdateTopic["Private Adressen"] ||
|
||||
this.topic == this.profilUpdateTopic["Add Adresse"] ||
|
||||
this.topic == this.profilUpdateTopic["Delete Adresse"]
|
||||
) {
|
||||
return "Adresse";
|
||||
} else if (
|
||||
this.topic == this.profilUpdateTopic["Private Kontakte"] ||
|
||||
this.topic == this.profilUpdateTopic["Add Kontakt"] ||
|
||||
this.topic == this.profilUpdateTopic["Delete Kontakt"]
|
||||
) {
|
||||
return "Kontakt";
|
||||
} else {
|
||||
return "text_input";
|
||||
}
|
||||
},
|
||||
cardHeader: function () {
|
||||
if (
|
||||
this.topic == this.profilUpdateTopic["Delete Addresse"] ||
|
||||
this.topic == this.profilUpdateTopic["Delete Kontakt"]
|
||||
) {
|
||||
return "Delete";
|
||||
} else if (
|
||||
this.topic == this.profilUpdateTopic["Add Adresse"] ||
|
||||
this.topic == this.profilUpdateTopic["Add Kontakt"]
|
||||
) {
|
||||
return "Add";
|
||||
} else {
|
||||
return "Update";
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
data: { type: Object },
|
||||
view: { type: String },
|
||||
status: { type: String },
|
||||
status_message: { type: String },
|
||||
status_timestamp: { type: String },
|
||||
updateID: { type: Number },
|
||||
topic: { type: String },
|
||||
},
|
||||
created() {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.getProfilRequestFiles(this.updateID))
|
||||
.then((res) => {
|
||||
this.files = res.data;
|
||||
});
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="row">
|
||||
|
||||
<div class="col">
|
||||
<div class="form-underline mb-2">
|
||||
<div class="form-underline-titel">{{$p.t('global','status')}}</div>
|
||||
<span class="form-underline-content">{{status}} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="form-underline mb-2">
|
||||
<div class="form-underline-titel">{{$p.t('global','datum')}}</div>
|
||||
<span class="form-underline-content">{{status_timestamp}} </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div v-if="status_message" class="form-underline mb-2 ">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','statusMessage')}}</div>
|
||||
<textarea class="form-control" rows="4" disabled>{{status_message}} </textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="fa" :class="{'fa-trash':cardHeader==='Delete', 'fa-edit':cardHeader==='Update', 'fa-plus':cardHeader==='Add'}" ></i>
|
||||
{{cardHeader}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<template v-if="getComponentView === 'text_input'">
|
||||
<div class="form-underline mb-2">
|
||||
<div class="form-underline-titel">{{topic}}</div>
|
||||
<span class="form-underline-content">{{data.value}} </span>
|
||||
</div>
|
||||
</template>
|
||||
<component v-else :is="getComponentView" :data="data"></component>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="files?.length" class="card mt-4">
|
||||
<div class="card-header">{{$p.t('profilUpdate','nachweisdokumente')}}</div>
|
||||
<div class="card-body">
|
||||
<a target="_blank" :href="getDocumentLink(file.dms_id)" v-for="file in files">{{file.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import Dms from "../../../../Form/Upload/Dms.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
dmsData: [],
|
||||
originalValue: null,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Dms,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
},
|
||||
withFiles: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
files: {
|
||||
type: Array,
|
||||
},
|
||||
updateID: {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
inject:["updateFileID"],
|
||||
computed: {
|
||||
didFilesChange: function () {
|
||||
this.updateFileID(this.dmsData);
|
||||
let res = false;
|
||||
//? case in which the profilRequest has already associated files
|
||||
if(this.files){
|
||||
Array.from(this.dmsData).forEach((file) => {
|
||||
if (this.files.some((f) => f.name !== file.name)) {
|
||||
res = true;
|
||||
}
|
||||
});
|
||||
return !(this.dmsData.length == this.files.length) || res;
|
||||
}
|
||||
//? case in which the user creates a new profilRequest
|
||||
else{
|
||||
return Array.from(this.dmsData).length? true:false;
|
||||
}
|
||||
},
|
||||
didDataChange: function(){
|
||||
return JSON.stringify(this.data) !== this.originalValue;
|
||||
},
|
||||
isChanged: function () {
|
||||
if (this.withFiles) {
|
||||
if(this.updateID){
|
||||
return (this.didDataChange || this.didFilesChange) && this.dmsData.length;
|
||||
}
|
||||
return this.didDataChange && this.didFilesChange;
|
||||
}
|
||||
return this.didDataChange
|
||||
},
|
||||
},
|
||||
emits: ["profilUpdate"],
|
||||
watch: {
|
||||
//? watcher to trigger the event emit when a file was uploaded or removed
|
||||
dmsData(value) {
|
||||
this.emitChanges();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
stringifyFile(file) {
|
||||
return JSON.stringify({
|
||||
lastModified: file.lastModified,
|
||||
lastModifiedDate: file.lastModifiedDate,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
});
|
||||
},
|
||||
emitChanges: function () {
|
||||
if (this.isChanged) {
|
||||
|
||||
this.$emit(
|
||||
"profilUpdate", { value: this.data.value }
|
||||
);
|
||||
} else {
|
||||
this.$emit("profilUpdate", null);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.originalValue = JSON.stringify(Vue.toRaw(this.data));
|
||||
|
||||
if (this.files) {
|
||||
this.dmsData = this.files;
|
||||
}
|
||||
},
|
||||
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" :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" :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>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import Kontakt from "../ProfilComponents/Kontakt.js";
|
||||
import EditKontakt from "./EditProfilComponents/EditKontakt.js";
|
||||
import Adresse from "../ProfilComponents/Adresse.js";
|
||||
import EditAdresse from "./EditProfilComponents/EditAdresse.js";
|
||||
import Status from "./EditProfilComponents/Status.js";
|
||||
import TextInputDokument from "./EditProfilComponents/TextInputDokument.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Kontakt,
|
||||
EditKontakt,
|
||||
Adresse,
|
||||
EditAdresse,
|
||||
Status,
|
||||
TextInputDokument,
|
||||
},
|
||||
inject: ["profilUpdateTopic"],
|
||||
props: {
|
||||
list: Object,
|
||||
|
||||
//? Prop used to determine how many options the select should initially show
|
||||
size: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
//? Content for the aria label of the select
|
||||
ariaLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
profilUpdate: String,
|
||||
topic: String,
|
||||
breadcrumb: String,
|
||||
},
|
||||
emits: {
|
||||
//? update:modelValue event is needed to notify the v-model when the value has changed
|
||||
["update:profilUpdate"]: null,
|
||||
["update:topic"]: null,
|
||||
["update:breadcrumb"]: null,
|
||||
submit: null,
|
||||
select: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
view: null,
|
||||
data: null,
|
||||
breadcrumbItems: [],
|
||||
modal_topic: this.topic,
|
||||
properties: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
addItem: function () {
|
||||
this.view =
|
||||
this.modal_topic == this.profilUpdateTopic["Private Kontakte"]
|
||||
? "EditKontakt"
|
||||
: "EditAdresse";
|
||||
|
||||
//? updates the topic when a Kontakt or an Address should be added
|
||||
this.modal_topic =
|
||||
this.modal_topic == this.profilUpdateTopic["Private Kontakte"]
|
||||
? this.profilUpdateTopic["Add Kontakt"]
|
||||
: this.profilUpdateTopic["Add Adresse"];
|
||||
this.$emit("update:topic", this.modal_topic);
|
||||
this.breadcrumbItems.push(this.modal_topic);
|
||||
this.$emit("update:breadcrumb", this.breadcrumbItems);
|
||||
|
||||
this.data =
|
||||
this.view == "EditAdresse"
|
||||
? {
|
||||
//? add flag
|
||||
add: true,
|
||||
adresse_id: null,
|
||||
strasse: null,
|
||||
typ: null,
|
||||
plz: null,
|
||||
ort: null,
|
||||
zustelladresse: false,
|
||||
}
|
||||
: {
|
||||
//? add flag
|
||||
add: true,
|
||||
kontakt_id: null,
|
||||
kontakttyp: null,
|
||||
kontakt: null,
|
||||
anmerkung: null,
|
||||
zustellung: false,
|
||||
};
|
||||
},
|
||||
|
||||
deleteItem: function (item) {
|
||||
//? delete flag
|
||||
item.data.delete = true;
|
||||
this.$emit("update:profilUpdate", item.data);
|
||||
//? updates the topic when a Kontakt or an Address should be deleted
|
||||
|
||||
this.modal_topic = this.modal_topic == this.profilUpdateTopic["Private Adressen"]
|
||||
? this.profilUpdateTopic["Delete Adresse"]
|
||||
: this.profilUpdateTopic["Delete Kontakt"];
|
||||
this.$emit("update:topic", this.modal_topic);
|
||||
this.$emit("submit");
|
||||
},
|
||||
|
||||
profilUpdateEmit: function (event) {
|
||||
//? passes the updated profil information to the parent component
|
||||
this.$emit("update:profilUpdate", event);
|
||||
},
|
||||
|
||||
updateOptions: function (event, item) {
|
||||
this.properties = item;
|
||||
this.data = item.data;
|
||||
this.view = item.view;
|
||||
if (item.title) {
|
||||
//? emits the selected topic to the parent component
|
||||
this.modal_topic = item.topic;
|
||||
this.$emit("update:topic", this.modal_topic);
|
||||
|
||||
//? emits the new item for the breadcrumb in the parent component
|
||||
this.breadcrumbItems.push(item.title);
|
||||
} else {
|
||||
if (item.data.kontakttyp) {
|
||||
this.breadcrumbItems.push(item.data.kontakttyp);
|
||||
this.breadcrumbItems.push(item.data.kontakt);
|
||||
} else if (item.data.strasse) {
|
||||
this.breadcrumbItems.push(item.data.strasse);
|
||||
}
|
||||
}
|
||||
this.$emit("update:breadcrumb", this.breadcrumbItems);
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
created() {
|
||||
//? JSON parse and stringify are used to deep clone the objects
|
||||
this.properties = {...this.list};
|
||||
this.data = this.list.data
|
||||
? JSON.parse(JSON.stringify(this.list.data))
|
||||
: null;
|
||||
this.view = this.list.view
|
||||
? JSON.parse(JSON.stringify(this.list.view))
|
||||
: null;
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
<template v-if="!view">
|
||||
<div class="list-group">
|
||||
<template v-for="item in data">
|
||||
<div class="d-flex flex-row align-items-center">
|
||||
<button style="position:relative" type="button" class=" list-group-item list-group-item-action" @click="updateOptions($event,item)" >
|
||||
<!-- render title of options -->
|
||||
<p v-if="item.title" class="my-1" >{{item.title}}</p>
|
||||
<!-- else render list view of items -->
|
||||
<div v-else class="my-2 me-4" >
|
||||
<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" :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" :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>
|
||||
<component @profilUpdate="profilUpdateEmit" :is="view" v-bind="properties" :data="data" ></component>
|
||||
</template>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,438 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import AusweisStatus from "./ProfilComponents/FhAusweisStatus.js";
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import Adresse from "./ProfilComponents/Adresse.js";
|
||||
import Kontakt from "./ProfilComponents/Kontakt.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js";
|
||||
import EditProfil from "./ProfilModal/EditProfil.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
Mailverteiler,
|
||||
AusweisStatus,
|
||||
QuickLinks,
|
||||
Adresse,
|
||||
Kontakt,
|
||||
ProfilEmails,
|
||||
RoleInformation,
|
||||
ProfilInformation,
|
||||
FetchProfilUpdates,
|
||||
EditProfil,
|
||||
},
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
collapseIconBetriebsmittel: true,
|
||||
editDataFilter: null,
|
||||
preloadedPhrasen:{},
|
||||
// tabulator options
|
||||
zutrittsgruppen_table_options: {
|
||||
persistenceID: "filterTableStudentProfilZutrittsgruppen",
|
||||
persistence: {
|
||||
columns: false
|
||||
},
|
||||
height: 200,
|
||||
layout: "fitColumns",
|
||||
columns: [{
|
||||
title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase),
|
||||
field: "bezeichnung"
|
||||
}],
|
||||
},
|
||||
betriebsmittel_table_options: {
|
||||
persistenceID: "filterTableStudentProfilBetriebsmittel",
|
||||
persistence: {
|
||||
columns: false
|
||||
},
|
||||
height: 300,
|
||||
layout: "fitColumns",
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
"<i id='collapseIconBetriebsmittel' role='button' class='fa-solid fa-angle-down '></i>",
|
||||
field: "collapse",
|
||||
headerSort: false,
|
||||
headerFilter: false,
|
||||
formatter: "responsiveCollapse",
|
||||
maxWidth: 40,
|
||||
headerClick: this.collapseFunction,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(()=>this.preloadedPhrasen.entlehnteBetriebsmittelPhrase),
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.inventarnummerPhrase) ,
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) ,
|
||||
field: "Ausgegeben_am",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
visible: true
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
props: {
|
||||
data: Object,
|
||||
editData: Object,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
studiengang_kz: Vue.computed({ get: () => this.data.studiengang_kz }),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
betriebsmittelTableBuilt: function () {
|
||||
this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
|
||||
},
|
||||
zutrittsgruppenTableBuilt: function () {
|
||||
this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns)
|
||||
this.$refs.zutrittsgruppenTable.tabulator.setData(
|
||||
this.data.zuttritsgruppen
|
||||
);
|
||||
},
|
||||
fetchProfilUpdates: function () {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.selectProfilRequest())
|
||||
.then((res) => {
|
||||
if (!res.error && res) {
|
||||
this.data.profilUpdates = res.data?.length
|
||||
? res.data.sort(this.sortProfilUpdates)
|
||||
: null;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
hideEditProfilModal: function () {
|
||||
//? checks the editModal component property result, if the user made a successful request or not
|
||||
if (this.$refs.editModal.result) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.selectProfilRequest())
|
||||
.then((request) => {
|
||||
if (!request.error && request.data) {
|
||||
this.data.profilUpdates = request.data;
|
||||
this.data.profilUpdates.sort(this.sortProfilUpdates);
|
||||
} else {
|
||||
console.error("Error when fetching profile updates: " + request);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
} else {
|
||||
// when modal was closed without submitting request
|
||||
}
|
||||
this.showModal = false;
|
||||
this.editDataFilter = null;
|
||||
},
|
||||
|
||||
showEditProfilModal(view) {
|
||||
if (view) {
|
||||
this.editDataFilter = view;
|
||||
}
|
||||
this.showModal = true;
|
||||
// after a state change, wait for the DOM updates to complete
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.editModal.show();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
|
||||
filteredEditData() {
|
||||
return this.editDataFilter
|
||||
? this.editData.data[this.editDataFilter]
|
||||
: this.editData;
|
||||
},
|
||||
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Vorname: this.data.vorname,
|
||||
Nachname: this.data.nachname,
|
||||
Username: this.data.username,
|
||||
Anrede: this.data.anrede,
|
||||
Titel: this.data.titel,
|
||||
Postnomen: this.data.postnomen,
|
||||
foto_sperre: this.data.foto_sperre,
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('person','personenkennzeichen')}`,
|
||||
value: this.data.personenkennzeichen
|
||||
},
|
||||
studiengang: {
|
||||
label: `${this.$p.t('lehre','studiengang')}`,
|
||||
value: this.data.studiengang
|
||||
},
|
||||
semester: {
|
||||
label: `${this.$p.t('lehre','semester')}`,
|
||||
value: this.data.semester
|
||||
},
|
||||
verband: {
|
||||
label: `${this.$p.t('lehre','lehrverband')}`,
|
||||
value: this.data.verband
|
||||
},
|
||||
gruppe: {
|
||||
label: `${this.$p.t('lehre','gruppe')}`,
|
||||
value: this.data.gruppe.trim()
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
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);
|
||||
},
|
||||
watch: {
|
||||
'language.value'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns)
|
||||
}
|
||||
},
|
||||
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))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<div class="d-md-none col-12 ">
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row py-2">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Bearbeiten Button -->
|
||||
<div v-if="isEditable" class="row ">
|
||||
<div class="col mb-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-10">{{$p.t('ui','bearbeiten')}}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row mb-3">
|
||||
<div class="col">
|
||||
<!-- 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 -->
|
||||
<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" :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>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4 ">
|
||||
<div class="col">
|
||||
<!-- PRIVATE KONTAKTE-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div @click="showEditProfilModal('Private_Kontakte')" class="col-auto" type="button">
|
||||
<i class="fa fa-edit"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span>{{$p.t('profil','privateKontakte')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body ">
|
||||
<div class="gy-3 row">
|
||||
<div v-for="element in data.kontakte" class="col-12">
|
||||
<Kontakt :data="element"></Kontakt>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- PRIVATE ADRESSEN-->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="row">
|
||||
<div @click="showEditProfilModal('Private_Adressen')" class="col-auto" type="button">
|
||||
<i class="fa fa-edit"></i>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span>{{$p.t('profil','privateAdressen')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gy-3 row ">
|
||||
<div v-for="element in data.adressen" class="col-12">
|
||||
<Adresse :data="element"></Adresse>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<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 class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="preloadedPhrasen.loaded"
|
||||
@tableBuilt="zutrittsgruppenTableBuilt"
|
||||
:title="$p.t('profil','zutrittsGruppen')"
|
||||
ref="zutrittsgruppenTable"
|
||||
:tabulator-options="zutrittsgruppen_table_options"
|
||||
tableOnly
|
||||
:sideMenu="false"
|
||||
noColumnFilter />
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF MAIN CONTENT COL -->
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- Bearbeiten Button -->
|
||||
<div class="row d-none d-md-block">
|
||||
<div class="col mb-3">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-10">{{$p.t('ui','bearbeiten')}}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
|
||||
<div class="col mb-3">
|
||||
<!-- PROFIL UPDATES -->
|
||||
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3" >
|
||||
<div class="col-12">
|
||||
<ausweis-status :data="data.zutrittsdatum"></ausweis-status>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- HIER SIND DIE MAILVERTEILER -->
|
||||
<mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler>
|
||||
</div>
|
||||
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER ROW-->
|
||||
</div>
|
||||
<!-- END OF CONTAINER -->
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
import QuickLinks from "./ProfilComponents/QuickLinks.js";
|
||||
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
|
||||
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
|
||||
import RoleInformation from "./ProfilComponents/RoleInformation.js";
|
||||
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
components: {
|
||||
QuickLinks,
|
||||
Mailverteiler,
|
||||
ProfilEmails,
|
||||
RoleInformation,
|
||||
ProfilInformation,
|
||||
},
|
||||
|
||||
props: ["data"],
|
||||
provide() {
|
||||
return {
|
||||
studiengang_kz: Vue.computed({ get: () => this.data.studiengang_kz }),
|
||||
}
|
||||
},
|
||||
|
||||
methods: {},
|
||||
|
||||
computed: {
|
||||
fotoStatus() {
|
||||
return this.data?.fotoStatus ?? null;
|
||||
},
|
||||
profilInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
Vorname: this.data.vorname,
|
||||
Nachname: this.data.nachname,
|
||||
Username: this.data.username,
|
||||
Anrede: this.data.anrede,
|
||||
Titel: this.data.titel,
|
||||
Postnomen: this.data.postnomen,
|
||||
foto_sperre: this.data.foto_sperre,
|
||||
foto: this.data.foto,
|
||||
};
|
||||
},
|
||||
|
||||
personEmails() {
|
||||
return this.data?.emails ? this.data.emails : [];
|
||||
},
|
||||
|
||||
roleInformation() {
|
||||
if (!this.data) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
geburtsdatum: {
|
||||
label: `${this.$p.t('profil','Geburtsdatum')}`,
|
||||
value: this.data.gebdatum
|
||||
},
|
||||
geburtsort: {
|
||||
label: `${this.$p.t('profil','Geburtsort')}`,
|
||||
value: this.data.gebort
|
||||
},
|
||||
personenkennzeichen: {
|
||||
label: `${this.$p.t('person','personenkennzeichen')}`,
|
||||
value: this.data.personenkennzeichen
|
||||
},
|
||||
studiengang: {
|
||||
label: `${this.$p.t('lehre','studiengang')}`,
|
||||
value: this.data.studiengang
|
||||
},
|
||||
semester: {
|
||||
label: `${this.$p.t('lehre','semester')}`,
|
||||
value: this.data.semester
|
||||
},
|
||||
verband: {
|
||||
label: `${this.$p.t('lehre','lehrverband')}`,
|
||||
value: this.data.verband
|
||||
},
|
||||
gruppe: {
|
||||
label: `${this.$p.t('lehre','gruppe')}`,
|
||||
value: this.data.gruppe.trim()
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
<!-- ROW -->
|
||||
<div class="row">
|
||||
<!-- HIDDEN QUICK LINKS -->
|
||||
<!-- 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 -->
|
||||
<!-- 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 -->
|
||||
<div class="row mb-4">
|
||||
<!-- FIRST KAESTCHEN -->
|
||||
<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')" :fotoStatus="fotoStatus"></profil-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
|
||||
<!-- END OF PROFIL INFORMATION ROW -->
|
||||
<!-- INFORMATION CONTENT END -->
|
||||
</div>
|
||||
<div class="col-xl-6 col-lg-12 ">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<!-- EMAILS -->
|
||||
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
|
||||
</div>
|
||||
</div>
|
||||
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
|
||||
<div class=" row mb-4">
|
||||
<div class=" col-lg-12">
|
||||
<role-information :title="$p.t('profil','studentInformation')" :data="roleInformation"></role-information>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
|
||||
<!-- END OF THE SECOND INFORMATION COLUMN -->
|
||||
</div>
|
||||
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
|
||||
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
|
||||
</div >
|
||||
<!-- END OF MAIN CONTENT COL -->
|
||||
</div>
|
||||
<!-- START OF SIDE PANEL -->
|
||||
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
|
||||
<!-- SRART OF QUICK LINKS IN THE SIDE PANEL -->
|
||||
<!-- START OF THE FIRDT ROW IN THE SIDE PANEL -->
|
||||
<!-- THESE QUCK LINKS ARE ONLY VISIBLE UNTIL VIEWPORT MD -->
|
||||
<!--TODO: uncomment when implemented
|
||||
<div class="row d-none d-md-block mb-3">
|
||||
<div class="col">
|
||||
|
||||
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
|
||||
|
||||
</div>
|
||||
</div>-->
|
||||
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- HIER SIND DIE MAILVERTEILER -->
|
||||
<mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler>
|
||||
</div>
|
||||
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF SIDE PANEL -->
|
||||
</div>
|
||||
<!-- END OF CONTAINER ROW-->
|
||||
</div>
|
||||
<!-- END OF CONTAINER -->
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
import BsModal from "../../Bootstrap/Modal.js";
|
||||
import Alert from "../../Bootstrap/Alert.js";
|
||||
import Kontakt from "../Profil/ProfilComponents/Kontakt.js";
|
||||
import Adresse from "../Profil/ProfilComponents/Adresse.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BsModal,
|
||||
Kontakt,
|
||||
Adresse,
|
||||
},
|
||||
inject: ["profilUpdateStates"],
|
||||
mixins: [BsModal],
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
},
|
||||
setLoading: {
|
||||
type: Function,
|
||||
},
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
data: this.value,
|
||||
loading: false,
|
||||
result: false,
|
||||
info: null,
|
||||
files: null,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
getProfilStatus: async function () {
|
||||
return (
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
`/Cis/ProfilUpdate/show/${dms_id}`
|
||||
);
|
||||
},
|
||||
getDocumentLink: function (dms_id) {
|
||||
return (
|
||||
FHC_JS_DATA_STORAGE_OBJECT.app_root +
|
||||
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
|
||||
`/Cis/ProfilUpdate/show/${dms_id}`
|
||||
);
|
||||
},
|
||||
handleRequest: function (type) {
|
||||
this.loading = true;
|
||||
this.setLoading(true);
|
||||
this.$api
|
||||
.call(ApiProfilUpdate[
|
||||
type.toLowerCase() == "accept"
|
||||
? "acceptProfilRequest"
|
||||
: "denyProfilRequest"
|
||||
](this.data))
|
||||
.then((res) => {
|
||||
this.result = true;
|
||||
})
|
||||
.catch((e) => this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
this.setLoading(false);
|
||||
this.loading = false;
|
||||
this.hide();
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
getComponentView: function () {
|
||||
if (this.data.topic.toLowerCase().includes("kontakt")) {
|
||||
return "kontakt";
|
||||
} else if (this.data.topic.toLowerCase().includes("adresse")) {
|
||||
return "adresse";
|
||||
} else {
|
||||
return "text_input";
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// only fetching the profilUpdate Attachemnts if the profilUpdate actually has attachments
|
||||
if (this.value.attachment_id) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.getProfilRequestFiles(
|
||||
this.data.profil_update_id
|
||||
))
|
||||
.then((res) => {
|
||||
this.files = res.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.modal = this.$refs.modalContainer.modal;
|
||||
},
|
||||
popup(options) {
|
||||
return BsModal.popup.bind(this)(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-slot:title>
|
||||
{{title}}
|
||||
</template>
|
||||
|
||||
|
||||
<template v-slot:default>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('global','status')}}: </div>
|
||||
|
||||
<span class="form-underline-content" >{{data.status}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div v-if="data.status!==profilUpdateStates['Pending']" class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','statusDate')}}: </div>
|
||||
<!-- only status timestamp and status message can be null in the database -->
|
||||
<span class="form-underline-content" >{{data.status_timestamp?data.status_timestamp:'-'}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','userID')}}: </div>
|
||||
|
||||
<span class="form-underline-content" >{{data.uid}}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('global','name')}}: </div>
|
||||
|
||||
<span class="form-underline-content" >{{data.name}}</span>
|
||||
</div>
|
||||
|
||||
<div class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','anfrageThema')}}: </div>
|
||||
|
||||
<span class="form-underline-content" >{{data.topic}}</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="form-underline mb-2 col-12 col-sm-6">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','anfrageDatum')}}:</div>
|
||||
|
||||
<span class="form-underline-content" >{{data.insertamum}}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Row with the status message is only visible if the request is not pending and the message is not empty -->
|
||||
<div v-if="data.status !== profilUpdateStates['Pending'] && data.status_message" class="row">
|
||||
<div class="col">
|
||||
<div class="form-underline mb-2 ">
|
||||
<div class="form-underline-titel">{{$p.t('profilUpdate','statusMessage')}}</div>
|
||||
<textarea class="form-control" rows="4" disabled>{{data.status_message}} </textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row my-4">
|
||||
<div class="col ">
|
||||
<div class="card">
|
||||
<div class="card-header">{{$p.t('profilUpdate','update')}}</div>
|
||||
<div class="card-body">
|
||||
<template v-if="getComponentView==='text_input'">
|
||||
<div class="form-underline mb-2">
|
||||
<div class="form-underline-titel">{{data.topic}}</div>
|
||||
|
||||
<span class="form-underline-content" >{{data.requested_change.value}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<component v-else :is="getComponentView" :withZustelladresse="getComponentView==='adresse'?true:false" :data="data.requested_change"></component>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="files?.length" class="card mt-3">
|
||||
<div class="card-header">{{$p.t('profilUpdate','nachweisdokumente')}}</div>
|
||||
<div class="card-body">
|
||||
<a v-for="file in files" target="_blank" :href="getDocumentLink(file.dms_id)" >{{file.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
<template v-if="data.status === profilUpdateStates['Pending']" v-slot:footer>
|
||||
<div class="form-underline flex-fill">
|
||||
<div class="form-underline-titel">{{$p.t('global','nachricht')}}</div>
|
||||
|
||||
<div class="d-flex flex-row gap-2">
|
||||
<input class="form-control " v-model="data.status_message" />
|
||||
<button @click="handleRequest('accept')" class="text-nowrap btn btn-success">{{$p.t('profilUpdate','accept')}} <i class="fa fa-check"></i></button>
|
||||
<button @click="handleRequest('deny')" class="text-nowrap btn btn-danger">{{$p.t('profilUpdate','deny')}} <i class="fa fa-xmark"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</bs-modal>`,
|
||||
};
|
||||
@@ -0,0 +1,416 @@
|
||||
import { CoreFilterCmpt } from "../../filter/Filter.js";
|
||||
import AcceptDenyUpdate from "./AcceptDenyUpdate.js";
|
||||
import Alert from "../../../components/Bootstrap/Alert.js";
|
||||
import Loading from "../../../components/Loader.js";
|
||||
|
||||
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
|
||||
|
||||
const sortProfilUpdates = (ele1, ele2, thisPointer) => {
|
||||
let result = 0;
|
||||
if (ele1.status === thisPointer.profilUpdateStates["Pending"]) {
|
||||
result = -1;
|
||||
} else if (ele1.status === thisPointer.profilUpdateStates["Accepted"]) {
|
||||
result =
|
||||
ele2.status === thisPointer.profilUpdateStates["Rejected"] ? -1 : 1;
|
||||
} else {
|
||||
result = 1;
|
||||
}
|
||||
|
||||
if (ele1.status === ele2.status) {
|
||||
//? if they have the same status , insert_date gets compared for order
|
||||
result =
|
||||
new Date(ele2.insertamum.split(".").reverse().join("-")) -
|
||||
new Date(ele1.insertamum.split(".").reverse().join("-"));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
Loading,
|
||||
AcceptDenyUpdate,
|
||||
},
|
||||
inject: ["profilUpdateStates"],
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
categoryLoaded: false,
|
||||
showModal: false,
|
||||
modalData: null,
|
||||
loading: false,
|
||||
filter: "Pending",
|
||||
profil_update_id: Number(this.id),
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
profilUpdateEvents: function () {
|
||||
return [
|
||||
{
|
||||
"event": "dataProcessed",
|
||||
"handler": this.handleDataProcessed
|
||||
}
|
||||
];
|
||||
},
|
||||
profilUpdateOptions: function () {
|
||||
return {
|
||||
ajaxURL: 'dummy',
|
||||
ajaxRequestFunc: (url, config, params) => {
|
||||
return this.$api.call(ApiProfilUpdate.getProfilUpdateWithPermission(params.filter));
|
||||
},
|
||||
ajaxParams: () => {
|
||||
let filter = '';
|
||||
switch (this.filter) {
|
||||
case this.profilUpdateStates["Pending"]:
|
||||
filter = this.profilUpdateStates["Pending"];
|
||||
break;
|
||||
case this.profilUpdateStates["Accepted"]:
|
||||
filter = this.profilUpdateStates["Accepted"];
|
||||
break;
|
||||
case this.profilUpdateStates["Rejected"]:
|
||||
filter = this.profilUpdateStates["Rejected"];
|
||||
break;
|
||||
default:
|
||||
filter = '';
|
||||
}
|
||||
return {
|
||||
"filter": filter
|
||||
};
|
||||
},
|
||||
ajaxResponse: (url, params, response) => {
|
||||
//url - the URL of the request
|
||||
//params - the parameters passed with the request
|
||||
//response - the JSON object returned in the body of the response.
|
||||
//? sorts the response data from the backend
|
||||
if (response?.data)
|
||||
response.data.sort((ele1, ele2) => sortProfilUpdates(ele1, ele2, this));
|
||||
|
||||
return response.data;
|
||||
},
|
||||
//? adds tooltip with the status message of a profil update request if its status is not pending
|
||||
columnDefaults: {
|
||||
tooltip: (e, cell, onRendered) => {
|
||||
//e - mouseover event
|
||||
//cell - cell component
|
||||
//onRendered - onRendered callback registration function
|
||||
let statusMessage = cell.getData().status_message;
|
||||
let statusDate = cell.getData().status_timestamp;
|
||||
let status = cell.getData().status;
|
||||
if (!statusMessage) {
|
||||
return null;
|
||||
}
|
||||
let el = document.createElement("div");
|
||||
el.classList.add("border", "border-dark");
|
||||
|
||||
let statusDateEl = document.createElement("span");
|
||||
statusDateEl.classList.add("d-block", "mb-1");
|
||||
statusDateEl.innerHTML =
|
||||
"Request was " + status + " on " + statusDate;
|
||||
let statusMessageEl = document.createElement("span");
|
||||
statusMessageEl.innerHTML = "Status message: " + statusMessage;
|
||||
|
||||
el.appendChild(statusDateEl);
|
||||
el.appendChild(statusMessageEl);
|
||||
return el;
|
||||
},
|
||||
},
|
||||
rowContextMenu: (e, component) => {
|
||||
let menu = [];
|
||||
if (
|
||||
component.getData().status === this.profilUpdateStates["Pending"]
|
||||
) {
|
||||
menu.push(
|
||||
{
|
||||
label: `<i class='fa fa-check'></i> ${this.$p.t(
|
||||
"profilUpdate",
|
||||
"acceptUpdate"
|
||||
)}`,
|
||||
action: (e, column) => {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.acceptProfilRequest(column.getData()))
|
||||
.then((res) => {
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
})
|
||||
.catch((e) => this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
label: ` <i style='width:16px' class='text-center fa fa-xmark'></i> ${this.$p.t(
|
||||
"profilUpdate",
|
||||
"denyUpdate"
|
||||
)}`,
|
||||
action: (e, column) => {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.denyProfilRequest(column.getData()))
|
||||
.then((res) => {
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
})
|
||||
.catch((e) => this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
},
|
||||
{
|
||||
separator: true,
|
||||
},
|
||||
{
|
||||
label: `<i class='fa fa-eye'></i> ${this.$p.t(
|
||||
"profilUpdate",
|
||||
"showRequest"
|
||||
)}`,
|
||||
action: (e, column) => {
|
||||
this.showAcceptDenyModal(column.getData());
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
menu.push({
|
||||
label: `<i class='fa fa-eye'></i> ${this.$p.t(
|
||||
"profilUpdate",
|
||||
"showRequest"
|
||||
)}`,
|
||||
action: (e, column) => {
|
||||
this.showAcceptDenyModal(column.getData());
|
||||
},
|
||||
});
|
||||
}
|
||||
return menu;
|
||||
},
|
||||
|
||||
height: 600,
|
||||
layout: "fitColumns",
|
||||
|
||||
columns: [
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "UID"),
|
||||
field: "uid",
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
headerFilter: true,
|
||||
//responsive:0,
|
||||
},
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "Name"),
|
||||
field: "name",
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
headerFilter: true,
|
||||
//responsive:0,
|
||||
},
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "Topic"),
|
||||
field: "topic",
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
headerFilter: true,
|
||||
//responsive:0,
|
||||
},
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "insertamum"),
|
||||
field: "insertamum",
|
||||
resizable: true,
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
//responsive:0,
|
||||
},
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "Status"),
|
||||
field: "status_translated",
|
||||
hozAlign: "center",
|
||||
headerFilter: true,
|
||||
formatter: (cell, para) => {
|
||||
let iconClasses = "";
|
||||
let status = cell.getRow().getData().status;
|
||||
switch (status) {
|
||||
case this.profilUpdateStates["Pending"]:
|
||||
iconClasses += "fa fa-lg fa-circle-info text-info ";
|
||||
break;
|
||||
case this.profilUpdateStates["Accepted"]:
|
||||
iconClasses += "fa fa-lg fa-circle-check text-success ";
|
||||
break;
|
||||
case this.profilUpdateStates["Rejected"]:
|
||||
iconClasses += "fa fa-lg fa-circle-xmark text-danger ";
|
||||
break;
|
||||
}
|
||||
return `<div class='row justify-content-center'><div class='col-2'><i class='${iconClasses}'></i></div> <div class='col-4'><span>${cell.getValue()}</span></div></div>`;
|
||||
},
|
||||
|
||||
resizable: true,
|
||||
minWidth: 200,
|
||||
//responsive:0,
|
||||
},
|
||||
{
|
||||
title: this.$p.t("profilUpdate", "actions"),
|
||||
headerSort: false,
|
||||
formatter: (cell, params) => {
|
||||
let STATUS_PENDING =
|
||||
cell.getRow().getData().status ==
|
||||
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 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>
|
||||
<button class="btn border-danger border-2" id="denyButton"><i class=' fa fa-lg fa-circle-xmark text-danger'></i></button>`
|
||||
:
|
||||
``
|
||||
}
|
||||
</div>`;
|
||||
|
||||
// Convert the HTML string to an HTML node
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
const node = doc.body.firstChild;
|
||||
|
||||
// Add event listeners
|
||||
node
|
||||
.querySelector("#showButton")
|
||||
.addEventListener("click", () => {
|
||||
this.showAcceptDenyModal(cell.getRow().getData());
|
||||
});
|
||||
|
||||
if (STATUS_PENDING) {
|
||||
node
|
||||
.querySelector("#acceptButton")
|
||||
.addEventListener("click", () => {
|
||||
this.acceptProfilUpdate(cell.getRow().getData());
|
||||
});
|
||||
node
|
||||
.querySelector("#denyButton")
|
||||
.addEventListener("click", () => {
|
||||
this.denyProfilUpdate(cell.getRow().getData());
|
||||
});
|
||||
}
|
||||
|
||||
return node;
|
||||
},
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
hozAlign: "center",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
},
|
||||
methods: {
|
||||
denyProfilUpdate: function (data) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.denyProfilRequest(data))
|
||||
.then((res) => {
|
||||
// block when the request was successful
|
||||
})
|
||||
.catch((e) => this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
});
|
||||
},
|
||||
acceptProfilUpdate: function (data) {
|
||||
this.$api
|
||||
.call(ApiProfilUpdate.acceptProfilRequest(data))
|
||||
.then((res) => {
|
||||
// block when the request was successful
|
||||
})
|
||||
.catch((e) => this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
// update the data inside the table
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
});
|
||||
},
|
||||
setLoading: function (newValue) {
|
||||
this.loading = newValue;
|
||||
},
|
||||
hideAcceptDenyModal: function () {
|
||||
//? checks the AcceptDenyModal component property result, if the user made a successful request or not
|
||||
if (this.$refs.AcceptDenyModal.result) {
|
||||
//? refetches the data, if any request was denied or accepted
|
||||
//* setData will call the ajaxURL again to refresh the data
|
||||
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
} else {
|
||||
// when modal was closed without submitting request
|
||||
}
|
||||
this.showModal = false;
|
||||
this.modalData = null;
|
||||
},
|
||||
|
||||
showAcceptDenyModal(value) {
|
||||
this.modalData = value;
|
||||
if (!this.modalData) {
|
||||
return;
|
||||
}
|
||||
this.showModal = true;
|
||||
|
||||
// after a state change, wait for the DOM updates to complete
|
||||
Vue.nextTick(() => {
|
||||
this.$refs.AcceptDenyModal.show();
|
||||
});
|
||||
},
|
||||
|
||||
updateData: function (event) {
|
||||
this.$refs.UpdatesTable.tabulator.setData();
|
||||
//? store the selected view in the session storage of the browser
|
||||
sessionStorage.setItem("filter", event.target.value);
|
||||
},
|
||||
handleDataProcessed: function () {
|
||||
if (this.profil_update_id) {
|
||||
const arrayRowData = this.$refs.UpdatesTable.tabulator
|
||||
.getData()
|
||||
.filter((row) => {
|
||||
return row.profil_update_id === this.profil_update_id;
|
||||
});
|
||||
if (arrayRowData.length) {
|
||||
this.showAcceptDenyModal(arrayRowData[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
loading: function (newValue, oldValue) {
|
||||
if (newValue) {
|
||||
this.$refs.loadingModalRef.show();
|
||||
} else {
|
||||
this.$refs.loadingModalRef.hide();
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$p.loadCategory("profilUpdate").then(() => {
|
||||
this.categoryLoaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
mounted() {
|
||||
//? opens the AcceptDenyUpdate Modal if a preselected profil_update_id was passed to the component (used for email links)
|
||||
if (sessionStorage.getItem("filter")) {
|
||||
this.filter = sessionStorage.getItem("filter");
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div>
|
||||
|
||||
<accept-deny-update :title="$p.t('profilUpdate','profilUpdateRequest')" v-if="showModal" ref="AcceptDenyModal" @hideBsModal="hideAcceptDenyModal" :value="JSON.parse(JSON.stringify(modalData))" :setLoading="setLoading" ></accept-deny-update>
|
||||
<div class="form-underline flex-fill ">
|
||||
<div class="form-underline-titel">{{$p.t('ui','anzeigen')}} </div>
|
||||
|
||||
<select class="mb-4 form-select" v-model="filter" @change="updateData" aria-label="Profil updates display selection">
|
||||
<option :selected="true" :value="profilUpdateStates['Pending']" >{{$p.t('profilUpdate','pendingRequests')}}</option>
|
||||
<option :value="profilUpdateStates['Accepted']">{{$p.t('profilUpdate','acceptedRequests')}}</option>
|
||||
<option :value="profilUpdateStates['Rejected']">{{$p.t('profilUpdate','rejectedRequests')}}</option>
|
||||
<option :value="'Alle'">{{$p.t('profilUpdate','allRequests')}}</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
<loading ref="loadingModalRef" :timeout="0"></loading>
|
||||
|
||||
<core-filter-cmpt v-if="profilUpdateStates && categoryLoaded" :title="$p.t('profilUpdate','profilUpdateRequests')" ref="UpdatesTable" :tabulatorEvents="profilUpdateEvents" :tabulator-options="profilUpdateOptions" tableOnly :sideMenu="false" />
|
||||
|
||||
</div>`,
|
||||
};
|
||||
@@ -0,0 +1,271 @@
|
||||
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
|
||||
import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiOrt from '../../../api/factory/ort.js'
|
||||
export const Raumsuche = {
|
||||
name: "Raumsuche",
|
||||
props: {
|
||||
|
||||
},
|
||||
components: {
|
||||
VueDatePicker,
|
||||
CoreFilterCmpt,
|
||||
InputNumber: primevue.inputnumber,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
phrasenPromise: null,
|
||||
phrasenResolved: false,
|
||||
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('rauminfo/raumnummer')), field: 'nummer', widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$p.t('rauminfo/personcap')), 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()
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
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.$api.call(ApiOrt.getRoomTypes())
|
||||
.then(res => {
|
||||
res?.data?.forEach(type => {
|
||||
type.beschreibung = type.beschreibung.replace('&', '&')
|
||||
})
|
||||
this.selectedType = this.defaultType
|
||||
this.roomtypes = res?.data ?? []
|
||||
})
|
||||
},
|
||||
loadRooms() {
|
||||
this.$api.call(ApiOrt.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() {
|
||||
this.phrasenPromise = this.$p.loadCategory(['rauminfo', 'global'])
|
||||
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
|
||||
},
|
||||
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-12 col-lg-3">
|
||||
<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-12 col-lg-3">
|
||||
<InputNumber v-model="anzahl"
|
||||
:prefix="$p.t('rauminfo/minCapacity') + ': '"
|
||||
inputId="anzahlInput" :min="1" :max="1000"
|
||||
:style="{'width': '100%'}"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-lg-2">
|
||||
<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
|
||||
v-if="phrasenResolved"
|
||||
@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,85 @@
|
||||
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 {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>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
|
||||
}
|
||||
},
|
||||
emits: ['languageChanged'],
|
||||
methods:{
|
||||
changeLanguage: function(lang){
|
||||
if(this.allActiveLanguages.some(l => l.sprache === lang))
|
||||
{
|
||||
const isReload = document.querySelector('[cis4Reload]')
|
||||
this.$p.setLanguage(lang)
|
||||
.then(res => res.data)
|
||||
.then(data =>
|
||||
{
|
||||
if(isReload) window.location.reload()
|
||||
else this.$emit('languageChanged', lang);
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
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 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>
|
||||
|
||||
`
|
||||
};
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import BsPrompt from "../Bootstrap/Prompt.js";
|
||||
import DashboardAdminEdit from "./Admin/Edit.js";
|
||||
import DashboardAdminWidgets from "./Admin/Widgets.js";
|
||||
import DashboardAdminPresets from "./Admin/Presets.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DashboardAdminEdit,
|
||||
DashboardAdminWidgets,
|
||||
DashboardAdminPresets
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
adminMode: true
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dashboards: [],
|
||||
current: -1,
|
||||
widgets: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
apiurl() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
|
||||
},
|
||||
dashboard() {
|
||||
return this.dashboards.find(el => el.dashboard_id == this.current);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
dashboardAdd() {
|
||||
let _name = '';
|
||||
BsPrompt.popup('New Dashboard name').then(
|
||||
name => {
|
||||
_name = name;
|
||||
return axios.post(this.apiurl + '/Dashboard/create', {
|
||||
dashboard_kurzbz: name
|
||||
})
|
||||
}
|
||||
).then(res => {
|
||||
let newDashboard = {
|
||||
dashboard_id: res.data.retval,
|
||||
dashboard_kurzbz: _name,
|
||||
beschreibung: ''
|
||||
};
|
||||
this.dashboards.push(newDashboard);
|
||||
this.current = newDashboard.dashboard_id;
|
||||
}).catch(err => err !== undefined ? console.error('ERROR:', err) : 0);
|
||||
},
|
||||
dashboardUpdate(dashboard) {
|
||||
// TODO(chris): Loading or message
|
||||
axios.post(this.apiurl + '/Dashboard/update', dashboard).then(() => {
|
||||
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
|
||||
old.dashboard_kurzbz = dashboard.dashboard_kurzbz;
|
||||
old.beschreibung = dashboard.beschreibung;
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
dashboardDelete(dashboard_id) {
|
||||
axios.post(this.apiurl + '/Dashboard/delete', {dashboard_id}).then(() => {
|
||||
this.current = -1;
|
||||
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
assignWidgets(widgets) {
|
||||
this.widgets = widgets;
|
||||
/*while (this.widgets.length)
|
||||
this.widgets.pop();
|
||||
for (var i in widgets)
|
||||
this.widgets.push(widgets[i]);*/
|
||||
}
|
||||
},
|
||||
created() {
|
||||
axios.get(this.apiurl + '/Dashboard').then(res => {
|
||||
this.dashboards = res.data.retval;
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
template: `<div class="dashboard-admin">
|
||||
<div class="input-group">
|
||||
<label for="dashbaord-select" class="input-group-text">Dashboard:</label>
|
||||
<select id="dashbaord-select" class="form-select" v-model="current">
|
||||
<option v-for="dashboard in dashboards" :key="dashboard.dashboard_id" :value="dashboard.dashboard_id">{{dashboard.dashboard_kurzbz}}</option>
|
||||
</select>
|
||||
<button class="btn btn-outline-secondary" type="button" @click="dashboardAdd"><i class="fa-solid fa-plus"></i></button>
|
||||
</div>
|
||||
<div v-if="dashboard">
|
||||
<ul class="nav nav-tabs mt-3" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">Edit</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="widgets-tab" data-bs-toggle="tab" data-bs-target="#widgets" type="button" role="tab" aria-controls="widgets" aria-selected="true">Widgets</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="presets-tab" data-bs-toggle="tab" data-bs-target="#presets" type="button" role="tab" aria-controls="presets" aria-selected="false">Presets</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content pt-3">
|
||||
<div class="tab-pane fade" id="edit" role="tabpanel" aria-labelledby="edit-tab">
|
||||
<dashboard-admin-edit v-bind="dashboard" :key="dashboard.dashboard_id" @change="dashboardUpdate($event)" @delete="dashboardDelete($event)"></dashboard-admin-edit>
|
||||
</div>
|
||||
<div class="tab-pane fade show active" id="widgets" role="tabpanel" aria-labelledby="widgets-tab">
|
||||
<dashboard-admin-widgets :key="dashboard.dashboard_id" :dashboard_id="dashboard.dashboard_id" :widgets="widgets" @change="test" @assign-widgets="assignWidgets"></dashboard-admin-widgets>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="presets" role="tabpanel" aria-labelledby="presets-tab">
|
||||
<dashboard-admin-presets :dashboard="dashboard.dashboard_kurzbz" :widgets="widgets"></dashboard-admin-presets>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import BsConfirm from '../../Bootstrap/Confirm.js';
|
||||
|
||||
export default {
|
||||
emits: [
|
||||
"change",
|
||||
"delete"
|
||||
],
|
||||
props: {
|
||||
dashboard_id: Number,
|
||||
dashboard_kurzbz: String,
|
||||
beschreibung: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
kurzbz: this.dashboard_kurzbz,
|
||||
desc: this.beschreibung
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendDelete() {
|
||||
BsConfirm.popup('Sure?').then(() => this.$emit('delete', this.dashboard_id)).catch();
|
||||
}
|
||||
},
|
||||
template: `<div class="dashboard-admin-edit px-3">
|
||||
<div class="mb-3">
|
||||
<label for="dashboard-admin-edit-kurzbz">Kurz Bezeichnung</label>
|
||||
<input id="dashboard-admin-edit-kurzbz" type="text" class="form-control" v-model="kurzbz">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="dashboard-admin-edit-beschreibung">Beschreibung</label>
|
||||
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-danger" @click="sendDelete">Delete</button>
|
||||
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">Update</button>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import DashboardSection from "../Section.js";
|
||||
import DashboardWidgetPicker from "../Widget/Picker.js";
|
||||
import ObjectUtils from "../../../helpers/ObjectUtils.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DashboardSection,
|
||||
DashboardWidgetPicker
|
||||
},
|
||||
props: {
|
||||
dashboard: String,
|
||||
widgets: Array
|
||||
},
|
||||
data: () => ({
|
||||
funktionen: {},
|
||||
sections: [],
|
||||
tmpLoading: ''
|
||||
}),
|
||||
computed: {
|
||||
apiurl() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
|
||||
},
|
||||
pickerWidgets() {
|
||||
return this.widgets.filter(widget => widget.allowed);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
widgetAdd(section_name, widget) {
|
||||
this.$refs.widgetpicker.getWidget().then(widget_id => {
|
||||
widget.widget = widget_id;
|
||||
delete widget.custom;
|
||||
widget.preset = 1;
|
||||
let loading = {...widget};
|
||||
loading.loading = true;
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name)
|
||||
section.widgets.push(loading);
|
||||
});
|
||||
|
||||
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
|
||||
db: this.dashboard,
|
||||
funktion_kurzbz: section_name,
|
||||
widgets: [widget]
|
||||
}).then(result => {
|
||||
let newId = Object.keys(result.data.retval.data[section_name].widgets).pop();
|
||||
widget.id = newId;
|
||||
widget.custom = 1;
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name) {
|
||||
section.widgets.splice(section.widgets.indexOf(loading),1);
|
||||
section.widgets.push(widget);
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
widgetUpdate(section_name, payload) {
|
||||
payload = payload[section_name];
|
||||
for (var k in payload) {
|
||||
for (var i in this.sections) {
|
||||
if (this.sections[i].name == section_name) {
|
||||
for (var wid in this.sections[i].widgets) {
|
||||
if (this.sections[i].widgets[wid].id == k) {
|
||||
payload[k] = ObjectUtils.mergeDeep(this.sections[i].widgets[wid], payload[k]);
|
||||
// NOTE(chris): remove internal props
|
||||
for (var prop in {_x:1,_y:1,_w:1,_h:1,index:1,id:1})
|
||||
if (payload[k][prop])
|
||||
delete payload[k][prop];
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
payload[k].widgetid = k;
|
||||
delete payload[k].custom;
|
||||
}
|
||||
axios.post(this.apiurl + '/Config/addWidgetsToPreset', {
|
||||
db: this.dashboard,
|
||||
funktion_kurzbz: section_name,
|
||||
widgets: payload
|
||||
}).then(() => {
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name) {
|
||||
section.widgets.forEach((widget, i) => {
|
||||
if (payload[widget.id]) {
|
||||
payload[widget.id].id = widget.id;
|
||||
payload[widget.id].index = widget.index;
|
||||
section.widgets[i] = payload[widget.id];
|
||||
section.widgets[i].custom = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
// TODO(chris): revert placement on failure
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
});
|
||||
},
|
||||
widgetRemove(section_name, id) {
|
||||
axios.post(this.apiurl + '/Config/removeWidgetFromPreset', {
|
||||
db: this.dashboard,
|
||||
funktion_kurzbz: section_name,
|
||||
widgetid: id
|
||||
}).then(() => {
|
||||
this.sections.forEach(section => {
|
||||
if (section.name == section_name)
|
||||
section.widgets = section.widgets.filter(widget => widget.id != id);
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('ERROR: ', error);
|
||||
alert('ERROR: ' + error.response.data.retval);
|
||||
});
|
||||
},
|
||||
loadSections(evt) {
|
||||
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
|
||||
this.sections = [];
|
||||
this.tmpLoading = funktionen.join('###');
|
||||
axios.get(this.apiurl + '/Config/presetBatch', {params: {
|
||||
db: this.dashboard,
|
||||
funktionen
|
||||
}}).then(res => {
|
||||
if (this.tmpLoading !== funktionen.join('###'))
|
||||
return; // NOTE(chris): prevent race condition
|
||||
for (var section in res.data.retval) {
|
||||
let widgets = [];
|
||||
for (var wid in res.data.retval[section]) {
|
||||
res.data.retval[section][wid].id = wid;
|
||||
res.data.retval[section][wid].custom = 1;
|
||||
widgets.push(res.data.retval[section][wid]);
|
||||
}
|
||||
this.sections.push({
|
||||
name: section,
|
||||
widgets
|
||||
});
|
||||
}
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
axios.get(this.apiurl + '/Config/funktionen').then(res => {
|
||||
this.funktionen = {general: 'GENERAL'};
|
||||
res.data.retval.forEach(funktion => {
|
||||
this.funktionen[funktion.funktion_kurzbz] = funktion.beschreibung;
|
||||
});
|
||||
}).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
watch: {
|
||||
dashboard() {
|
||||
// TODO(chris): this should be done without a watcher
|
||||
this.loadSections({target:this.$refs.funktionenList});
|
||||
}
|
||||
},
|
||||
template: `<div class="dashboard-admin-presets">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<select ref="funktionenList" style="height:30em" class="form-control" multiple @input="loadSections">
|
||||
<option v-for="name,id in funktionen" :key="id" :value="id">{{ name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<dashboard-section v-for="section in sections" :key="section.name" :name="section.name" :widgets="section.widgets" @widget-add="widgetAdd" @widget-update="widgetUpdate" @widget-remove="widgetRemove"></dashboard-section>
|
||||
</div>
|
||||
</div>
|
||||
<dashboard-widget-picker ref="widgetpicker" :widgets="pickerWidgets"></dashboard-widget-picker>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
emits: [
|
||||
"change",
|
||||
"assignWidgets"
|
||||
],
|
||||
props: {
|
||||
dashboard_id: Number,
|
||||
widgets: Array
|
||||
},
|
||||
computed: {
|
||||
apiurl() {
|
||||
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/dashboard';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
sendChange(widget_id) {
|
||||
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
|
||||
axios.post(this.apiurl + '/Widget/setAllowed', {
|
||||
dashboard_id: this.dashboard_id,
|
||||
widget_id,
|
||||
action: allow ? 'add' : 'delete'
|
||||
}).catch(err => console.error('ERROR: ' + err));
|
||||
}
|
||||
},
|
||||
created() {
|
||||
axios.get(this.apiurl + '/Widget/getAll', {
|
||||
params:{
|
||||
dashboard_id: this.dashboard_id
|
||||
}
|
||||
}).then(
|
||||
result => {
|
||||
this.$emit('assignWidgets', result.data.retval.map(el => ({
|
||||
...el,
|
||||
...{setup:JSON.parse(el.setup),arguments:JSON.parse(el.arguments),allowed:!!el.allowed}
|
||||
})));
|
||||
}
|
||||
).catch(err => console.error('ERROR:', err));
|
||||
},
|
||||
template: `
|
||||
<div class="dashboard-admin-widgets">
|
||||
<div v-for="widget in widgets" :key="widget.widget_id" class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" :id="'dashboard-admin-widgets-' + widget.widget_id" v-model="widget.allowed" @input.prevent="sendChange(widget.widget_id)">
|
||||
<label class="form-check-label" :for="'dashboard-admin-widgets-' + widget.widget_id">{{(widget.setup && widget.setup.name) || widget.widget_kurzbz}}</label>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user