Merge branch 'master' into feature-63437/cis4_iframe_component

This commit is contained in:
Harald Bamberger
2025-09-22 17:03:42 +02:00
143 changed files with 9975 additions and 1548 deletions
+4 -1
View File
@@ -16,7 +16,10 @@
*/
import person from "./notiz/person.js";
import lehreinheit from "./notiz/lehreinheit.js";
export default {
person
person,
lehreinheit
};
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Person from './person.js';
export default {
...Person,
getNotizen(id, type) {
return {
method: 'get',
url: 'api/frontend/v1/notiz/notizLehreinheit/getNotizen/' + encodeURIComponent(id) + '/' + encodeURIComponent(type)
};
},
};
+14
View File
@@ -22,5 +22,19 @@ export default {
url: '/api/frontend/v1/Ort/ContentID',
params: { ort_kurzbz: ort_kurbz }
};
},
getRooms(datum, von, bis, typ, personenanzahl = 0) {
return {
method: 'get',
url: '/api/frontend/v1/Ort/getRooms',
params: { datum, von, bis, typ, personenanzahl }
};
},
getRoomTypes() {
return {
method: 'get',
url: '/api/frontend/v1/Ort/getTypes',
params: { }
};
}
};
+7
View File
@@ -68,6 +68,13 @@ export default {
params: dms
};
},
getProfilUpdateWithPermission(filter) {
const url_filter = (filter !== '') ? '/' + encodeURIComponent(filter) : '';
return {
method: 'get',
url: '/api/frontend/v1/ProfilUpdate/getProfilUpdateWithPermission' + url_filter
};
},
getProfilRequestFiles(requestID) {
return {
method: 'get',
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
info(app, path) {
return {
method: 'post',
url: '/api/frontend/v1/RouteInfo/info',
params: {
app: app,
path: path
}
};
}
};
+36
View File
@@ -0,0 +1,36 @@
export default {
getByLV(lehrveranstaltung_id)
{
return {
method: 'get',
url: '/api/frontend/v1/Lehrveranstaltung/loadByLV/' + encodeURIComponent(lehrveranstaltung_id)
};
},
getByStg(studiensemester_kurzbz, studiengang_kz, semester = null)
{
let path = "/api/frontend/v1/Lehrveranstaltung/getByStg/" + encodeURIComponent(studiensemester_kurzbz) + "/" + encodeURIComponent(studiengang_kz);
if (semester)
path += "/" + encodeURIComponent(semester);
return path;
},
getByEmp(studiensemester_kurzbz, mitarbeiter_uid, stg = null)
{
let path = "/api/frontend/v1/Lehrveranstaltung/getByEmp/" + encodeURIComponent(studiensemester_kurzbz) + "/" + encodeURIComponent(mitarbeiter_uid);
if (stg)
path += "/" + encodeURIComponent(stg);
return path;
},
getTable(url)
{
return {
method: 'get',
url: url
};
},
}
@@ -0,0 +1,30 @@
export default {
getStudiensemester()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getStudiensemester/'
};
},
getSprache()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getSprache/'
};
},
getLehrform()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getLehrform/'
};
},
getRaumtyp()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getRaumtyp/'
};
},
}
@@ -0,0 +1,26 @@
export default {
add(newData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/DirektGruppe/add/',
params: newData
};
},
delete(deleteData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/DirektGruppe/delete/',
params: deleteData
};
},
getByLehreinheit(lehreinheit_id)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/DirektGruppe/getByLehreinheit/' + encodeURIComponent(lehreinheit_id)
};
},
}
+56
View File
@@ -0,0 +1,56 @@
export default {
delete(deleteData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/gruppe/delete/',
params: deleteData
};
},
add(newData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/gruppe/add/',
params: newData
};
},
getByLehreinheit(lehreinheit_id)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/gruppe/getByLehreinheit/' + encodeURIComponent(lehreinheit_id)
};
},
deleteFromLVPlan(deleteData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/gruppe/deleteFromLVPlan/',
params: deleteData
};
},
/*------------- details -------- */
getAll()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/gruppe/getAll/'
};
},
getBenutzer()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/gruppe/getBenutzer/'
};
}
}
@@ -0,0 +1,41 @@
export default {
copy(data)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lehreinheit/copy/',
params: data
};
},
delete(data)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lehreinheit/delete/',
params: data
};
},
add(newData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lehreinheit/add/',
params: newData
};
},
get(lehreinheit)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lehreinheit/get/' + encodeURIComponent(lehreinheit)
};
},
update(updatedData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lehreinheit/update/',
params: updatedData
};
},
}
+78
View File
@@ -0,0 +1,78 @@
export default {
getLehrfunktionen()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLehrfunktionen/'
};
},
getLektoren()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLektoren/'
};
},
getByLehreinheit(lehreinheit_id)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLektorenByLE/' + encodeURIComponent(lehreinheit_id)
};
},
add(newData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lektor/add/',
params: newData
};
},
update(updatedData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lektor/update/',
params: updatedData
};
},
deletePerson(deleteData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lektor/deletePerson/',
params: deleteData
};
},
deleteFromLVPlan(deleteData)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lektor/deleteLVPlan/',
params: deleteData
};
},
getLektorDaten(lehreinheit_id, mitarbeiter_uid)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLektorDaten/' + encodeURIComponent(lehreinheit_id) + '/' + encodeURIComponent(mitarbeiter_uid)
};
},
}
+9
View File
@@ -0,0 +1,9 @@
export default {
getTabs()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getTabs/'
};
},
}
@@ -0,0 +1,43 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
get(path) {
let url = 'api/frontend/v1/lv/StgTree';
if (path)
url += '/' + path;
return {
method: 'get',
url
};
},
favorites: {
get() {
return {
method: 'get',
url: 'api/frontend/v1/lv/favorites'
};
},
set(favorites) {
return {
method: 'post',
url: 'api/frontend/v1/lv/favorites/set',
params: { favorites }
};
}
}
};
+54
View File
@@ -0,0 +1,54 @@
export default {
getTag(data)
{
return {
method: 'get',
url: 'api/frontend/v1/lv/Tags/getTag',
params: data
};
},
getTags(data)
{
return {
method: 'get',
url: 'api/frontend/v1/lv/Tags/getTags'
};
},
addTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/lv/Tags/addTag',
params: data
};
},
updateTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/lv/Tags/updateTag',
params: data
};
},
doneTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/lv/Tags/doneTag',
params: data
};
},
deleteTag(data)
{
return {
method: 'post',
url: 'api/frontend/v1/lv/Tags/deleteTag',
params: data
};
},
};
@@ -0,0 +1,22 @@
export default {
getByLeEmp(lehreinheit_id, mitarbeiter_uid)
{
return {
method: 'get',
url: '/api/frontend/v1/lv/lektor/getLektorDaten/' + encodeURIComponent(lehreinheit_id) + '/' + encodeURIComponent(mitarbeiter_uid),
};
},
cancelByLeEmp(needUpdate)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/lektor/cancelVertrag/',
params: needUpdate
};
},
}
+8 -2
View File
@@ -18,6 +18,7 @@ import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.j
import Studium from "../../components/Cis/Studium/Studium.js";
import ApiRenderers from '../../api/factory/renderers.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js';
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
@@ -138,10 +139,10 @@ const router = VueRouter.createRouter({
props: true
},
{
path: `/Cis/MyLv`,
path: `/Cis/MyLv/:studiensemester?`,
name: 'MyLv',
component: MylvStudent,
props: true
props: true,
},
{
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
@@ -319,6 +320,7 @@ const app = Vue.createApp({
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.use(router);
app.use(primevue.config.default, {
zIndex: {
@@ -331,3 +333,7 @@ app.use(PluginsPhrasen);
app.use(Theme);
app.directive('contrast', contrast);
app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
+102
View File
@@ -0,0 +1,102 @@
import LVVerwaltung from "../components/LVVerwaltung/LVVerwaltung.js";
import Phrasen from "../plugins/Phrasen.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}/LVVerwaltung`),
routes: [
{
name: 'index',
path: `/`,
component: LVVerwaltung
},
{
name: `byEmp`,
path: `/emp/:studiensemester_kurzbz/:emp/:stg?/:semester?`,
component: LVVerwaltung,
props: route => {
let {emp, stg, semester, studiensemester_kurzbz} = route.params;
if (emp === '')
emp = undefined;
if (stg === '')
stg = undefined;
if (studiensemester_kurzbz === '')
studiensemester_kurzbz = undefined;
return {
studiensemester_kurzbz: studiensemester_kurzbz,
emp: emp,
stg: stg,
semester: semester
};
},
beforeEnter: (to, from, next) => {
const { studiensemester_kurzbz } = to.params;
const isSemester = /^(SS|WS)\d{4}$/.test(studiensemester_kurzbz);
if (!isSemester)
return next({ path: '/' });
else
next();
}
},
/*{
name: `byFachbereich`,
path: `/fachbereich/:fachbereich/:emp?`,
component: LVVerwaltung
},*/
{
name: `byStg`,
path: '/stg/:studiensemester_kurzbz/:stg?/:semester?/',
component: LVVerwaltung,
props: route => {
let { studiensemester_kurzbz, stg, semester } = route.params;
if (semester === '')
semester = undefined;
if (studiensemester_kurzbz === '')
studiensemester_kurzbz = undefined;
if (stg === '')
stg = undefined;
return {
studiensemester_kurzbz: studiensemester_kurzbz,
stg: stg,
semester: semester,
};
},
beforeEnter: (to, from, next) => {
const studiensemester_kurzbz = to.params?.studiensemester_kurzbz
const isSemester = /^(SS|WS)\d{4}$/.test(studiensemester_kurzbz);
if (!isSemester)
return next({ path: '/' });
else
next();
}
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
},
]
});
const app = Vue.createApp();
app
.use(router)
.use(primevue.config.default, {
zIndex: {
overlay: 1100
}
})
.use(Phrasen)
.mount('#main');
@@ -57,65 +57,70 @@ export default {
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)";
}
});
}
},
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)";
}
});
},
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">
@@ -1,6 +1,6 @@
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
export default {
name: "GeneralComponent",
name: "NewsContentType",
props:{
content:{
type:String,
@@ -12,7 +12,7 @@ export default {
required:true,
},
content_id:{
type:Number,
type: [Number, String],
}
},
methods: {
@@ -129,7 +129,7 @@ export default {
template: /*html*/ `
<!-- div that contains the content -->
<!-- TODO: test with more img content from cms-->
<div v-if="imgContent"><img v-bind="imgContent"></img></div>
<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>
`,
+8 -1
View File
@@ -43,6 +43,13 @@ export default {
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>
@@ -54,7 +61,7 @@ export default {
: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">{{menuItem.name}}</p>
<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">
@@ -19,6 +19,7 @@ export default {
props: {
lehrveranstaltung_id: Number,
bezeichnung: String,
bezeichnung_eng: String,
module: String,
farbe: String,
lvinfo: Boolean,
+12 -7
View File
@@ -75,21 +75,26 @@ export default {
this.$refs.studiensemester.selectedIndex++;
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
},
setHash(val) {
// TODO: make this a router param to enable history
location.hash = val;
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 hash = location.hash.substring(1);
if (hash && this.studiensemester.filter(s => s.studiensemester_kurzbz == hash).length)
this.currentSemester = hash;
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>
@@ -104,7 +109,7 @@ export default {
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)">
<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">
@@ -11,6 +11,7 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js';
export default {
components: {
@@ -40,7 +41,7 @@ export default {
persistence: {
columns: false
},
height: 300,
minHeight: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
@@ -74,18 +75,24 @@ export default {
{
title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase),
field: "Gültig_von",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
resizable: true,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
{
title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase),
field: "Gültig_bis",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
resizable: true,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
{
title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase),
@@ -102,7 +109,7 @@ export default {
persistence: {
columns: false
},
height: 300,
minHeight: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
@@ -138,9 +145,12 @@ export default {
{
title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase),
field: "Ausgegeben_am",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
],
}
@@ -167,11 +177,11 @@ export default {
this.$api
.call(ApiProfilUpdate.selectProfilRequest())
.then((request) => {
if (!request.error && 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: " + res.data);
console.error("Error when fetching profile updates: " + request);
}
})
.catch((err) => {
@@ -211,6 +221,15 @@ export default {
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)
},
datetimeFormatterParams: function() {
const params = {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd.MM.yyyy",
invalidPlaceholder:"(invalid date)",
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone
};
return params;
}
},
@@ -5,6 +5,8 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import { dateFilter } from '../../../tabulator/filters/Dates.js';
export default {
components: {
CoreFilterCmpt,
@@ -24,7 +26,7 @@ export default {
persistence: {
columns: false
},
height: 300,
minHeight: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
@@ -60,18 +62,24 @@ export default {
{
title: Vue.computed(() => this.$p.t('global/gueltigVon')),
field: "Gültig_von",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
resizable: true,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
{
title: Vue.computed(() => this.$p.t('global/gueltigBis')),
field: "Gültig_bis",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
resizable: true,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
{
title: Vue.computed(() => this.$p.t('profil/wochenstunden')),
@@ -91,6 +99,15 @@ export default {
funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
},
datetimeFormatterParams: function() {
const params = {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd.MM.yyyy",
invalidPlaceholder:"(invalid date)",
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone
};
return params;
}
},
watch: {
'data.funktionen'(newVal) {
@@ -88,6 +88,18 @@ export default {
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
@@ -10,21 +10,34 @@ export default {
type: String,
}
},
inject: [
'studiengang_kz', // inject info that should not be displayed
],
inject: {
// inject info that should not be displayed
'studiengang_kz': {
from: 'studiengang_kz',
default: false
},
},
computed: {
getLinkGruppeListe() {
if(this.studiengang_kz === false) {
return '';
}
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() {
if(this.studiengang_kz === false) {
return '';
}
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() {
if(this.studiengang_kz === false) {
return '';
}
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 : ''
}
@@ -1,8 +1,11 @@
import Dms from "../../../../Form/Upload/Dms.js";
import ApiProfil from '../../../../../api/factory/profil.js';
export default {
components: {
AutoComplete: primevue.autocomplete,
Dms: Dms
},
props: {
@@ -11,9 +14,13 @@ export default {
type: Boolean,
default: false,
},
files: {
type: Array,
default: []
},
},
inject: ["getZustelladressenCount"],
inject: ["getZustelladressenCount", "updateFileID"],
data() {
return {
@@ -23,6 +30,8 @@ export default {
nationenList: [],
originalValue: null,
zustellAdressenCount: null,
dmsData: [],
fileschanged: false
};
},
@@ -83,6 +92,11 @@ export default {
//? 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;
}
@@ -91,6 +105,11 @@ export default {
// update the zustellAdressen count
this.zustellAdressenCount = this.getZustelladressenCount();
},
deleteDmsData: function() {
this.dmsData = [];
this.updateValue(null, 'files');
}
},
computed: {
@@ -111,12 +130,14 @@ export default {
!this.data.strasse ||
!this.data.plz ||
!this.data.ort ||
!this.data.typ
!this.data.typ ||
this.dmsData.length === 0
) {
return false;
}
return this.originalValue !== JSON.stringify(this.data);
const datachanged = this.originalValue !== JSON.stringify(this.data);
return datachanged || this.fileschanged;
},
},
@@ -133,6 +154,12 @@ export default {
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 -->
@@ -212,9 +239,28 @@ export default {
</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>&nbsp;</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>
`,
@@ -16,7 +16,7 @@ export default {
mixins: [BsModal],
props: {
titel: {
type: Object,
type: String,
},
files: {
type: Array,
@@ -57,7 +57,7 @@ export default {
},
template: /*html*/`
<bs-modal v-show="!loading" ref="modalContainer" v-bind="$props" body-class="" dialog-class="modal-lg" class="bootstrap-alert" :backdrop="false">
<bs-modal 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>
@@ -112,14 +112,17 @@ export default {
<div class="form-underline-titel">{{topic}}</div>
<span class="form-underline-content">{{data.value}} </span>
</div>
<div v-if="files?.length" class="ms-2">
<a target="_blank" :href="getDocumentLink(file.dms_id)" v-for="file in files">{{file.name}}</a>
</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>
`,
};
@@ -11,6 +11,7 @@ import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js";
import EditProfil from "./ProfilModal/EditProfil.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js';
export default {
components: {
@@ -39,7 +40,7 @@ export default {
persistence: {
columns: false
},
height: 200,
minHeight: 200,
layout: "fitColumns",
columns: [{
title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase),
@@ -51,7 +52,7 @@ export default {
persistence: {
columns: false
},
height: 300,
minHeight: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
@@ -85,9 +86,12 @@ export default {
{
title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) ,
field: "Ausgegeben_am",
headerFilter: true,
headerFilterFunc: 'dates',
headerFilter: dateFilter,
minWidth: 200,
visible: true
visible: true,
formatter:"datetime",
formatterParams: this.datetimeFormatterParams()
},
],
},
@@ -133,11 +137,11 @@ export default {
this.$api
.call(ApiProfilUpdate.selectProfilRequest())
.then((request) => {
if (!request.error && res) {
if (!request.error && request.data) {
this.data.profilUpdates = request.data;
this.data.profilUpdates.sort(this.sortProfilUpdates);
} else {
console.error("Error when fetching profile updates: " + res.data);
console.error("Error when fetching profile updates: " + request);
}
})
.catch((err) => {
@@ -160,6 +164,15 @@ export default {
this.$refs.editModal.show();
});
},
datetimeFormatterParams: function() {
const params = {
inputFormat:"yyyy-MM-dd",
outputFormat:"dd.MM.yyyy",
invalidPlaceholder:"(invalid date)",
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone
};
return params;
}
},
computed: {
@@ -188,16 +188,21 @@ export default {
<span class="form-underline-content" >{{data.requested_change.value}}</span>
</div>
<div v-if="files?.length" class="ms-2">
<a v-for="file in files" target="_blank" :href="getDocumentLink(file.dms_id)" >{{file.name}}</a>
</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>
@@ -31,10 +31,10 @@ export default {
Loading,
AcceptDenyUpdate,
},
inject: ["profilUpdateTopic", "profilUpdateStates"],
inject: ["profilUpdateStates"],
props: {
id: {
type: Number,
type: String,
},
},
data() {
@@ -44,51 +44,53 @@ export default {
modalData: null,
loading: false,
filter: "Pending",
events: [],
profil_update_id: Number(this.id),
};
},
computed:{
profilUpdateOptions: function(){
return {
ajaxURL:
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
`/Cis/ProfilUpdate/`,
ajaxURLGenerator: (url, config, params) => {
//? this function needs to be an array function in order to access the this properties of the Vue component
switch (this.filter) {
case this.profilUpdateStates["Pending"]:
return (
url +
`getProfilUpdateWithPermission/${this.profilUpdateStates["Pending"]}`
);
case this.profilUpdateStates["Accepted"]:
return (
url +
`getProfilUpdateWithPermission/${this.profilUpdateStates["Accepted"]}`
);
case this.profilUpdateStates["Rejected"]:
return (
url +
`getProfilUpdateWithPermission/${this.profilUpdateStates["Rejected"]}`
);
default:
return url + `getProfilUpdateWithPermission`;
}
},
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)
response.sort((ele1, ele2) => sortProfilUpdates(ele1, ele2, this));
if (response?.data)
response.data.sort((ele1, ele2) => sortProfilUpdates(ele1, ele2, this));
return response;
return response.data;
},
//? adds tooltip with the status message of a profil update request if its status is not pending
columnDefaults: {
@@ -356,7 +358,19 @@ export default {
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) {
@@ -374,20 +388,7 @@ export default {
},
mounted() {
//? opens the AcceptDenyUpdate Modal if a preselected profil_update_id was passed to the component (used for email links)
if (this.profil_update_id) {
this.$refs.UpdatesTable.tabulator.on("dataProcessed", () => {
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]);
}
});
}
//? 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");
}
@@ -409,7 +410,7 @@ export default {
</div>
<loading ref="loadingModalRef" :timeout="0"></loading>
<core-filter-cmpt v-if="profilUpdateStates && categoryLoaded" :title="$p.t('profilUpdate','profilUpdateRequests')" ref="UpdatesTable" :tabulatorEvents="events" :tabulator-options="profilUpdateOptions" tableOnly :sideMenu="false" />
<core-filter-cmpt v-if="profilUpdateStates && categoryLoaded" :title="$p.t('profilUpdate','profilUpdateRequests')" ref="UpdatesTable" :tabulatorEvents="profilUpdateEvents" :tabulator-options="profilUpdateOptions" tableOnly :sideMenu="false" />
</div>`,
};
+21 -22
View File
@@ -1,6 +1,6 @@
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: {
@@ -13,6 +13,8 @@ export const Raumsuche = {
},
data() {
return {
phrasenPromise: null,
phrasenResolved: false,
tabulatorUuid: Vue.ref(0),
tableBuiltResolve: null,
tableBuiltPromise: null,
@@ -54,8 +56,8 @@ export const Raumsuche = {
columns: [
{title: Vue.computed(() => this.$p.t('rauminfo/raum_kurzbz')), field: 'ort_kurzbz', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('global/bezeichnung')), field: 'bezeichnung', widthGrow: 2},
{title: Vue.computed(() => this.$p.t('global/nummer')), field: 'nummer', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('global/personen')), field: 'personen', widthGrow: 1},
{title: Vue.computed(() => this.$p.t('rauminfo/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')),
@@ -68,17 +70,6 @@ export const Raumsuche = {
handler: async () => {
this.tableBuiltResolve()
}
},
{
event: "cellClick",
handler: async (e, cell) => {
if((cell.column.field === 'linkInfo' || cell.column.field === 'linkRes') && cell.value){
window.open(cell.value, '_blank');
e.stopPropagation();
}
}
}
]};
},
@@ -132,7 +123,8 @@ export const Raumsuche = {
this.$refs.raumsucheTable.tabulator.setData(d);
},
loadRoomTypes() {
this.$fhcApi.factory.ort.getRoomTypes().then(res => {
this.$api.call(ApiOrt.getRoomTypes())
.then(res => {
res?.data?.forEach(type => {
type.beschreibung = type.beschreibung.replace('&amp;', '&')
})
@@ -141,7 +133,7 @@ export const Raumsuche = {
})
},
loadRooms() {
this.$fhcApi.factory.ort.getRooms(this.datum.toISOString(), this.getTimeString(this.von), this.getTimeString(this.bis), this.selectedType?.raumtyp_kurzbz ?? '', this.anzahl)
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)
})
@@ -167,6 +159,7 @@ export const Raumsuche = {
return `${hours}:${minutes}`;
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
@@ -182,7 +175,7 @@ export const Raumsuche = {
if(this.$refs.raumsucheTable) {
this.$refs.raumsucheTable.$refs.table.style.setProperty('height', h+'px')
}
}
},
computed: {
@@ -191,7 +184,8 @@ export const Raumsuche = {
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['rauminfo', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
},
mounted() {
this.setupMounted()
@@ -239,7 +233,7 @@ export const Raumsuche = {
</VueDatePicker>
</div>
<div class="col-lg-auto">
<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>
@@ -248,16 +242,21 @@ export const Raumsuche = {
</div>
<div class="col-4 col-lg-2">
<InputNumber v-model="anzahl" :prefix="$p.t('rauminfo/anzahlPersonen') + ': '" inputId="anzahlInput" :min="1" :max="100" />
<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-8 col-lg-2 d-flex justify-content-center align-items-center">
<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"
@@ -23,14 +23,6 @@ export default {
this.$p.t('person/ort'),
this.event.ort_kurzbz
].join(": "));
this.event.lektor = [
this.event.lektor[0],
this.event.lektor[0],
this.event.lektor[0],
this.event.lektor[0],
this.event.lektor[0],
];
if (Array.isArray(this.event.lektor) && this.event.lektor.length > 0) {
if (this.event.lektor.length > 3) {
+1 -1
View File
@@ -156,7 +156,7 @@ export default {
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</div>
<div v-else-if="!hidden || editMode" :id="widgetID" class="dashboard-item card overflow-hidden h-100 position-relative" :class="{'draggedItem':dragstate, 'dashboard-item-overlay':resizeOverlay, [arguments?.className]:arguments && arguments.className}">
<div v-else-if="!hidden || editMode" :id="widgetID" class="dashboard-item card overflow-hidden h-100 position-relative" :class="{'hiddenWidget':hidden, 'draggedItem':dragstate, 'dashboard-item-overlay':resizeOverlay, [arguments?.className]:arguments && arguments.className}">
<div v-show="!dragstate" class="h-100 card border-0">
<div v-if="widget" class="card-header d-flex ps-0 pe-2 align-items-center">
<Transition>
+5 -5
View File
@@ -106,7 +106,7 @@ export default {
setPrev() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1
this.setSelected(this.allNewsList[prevIndex])
this.updateNewsContentClasses();
},
@@ -118,14 +118,14 @@ export default {
return classString
},
async setSelected(news) {
let clickedElement = document.getElementById('card-'+news.news_id);
let clickedElementIndex = this.allNewsList.indexOf(news);
let oldElementIndex = this.allNewsList.indexOf(this.selected);
const clickedElement = document.getElementById('card-'+news.news_id);
const clickedElementIndex = this.allNewsList.indexOf(news);
const oldElementIndex = this.allNewsList.indexOf(this.selected);
//if the clicked element is already active, do nothing
if(clickedElementIndex === oldElementIndex) return;
//add prev/next class to the clicked element
if(clickedElementIndex > oldElementIndex){
if(clickedElementIndex > oldElementIndex) {
clickedElement.classList.add('carousel-item-next');
}else{
clickedElement.classList.add('carousel-item-prev');
+1 -1
View File
@@ -159,7 +159,7 @@ export default {
// this.$emit('setConfig', true); -> use this to enable widget config mode if needed
},
template: /*html*/ `
<div class="widgets-url w-100 h-100 overflow-scroll" style="padding: 1rem 1rem;">
<div class="widgets-url w-100 h-100 overflow-auto" style="padding: 1rem 1rem;">
<div class="d-flex flex-column justify-content-between">
<button class="btn btn-outline-secondary btn-sm w-100 mt-2 card" @click="openCreateModal" type="button">{{$p.t('bookmark','newLink')}}</button>
+46 -20
View File
@@ -274,8 +274,23 @@ export default {
dragging(event){
if(this.mode == MODE_MOVE){
this.toggleDraggedItemOverlay(true);
this.clonedWidget.style.top = `${this.clientY-20}px`;
this.clonedWidget.style.left = `${this.clientX-15}px`;
const containerRect = this.$refs.container.getBoundingClientRect();
const clonedWidgetRect = this.clonedWidget.getBoundingClientRect();
let desiredTop = this.clientY - 20;
let desiredLeft = this.clientX - 15;
const minTop = 0;
const maxTop = containerRect.height - clonedWidgetRect.height;
const minLeft = 0;
const maxLeft = containerRect.width - clonedWidgetRect.width;
const constrainedTop = Math.max(minTop, Math.min(maxTop, desiredTop));
const constrainedLeft = Math.max(minLeft, Math.min(maxLeft, desiredLeft));
this.clonedWidget.style.top = `${constrainedTop}px`;
this.clonedWidget.style.left = `${constrainedLeft}px`;
}
},
createNewGrid(items) {
@@ -413,11 +428,14 @@ export default {
setTimeout(() => {
this.draggedNode = evt.target.closest(".drop-grid-item");
//clones the widget for the drag Image
let clone = evt.target.closest(".drop-grid-item")?.cloneNode(true);
clone.style.zIndex = 5;
clone.classList.add("widgetClone");
this.$refs.container.appendChild(clone);
const hiddenWidget = clone.querySelector("[style='display: none;']");
hiddenWidget.style.removeProperty("display");
this.clonedWidget = clone;
}, 0);
@@ -434,12 +452,9 @@ export default {
},
dragOver(evt) {
if ((this.y + 1) > this.rows && (this.mode == MODE_MOVE || this.mode == MODE_RESIZE)) {
this.positionUpdates = this.positionUpdates?.filter(item => {
return item.widgetid == this.draggedItem.data.widgetid;
})
this.dragEnd();
this.dragCancel();
}
}
if (!this.active)
return this.dragCancel();
this.checkPinnedWidgetAnimation();
@@ -454,17 +469,17 @@ export default {
let x = this.x + this.draggedOffset[0];
let y = this.y + this.draggedOffset[1];
if (x < 0) {
this.draggedOffset[0] -= x;
this.draggedOffset[0] += x;
x = 0;
} else if (x + this.draggedItem.w > this.cols) {
this.draggedOffset[0] += this.cols - this.draggedItem.w - x;
x = this.cols - this.draggedItem.w;
}
if (y < 0) {
this.draggedOffset[1] -= y;
this.draggedOffset[1] += y;
y = 0;
}
this.positionUpdates = this.dragGrid.move(this.draggedItem, x, y);
this.positionUpdates= this.dragGrid.move(this.draggedItem, x, y);
break;
}
case MODE_RESIZE: {
@@ -481,6 +496,7 @@ export default {
}
},
dragCancel() {
this.removeWidgetClones();
this.additionalRowComputed = false;
this.toggleDraggedItemOverlay(false);
this.mode = MODE_IDLE;
@@ -492,8 +508,12 @@ export default {
},
dragEnd() {
if (this.mode == MODE_IDLE)
this.removeWidgetClones();
this.toggleDraggedItemOverlay(false);
if (this.mode == MODE_IDLE){
return;
}
// clean up unused classes
let draggedItemNode = document.getElementById(this.draggedItem.data.widgetid);
draggedItemNode.classList.remove("border-danger");
@@ -501,19 +521,19 @@ export default {
ele.classList.remove("denied-dragging-animation");
})
let widgetClones = document.getElementsByClassName("widgetClone");
for (let i=0; i <widgetClones.length; i++){
this.$refs.container.removeChild(widgetClones[i]);
}
if (!this.active || this.x < 0 || this.y < 0 || this.x >= this.cols)
return this.dragCancel();
//if (!this.active || this.x < 0 || this.y < 0 || this.x >= this.cols)
//return this.dragCancel();
this.mode = MODE_IDLE;
let updated = [];
this.convertGridResultToUpdate(this.positionUpdates, updated);
updated = this._updateFixedPositions(updated);
if (updated.length)
this.$emit('rearrangeItems', updated.filter(v => v));
this.draggedItem = null;
this.draggedNode = null;
this.$emit('draggedItem', null);
},
_updateFixedPositions(updated) {
updated.forEach((item, index) => {
@@ -597,6 +617,12 @@ export default {
draggedItemNode.classList.remove("border-danger");
}
},
removeWidgetClones(){
let widgetClones = Array.from(document.getElementsByClassName("widgetClone"));
for (let i = 0; i < widgetClones.length; i++) {
this.$refs.container.removeChild(widgetClones[i]);
}
},
mouseDown(){
this.mode = MODE_MOUSE_DOWN;
},
@@ -612,7 +638,7 @@ export default {
@touchmove="dragOver"
@touchend="dragCancel"
@dragover.prevent="dragOver"
@drop="dragEnd"
@drop="dragEnd($event)"
@mousemove="updateCursorOnMouseMove"
@mouseleave="mouseLeave">
<TransitionGroup tag="div">
@@ -626,7 +652,7 @@ export default {
@mouse-up="mouseUp"
@start-resize="startResize"
@dragging="dragging"
@end-drag="dragCancel"
@end-drag="dragEnd"
@touch-end="dragEnd();mouseUp();"
@touch-start="updateCursorOnMouseMove($event); mouseDown();"
class="position-absolute"
@@ -0,0 +1,155 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiDirektGruppe from "../../../api/lehrveranstaltung/direktgruppe.js";
export default{
name: "LVDirektGruppen",
components: {
CoreFilterCmpt,
FormForm,
FormInput
},
props: {
lehreinheit_id: Number
},
inject: {
dropdowns: {
from: 'dropdowns'
},
},
computed: {
tabulatorOptions() {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiDirektGruppe.getByLehreinheit(this.lehreinheit_id)),
ajaxResponse: (url, params, response) => { return response.data || [] },
columns:[
{title: this.$p.t('person', 'uid'), field:"uid"},
{title: this.$p.t('person', 'vorname'), field:"vorname"},
{title: this.$p.t('person', 'nachname'), field:"nachname"},
{title: this.$p.t('lehre', 'gruppe'), field:"gruppe_kurzbz"},
{
title: this.$p.t('global', 'actions'), field: 'actions',
minWidth: 150,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
let button = document.createElement('button');
container.className = "d-flex gap-1";
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', (event) => {
event.stopPropagation();
this.deleteDirektGroup(cell.getData().gruppe_kurzbz, cell.getData().uid)
});
container.append(button);
return container;
},
frozen: true
},
],
layout: 'fitDataFill',
selectable: true,
persistenceID: 'lvverwaltung_direkt_gruppen_2025_05_27_v1',
height: 'auto',
}
}
},
data() {
return{
lastSelected: null,
gruppen: [],
tabulatorEvents: [],
showAutocomplete: false,
selectedUser: null,
filteredUsers: [],
abortController: null,
searchTimeout: null,
}
},
watch: {
lehreinheit_id(n) {
this.$refs.table.reloadTable();
}
},
methods:{
reload() {
this.$refs.table.reloadTable();
},
deleteDirektGroup(gruppe_kurzbz, uid)
{
let deleteData = {
'gruppe_kurzbz': gruppe_kurzbz,
'uid': uid,
'lehreinheit_id': this.lehreinheit_id
}
this.$api.call(ApiDirektGruppe.delete(deleteData))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
this.reload();
})
},
searchUser(event)
{
const query = event.query.toLowerCase().trim();
this.filteredUsers = this.dropdowns.benutzer_array.filter(user => {
const fullName = `${user.vorname.toLowerCase()} ${user.nachname.toLowerCase()}`;
const reverseFullName = `${user.nachname.toLowerCase()} ${user.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || user.uid.toLowerCase().includes(query) || user.studiengang.toLowerCase().includes(query);
}).map(user => ({
label: user.studiengang
? `${user.nachname} ${user.vorname} ${user.uid} ${user.studiengang} ${user.semester}`
: `${user.nachname} ${user.vorname} ${user.uid}`,
uid: user.uid
}));
},
addUser()
{
let newData = {
'uid': this.selectedUser.uid,
'lehreinheit_id': this.lehreinheit_id
}
return this.$api.call(ApiDirektGruppe.add(newData))
.then(result => {
this.reload()
this.selectedUser = ''
})
.catch(this.$fhcAlert.handleSystemError);
},
},
template: `
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
:reload=true
:new-btn-label="$p.t('lehre', 'assignPerson')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredUsers"
:placeholder="$p.t('lehre', 'assignPerson')"
v-model="selectedUser"
field="label"
:minLength="3"
@item-select="addUser"
@complete="searchUser"
></form-input>
</template>
</core-filter-cmpt>
`
};
@@ -0,0 +1,221 @@
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
export default {
name: "LVDetailsForm",
components: {
FormInput,
CoreForm
},
props: {
data: {
type: Object,
required: true
}
},
inject: {
dropdowns: {
from: 'dropdowns'
},
showLVID: {
from: 'permissionLehrveranstaltung',
default: false
},
showGewichtung: {
from: 'configShowGewichtung',
default: true
}
},
computed: {
formattedAnmerkung: {
get() {
return (this.data.anmerkung || '').replace(/\\n/g, '\n');
},
set(value) {
this.data.anmerkung = (value || '').replace(/\n/g, '\\n');
}
}
},
template: `
<div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'lehrfach')"
type="select"
container-class="col-3"
v-model="data.lehrfach_id"
name="lehrfach_id"
>
<option
v-for="lehrfach in data.lehrfaecher"
:value="lehrfach.lehrveranstaltung_id"
:key="lehrfach.lehrfach"
>
{{ lehrfach.lehrfach }}
</option>
</form-input>
<form-input
:label="$p.t('lehre', 'lehrform')"
type="select"
container-class="col-3"
v-model="data.lehrform_kurzbz"
name="lehrform_kurzbz"
>
<option
v-for="lehrform in dropdowns.lehrform_array"
:value="lehrform.lehrform_kurzbz"
:key="lehrform.lehrform_kurzbz"
>
{{ lehrform.bez_kurz }} {{ lehrform.bez }}
</option>
</form-input>
<form-input
:label="$p.t('lehre', 'detailanmerkung')"
type="textarea"
container-class="col-3"
v-model="formattedAnmerkung"
name="anmerkung"
id="anmerkung"
rows="10"
/>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'raumtyp')"
type="select"
container-class="col-3"
v-model="data.raumtyp"
name="raumtyp"
>
<option
v-for="raumtyp in dropdowns.raumtyp_array"
:value="raumtyp.raumtyp_kurzbz"
:key="raumtyp.raumtyp_kurzbz"
>
{{ raumtyp.raumtyp_kurzbz }} {{ raumtyp.beschreibung }}
</option>
</form-input>
<form-input
:label="$p.t('lehre', 'raumtypalternativ')"
type="select"
container-class="col-3"
v-model="data.raumtypalternativ"
name="raumtypalternativ"
>
<option
v-for="raumtyp in dropdowns.raumtyp_array"
:value="raumtyp.raumtyp_kurzbz"
:key="raumtyp.raumtyp_kurzbz + '-alt'"
>
{{ raumtyp.raumtyp_kurzbz }} {{ raumtyp.beschreibung }}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('global', 'sprache')"
type="select"
container-class="col-3"
v-model="data.sprache"
name="sprache"
>
<option
v-for="sprache in dropdowns.sprachen_array"
:key="sprache.sprache"
:value="sprache.sprache"
>
{{ sprache.sprache }}
</option>
</form-input>
<form-input
:label="$p.t('lehre', 'studiensemester')"
type="select"
container-class="col-3"
v-model="data.studiensemester_kurzbz"
name="studiensemester_kurzbz"
>
<option
v-for="semester in dropdowns.studiensemester_array"
:key="semester.studiensemester_kurzbz"
:value="semester.studiensemester_kurzbz"
>
{{ semester.studiensemester_kurzbz }}
</option>
</form-input>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'startkw')"
type="number"
min="0"
container-class="col-1"
v-model="data.start_kw"
name="start_kw"
/>
<form-input
:label="$p.t('lehre', 'stundenblockung')"
type="number"
min="0"
container-class="col-1"
v-model="data.stundenblockung"
name="stundenblockung"
/>
<form-input
:label="$p.t('lehre', 'wochenrhythmus')"
type="number"
min="0"
container-class="col-1"
v-model="data.wochenrythmus"
name="wochenrythmus"
/>
<div class="col-3 d-flex align-items-end">
<form-input
:label="$p.t('lehre', 'lehre')"
type="checkbox"
v-model="data.lehre"
name="lehre"
/>
</div>
</div>
<div class="row mb-3">
<form-input
v-if="showLVID"
:label="$p.t('lehre', 'lehrveranstaltung_id')"
type="text"
container-class="col-2"
v-model="data.lehrveranstaltung_id"
name="lehrveranstaltung_id"
/>
<form-input
:label="$p.t('lehre', 'unr')"
type="text"
container-class="col-1"
v-model="data.unr"
name="unr"
/>
<form-input
v-if="showGewichtung"
:label="$p.t('lehre', 'gewicht')"
type="text"
container-class="col-1"
v-model="data.gewicht"
name="gewicht"
/>
</div>
</div>
`
};
@@ -0,0 +1,195 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiGruppe from "../../../api/lehrveranstaltung/gruppe.js";
export default{
name: "LVGruppen",
components: {
CoreFilterCmpt,
FormForm,
FormInput
},
props: {
lehreinheit_id: Number
},
inject: {
dropdowns: {
from: 'dropdowns'
},
showLVPlanGruppenDel: {
from: 'permissionGruppenEntfernen',
default: false
},
},
computed: {
tabulatorOptions() {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiGruppe.getByLehreinheit(this.lehreinheit_id)),
ajaxResponse: (url, params, response) => response.data,
columns:[
{title: this.$p.t('global', 'bezeichnung'), field:"bezeichnung"},
{title: this.$p.t('global', 'beschreibung'), field:"beschreibung"},
{title: this.$p.t('lehre', 'studiengang'), field:"studiengang"},
{title: "ID", field:"id", visible:false},
{
title: this.$p.t('lehre', 'verplant'),
field:"verplant",
formatter:"tickCross",
hozAlign:"center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{
title: this.$p.t('global', 'actions'), field: 'actions',
minWidth: 150,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
let button = document.createElement('button');
container.className = "d-flex gap-1";
button.className = 'btn btn-outline-secondary';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', (event) => {
event.stopPropagation();
this.deleteGroup(cell.getData().lehreinheitgruppe_id)
});
container.append(button);
if (this.showLVPlanGruppenDel)
{
button = document.createElement('button');
container.className = "d-flex gap-2";
button.className = 'btn btn-outline-secondary';
button.innerHTML = '<i class="fa fa-calendar-xmark"></i>';
button.title = this.$p.t('lehre', 'auslvplanentfernen');
button.disabled = !cell.getData().verplant;
button.addEventListener('click', (event) => {
event.stopPropagation();
this.deleteLVPlan(cell.getData().lehreinheitgruppe_id)
});
container.append(button);
}
return container;
},
frozen: true
},
],
layout: 'fitDataFill',
selectable: true,
persistenceID: 'lvverwaltung_gruppen_2025_05_27_v1',
height: 'auto',
}
}
},
data() {
return{
tabulatorEvents: [],
showAutocomplete: false,
filteredGroups: [],
selectedGroup: null
}
},
watch: {
lehreinheit_id() {
this.$refs.table.reloadTable();
}
},
methods:{
searchGroup(event)
{
const query = event.query.toLowerCase().trim();
this.filteredGroups = this.dropdowns.gruppen_array.filter(gruppe => {
return gruppe.gruppe_kurzbz.toLowerCase().includes(query) || gruppe?.bezeichnung?.toLowerCase().includes(query);
}).map(gruppe => ({
label: gruppe.bezeichnung
? `${gruppe.gruppe_kurzbz.trim()} (${gruppe.bezeichnung})`
: gruppe.gruppe_kurzbz.trim(),
gid: gruppe.gid,
gruppe_kurzbz: gruppe.gruppe_kurzbz.trim(),
lehrverband: gruppe.lehrverband,
}));
},
reload() {
this.$refs.table.reloadTable();
},
addGroup()
{
let newData = {
'gid': this.selectedGroup.gid,
'lehreinheit_id': this.lehreinheit_id,
'lehrverband': this.selectedGroup.lehrverband,
'gruppe_kurzbz' : this.selectedGroup.gruppe_kurzbz
}
return this.$api.call(ApiGruppe.add(newData))
.then(result => {
this.reload()
this.selectedGroup = ''
})
.catch(this.$fhcAlert.handleSystemError);
},
deleteLVPlan(lehreinheitgruppe_id)
{
let deleteData = {
'lehreinheitgruppe_id': lehreinheitgruppe_id,
'lehreinheit_id': this.lehreinheit_id
}
this.$api.call(ApiGruppe.deleteFromLVPlan(deleteData))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
this.reload();
})
},
deleteGroup(lehreinheitgruppe_id)
{
let deleteData = {
'lehreinheitgruppe_id': lehreinheitgruppe_id,
'lehreinheit_id': this.lehreinheit_id
}
this.$api.call(ApiGruppe.delete(deleteData))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
this.reload();
})
},
},
template: `
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
:reload=true
:new-btn-label="$p.t('lehre', 'addGroup')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredGroups"
:placeholder="$p.t('lehre', 'addGroup')"
v-model="selectedGroup"
field="label"
@item-select="addGroup"
@complete="searchGroup"
></form-input>
</template>
</core-filter-cmpt>
`
};
@@ -0,0 +1,292 @@
import CoreSearchbar from "../searchbar/searchbar.js";
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import StvVerband from "../Stv/Studentenverwaltung/Verband.js";
import StvStudiensemester from "../Stv/Studentenverwaltung/Studiensemester.js";
import LvTable from "./Setup/Table.js";
import LvTabs from "./Setup/Tabs.js";
import ApiDetails from "../../api/lehrveranstaltung/details.js";
import ApiLektor from "../../api/lehrveranstaltung/lektor.js";
import ApiGruppe from "../../api/lehrveranstaltung/gruppe.js";
import ApiStudiengangTree from "../../api/lehrveranstaltung/studiengangtree.js";
import ApiSearchbar from "../../api/factory/searchbar.js";
export default {
name: "LVVerwaltung",
components: {
CoreSearchbar,
VerticalSplit,
StvVerband,
StvStudiensemester,
LvTable,
LvTabs,
},
props: {
defaultSemester: String,
lvRoot: String,
permissions: Object,
config: Object,
stg: { type: String, required: false },
semester: { type: [Number, String], required: false, default: null },
studiensemester_kurzbz: { type: String, required: false, default: null },
emp: { type: String, required: false, default: null }
},
provide() {
return {
currentSemester: Vue.computed(() => this.selectedStudiensemester),
dropdowns: this.dropdowns,
configShowVertragsdetails: this.config.showVertragsdetails,
configShowGewichtung: this.config.showGewichtung,
lehreinheitAnmerkungDefault: this.config.lehreinheitAnmerkungDefault,
lehreinheitRaumtypDefault: this.config.lehreinheitRaumtypDefault,
lehreinheitRaumtypAlternativeDefault: this.config.lehreinheitRaumtypAlternativeDefault,
permissionLehrveranstaltung: this.permissions['lehre/lehrveranstaltung'],
permissionGruppenEntfernen: this.permissions['lv-plan/gruppenentfernen'],
permissionLektorEntfernen: this.permissions['lv-plan/lektorentfernen'],
}
},
mounted() {
this.updateFilter();
},
watch: {
stg() {
this.updateFilter();
},
semester() {
this.updateFilter();
},
selectedStudiensemester() {
this.updateFilter();
},
emp() {
this.updateFilter();
},
studiensemester_kurzbz(newVal) {
this.selectedStudiensemester = newVal ?? this.defaultSemester;
}
},
data() {
return {
selected: [],
studiengang: "",
filter: {},
selectedStudiensemester: this.studiensemester_kurzbz ?? this.defaultSemester,
endpoint: ApiStudiengangTree,
dropdowns: {
studiensemester_array: [],
sprachen_array: [],
lehrform_array: [],
raumtyp_array: [],
lektor_array: [],
gruppen_array: [],
benutzer_array: [],
},
selectedStudiengang: '',
searchbaroptions: {
origin: 'lvverwaltung',
cssclass: "position-relative",
calcheightonly: true,
types: [
"mitarbeiter",
"mitarbeiter_ohne_zuordnung"
],
actions: {
employee: {
defaultaction: {
type: "function",
action: (data) => {
this.onSelectEmployee(data.uid);
}
},
childactions: [
]
},
}
},
}
},
methods: {
updateFilter()
{
const filter = {
stg: this.stg,
emp: this.emp,
semester: this.semester,
studiensemester_kurzbz: this.selectedStudiensemester,
activeFilter: this.emp ? 'employee' : this.stg ? 'verband' : null
}
if (this.stg !== undefined)
{
this.selectedStudiengang = this.semester !== '' && this.semester
? `${this.stg}/${this.semester}`
: this.stg;
}
this.filter = filter;
},
handleRowClicked(data)
{
this.selected = data
},
onSelectEmployee(emp)
{
const { stg, semester } = this.filter;
let studiensemester_kurzbz = this.selectedStudiensemester;
const params = { emp };
if (stg)
params.stg = stg;
if (semester !== null)
params.semester = semester;
if (studiensemester_kurzbz)
params.studiensemester_kurzbz = studiensemester_kurzbz;
this.$router.push({ name: 'byEmp', params })
},
onSelectVerband({link})
{
let stg = null;
let semester = null;
let studiensemester_kurzbz = this.selectedStudiensemester;
if (typeof link === 'number')
stg = link;
else if (typeof link === 'string')
{
[stg, semester] = link.split('/');
}
const routeName = this.filter.emp ? 'byEmp' : 'byStg';
const params = { stg };
if (semester !== null)
params.semester = semester;
if (studiensemester_kurzbz)
params.studiensemester_kurzbz = studiensemester_kurzbz;
if (this.filter.emp)
params.emp = this.filter.emp;
this.$router.push({ name: routeName, params });
this.selected = [];
},
resetEmployeeFilter()
{
const newParams = { ...this.filter, activeFilter: 'verband' };
if (newParams.stg === '')
this.$router.replace({ name: 'index' });
else
{
delete newParams.emp;
this.$router.replace({ name: 'byStg', params: newParams });
}
},
searchfunction(params) {
return this.$api.call(ApiSearchbar.search(params));
},
studiensemesterChanged(newValue) {
const routeName = this.filter.activeFilter === 'employee' ? 'byEmp' : 'byStg';
const newParams = {...this.filter, studiensemester_kurzbz: newValue};
this.$router.push({ name: routeName, params: newParams });
this.selected = [];
},
},
created() {
this.$p.loadCategory(['lehre', 'person', 'global'])
this.$api.call(ApiDetails.getStudiensemester())
.then(result => {
this.dropdowns.studiensemester_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiDetails.getSprache())
.then(result => {
this.dropdowns.sprachen_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiDetails.getLehrform())
.then(result => {
this.dropdowns.lehrform_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiDetails.getRaumtyp())
.then(result => {
this.dropdowns.raumtyp_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiLektor.getLehrfunktionen())
.then(result => {
this.dropdowns.lehrfunktion_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiLektor.getLektoren())
.then(result => {
this.dropdowns.lektor_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiGruppe.getAll())
.then(result => {
this.dropdowns.gruppen_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(ApiGruppe.getBenutzer())
.then(result => {
this.dropdowns.benutzer_array = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div class="stv">
<header class="navbar navbar-expand-lg navbar-dark bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-4 col-lg-3 col-xl-2 me-0 px-3">LV Verwaltung</a>
<core-searchbar :searchoptions="searchbaroptions" :searchfunction=searchfunction class="searchbar w-100"></core-searchbar>
</header>
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<stv-verband :preselectedKey="selectedStudiengang" :endpoint="endpoint" @select-verband="onSelectVerband" class="col" style="height:0%"></stv-verband>
<stv-studiensemester v-model:studiensemester-kurzbz="selectedStudiensemester" @update:studiensemester-kurzbz="studiensemesterChanged"></stv-studiensemester>
</nav>
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
<vertical-split>
<template #top>
<lv-table ref="lvTable"
v-model:selected="selected"
@row-clicked="handleRowClicked"
:filter="filter"
>
<template #filterzuruecksetzen v-if="filter.activeFilter === 'employee'">
<button type="button"
class="btn btn-outline-secondary btn-action"
title="Mitarbeiter Filter entfernen"
@click="resetEmployeeFilter">
<i class="fa fa-xmark"></i>
</button>
</template>
</lv-table>
</template>
<template #bottom>
<lv-tabs ref="details" :lv="selected"></lv-tabs>
</template>
</vertical-split>
</main>
</div>
</div>
</div>`
};
@@ -0,0 +1,278 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiLektor from "../../../api/lehrveranstaltung/lektor.js";
export default{
name: "LVLektorDaten",
components: {
CoreFilterCmpt,
CoreForm,
FormInput
},
props: {
lehreinheit_id: Number,
mitarbeiter_uid: String
},
emits: [
'changedLektor',
'changedCosts',
],
inject: {
dropdowns: {
from: 'dropdowns'
}
},
data() {
return{
original: null,
data: null,
changed: {},
internal_mitarbeiter_uid: null,
filteredLektor: [],
}
},
computed: {
changedLength() {
return Object.keys(this.changed).length;
},
berechneteGesamtkosten() {
if (!this.data) return 0;
const stunden = Number(this.data.semesterstunden) || 0;
const stundensatz = Number(this.data.stundensatz) || 0;
return (stunden * stundensatz).toFixed(2);
}
},
watch: {
lehreinheit_id:
{
deep: true,
handler(newVal, oldVal) {
this.data = null;
this.original = null;
this.internal_mitarbeiter_uid = null;
}
},
mitarbeiter_uid:
{
deep: true,
handler(newVal, oldVal) {
this.internal_mitarbeiter_uid = newVal;
if (newVal === null)
this.data = null;
else if (newVal !== undefined && this.lehreinheit_id !== undefined)
this.getLektorData();
}
},
data: {
handler(newValue) {
if (newValue === null)
{
this.changed = {}
return
}
let changed = {};
let keys = Object.keys(this.original);
for (let key of keys)
{
if (this.original[key] !== newValue[key])
changed[key] = newValue[key];
}
this.changed = changed;
},
deep: true
},
},
methods: {
getLektorData()
{
if (!this.lehreinheit_id || !this.internal_mitarbeiter_uid)
return;
return this.$api.call(ApiLektor.getLektorDaten(this.lehreinheit_id, this.internal_mitarbeiter_uid))
.then(result => {
this.data = result.data;
this.original = { ...this.data };
})
.catch(this.$fhcAlert.handleSystemError);
},
updateDaten()
{
if (!this.changedLength)
return;
if (this.changed.mitarbeiter_uid && this.changed.mitarbeiter_uid.uid)
{
this.changed.mitarbeiter_uid = this.changed.mitarbeiter_uid.uid;
}
this.$refs.form.clearValidation();
let updatedData = {
lehreinheit_id: this.lehreinheit_id,
mitarbeiter_uid: this.mitarbeiter_uid,
formData: this.changed
}
this.$refs.form.call(ApiLektor.update(updatedData))
.then(result => {
let warning = result.data?.retval?.warning;
if (warning)
this.$fhcAlert.alertWarning(warning)
this.original = {...this.data};
if (this.changed.mitarbeiter_uid)
{
this.$emit('changedLektor', this.changed.mitarbeiter_uid);
}
if (this.changed.semesterstunden || this.changed.stundensatz)
{
this.$emit('changedCosts');
}
this.changed = {};
})
.catch(this.$fhcAlert.handleSystemError);
},
searchLektor(event)
{
const query = event.query.toLowerCase().trim();
this.filteredLektor = this.dropdowns.lektor_array.filter(lektor => {
const fullName = `${lektor.vorname.toLowerCase()} ${lektor.nachname.toLowerCase()}`;
const reverseFullName = `${lektor.nachname.toLowerCase()} ${lektor.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || lektor.uid.toLowerCase().includes(query);
}).map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
}));
},
},
created() {
this.getLektorData()
},
template: `
<core-form ref="form" @submit.prevent="updateDaten">
<div class="position-sticky top-0 z-1">
<button type="submit" class="btn btn-primary position-absolute top-0 end-0" :disabled="!changedLength">{{$p.t('ui', 'speichern')}}</button>
</div>
<fieldset class="overflow-hidden">
<legend>{{$p.t('lehre', 'daten')}}</legend>
<template v-if="data">
<div class="row align-items-start mb-3">
<form-input
:label="$p.t('lehre', 'lehrfunktion')"
type="select"
container-class="col-3"
v-model="data.lehrfunktion_kurzbz"
name="lehrfunktion_kurzbz"
>
<option
v-for="lehrfunktion in dropdowns.lehrfunktion_array"
:value="lehrfunktion.lehrfunktion_kurzbz"
>
{{ lehrfunktion.lehrfunktion_kurzbz }}
</option>
</form-input>
<form-input
type="autocomplete"
:label="$p.t('lehre', 'lektor')"
:disabled="data.vertrag_id !== null"
:suggestions="filteredLektor"
placeholder="Mitarbeiter hinzufügen"
v-model="data.mitarbeiter_uid"
field="label"
container-class="col-3"
dropdown
@complete="searchLektor"
name="lektorautocomplete"
></form-input>
<form-input
:label="$p.t('lehre', 'anmerkung')"
type="text"
container-class="col-6"
v-model="data.anmerkung"
name="anmerkung"
>
</form-input>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'las')"
type="number"
min="0"
step="0.01"
container-class="col-3"
:disabled="data.vertrag?.vertragsstatus_kurzbz === 'akzeptiert'"
v-model="data.semesterstunden"
name="semesterstunden"
>
</form-input>
<form-input
:label="$p.t('lehre', 'planstunden')"
type="number"
min="0"
step="0.01"
container-class="col-3"
v-model="data.planstunden"
name="planstunden"
>
</form-input>
</div>
<div class="row mb-3 d-flex align-items-end">
<form-input
:label="data.default_stundensatz !== null
? $p.t('lehre', 'stundensatz') + ' (' + $p.t('lehre', 'default') + ': ' + data.default_stundensatz + ')'
: $p.t('lehre', 'stundensatz')"
type="number"
min="0"
step="0.01"
container-class="col-3"
v-model="data.stundensatz"
:disabled="data.vertrag?.vertragsstatus_kurzbz === 'akzeptiert'"
name="stundensatz"
>
</form-input>
<form-input
:label="$p.t('lehre', 'bismelden')"
type="checkbox"
container-class="col-3"
v-model="data.bismelden"
name="bismelden"
>
</form-input>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'gesamtkosten')"
type="number"
name="gesamtkosten"
container-class="col-3"
readonly
v-model="berechneteGesamtkosten"
:style="{ color: berechneteGesamtkosten <= 0 ? 'red' : 'black' }"
>
</form-input>
</div>
</template>
</fieldset>
</core-form>
`
};
@@ -0,0 +1,223 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiLektor from "../../../api/lehrveranstaltung/lektor.js";
export default{
name: "LVLektorTable",
components: {
CoreFilterCmpt,
FormForm,
FormInput
},
props: {
lehreinheit_id: Number,
selected: String
},
inject: {
dropdowns: {
from: 'dropdowns'
},
showLVPlanLektorDel: {
from: 'permissionLektorEntfernen',
default: false
},
},
emits: ['update:selected'],
data() {
return {
tabulatorEvents: [
{
event: 'rowSelectionChanged',
handler: this.lektorSelected
}
],
showAutocomplete: false,
filteredLektor: [],
selectedLektor: ''
}
},
computed: {
tabulatorOptions() {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiLektor.getByLehreinheit(this.lehreinheit_id)),
ajaxResponse: (url, params, response) => { return response.data || [] },
columns:[
{title: this.$p.t('person', 'nachname'), field:"nachname"},
{title: this.$p.t('person', 'vorname'), field:"vorname"},
{title: this.$p.t('person', 'uid'), field:"mitarbeiter_uid"},
{title: this.$p.t('lehre', 'lehreinheit_id'), field:"lehreinheit_id"},
{
title: this.$p.t('lehre', 'verplant'),
field:"verplant",
formatter:"tickCross",
hozAlign:"center",
formatterParams: {
tickElement: '<i class="fa fa-check text-success"></i>',
crossElement: '<i class="fa fa-xmark text-danger"></i>'
}
},
{
title: this.$p.t('global', 'actions'), field: 'actions',
minWidth: 150,
formatter: (cell, formatterParams, onRendered) => {
let container = document.createElement('div');
let button = document.createElement('button');
container.className = "d-flex gap-2";
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
button.title = this.$p.t('ui', 'loeschen');
button.addEventListener('click', (event) => {
event.stopPropagation();
this.deletePerson(cell.getData().mitarbeiter_uid, cell.getData().lehreinheit_id);
});
container.append(button);
if (this.showLVPlanLektorDel)
{
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-calendar-xmark"></i>';
button.disabled = !cell.getData().verplant;
button.title = this.$p.t('lehre', 'auslvplanentfernen');
button.addEventListener('click', (event) => {
event.stopPropagation();
this.deleteLVPlan(cell.getData().mitarbeiter_uid, cell.getData().lehreinheit_id)
});
container.append(button);
}
return container;
},
frozen: true
},
],
layout: 'fitDataFill',
height: 'auto',
selectable: true,
selectableRangeMode: 'click',
selectableRows:1,
persistenceID: 'lehrveranstaltungen_lektor_table_2025_05_27_v1',
}},
},
mounted(){
},
watch: {
lehreinheit_id() {
this.$refs.table.tabulator.setData('api/frontend/v1/lv/lektor/getLektorenByLE/' + encodeURIComponent(this.lehreinheit_id));
},
},
methods:{
lektorSelected(data)
{
if (data.length > 0)
{
let mitarbeiter_uid = data[0].mitarbeiter_uid;
this.$emit('update:selected', mitarbeiter_uid);
}
else
{
this.$emit('update:selected', null);
}
},
reload() {
this.$refs.table.reloadTable();
},
deleteLVPlan(uid, lehreinheit_id)
{
let deleteData = {
'mitarbeiter_uid': uid,
'lehreinheit_id': lehreinheit_id,
}
this.$api.call(ApiLektor.deleteFromLVPlan(deleteData))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
this.reload();
})
},
deletePerson(uid, lehreinheit_id)
{
let deleteData = {
'mitarbeiter_uid': uid,
'lehreinheit_id': lehreinheit_id,
}
this.$api.call(ApiLektor.deletePerson(deleteData))
.then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
this.reload();
})
.catch(this.$fhcAlert.handleSystemError)
.finally(()=> {
})
},
searchLektor(event)
{
const query = event.query.toLowerCase().trim();
this.filteredLektor = this.dropdowns.lektor_array.filter(lektor => {
const fullName = `${lektor.vorname.toLowerCase()} ${lektor.nachname.toLowerCase()}`;
const reverseFullName = `${lektor.nachname.toLowerCase()} ${lektor.vorname.toLowerCase()}`;
return fullName.includes(query) || reverseFullName.includes(query) || lektor.uid.toLowerCase().includes(query);
}).map(lektor => ({
label: `${lektor.nachname} ${lektor.vorname} (${lektor.uid})`,
uid: lektor.uid
}));
},
addLektor()
{
let newData = {
'mitarbeiter_uid': this.selectedLektor.uid,
'lehreinheit_id': this.lehreinheit_id
}
this.$api.call(ApiLektor.add(newData))
.then(result => {
this.reload()
this.selectedLektor = ''
})
.catch(this.$fhcAlert.handleSystemError);
},
},
template: `
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
reload
:new-btn-label="$p.t('lehre', 'addLektor')"
new-btn-show
@click:new="showAutocomplete = !showAutocomplete"
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
v-if="showAutocomplete"
type="autocomplete"
:suggestions="filteredLektor"
:placeholder="$p.t('lehre', 'addLektor')"
v-model="selectedLektor"
field="label"
@item-select="addLektor"
@complete="searchLektor"
></form-input>
</template>
</core-filter-cmpt>
`
};
@@ -0,0 +1,167 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiVertrag from "../../../api/lehrveranstaltung/vertrag.js";
export default{
name: "LVLektorVertrag",
components: {
CoreFilterCmpt,
CoreForm,
FormInput
},
emits: [
'canceledVertrag'
],
props: {
lehreinheit_id: Number,
mitarbeiter_uid: String
},
inject: {
dropdowns: {
from: 'dropdowns'
},
showVertragsdetails: {
from: 'configShowVertragsdetails',
default: false
}
},
data() {
return{
data: null,
internal_mitarbeiter_uid: null,
}
},
watch: {
lehreinheit_id:
{
deep: true,
handler(newVal, oldVal) {
this.data = null;
this.internal_mitarbeiter_uid = null;
}
},
mitarbeiter_uid:
{
deep: true,
handler(newVal, oldVal) {
this.internal_mitarbeiter_uid = newVal;
if (newVal === null)
this.data = null;
else if (newVal !== undefined && this.lehreinheit_id !== undefined)
this.getLektorVertrag();
}
},
},
computed: {
vertragsstatus() {
if (!this.data || !this.data.vertrag) return;
const betragVertrag = Number(this.data.vertrag.betrag) || 0;
const stundenVertrag = Number(this.data.vertrag.vertragsstunden) || 0;
const semStunden = Number(this.data.semesterstunden) || 0;
const stundensatz = Number(this.data.stundensatz) || 0;
const kostenAktuell = semStunden * stundensatz;
return (stundenVertrag !== semStunden || betragVertrag !== kostenAktuell) ? 'Geändert' : (this.data.vertrag.vertragsstatus || '');
},
},
methods: {
getLektorVertrag ()
{
if (this.showVertragsdetails === false)
return;
if (!this.lehreinheit_id || !this.internal_mitarbeiter_uid)
return;
this.$api.call(ApiVertrag.getByLeEmp(this.lehreinheit_id, this.internal_mitarbeiter_uid))
.then(result => {
this.data = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
async cancelVertrag()
{
if (await this.$fhcAlert.confirm({
message: this.$p.t('lehre', 'vertragConfirm'),
acceptLabel: this.$p.t('ui', 'ja').charAt(0).toUpperCase() + this.$p.t('ui', 'ja').slice(1),
acceptClass: 'btn btn-danger'}) === false)
return;
let needUpdate = {
vertrag_id: this.data.vertrag.vertrag_id,
mitarbeiter_uid: this.mitarbeiter_uid,
lehreinheit_id: this.lehreinheit_id
}
this.$api.call(ApiVertrag.cancelByLeEmp(needUpdate))
.then(result => {
this.data.vertrag = null;
this.$emit('canceledVertrag');
})
.catch(this.$fhcAlert.handleSystemError);
},
},
template: `
<core-form ref="form">
<fieldset class="overflow-hidden" v-if="showVertragsdetails">
<legend> {{$p.t('lehre', 'vertragsdetails')}}
{{ data === null ? ' Noch kein Vertrag' : '' }}
</legend>
<template v-if="data?.vertrag">
<div class="row align-items-end mb-3">
<form-input
:label="$p.t('lehre', 'vertragsstatus')"
type="text"
readonly
container-class="col-3"
v-model="vertragsstatus"
:style="{fontWeight: vertragsstatus === 'Geändert' ? 'bold' : 'normal'}"
name="vertragsstatus"
/>
<div class="col-3 d-flex align-items-end">
<button
type="button"
class="btn btn-outline-secondary w-100"
@click="cancelVertrag"
:title="$p.t('lehre', 'cancelvertrag')"
>
<i class="fa-solid fa-ban"></i>
</button>
</div>
</div>
{{$p.t('lehre', 'vertragurfassung')}}
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'semesterstunden')"
type="text"
container-class="col-3"
readonly
v-model="data.vertrag.vertragsstunden"
name="vertragsstunden"
>
</form-input>
</div>
<div class="row mb-3">
<form-input
:label="$p.t('lehre', 'studiensemester')"
type="text"
container-class="col-3"
readonly
v-model="data.vertrag.vertragsstunden_studiensemester_kurzbz"
name="vertragsstunden_studiensemester_kurzbz"
>
</form-input>
</div>
</template>
</fieldset>
</core-form>
`
};
@@ -0,0 +1,696 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import BsModal from "../../Bootstrap/Modal.js";
import DetailsForm from "../Details/Form.js";
import CoreTag from '../../Tag/Tag.js';
import { tagHeaderFilter } from '../../../../js/tabulator/filters/extendedHeaderFilter.js';
import { extendedHeaderFilter } from "../../../../js/tabulator/filters/extendedHeaderFilter.js";
import ApiLv from "../../../api/lehrveranstaltung.js";
import ApiTag from "../../../api/lehrveranstaltung/tag.js";
import ApiLehreinheit from "../../../api/lehrveranstaltung/lehreinheit.js";
export default {
name: "LVVerwaltungTable",
components: {
CoreFilterCmpt,
BsModal,
DetailsForm,
CoreTag
},
props: {
selected: Object,
filter: {
type: Object,
default: () => ({})
}
},
inject: {
currentSemester: {
from: 'currentSemester'
},
lehreinheitAnmerkungDefault: {
from: 'lehreinheitAnmerkungDefault',
default: ''
},
lehreinheitRaumtypDefault: {
from: 'lehreinheitRaumtypDefault',
default: ''
},
lehreinheitRaumtypAlternativeDefault: {
from: 'lehreinheitRaumtypAlternativeDefault',
default: ''
}
},
emits: [
'update:selected',
'row-clicked'
],
watch: {
filter: {
handler() {
if (this.$refs.table && this.$refs.table.tabulator)
{
this.expanded = [];
this.reload();
}
},
deep: true,
},
currentSemester: {
handler(newVal)
{
this.lv_info_default.studiensemester_kurzbz = newVal
this.lv_info = false;
}
}
},
data() {
return {
expanded: [],
selectedColumnValues: [],
tagEndpoint: ApiTag,
tabulatorEvents: [
{
event: 'rowSelectionChanged',
handler: this.rowSelectionChanged
},
{
event: 'dataProcessed',
handler: (data) => {
this.reexpandRows()
this.$emit('update:selected', {})
}
},
{
event: 'dataTreeRowExpanded',
handler: (data) => {
this.getExpandedRows()
}
},
{
event: 'dataTreeRowCollapsed',
handler: (data) => {
this.getExpandedRows()
}
}
],
formData: {},
lv_info: false,
lv_info_default: {
stundenblockung: 2,
wochenrythmus: 1,
studiensemester_kurzbz: this.currentSemester,
lehrform_kurzbz: 'UE',
anmerkung: this.lehreinheitAnmerkungDefault.replace("'","\'"),
raumtyp: this.lehreinheitRaumtypDefault,
raumtypalternativ: this.lehreinheitRaumtypAlternativeDefault,
lehrfach_id: ''
}
}
},
computed: {
tabulatorOptions() {
return {
index: 'uniqueindex',
ajaxURL: 'dummy',
ajaxRequestFunc: async (url, config, params) => {
let realUrl = this.buildApiUrl();
if (realUrl)
return this.$api.call(ApiLv.getTable(this.buildApiUrl()));
},
ajaxResponse: (url, params, response) => { return response?.data || [] },
dataTree: true,
initialSort:[
{column: 'lv_bezeichnung', dir: 'desc'},
],
dataTreeChildIndent: 20,
dataTreeElementColumn: "lv_kurzbz",
dataTreeStartExpanded: false,
dataTreeCollapseElement: '<i class="fa-solid fa-caret-down"></i>',
dataTreeExpandElement: '<i class="fa-solid fa-caret-right"></i>',
columnDefaults: {
tooltip: true,
headerFilter: "input",
headerFilterFunc: extendedHeaderFilter,
},
layout: 'fitDataStretch',
persistenceID: 'lehrveranstaltungen_2025_07_31_v1',
selectableRowsRangeMode: 'click',
selectableRows: true,
rowContextMenu: (component, e) => {
if (e.getData()?.lehreinheit_id === undefined)
return;
return [
{
label: "LV-Teil kopieren",
menu: [
{
label: "Alles",
action: (e, row) =>
{
this.copyLehreinheit(row, "alle");
},
},
{
label: "Nur LV-Teil",
action: (e, row) =>
{
this.copyLehreinheit(row, "lvteil");
},
},
{
label: "Nur mit Gruppen",
action: (e, row) =>
{
this.copyLehreinheit(row, "gruppen");
},
},
{
label: "Nur mit Lehrenden",
action: (e, row) =>
{
this.copyLehreinheit(row, "lektoren");
},
},
],
},
{
label: "Entfernen",
action: (e, row) => {
this.deleteLehreinheit(row)
},
},
];
},
columns: [
{
title: this.$p.t('lehre', 'kurzbz'),
field: "lv_kurzbz",
headerFilterFuncParams: {field: 'lv_kurzbz'},
headerFilter: true,
formatter: (cell, formatterParams) => {
const rowData = cell.getRow().getData();
const iconKey = (rowData.lehrtyp_kurzbz || '').toLowerCase();
const lvkurzbz = (cell.getValue()).toUpperCase();
const parentspan = document.createElement('span');
const span = document.createElement('span');
span.classList.add('lv_table_icon', `icon-${iconKey}`);
span.title = iconKey || 'LV-Teil';
parentspan.appendChild(span);
parentspan.appendChild(document.createTextNode(` ${lvkurzbz}`));
return parentspan
},
cellClick: (e, cell) => {
cell.getRow().treeToggle();
}
},
{
title: 'Tags',
field: 'tags',
tooltip: false,
headerFilter: "input",
headerFilterFunc: tagHeaderFilter,
headerFilterFuncParams: {field: 'tags'},
formatter: (cell) => {
let tags = cell.getValue();
if (!tags) return;
let container = document.createElement('div');
container.className = "d-flex gap-1";
let parsedTags = JSON.parse(tags);
let maxVisibleTags = 2;
const rowData = cell.getRow().getData();
if (rowData._tagExpanded === undefined) {
rowData._tagExpanded = false;
}
const renderTags = () => {
container.innerHTML = '';
parsedTags = parsedTags.filter(item => item !== null);
parsedTags.sort((a, b) => {
let adone = a.done ? 1 : 0;
let bbone = b.done ? 1 : 0;
if (adone !== bbone)
{
return adone - bbone;
}
return b.id - a.id;
});
const tagsToShow = rowData._tagExpanded ? parsedTags : parsedTags.slice(0, maxVisibleTags);
tagsToShow.forEach(tag => {
if (!tag) return;
let tagElement = document.createElement('span');
tagElement.innerText = tag.beschreibung;
tagElement.title = tag.notiz;
tagElement.className = "tag " + tag.style;
if (tag.done) tagElement.className += " tag_done";
tagElement.addEventListener('click', (event) => {
event.stopPropagation();
event.preventDefault();
this.$refs.tagComponent.editTag(tag.id);
});
container.appendChild(tagElement);
});
if (parsedTags.length > maxVisibleTags) {
let toggle = document.createElement('button');
toggle.innerText = (rowData._tagExpanded ? '- ' : '+ ') + (parsedTags.length - maxVisibleTags);
toggle.className = "display_all";
toggle.title = rowData._tagExpanded ? "Tags ausblenden" : "Tags einblenden";
toggle.addEventListener('click', () => {
rowData._tagExpanded = !rowData._tagExpanded;
renderTags();
});
container.appendChild(toggle);
}
};
renderTags();
return container;
},
width: 150,
},
{
title: this.$p.t('lehre', 'lehrveranstaltung_id'),
field: "lehrveranstaltung_id",
headerFilterFuncParams: {field: 'lehrveranstaltung_id'},
headerFilter: true,
visible: false
},
{title: this.$p.t('ui', 'bezeichnung'), field: "lv_bezeichnung", headerFilter: true, headerFilterFuncParams: {field: 'lv_bezeichnung'}},
{title: this.$p.t('lehre', 'bezeichnungeng'), field: "lv_bezeichnung_english", headerFilter: true, headerFilterFuncParams: {field: 'lv_bezeichnung_english'}, visible: false},
{
title: this.$p.t('lehre', 'studiengangskennzahlLehre'),
field: "lv_studiengang_kz",
headerFilter: true,
headerFilterFuncParams: {field: 'lv_studiengang_kz'},
visible: false
},
{title: this.$p.t('lehre', 'studiengang'), field: "studiengang", headerFilter: true, headerFilterFuncParams: {field: 'studiengang'}, visible: false},
{title: this.$p.t('lehre', 'semester'), field: "semester", headerFilter: true, headerFilterFuncParams: {field: 'semester'}},
{title: this.$p.t('global', 'sprache'), field: "sprache", headerFilter: true, headerFilterFuncParams: {field: 'sprache'}, visible: false},
{title: this.$p.t('lehre', 'ects'), field: "lv_ects", headerFilter: true, headerFilterFuncParams: {field: 'lv_ects'}, visible: false},
{title: this.$p.t('lehre', 'semesterstunden'), field: "semesterstunden", headerFilter: true, headerFilterFuncParams: {field: 'semesterstunden'}, visible: false},
{title: this.$p.t('global', 'anmerkung'), field: "anmerkung", headerFilter: true, headerFilterFuncParams: {field: 'anmerkung'}, visible: false},
{title: this.$p.t('lehre', 'lehre'), field: "lehre", headerFilter: true, headerFilterFuncParams: {field: 'lehre'}, formatter: (cell) => cell.getValue() ? this.toUpperCase(this.$p.t('ui', 'ja')) : this.toUpperCase(this.$p.t('ui', 'nein')), visible: false},
{title: this.$p.t('person', 'aktiv'), field: "aktiv", headerFilter: true, headerFilterFuncParams: {field: 'aktiv'}, formatter: (cell) => cell.getValue() ? this.toUpperCase(this.$p.t('ui', 'ja')) : this.toUpperCase(this.$p.t('ui', 'nein')), visible: false},
{title: this.$p.t('lehre', 'organisationsform'), field: "orgform_kurzbz", headerFilter: true, headerFilterFuncParams: {field: 'orgform_kurzbz'}},
{title: this.$p.t('ui', 'studienplan_id'), field: "studienplan_id", headerFilter: true, headerFilterFuncParams: {field: 'studienplan_id'}, visible: false},
{title: this.$p.t('lehre', 'studienplan'), field: "studienplan_bezeichnung", headerFilter: true, headerFilterFuncParams: {field: 'studienplan_bezeichnung'}, visible: false},
{title: this.$p.t('lehre', 'lehrtyp'), field: "lehrtyp_kurzbz", headerFilter: true, headerFilterFuncParams: {field: 'lehrtyp_kurzbz'}, visible: false},
{title: this.$p.t('lehre', 'lehrform'), field: "lehrform_kurzbz", headerFilter: true, headerFilterFuncParams: {field: 'lehrform_kurzbz'}},
{title: this.$p.t('lehre', 'leplanstunden'), field: "le_planstunden", headerFilter: true, headerFilterFuncParams: {field: 'le_planstunden'}, visible: false},
{title: this.$p.t('lehre', 'lehreinheit_id'), field: "lehreinheit_id", headerFilter: true, headerFilterFuncParams: {field: 'lehreinheit_id'}, visible: false},
{title: this.$p.t('lehre', 'studiensemester'), field: "studiensemester_kurzbz", headerFilter: true, headerFilterFuncParams: {field: 'studiensemester_kurzbz'}, visible: false},
{title: this.$p.t('lehre', 'unr'), field: "unr", headerFilter: true, headerFilterFuncParams: {field: 'unr'}, visible: false},
{title: this.$p.t('lehre', 'fachbereich'), field: "fachbereich", headerFilter: true, headerFilterFuncParams: {field: 'fachbereich'}, visible: false},
{title: this.$p.t('lehre', 'stundenblockung'), field: "stundenblockung", headerFilter: true, headerFilterFuncParams: {field: 'stundenblockung'}, visible: false},
{title: this.$p.t('lehre', 'wochenrhythmus'), field: "wochenrythmus", headerFilter: true, headerFilterFuncParams: {field: 'wochenrythmus'}, visible: false},
{title: this.$p.t('lehre', 'startkw'), field: "start_kw", headerFilter: true, headerFilterFuncParams: {field: 'startkw'}, visible: false},
{title: this.$p.t('lehre', 'raumtyp'), field: "raumtyp", headerFilter: true, headerFilterFuncParams: {field: 'raumtyp'}, visible: false},
{title: this.$p.t('lehre', 'raumtypalternativ'), field: "raumtypalternativ", headerFilter: true, headerFilterFuncParams: {field: 'raumtypalternativ'}, visible: false},
{title: this.$p.t('lehre', 'gruppen'), field: "gruppen", headerFilter: true, headerFilterFuncParams: {field: 'gruppen'}},
{title: this.$p.t('lehre', 'lehrende'), field: "lektoren", headerFilter: true, headerFilterFuncParams: {field: ['lektoren', 'vorname', 'nachname']}},
],
}
}
},
mounted() {
if (this.shouldAutoLoad())
{
this.reload();
}
},
methods: {
shouldAutoLoad() {
return this.filter && this.filter.activeFilter;
},
toUpperCase(str) {
if (!str)
return '';
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
},
async reload()
{
if (this.shouldAutoLoad)
{
this.$refs.table.reloadTable();
}
},
rowSelectionChanged(data) {
this.selectedRows = this.$refs.table.tabulator.getSelectedRows();
this.selectedColumnValues = this.selectedRows.filter(row => row.getData().lehreinheit_id !== undefined && row.getData().lehreinheit_id).map(row => row.getData().lehreinheit_id);
if (data[0]?.lehreinheit_id !== undefined && this.selectedColumnValues.length === 1)
{
this.$emit('update:selected', [data[0]]);
this.lv_info = false
}
else if (data[0]?.lehrveranstaltung_id)
{
this.$emit('update:selected', {});
this.getLVInfos(data[0]);
}
},
getLVInfos(data)
{
this.$api.call(ApiLv.getByLV(data.lehrveranstaltung_id))
.then(result => {
if (result.data?.lehrfach_id === undefined
&& Array.isArray(result.data?.lehrfaecher))
{
const match = result.data.lehrfaecher?.find(
lf => lf.lehrfach?.startsWith(result.data.lvbezeichnung)
);
if (match)
{
result.data.lehrfach_id = match.lehrveranstaltung_id;
}
}
this.lv_info = {...this.lv_info_default, ...result.data};
})
.catch(this.$fhcAlert.handleSystemError);
},
buildApiUrl()
{
if (this.filter.activeFilter === 'employee' && this.filter.emp)
{
const { emp, stg, studiensemester_kurzbz } = this.filter;
return this.$api.getUri(ApiLv.getByEmp(studiensemester_kurzbz, emp, stg));
}
if (this.filter.activeFilter === 'verband' && this.filter.stg)
{
const { stg, semester, studiensemester_kurzbz } = this.filter;
return this.$api.getUri(ApiLv.getByStg(studiensemester_kurzbz, stg, semester));
}
},
resetEmployeeFilter()
{
const newFilter = { ...this.filter };
delete newFilter.emp;
newFilter.activeFilter = 'verband';
},
showLehreinheitModal() {
this.resetModal();
this.$refs.lehreinheitModal.show();
},
addNewLehreinheit()
{
return this.$api.call(ApiLehreinheit.add(this.lv_info))
.then(result => {
this.$refs.lehreinheitModal.hide()
this.reload()
})
.catch(this.$fhcAlert.handleSystemError);
},
resetModal()
{
this.lv_info_default = {
stundenblockung: 2,
wochenrythmus: 1,
studiensemester_kurzbz: this.currentSemester,
lehrform_kurzbz: 'UE',
anmerkung: this.lehreinheitAnmerkungDefault.replace("'","\'"),
raumtyp: this.lehreinheitRaumtypDefault,
raumtypalternativ: this.lehreinheitRaumtypAlternativeDefault,
lehrfach_id: ''
}
},
addedTag(addedTag)
{
const table = this.$refs.table.tabulator;
this.selectedRows.forEach(row =>
{
if (Array.isArray(addedTag.response))
{
addedTag.response.forEach(tag => {
const targetRow = this.allRows.find(row => row.getData().lehreinheit_id === tag.lehreinheit_id);
if (targetRow)
{
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
const tagExists = tags.some((t) => t.id === tag.id);
if (!tagExists)
{
addedTag.id = tag.id;
tags.unshift({ ...addedTag });
targetRow.update({ tags: JSON.stringify(tags) });
targetRow.reformat();
}
}
});
}
});
},
deletedTag(deletedTag) {
const targetRow = this.allRows.find(row => {
const rowData = row.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
return tags.some(tag => tag.id === deletedTag);
});
if (targetRow) {
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
const filteredTags = tags.filter(t => t.id !== deletedTag);
const updatedTags = JSON.stringify(filteredTags);
if (updatedTags !== rowData.tags) {
targetRow.update({
tags: updatedTags
});
targetRow.reformat();
}
}
},
updatedTag(updatedTag) {
const targetRow = this.allRows.find(row => {
const rowData = row.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
return tags.some(t => t?.id === updatedTag.id);
});
if (targetRow)
{
const rowData = targetRow.getData();
let tags = [];
try {
tags = JSON.parse(rowData.tags || '[]');
} catch (e) {}
let changed = false;
const tagIndex = tags.findIndex(tag => tag?.id === updatedTag.id);
if (tagIndex !== -1) {
tags[tagIndex] = { ...updatedTag };
changed = true;
}
if (changed)
{
targetRow.update({
tags: JSON.stringify(tags),
});
targetRow.reformat();
}
}
},
async copyLehreinheit(row, art)
{
let data = {
lehreinheit_id: row.getData().lehreinheit_id,
art: art
}
return this.$api.call(ApiLehreinheit.copy(data))
.then(result => {
this.reload()
})
.catch(this.$fhcAlert.handleSystemError)
},
async getExpandedRows() {
this.expanded = [];
this.allRows.forEach(row => {
if (row.getTreeChildren().length > 0 && row.isTreeExpanded())
{
this.expanded.push(row.getData().uniqueindex);
}
});
},
reexpandRows() {
this.allRows = this.getAllRows(this.$refs.table.tabulator.getRows());
const matchingRows = this.allRows.filter(row =>
this.expanded.includes(row.getData().uniqueindex)
);
if (matchingRows.length === 0)
this.currentTreeLevel = 0;
matchingRows.forEach((row, index) => {
row._row.modules.dataTree.open = true;
if (index === matchingRows.length - 1)
{
row.treeExpand();
}
});
},
deleteLehreinheit(row)
{
let deleteData = {
lehreinheit_id: row.getData().lehreinheit_id,
}
return this.$api.call(ApiLehreinheit.delete(deleteData))
.then(result => {
this.reload()
})
.catch(this.$fhcAlert.handleSystemError);
},
getAllRows(rows)
{
let result = [];
rows.forEach(row =>
{
result.push(row);
let children = row.getTreeChildren();
if(children && children.length > 0)
{
result = result.concat(this.getAllRows(children));
}
});
return result;
},
resetTree() {
this.allRows.forEach(row => {
row._row.modules.dataTree.open = false;
});
let rootRows = this.$refs.table.tabulator.getRows(true);
var lastRow = rootRows[rootRows.length - 1];
lastRow?.treeCollapse(true)
this.currentTreeLevel = 0;
},
expandTree()
{
this.currentTreeLevel = (this.currentTreeLevel || 0) + 1;
let lastMatchingRow = null;
this.allRows.forEach(row => {
const level = row._row.modules.dataTree?.index ?? 0;
if (level === this.currentTreeLevel - 1 )
{
row._row.modules.dataTree.open = true;
if (row._row.data._children?.length > 0)
{
lastMatchingRow = row;
}
}
});
if (lastMatchingRow)
{
lastMatchingRow.treeExpand();
}
this.$refs.table.tabulator.redraw();
},
},
template: `
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
:reload=true
new-btn-label="LV-Teil hinzufügen"
new-btn-show
:new-btn-disabled="!lv_info"
@click:new="showLehreinheitModal">
<template #actions>
<core-tag ref="tagComponent"
:endpoint="tagEndpoint"
:values="selectedColumnValues"
@added="addedTag"
@deleted="deletedTag"
@updated="updatedTag"
zuordnung_typ="lehreinheit_id"
></core-tag>
<button @click="expandTree" class="btn btn-outline-secondary" type="button"><i class="fa-solid fa-maximize"></i></button>
<button @click="resetTree" class="btn btn-outline-secondary" type="button"><i id="togglegroup" class="fa-solid fa-minimize"></i></button>
</template>
<template #search>
<slot name="filterzuruecksetzen"></slot>
</template>
</core-filter-cmpt>
<bs-modal ref="lehreinheitModal" dialogClass="modal-xxl">
<template #title>
<p class="fw-bold mt-3">{{$p.t('lehre', 'newlehreinheit')}}</p>
</template>
<template v-if="lv_info">
<details-form :data="lv_info"/>
</template>
<template #footer>
<button type="button" class="btn btn-primary" @click="addNewLehreinheit">{{$p.t('ui', 'speichern')}}</button>
</template>
</bs-modal>
`
};
@@ -0,0 +1,61 @@
import FhcTabs from "../../Tabs.js";
import Setup from "../../../api/lehrveranstaltung/setup.js";
export default {
name: "LVVerwaltungTabs",
components: {
FhcTabs
},
data() {
return {
configLVTabs: {},
};
},
props: {
lv: Object
},
computed: {
config() {
if (!this.lv || !this.lv.length)
return {};
return this.configLVTabs;
}
},
methods: {
reload() {
if (this.$refs.tabs?.$refs?.current?.reload)
{
this.$refs.tabs.$refs.current.reload();
}
}
},
created() {
this.$api.call(Setup.getTabs())
.then(result => {
this.configLVTabs = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div class="stv-details h-100 pb-3 d-flex flex-column">
<div v-if="!lv?.length" class="justify-content-center d-flex h-100 align-items-center">
Bitte eine Lehreinheit auswählen!
</div>
<div v-else-if="configLVTabs" class="d-flex flex-column h-100 pb-3">
<fhc-tabs
v-if="lv.length === 1"
ref="tabs"
:modelValue="lv[0]"
:config="configLVTabs"
:default="$route.params.tab"
@changed="reload"
/>
</div>
<div v-else>
Loading...
</div>
</div>
`
};
@@ -0,0 +1,124 @@
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import GruppenTable from '../Details/Gruppen.js';
import GruppenDirektTable from '../Details/Direktinskription.js';
import DetailsForm from '../Details/Form.js'
import ApiLehreinheit from "../../../api/lehrveranstaltung/lehreinheit.js";
export default {
name: "LVTabDetails",
components: {
CoreForm,
FormInput,
GruppenTable,
GruppenDirektTable,
DetailsForm
},
props: {
modelValue: Object,
config: {
type: Object,
default: {}
},
},
inject: {
dropdowns: {
from: 'dropdowns'
}
},
watch: {
modelValue(newValue)
{
this.updateLE(newValue)
},
data: {
handler(newValue) {
if (newValue === null)
{
this.changed = {}
return
}
let changed = {};
let keys = Object.keys(this.original);
for (let key of keys) {
if (this.original[key] !== newValue[key])
{
changed[key] = newValue[key];
}
}
this.changed = changed;
},
deep: true
}
},
data() {
return {
original: null,
data: null,
changed: {}
}
},
computed: {
changedLength() {
return Object.keys(this.changed).length;
}
},
methods: {
updateLE(le) {
if (le?.lehreinheit_id === undefined)
{
this.data = null;
return;
}
return this.$api.call(ApiLehreinheit.get(le.lehreinheit_id))
.then(result => {
this.data = result.data;
this.original = {...(this.original || {}), ...this.data};
})
.catch(this.$fhcAlert.handleSystemError);
},
save() {
if (!this.changedLength)
return;
this.$refs.form.clearValidation();
let updatedData = {
lehreinheit_id: this.modelValue.lehreinheit_id,
formData: this.changed
}
return this.$refs.form.call(ApiLehreinheit.update(updatedData))
.then(result => {
this.original = {...this.data};
this.changed = {};
this.$refs.form.setFeedback(true, result.data);
})
.catch(this.$fhcAlert.handleSystemError);
},
reload(){
this.updateLE(this.modelValue);
}
},
created() {
this.updateLE(this.modelValue);
},
template: `
<core-form ref="form" @submit.prevent="save">
<div class="position-sticky top-0 z-1">
<button type="submit" class="btn btn-primary position-absolute top-0 end-0" :disabled="!changedLength">{{$p.t('ui', 'speichern')}}</button>
</div>
<fieldset class="overflow-hidden">
<div class="core-header d-flex flex-column w-100 overflow-auto pb-3 gap-0">
<legend>{{ this.$p.t('lehre', 'lehreinheit') }}</legend>
<small v-if="data" v-html="data.lehreinheit_id" class="text-muted"></small>
</div>
<template v-if="data">
<details-form :data="data"/>
</template>
</fieldset>
</core-form>
`
};
@@ -0,0 +1,39 @@
import GruppenTable from '../Details/Gruppen.js';
import GruppenDirektTable from '../Details/Direktinskription.js';
export default {
name: "LVTabGruppen",
components: {
GruppenTable,
GruppenDirektTable,
},
props: {
modelValue: Object,
config: {
type: Object,
default: {}
},
},
inject: {
dropdowns: {
from: 'dropdowns'
}
},
template: `
<fieldset class="overflow-hidden">
<div class="row">
<div class="col-6">
<legend>{{this.$p.t('lehre', 'gruppen')}}</legend>
<gruppen-table ref="gruppen_table" :lehreinheit_id="modelValue.lehreinheit_id"></gruppen-table>
</div>
<div class="col-6">
<legend>{{this.$p.t('lehre', 'assignedPersons')}}</legend>
<gruppen-direkt-table ref="gruppen_direkt_table" :lehreinheit_id="modelValue.lehreinheit_id"></gruppen-direkt-table>
</div>
</div>
</fieldset>
`
};
@@ -0,0 +1,70 @@
import CoreForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import LektorTable from '../Lektor/Table.js'
import LektorDaten from '../Lektor/Daten.js'
import LektorVertrag from '../Lektor/Vertrag.js'
export default {
name: "LVTabLektor",
components: {
CoreForm,
FormInput,
LektorTable,
LektorDaten,
LektorVertrag
},
props: {
modelValue: Object,
config: {
type: Object,
default: {}
},
},
data() {
return {
mitarbeiter_uid: null
}
},
methods: {
changedLektor(uid) {
const lektorTable = this.$refs.lektor_table;
const tabulator = lektorTable.$refs.table.tabulator;
const selectUID = () => {
let row = tabulator.getRows().find(r => r.getData().mitarbeiter_uid === uid);
if (row)
{
row.select();
}
tabulator.off('dataProcessed', selectUID);
};
tabulator.on('dataProcessed', selectUID);
lektorTable.reload();
},
changedCosts() {
this.$refs.lektor_vertrag?.getLektorVertrag();
},
canceledVertrag() {
this.$refs.lektor_daten?.getLektorData();
}
},
template: `
<fieldset class="overflow-hidden">
<div class="row">
<div class="col-5">
<legend>{{ $p.t('lehre', 'lektorInnen')}}</legend>
<lektor-table ref="lektor_table" :lehreinheit_id="modelValue.lehreinheit_id" v-model:selected="mitarbeiter_uid"></lektor-table>
</div>
<div class="col-5">
<lektor-daten ref="lektor_daten" :lehreinheit_id="modelValue.lehreinheit_id" :mitarbeiter_uid="mitarbeiter_uid" @changedLektor="changedLektor" @changedCosts="changedCosts"></lektor-daten>
</div>
</div>
<div class="row">
<div class="col-5">
</div>
<div class="col-5">
<lektor-vertrag ref="lektor_vertrag" :lehreinheit_id="modelValue.lehreinheit_id" :mitarbeiter_uid="mitarbeiter_uid" @canceledVertrag="canceledVertrag"></lektor-vertrag>
</div>
</div>
</fieldset>
`
};
@@ -0,0 +1,33 @@
import CoreNotiz from "../../Notiz/Notiz.js";
import ApiNotizLehreinheit from '../../../api/factory/notiz/lehreinheit.js';
export default {
name: "LVTabNotiz",
components: {
CoreNotiz
},
props: {
modelValue: Object
},
data() {
return {
endpoint: ApiNotizLehreinheit
};
},
template: `
<div class="stv-details-notizen h-100 pb-3">
<core-notiz
class="overflow-hidden"
:endpoint="endpoint"
ref="formc"
notiz-layout="popupModal"
type-id="lehreinheit_id"
:id="modelValue.lehreinheit_id"
show-document
show-tiny-mce
showErweitert
:visible-columns="['titel','text','verfasser','bearbeiter','dokumente']"
>
</core-notiz>
</div>
`
};
@@ -318,16 +318,6 @@ export default {
</form-input>
</div>
<div class="row mb-3">
<form-input
v-if="!config.hiddenFields.includes('svnr')"
container-class="col-4 stv-details-details-svnr"
:label="$p.t('person', 'svnr')"
type="text"
v-model="data.svnr"
name="svnr"
maxlength="16"
>
</form-input>
<form-input
v-if="!config.hiddenFields.includes('ersatzkennzeichen')"
container-class="col-4 stv-details-details-ersatzkennzeichen"
@@ -151,7 +151,17 @@ export default {
ApiStvStatus.addStudent(prestudent_id, data),
{ errorHeader: prestudent_id }
]))
.then(() => {
.then(result => {
const messagesSuccessful = result.filter(res => res.status == 'fulfilled');
if (messagesSuccessful.length) {
this.$fhcAlert.alertDefault(
'info',
'Feedback',
messagesSuccessful.length + " erfolgreiche Statusänderung(en) durchgeführt", // TODO(chris): translate
false,
true
);
}
this.$emit('reloadTable');
this.$reloadList();
});
@@ -50,7 +50,6 @@ export default {
{title:"Wahlname", field:"wahlname", visible:false, headerFilter: true},
{title:"Vornamen", field:"vornamen", visible:false, headerFilter: true},
{title:"TitelPost", field:"titelpost", headerFilter: "list", headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"}},
{title:"SVNR", field:"svnr", headerFilter: true},
{title:"Ersatzkennzeichen", field:"ersatzkennzeichen", headerFilter: true},
{title:"Geburtsdatum", field:"gebdatum", formatter:dateFormatter,
headerFilter: true, headerFilterFunc: function(headerValue, rowValue, rowData, filterParams) {
@@ -190,6 +190,24 @@ export default {
if (!currentNode)
return;
const currentSelectedKey = Object.keys(this.selectedKey).find(Boolean);
if (currentSelectedKey) {
if (currentSelectedKey == currentKey)
return;
/**
* Do not select a new entry if the current is a child of the new one.
* This happens if a child entry of a new stg is selected and the router
* tries to select the stg root entry (because subtrees do not have
* routes yet)
*/
const isChild = this.findNodeByKey(
currentSelectedKey,
currentNode.children
);
if (isChild)
return;
}
for (let i = 1; i < parts.length; i++)
{
this.expandedKeys[currentNode.key] = true;
+13 -9
View File
@@ -57,7 +57,7 @@ export default {
init() {
if (!this.endpoint)
return;
this.endpoint.getTags()
this.$api.call(this.endpoint.getTags())
.then(response => response.data)
.then(response => {
this.tags = response
@@ -71,7 +71,7 @@ export default {
'id': tag_id
};
this.endpoint.getTag(getData)
this.$api.call(this.endpoint.getTag(getData))
.then(result => result.data)
.then(result => this.openModal(result))
},
@@ -86,6 +86,7 @@ export default {
this.tagData.updateamum = this.formatDateTime(item.updateamum)
this.tagData.bearbeiter = item.bearbeiter;
this.tagData.verfasser = item.verfasser;
this.tagData.readonly = item.readonly;
if (item && item.notiz_id)
{
@@ -118,7 +119,7 @@ export default {
{
postData.id = this.selectedTagId;
this.tagData.id = this.selectedTagId;
this.endpoint.updateTag(postData);
this.$api.call(this.endpoint.updateTag(postData));
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
}
@@ -130,7 +131,7 @@ export default {
return;
}
this.endpoint.addTag(postData)
this.$api.call(this.endpoint.addTag(postData))
.then(response => response.data)
.then(response => {
if (typeof response === 'number') {
@@ -154,9 +155,10 @@ export default {
let postData = {
id: this.selectedTagId,
done: !this.tagData.done
done: !this.tagData.done,
notiz: this.tagData.notiz,
}
this.endpoint.doneTag(postData)
this.$api.call(this.endpoint.doneTag(postData))
this.$emit("updated", this.tagData);
this.$refs.tagModal.hide();
},
@@ -165,7 +167,7 @@ export default {
let postData = {
id: this.selectedTagId
}
this.endpoint.deleteTag(postData)
this.$api.call(this.endpoint.deleteTag(postData))
this.$emit("deleted", this.selectedTagId)
this.$refs.tagModal.hide();
},
@@ -182,7 +184,8 @@ export default {
verfasser: "",
updateamum: "",
bearbeiter: "",
response: ""
response: "",
readonly: false
};
this.selectedTagId = null;
this.mode = "create";
@@ -230,6 +233,7 @@ export default {
v-model="tagData.notiz"
type="textarea"
field="notiz"
:readonly="tagData.readonly"
placeholder="Notiz..."
></form-input>
<div class="modificationdate">
@@ -243,7 +247,7 @@ export default {
</div>
</div>
</template>
<template #footer>
<template #footer v-if="!tagData.readonly">
<div class="d-flex justify-content-between w-100">
<div>
<button
+1 -1
View File
@@ -706,7 +706,7 @@ export const CoreFilterCmpt = {
</button>
<span v-if="$slots.actions && tabulatorHasSelector && useSelectionSpan">
<span v-if="countOnly">{{ selectedData.length }} ausgewählt</span>
<span v-else> Mit {{ selectedData.length }} ausgewählten:</span>
<span v-else id="selected-info-text"> Mit {{ selectedData.length }} ausgewählten:</span>
</span>
<slot name="actions" v-bind="{selected: tabulatorHasSelector ? selectedData : []}"></slot>
<slot name="search"></slot>
+2 -1
View File
@@ -116,7 +116,7 @@ class GridLogic {
prefer = DIR_RIGHT;
}
const originalFrame = [...item.frame];
const originalFrame = Array.isArray(item.frame) ? [...item.frame] : [item.frame];
const currItem = {...item};
currItem.x = x;
@@ -146,6 +146,7 @@ class GridLogic {
}
}
replaceUpdate[item.index] = { index: item.index, x, y };
return replaceUpdate;
}
+406 -246
View File
@@ -10,7 +10,232 @@ export default {
if (!app.config.globalProperties.$fhcAlert)
app.use(FhcAlert);
function _get_config(form, uri, data, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
function _send_array_or_object(errors, func) {
if (Array.isArray(errors))
errors.forEach(func);
else
Object.entries(errors).forEach(
([title, errs]) => errs.forEach(
error => func(error, title)
)
);
}
let DEFAULT_ERROR_CONFIG = {
success: true,
fail: true,
combine: {
form: ['validation', 'general'],
toast: ['validation', 'general', 'not_found', 'site_failed']
},
handler: {
form(form, errors) {
form.clearValidation();
errors.forEach(err => form.setFeedback(
false,
err.messages || err.message
));
},
async toast(errors) {
const $p = app.config.globalProperties.$p;
if (!$p)
return Promise.reject('Phrasen plugin not loaded!');
async function _format_toast(errors) {
errors = errors.reduce((result, err) => {
switch (err.type) {
case 'not_found':
case 'site_failed':
if (err.message)
result[err.message] = [err.url];
else
result._default = [err.url];
break;
case 'general':
if (!result._default)
result._default = [];
result._default.push(err.message);
break;
case 'validation':
Object.entries(err.messages)
.forEach(([field, msg]) => {
if (!result[field])
result[field] = [];
if (Array.isArray(msg))
result[field].push(...msg);
else
result[field].push(msg);
});
break;
}
return result;
}, {});
let counter = 0;
const msgs = await Promise.all(Object.entries(errors)
.sort((a, b) => ['_default'].indexOf(b[0]) - ['_default'].indexOf(a[0])) // sort _default first
.map(async ([field, msgs]) => {
if (field == '_default') {
await $p.loadCategory('dashboard');
const general = $p.t('dashboard/general');
field = '<dt class="d-none">' + general + '</dt>';
} else {
field = '<dt>' + field + '</dt>';
}
counter += msgs.length;
return field
+ '<dd>'
+ msgs.join('</dd><dd>')
+ '</dd>';
}));
return {
counter,
msgs
}
}
let counter, msgs;
if (Array.isArray(errors)) {
({ counter, msgs } = await _format_toast(errors));
} else {
({ counter, msgs } = await Object.entries(errors)
.reduce(async (res, [title, errs]) => {
const result = await res;
const { counter, msgs } = await _format_toast(errs);
result.counter += counter;
result.msgs.push('<dt>'
+ title
+ '</dt><dd><dl>'
+ msgs.join('')
+ '</dl></dd>');
return result;
}, Promise.resolve({ counter: 0, msgs: []})));
}
await $p.loadCategory('ui');
const n_errors = $p.t('ui/n_errors', { n: counter });
$fhcAlert.alertDefault(
'error',
n_errors,
'<dl>' + msgs.join('') + '</dl>',
true,
true
);
},
php(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
switch (error.severity) {
case 'Warning':
case 'Core Warning':
case 'Compile Warning':
case 'User Warning':
if (title)
title += ': PHP ' + error.severity;
else
title = 'PHP ' + error.severity;
$fhcAlert.alertDefault('warn', title, message, true);
break;
case 'Notice':
case 'User Notice':
case 'Runtime Notice':
if (title)
title += ': PHP ' + error.severity;
else
title = 'PHP ' + error.severity;
$fhcAlert.alertDefault('info', title, message, true);
break;
default:
message = 'Type: PHP ' + error.severity + '\n\n' + message;
if (title)
message = title + '\n\n' + message;
$fhcAlert.alertSystemError(message);
break;
}
});
},
exception(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
if (title)
message += title + '\n\n';
message += 'Type: ' + error.class + '\n\n';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
$fhcAlert.alertSystemError(message);
});
},
db(errors) {
_send_array_or_object(errors, (error, title) => {
var message = '';
if (title)
message += title + '\n\n';
if (error.heading !== undefined)
message += error.heading + '\n\n';
if (error.code !== undefined)
message += 'Code: ' + error.code + '\n\n';
if (error.sql !== undefined)
message += 'SQL: ' + error.sql + '\n\n';
if (error.message !== undefined)
message += 'Message: ' + error.message + '\n\n';
else if (error.messages !== undefined)
message += 'Messages: ' + error.messages.join('\n\t') + '\n\n';
if (error.filename !== undefined)
message += 'Filename: ' + error.filename + '\n';
if (error.line !== undefined)
message += 'Line Number: ' + error.line + '\n';
$fhcAlert.alertSystemError(message);
});
},
auth(errors) {
_send_array_or_object(errors, (error, title) => {
if (title)
title += ': ' + error.message;
else
title = error.message;
var message = '';
message += 'Controller name: ' + error.controller + '\n';
message += 'Method name: ' + error.method + '\n';
message += 'Required permissions: ' + error.required_permissions;
$fhcAlert.alertDefault(
'error',
title,
message,
true
);
});
}
}
};
if (options?.errorHandling !== undefined)
DEFAULT_ERROR_CONFIG = _merge_error_config(options.errorHandling);
function get_config(form, uri, data, config) {
if (typeof form == 'string' && config === undefined) {
[uri, data, config] = [form, uri, data];
form = undefined;
@@ -40,10 +265,9 @@ export default {
return [uri, data, config];
}
function _clean_return_value(response) {
function clean_return_value(response) {
if (typeof response.data === 'string' || response.data instanceof String)
return _clean_return_value({ data: response });
return clean_return_value({ data: response });
const result = response.data;
delete response.data;
@@ -55,10 +279,111 @@ export default {
result.meta.response = response;
return result;
}
const baseURL = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/";
function _merge_error_config(config) {
if (config === false || config === 'off')
return { ...DEFAULT_ERROR_CONFIG, success: false, fail: false };
if (!config || config === true)
return { ...DEFAULT_ERROR_CONFIG };
if (config === 'success')
return { ...DEFAULT_ERROR_CONFIG, fail: false };
if (config === 'fail')
return { ...DEFAULT_ERROR_CONFIG, success: false };
const { success, fail, handler, combine } = config;
config = { ...DEFAULT_ERROR_CONFIG };
Object.entries({ fail, success }).forEach(([key, value]) => {
if (value !== undefined)
config[key] = value;
});
Object.entries({ handler, combine }).forEach(([key, value]) => {
if (value !== undefined)
config[key] = { ...config[key], ...value };
});
return config;
}
function get_error_handler(config) {
const result = _merge_error_config(config?.errorHandling);
if (!config?.form) {
result.combine = { ...result.combine, form: [] };
} else {
const formHandler = result.handler.form;
result.handler = { ...result.handler, form: errors => formHandler(config.form, errors) };
}
return result;
}
function get_error_list(error) {
if (error.response) {
if (error.response.status == 404) {
return [{
type: 'not_found',
message: error.message,
url: error.request.responseURL
}];
} else {
return error.response.data.errors;
}
} else if (error.request) {
return [{
type: 'site_failed',
message: error.message,
url: error.request.responseURL
}];
} else {
return [{
type: 'script',
message: error.message
}];
}
}
function popHandleableErrors(errorHandling, errors) {
const result = {};
const copy = [];
while (errors.length)
copy.push(errors.pop());
for (var error of copy) {
let type = error.type;
let newType = null;
for (var t in errorHandling.combine) {
let newTypeCombinesType = errorHandling
.combine[t]
.includes(type);
let newTypeHasHandler = errorHandling.handler[t];
if (newTypeCombinesType && newTypeHasHandler) {
newType = t;
if (newType == 'form')
break;
}
}
if (newType)
type = newType;
const handler = errorHandling.handler[type];
if (handler) {
if (!result[type])
result[type] = [];
if (Array.isArray(error))
result[type].push(...error);
else
result[type].push(error);
continue;
}
errors.push(error);
}
return result;
}
const fhcApiAxios = axios.create({
timeout: 500000,
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + "/"
baseURL: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ FHC_JS_DATA_STORAGE_OBJECT.ci_router
+ "/"
});
fhcApiAxios.interceptors.request.use(config => {
@@ -100,57 +425,51 @@ export default {
return config;
});
fhcApiAxios.interceptors.response.use(response => {
if (response.config?.errorHandling == 'off'
|| response.config?.errorHandling === false
|| response.config?.errorHandling == 'fail')
return _clean_return_value(response);
// NOTE(chris): loop through errors
if (response.data.errors)
response.data.errors = response.data.errors.filter(
err => (response.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, response.config)
);
return _clean_return_value(response);
}, error => {
if (error.code == 'ERR_CANCELED')
return Promise.reject({...{handled: true}, ...error});
if (error.config?.errorHandling == 'off'
|| error.config?.errorHandling === false
|| error.config?.errorHandling == 'success')
return Promise.reject(error);
if (error.response) {
if (error.response.status == 404) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL, true);
return Promise.reject({...{handled: true}, ...error});
}
fhcApiAxios.interceptors.response.use(
response => {
if (response.config?.errorHandling == 'off'
|| response.config?.errorHandling === false
|| response.config?.errorHandling == 'fail')
return clean_return_value(response);
// NOTE(chris): loop through errors
error.response.data.errors = error.response.data.errors.filter(
err => (error.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, error.config)
);
if (!error.response.data.errors.length)
return Promise.reject({...{handled: true}, ...error});
} else if (error.request) {
app.config.globalProperties.$fhcAlert.alertDefault('error', error.message, error.request.responseURL);
return Promise.reject({...{handled: true}, ...error});
} else {
app.config.globalProperties.$fhcAlert.alertError(error.message);
return Promise.reject({...{handled: true}, ...error});
}
if (response.data.errors)
response.data.errors = response.data.errors.filter(
err => (response.config[err.type + 'ErrorHandler'] || app.config.globalProperties.$api._defaultErrorHandlers[err.type])(err, response.config)
);
return Promise.reject(error);
});
return clean_return_value(response);
},
error => {
if (error.code == 'ERR_CANCELED')
return Promise.reject({ handled: true, ...error });
const errorConfig = get_error_handler(error.config);
if (!errorConfig.fail)
return Promise.reject(error);
const remaining = get_error_list(error);
const errors = popHandleableErrors(errorConfig, remaining);
for (var type in errors) {
errorConfig.handler[type](errors[type]);
}
if (remaining.length)
return Promise.reject(error);
return Promise.reject({ handled: true, ...error });
}
);
app.config.globalProperties.$api = {
getUri(url) {
return fhcApiAxios.getUri({url});
},
get(form, uri, params, config) {
[uri, params, config] = _get_config(form, uri, params, config);
[uri, params, config] = get_config(form, uri, params, config);
if (params) {
if (config)
config.params = params;
@@ -160,82 +479,58 @@ export default {
return fhcApiAxios.get(uri, config);
},
post(form, uri, data, config) {
[uri, data, config] = _get_config(form, uri, data, config);
[uri, data, config] = get_config(form, uri, data, config);
return fhcApiAxios.post(uri, data, config);
},
call(factory, configoverwrite, form) {
if (Array.isArray(factory)) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
const $api = app.config.globalProperties.$api;
Promise
return Promise
.allSettled(factory.map((config, index) => {
if (Array.isArray(config))
return $api.call(config[1], {
errorHeader: config[0],
errorHandling: false
});
else
return $api.call(config, {
errorHeader: '#' + index,
errorHandling: false
});
}))
.then(res => {
// TODO(chris): obey form & configoverwrite
let messagesError = [];
let messagesSuccessful = [];
res.forEach(result => {
if (result.status === 'fulfilled') {
//console.log(JSON.parse(result.value.data));
const successTitle = "<dt>" + result.value.data + "</dt>";
messagesSuccessful.push(successTitle + "ok");
} else {
const errorTitle = "<dt>" + result.reason.config.errorHeader + "</dt>";
const errorMsg = JSON.parse(result.reason.request.response);
const fullMessage = errorMsg.errors.map(error => {
if (error.type == 'validation') {
// TODO(chris): do we want the keys?
return '<dd>' + Object.values(error.messages).join("</dd><dd>") + '</dd>';
}
// TODO(chris): other types
if (error.message)
return '<dd>' + error.message + '</dd>';
if (error.messages)
return '<dd>' + error.messages.join("\n") + '</dd>';
// TODO(chris): what to do here
return '<dd>' + "Generic Error" + '</dd>'; // TODO(chris): translate
}).join("\n");
messagesError.push(errorTitle + fullMessage);
}
if (!Array.isArray(config))
config = ['#' + index, config];
return $api.call(config[1], {
errorHeader: config[0],
errorHandling: false
});
}))
.then(result => {
const [ , , config ] = get_config(form, undefined, undefined, configoverwrite || {});
const errorConfig = get_error_handler(config);
if (messagesError.length)
{
const test = document.createElement('b');
$fhcAlert.alertDefault(
'error',
messagesError.length + " Fehler", // TODO(chris): translate
'<dl>' + messagesError.join("") + '</dl>',
true,
true
);
if (!errorConfig.success && !errorConfig.fail) {
return result;
}
if (messagesSuccessful.length)
{
const test = document.createElement('b');
$fhcAlert.alertDefault(
'info',
'Feedback',
messagesSuccessful.length + " erfolgreiche Statusänderung(en) durchgeführt", // TODO(chris): translate
false,
true
);
const typedErrors = {};
for (var res of result) {
const [ allowed, item ] = res.status === 'fulfilled'
? [ errorConfig.success, res.value ]
: [ errorConfig.fail, res.reason ];
if (!allowed)
return;
const errors = popHandleableErrors(errorConfig, get_error_list(item));
for (var type in errors) {
if (!typedErrors[type])
typedErrors[type] = {
[item.config.errorHeader]: errors[type]
};
else
typedErrors[type][item.config.errorHeader] = errors[type];
}
};
for (var errType in typedErrors) {
errorConfig.handler[errType](typedErrors[errType]);
}
return result;
});
}
let {method, url, params, config} = factory;
let { method, url, params, config } = factory;
if (configoverwrite !== undefined) {
config = configoverwrite;
}
@@ -251,141 +546,6 @@ export default {
} else {
console.error("FhcApi: method not allowed:", method);
}
},
_defaultErrorHandlers: {
validation(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (config?.form) {
config.form.clearValidation();
config.form.setFeedback(false, error.messages);
return false;
}
if (Array.isArray(error.messages)) {
error.messages.forEach($fhcAlert.alertError);
return false;
} else if (typeof error.messages == 'object') {
if (config?.errorHeader)
Object.values(error.messages).forEach(
value => $fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
value,
true
)
);
else
Object.entries(error.messages).forEach(
([key, value]) => $fhcAlert.alertDefault('error', key, value, true)
);
return false;
}
return true;
},
general(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
if (config?.form)
config.form.setFeedback(false, error.message);
else if (config?.errorHeader)
$fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
error.message,
true
);
else
$fhcAlert.alertError(error.message);
},
php(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
switch (error.severity) {
case 'Warning':
case 'Core Warning':
case 'Compile Warning':
case 'User Warning':
$fhcAlert.alertDefault('warn', 'PHP ' + error.severity, message, true);
break;
case 'Notice':
case 'User Notice':
case 'Runtime Notice':
$fhcAlert.alertDefault('info', 'PHP ' + error.severity, message, true);
break;
default:
message = 'Type: PHP ' + error.severity + '\n\n' + message;
$fhcAlert.alertSystemError(message);
break;
}
},
exception(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Type: ' + error.class + '\n\n';
message += 'Message: ' + error.message + '\n\n';
message += 'Filename: ' + error.filename + '\n';
message += 'Line Number: ' + error.line + '\n';
if (error.backtrace && error.backtrace.length) {
message += '\nBacktrace: ';
error.backtrace.forEach(err => {
message += '\n\tFile: ' + err.file + '\n';
message += '\tLine: ' + err.line + '\n';
message += '\tFunction: ' + err.function + '\n';
});
}
$fhcAlert.alertSystemError(message);
},
db(error) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
if (error.heading !== undefined)
message += error.heading + '\n\n';
if (error.code !== undefined)
message += 'Code: ' + error.code + '\n\n';
if (error.sql !== undefined)
message += 'SQL: ' + error.sql + '\n\n';
if (error.message !== undefined)
message += 'Message: ' + error.message + '\n\n';
else if (error.messages !== undefined)
message += 'Messages: ' + error.messages.join('\n\t') + '\n\n';
if (error.filename !== undefined)
message += 'Filename: ' + error.filename + '\n';
if (error.line !== undefined)
message += 'Line Number: ' + error.line + '\n';
$fhcAlert.alertSystemError(message);
},
auth(error, config) {
const $fhcAlert = app.config.globalProperties.$fhcAlert;
var message = '';
message += 'Controller name: ' + error.controller + '\n';
message += 'Method name: ' + error.method + '\n';
message += 'Required permissions: ' + error.required_permissions;
if (config?.errorHeader)
$fhcAlert.alertDefault(
'error',
Array.isArray(config.errorHeader) ? app.config.globalProperties.$p.t.apply(null, config.errorHeader) : config.errorHeader,
error.message,
true
);
else
$fhcAlert.alertDefault('error', error.message, message);
}
}
};
@@ -39,8 +39,19 @@ function parseFilterExpression(expression)
return collections;
}
export function extendedHeaderFilter(headerValue, rowValue)
export function extendedHeaderFilter(headerValue, rowValue, rowData, filterParams)
{
const fields = Array.isArray(filterParams?.field)
? filterParams.field
: [filterParams?.field];
if (fields.length > 1 && rowData)
{
rowValue = fields
.map(f => rowData[f] ?? '')
.filter(Boolean)
.join(' ');
}
if (typeof headerValue === 'boolean')
{
return rowValue === headerValue;
@@ -48,52 +59,79 @@ export function extendedHeaderFilter(headerValue, rowValue)
const collections = parseFilterExpression(headerValue);
try {
function matchValue(value)
{
try {
return collections.some(collection => {
return collections.some(collection => {
let positives = collection.positives.length === 0 || collection.positives.every(condition => {
let positives = collection.positives.length === 0 || collection.positives.every(condition => {
if (condition.type === 'comparison')
{
let value = parseFloat(rowValue);
if (isNaN(value)) return false;
if (condition.type === 'comparison')
{
let value = parseFloat(rowValue);
if (isNaN(value)) return false;
switch (condition.operator) {
case '<':
return value < condition.number;
case '>':
return value > condition.number;
case '<=':
return value <= condition.number;
case '>=':
return value >= condition.number;
case '=':
return value === condition.number;
case '!=':
return value !== condition.number;
default:
return false;
switch (condition.operator) {
case '<':
return value < condition.number;
case '>':
return value > condition.number;
case '<=':
return value <= condition.number;
case '>=':
return value >= condition.number;
case '=':
return value === condition.number;
case '!=':
return value !== condition.number;
default:
return false;
}
}
}
else if (condition.type === 'regex')
{
return condition.regex.test(rowValue);
}
return false;
else if (condition.type === 'regex')
{
return condition.regex.test(rowValue);
}
return false;
});
let negatives = collection.negatives.every(condition => {
return !condition.regex.test(rowValue);
});
return positives && negatives;
});
} catch (e) {
let negatives = collection.negatives.every(condition => {
return !condition.regex.test(rowValue);
});
return positives && negatives;
});
} catch (e) {
}
}
if (matchValue(rowValue))
return true;
if (rowData && filterParams)
{
const childrenField = filterParams?.children || '_children';
const field = filterParams?.field;
const children = rowData[childrenField];
if (Array.isArray(children))
{
for (let child of children)
{
let childValue = child[field];
if (extendedHeaderFilter(headerValue, childValue, child, filterParams))
{
return true;
}
}
}
}
return false;
}
export function tagHeaderFilter(headerValue, rowValue) {
export function tagHeaderFilter(headerValue, rowValue, rowData, filterParams)
{
let data;
@@ -108,18 +146,18 @@ export function tagHeaderFilter(headerValue, rowValue) {
if (Array.isArray(data))
{
combinedText = data
.filter(item => item?.done === false)
.map(item => `${item?.beschreibung} ${item?.notiz}`)
.join(' ');
}
else if (typeof data === 'object' && data !== null)
{
combinedText = `${data?.beschreibung} ${data?.notiz}`;
combinedText = data?.erledigt === false ? `${data?.beschreibung} ${data?.notiz}` : '';
}
else
{
combinedText = String(data);
}
return extendedHeaderFilter(headerValue, combinedText)
}
return extendedHeaderFilter(headerValue, combinedText, rowData, filterParams)
}