Merge branch 'feature-77372/lv_verwaltung_release_fine_tuning' into studvw_lvvw_2026_08_rc

This commit is contained in:
Harald Bamberger
2026-07-28 15:50:06 +02:00
46 changed files with 1725 additions and 431 deletions
+6
View File
@@ -38,3 +38,9 @@ textarea[name="anmerkung"] {
{
border-color: black;
}
.node-verband {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
}
+10 -6
View File
@@ -16,16 +16,20 @@
*/
export default {
getCourselist(params) {
getCourselist(student_uid, start_date, end_date, stundenplan) {
return {
method: 'get',
url: 'api/frontend/v1/stv/LvTermine/getStundenplan/' + params.student_uid + '/'
+ params.start_date + '/'
+ params.end_date + '/'
+ params.group_consecutiveHours + '/'
+ params.dbStundenplanTable
url: 'api/frontend/v1/stv/LvTermine/getStundenplan/' + encodeURIComponent(student_uid) + '/'
+ encodeURIComponent(start_date) + '/'
+ encodeURIComponent(end_date) + '/'
+ encodeURIComponent(stundenplan) + '/'
+ encodeURIComponent(true)
};
},
exportCalendar(student_uid, stundenplan)
{
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?student_uid=' + encodeURIComponent(student_uid) + '&db_stpl_table='+encodeURIComponent(stundenplan);
},
getStudiensemester(){
return {
method: 'get',
+15
View File
@@ -0,0 +1,15 @@
export default {
get() {
return {
method: 'get',
url: '/api/frontend/v1/lv/config/get'
};
},
set(params) {
return {
method: 'post',
url: 'api/frontend/v1/lv/config/set',
params
};
}
};
@@ -18,6 +18,14 @@ export default {
params: newData
};
},
getGruppe(gruppe)
{
return {
method: 'post',
url: '/api/frontend/v1/lv/gruppe/getGruppe/',
params: gruppe
};
},
getByLehreinheit(lehreinheit_id)
{
return {
@@ -0,0 +1,19 @@
export default {
getCourselist(le_id, start_date, end_date, stundenplan)
{
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/getLeEvents/' + encodeURIComponent(le_id) + "/" + encodeURIComponent(start_date) + "/" + encodeURIComponent(end_date) + "/" + encodeURIComponent(stundenplan),
};
},
exportCalendar(le_id, stundenplan)
{
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?lehreinheit_id=' + encodeURIComponent(le_id) + '&db_stpl_table='+encodeURIComponent(stundenplan);
},
getStudiensemester(){
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getStudiensemester/'
};
},
};
@@ -0,0 +1,19 @@
export default {
getCourselist(lv_id, start_date, end_date, stundenplan)
{
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/getLvEvents/' + encodeURIComponent(lv_id) + "/" + encodeURIComponent(start_date) + "/" + encodeURIComponent(end_date) + "/" + encodeURIComponent(stundenplan),
};
},
exportCalendar(lv_id, stundenplan)
{
return FHC_JS_DATA_STORAGE_OBJECT.app_root + 'content/statistik/termine.xls.php?lehrveranstaltung_id=' + encodeURIComponent(lv_id) + '&db_stpl_table='+encodeURIComponent(stundenplan);
},
getStudiensemester(){
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getStudiensemester/'
};
},
};
+25
View File
@@ -0,0 +1,25 @@
import Grades from '../factory/stv/grades.js';
export default {
...Grades,
getCertificate(lv_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/lv/noten/getCertificate/' + encodeURIComponent(lv_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: url
};
},
getTeacherProposal(lv_id, studiensemester_kurzbz) {
let url = 'api/frontend/v1/lv/noten/getTeacherProposal/' + encodeURIComponent(lv_id);
if (!!studiensemester_kurzbz) {
url = url + '/' + encodeURIComponent(studiensemester_kurzbz);
}
return {
method: 'get',
url: url
};
},
};
+9 -2
View File
@@ -1,9 +1,16 @@
export default {
getTabs()
getLETabs()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getTabs/'
url: '/api/frontend/v1/lv/setup/getLETabs/'
};
},
getLVTabs()
{
return {
method: 'get',
url: '/api/frontend/v1/lv/setup/getLVTabs/'
};
},
}
@@ -2,6 +2,7 @@ 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";
import drop from '../../../directives/drop.js';
export default{
name: "LVGruppen",
components: {
@@ -9,6 +10,9 @@ export default{
FormForm,
FormInput
},
directives: {
drop
},
props: {
lehreinheit_id: Number
},
@@ -187,28 +191,48 @@ export default{
this.reload();
})
},
onDropVerband(node, gruppe) {
this.$api.call(ApiGruppe.getGruppe(gruppe))
.then(result => result.data)
.then(result => {
let newData = {
'gid': result,
'lehreinheit_id': this.lehreinheit_id,
'lehrverband': gruppe.lehrverband,
}
this.$api.call(ApiGruppe.add(newData))
.then(result => {
this.reload()
})
});
},
},
template: `
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
:reload=true
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
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>
<div v-drop:link-strict.verband="(evt, gruppe) => onDropVerband(node, gruppe)">
<core-filter-cmpt
ref="table"
:tabulator-options="tabulatorOptions"
:tabulator-events="tabulatorEvents"
table-only
:side-menu="false"
:reload=true
>
<template #search> <!--TODO (david) Slot prüfen -->
<form-input
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>
</div>
`
};
@@ -9,6 +9,8 @@ import LvTabs from "./Setup/Tabs.js";
import ApiDetails from "../../api/lehrveranstaltung/details.js";
import ApiLektor from "../../api/lehrveranstaltung/lektor.js";
import ApiSearchbar from "../../api/factory/searchbar.js";
import AppConfig from "../AppConfig.js";
import ApiLvConfig from "../../api/lehrveranstaltung/config.js";
export default {
@@ -21,8 +23,11 @@ export default {
StvStudiensemester,
LvTable,
LvTabs,
AppConfig
},
props: {
avatarUrl: String,
logoutUrl: String,
defaultSemester: String,
lvRoot: String,
permissions: Object,
@@ -37,7 +42,6 @@ export default {
lehreinheitAnmerkungDefault: (this.config.lehreinheitAnmerkungDefault || '').replace(/\\n/g, '\n'),
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'],
@@ -45,6 +49,8 @@ export default {
},
data() {
return {
appconfig:{},
configEndpoints: ApiLvConfig,
selected: [],
studiengang: "",
selectedStudiensemester: this.defaultSemester,
@@ -104,6 +110,71 @@ export default {
semester() {
return this.filter.semester;
},
appMenuExtraItems()
{
let extraItems = [];
const studiengang_kz = this.stg || '';
const studiensemester = this.selectedStudiensemester;
const semester = this.semester || '';
const uid = this.emp || '';
extraItems.push(
{
description: 'lehre/berichte',
requires: ['stg'],
children: [
{
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/statistik/lvplanung.xls.php'
+ '?studiengang_kz=' + studiengang_kz
+ '&studiensemester_kurzbz=' + studiensemester
+ '&semester=' + semester,
description: 'lehre/lvplanung',
requires: ['stg']
},
{
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/statistik/lehrauftragsliste_gst.xls.php'
+ '?studiengang_kz=' + studiengang_kz
+ '&studiensemester_kurzbz=' + studiensemester
+ '&semester=' + semester,
description: 'lehre/lehrauftragsliste',
requires: ['stg']
},
{
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/pdfExport.php?xml=lehrauftrag.xml.php'
+ '&xsl=Lehrauftrag'
+ '&stg_kz=' + studiengang_kz
+ '&ss=' + studiensemester,
description: 'lehre/lehrauftraege',
requires: ['stg']
},
{
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'content/pdfExport.php?xml=lehrauftrag.xml.php'
+ '&xsl=Lehrauftrag'
+ '&stg_kz=' + studiengang_kz
+ '&ss=' + studiensemester
+ '&uid=' + uid,
description: 'lehre/lehrauftragslisteemp',
requires: ['emp']
}
]
},
{
link: FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'vilesci/lehre/lehrveranstaltung.php'
+ '?stg_kz=' + studiengang_kz,
description: 'lehre/extrakvverwaltung',
requires: ['stg']
}
);
return extraItems;
}
},
methods: {
updateFilter()
@@ -188,6 +259,21 @@ export default {
}
this.selectedStudiensemester = stdsem;
this.selected = [];
},
isDisabled(item)
{
if (!item?.requires?.length)
return false;
const values = {
stg: this.stg,
emp: this.emp,
studiensemester: this.selectedStudiensemester,
semester: this.semester
};
return item.requires.some(req => !values[req]);
}
},
created() {
@@ -261,6 +347,51 @@ export default {
<span class="fa-solid fa-table-list"></span>
</button>
<core-searchbar :searchoptions="searchbaroptions" :searchfunction=searchfunction class="searchbar w-100"></core-searchbar>
<div id="nav-user" class="dropdown">
<button
id="nav-user-btn"
class="btn btn-link rounded-0 py-0"
type="button"
data-bs-toggle="dropdown"
data-bs-target="#nav-user-menu"
aria-expanded="false"
aria-controls="nav-user-menu"
>
<img
:src="avatarUrl"
:alt="$p.t('profilUpdate/profilBild')"
class="bg-light avatar rounded-circle border border-light"
/>
</button>
<ul
ref="navUserDropdown"
class="dropdown-menu dropdown-menu-dark dropdown-menu-end rounded-0 text-center m-0"
aria-labelledby="nav-user-btn"
>
<li>
<button
type="button"
class="dropdown-item"
data-bs-toggle="modal"
data-bs-target="#configModal"
>
{{ $p.t('ui/settings') }}
</button>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<nav-language
item-class="dropdown-item border-left-dark"
/>
</li>
<li><hr class="dropdown-divider m-0"/></li>
<li>
<a class="dropdown-item" :href="logoutUrl">
{{ $p.t('ui/logout') }}
</a>
</li>
</ul>
</div>
</header>
<div class="container-fluid overflow-hidden">
<div class="row h-100">
@@ -270,7 +401,40 @@ export default {
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<div class="offcanvas-body">
<app-menu app-identifier="lvv" />
<app-menu app-identifier="lvv">
<template v-for="(item, key) in appMenuExtraItems" :key="key">
<li v-if="item.children" class="dropend">
<a
class="dropdown-toggle"
href="#"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false"
data-bs-popper-config='{"strategy":"fixed"}'
>
{{ $p.t(item.description) }}
</a>
<ul class="dropdown-menu p-0">
<li
v-for="(child, childKey) in item.children"
:key="childKey"
>
<a class="dropdown-item" :href="child.link" target="_blank" :class="{ disabled: isDisabled(child) }">
{{ $p.t(child.description) }}
</a>
</li>
</ul>
</li>
<li v-else>
<a
:href="item.link"
target="_blank"
:class="{ disabled: isDisabled(item) }">
{{ $p.t(item.description) }}
</a>
</li>
</template>
</app-menu>
</div>
</aside>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
@@ -325,5 +489,6 @@ export default {
</main>
</div>
</div>
<app-config ref="config" v-model="appconfig" :endpoints="configEndpoints"></app-config>
</div>`
};
@@ -140,7 +140,7 @@ export default{
{
this.$emit('changedLektor', this.changed.mitarbeiter_uid);
}
if (this.changed.semesterstunden || this.changed.stundensatz)
else if (this.changed.semesterstunden || this.changed.stundensatz)
{
this.$emit('changedCosts');
}
@@ -183,9 +183,6 @@ export default{
},
},
created() {
this.getLektorData()
},
template: `
<core-form ref="form" @submit.prevent="updateDaten">
<div class="position-sticky top-0 z-1">
@@ -57,6 +57,7 @@ export default {
if (this.$refs.table && this.$refs.table.tabulator)
{
this.expanded = [];
this.currentTreeLevel = 0;
this.reload();
}
},
@@ -143,7 +144,8 @@ export default {
headerFilterFunc: extendedHeaderFilter,
},
layout: 'fitDataStretch',
persistenceID: 'lehrveranstaltungen_2025_07_31_v1',
height: '100%',
persistenceID: 'lehrveranstaltungen_2026_07_15_v1',
selectableRowsRangeMode: 'click',
selectableRows: true,
rowContextMenu: (component, e) => {
@@ -238,14 +240,13 @@ export default {
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]);
}
this.$emit('update:selected', data);
},
getLVInfos(data)
{
@@ -455,47 +456,50 @@ export default {
},
},
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>
<button @click="expandTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'aufklappen')"><i class="fa-solid fa-maximize"></i></button>
<button @click="resetTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'zuklappen')"><i id="togglegroup" class="fa-solid fa-minimize"></i></button>
<core-tag ref="tagComponent"
:endpoint="tagEndpoint"
:values="selectedColumnValues"
@added="addedTag"
@deleted="deletedTag"
@updated="updatedTag"
zuordnung_typ="lehreinheit_id"
></core-tag>
</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>
<div class="lv-list h-100 pt-3">
<div class="tabulator-container d-flex flex-column h-100">
<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>
<button @click="expandTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'aufklappen')"><i class="fa-solid fa-maximize"></i></button>
<button @click="resetTree" class="btn btn-outline-secondary" type="button" :title="$p.t('lehre', 'zuklappen')"><i id="togglegroup" class="fa-solid fa-minimize"></i></button>
<core-tag ref="tagComponent"
:endpoint="tagEndpoint"
:values="selectedColumnValues"
@added="addedTag"
@deleted="deletedTag"
@updated="updatedTag"
zuordnung_typ="lehreinheit_id"
></core-tag>
</template>
<template #search>
<slot name="filterzuruecksetzen"></slot>
</template>
</core-filter-cmpt>
</div>
<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>
</div>
`
};
@@ -8,6 +8,7 @@ export default {
},
data() {
return {
configLETabs: {},
configLVTabs: {},
};
},
@@ -19,7 +20,7 @@ export default {
if (!this.lv || !this.lv.length)
return {};
return this.configLVTabs;
return this.configLETabs;
}
},
methods: {
@@ -31,26 +32,42 @@ export default {
}
},
created() {
this.$api.call(Setup.getTabs())
this.$api.call(Setup.getLETabs())
.then(result => {
this.configLETabs = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api.call(Setup.getLVTabs())
.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">
<div v-else-if="configLETabs && configLVTabs" class="d-flex flex-column h-100 pb-3">
<fhc-tabs
v-if="lv.length === 1"
v-if="lv.length === 1 && lv[0]?.lehreinheit_id"
ref="tabs"
:useprimevue="true"
:modelValue="lv[0]"
:config="configLETabs"
:default="$route.params.tab"
style="flex: 1 1 0%; height: 0%"
@changed="reload"
/>
<fhc-tabs
v-else-if="lv.length === 1"
ref="tabs"
:useprimevue="true"
:modelValue="lv[0]"
:config="configLVTabs"
:default="$route.params.tab"
style="flex: 1 1 0%; height: 0%"
@changed="reload"
/>
</div>
@@ -0,0 +1,21 @@
import TableLvList from "../../Stv/Studentenverwaltung/Details/Lehrveranstaltungstermine/ListLehrveranstaltungstermine.js";
import ApiLVTermine from "../../../api/lehrveranstaltung/lvtermine.js";
export default {
name: "LVTabTermine",
components: {
TableLvList
},
props: {
modelValue: Object,
},
data() {
return {
endpoint: ApiLVTermine
};
},
template: `
<div class="lv-details-course-list h-100 d-flex flex-column">
<table-lv-list ref="tbl_course_list" :id="modelValue.lehrveranstaltung_id" :endpoint="endpoint"></table-lv-list>
</div>`
};
@@ -0,0 +1,116 @@
import NotenZeugnis from "../../Stv/Studentenverwaltung/Details/Noten/Zeugnis.js";
import NotenTeacher from "../../Stv/Studentenverwaltung/Details/Noten/Teacher.js";
import NotenRepeater from "../../Stv/Studentenverwaltung/Details/Noten/Repeater.js";
import ApiLVNoten from "../../../api/lehrveranstaltung/noten.js";
import { highlightGesamtnote } from "../../../helpers/DocumentHelper.js";
const LOCAL_STORAGE_ID = 'lv_details_noten_2025_12_02_stdsem_all';
export default {
name: "LVTabNoten",
components: {
NotenZeugnis,
NotenTeacher,
NotenRepeater
},
provide() {
return {
config: this.config
}
},
props: {
modelValue: Object,
config: Object
},
data() {
return {
stdsem: '',
endpoint: ApiLVNoten,
tabulatorOptions: {
visibleColumns: {
vorname: true,
nachname: true,
lehrveranstaltung_bezeichnung: false
},
headerFilter: {
vorname: true,
nachname: true,
lehrveranstaltung_bezeichnung: false
},
persistenceZeugnisID: 'lv-details-noten-zeugnis-2025120401',
persistenceTeacherID: 'lv-details-noten-teacher-2025120401',
},
zeugnisLoaded: false,
teacherLoaded: false,
};
},
methods: {
reload() {
this.zeugnisLoaded = false;
this.teacherLoaded = false;
this.$refs.zeugnis.$refs.table.reloadTable();
this.$refs.teacher.$refs.table.reloadTable();
},
saveStdsem(event) {
window.localStorage.setItem(LOCAL_STORAGE_ID, event.target.value);
},
onZeugnisLoaded() {
this.zeugnisLoaded = true;
this.checkHighlight();
},
onTeacherLoaded() {
this.teacherLoaded = true;
this.checkHighlight();
},
checkHighlight()
{
if (!this.zeugnisLoaded || !this.teacherLoaded)
return;
if (!this.$refs.zeugnis || !this.$refs.teacher)
return;
let zeugnisTable = this.$refs.zeugnis.$refs.table.tabulator;
let teacherTable = this.$refs.teacher.$refs.table.tabulator;
if (!zeugnisTable || !teacherTable)
return;
highlightGesamtnote(zeugnisTable, teacherTable);
}
},
created() {
const savedPath = window.localStorage.getItem(LOCAL_STORAGE_ID);
this.stdsem = savedPath || '';
},
template: `
<div class="stv-details-noten d-flex flex-column overflow-hidden">
<div class="mb-3">
<select class="form-select" v-model="stdsem" @input="saveStdsem" v-if="config?.semesterSelect ?? true">
<option value="">{{ $p.t('ui/current_semester') }}</option>
<option value="true">{{ $p.t('ui/all_semester') }}</option>
</select>
</div>
<div class="row">
<div class="col-8">
<noten-zeugnis
ref="zeugnis"
:id="modelValue.lehrveranstaltung_id"
:all-semester="!!stdsem"
:endpoint="endpoint"
:optionalTabulatorOptions="tabulatorOptions"
@loaded="onZeugnisLoaded"/>
</div>
<div class="col-4">
<noten-teacher
ref="teacher"
:id="modelValue.lehrveranstaltung_id"
:all-semester="!!stdsem"
:endpoint="endpoint"
@copied="reload"
:optionalTabulatorOptions="tabulatorOptions"
@loaded="onTeacherLoaded"/>
</div>
</div>
</div>`
};
@@ -0,0 +1,21 @@
import TableLvList from "../../Stv/Studentenverwaltung/Details/Lehrveranstaltungstermine/ListLehrveranstaltungstermine.js";
import ApiLETermine from "../../../api/lehrveranstaltung/letermine.js";
export default {
name: "LETabTermine",
components: {
TableLvList
},
props: {
modelValue: Object,
},
data() {
return {
endpoint: ApiLETermine
};
},
template: `
<div class="le-details-course-list h-100 d-flex flex-column">
<table-lv-list ref="tbl_course_list" :id="modelValue.lehreinheit_id" :endpoint="endpoint"></table-lv-list>
</div>`
};
@@ -1,4 +1,5 @@
import TableLvList from "./Lehrveranstaltungstermine/ListLehrveranstaltungstermine.js";
import ApiStvCoursedates from "../../../../api/factory/stv/coursedates.js";
export default {
name: "TabCourseList",
@@ -8,11 +9,13 @@ export default {
props: {
modelValue: Object,
},
data(){
return {}
data() {
return {
endpoint: ApiStvCoursedates
};
},
template: `
<div class="stv-details-course-list h-100 d-flex flex-column">
<table-lv-list ref="tbl_course_list" :student="modelValue"></table-lv-list>
<table-lv-list ref="tbl_course_list" :id="modelValue.uid" :endpoint="endpoint"></table-lv-list>
</div>`
};
@@ -2,8 +2,6 @@ import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import FormInput from "../../../../Form/Input.js";
import FormForm from '../../../../Form/Form.js';
import ApiStvCoursedates from "../../../../../api/factory/stv/coursedates.js";
export default {
name: "TblCourseList",
components: {
@@ -17,7 +15,14 @@ export default {
},
},
props: {
student: Object
id: {
type: [Number, String],
required: true
},
endpoint: {
type: Object,
required: true
},
},
data(){
return {
@@ -48,14 +53,7 @@ export default {
initTabulatorOptions(){
this.tabulatorOptions = {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(
ApiStvCoursedates.getCourselist({
student_uid: this.student.uid,
start_date: this.dataSem.start,
end_date: this.dataSem.ende,
group_consecutiveHours: true,
dbStundenplanTable: this.dbStundenplanTable})
),
ajaxRequestFunc: () => this.$api.call(this.endpoint.getCourselist(this.id, this.dataSem.start, this.dataSem.ende, this.dbStundenplanTable)),
ajaxResponse: (url, params, response) => {
return response.data;
},
@@ -131,7 +129,7 @@ export default {
this.dataSem = this.listStudiensemester.find(item => item.studiensemester_kurzbz === studiensemester_kurzbz);
},
exportToExcel(){
window.open(this.downloadLink, '_blank');
window.open(this.endpoint.exportCalendar(this.id, this.dbStundenplanTable), '_blank');
},
reload() {
this.$refs.table.reloadTable();
@@ -145,10 +143,13 @@ export default {
currentSemester(newVal, oldVal) {
this.getDatesOfSemester(newVal);
},
id() {
this.reload();
}
},
created(){
this.$api
.call(ApiStvCoursedates.getStudiensemester())
.call(this.endpoint.getStudiensemester())
.then(result => {
this.listStudiensemester = result.data;
this.getDatesOfSemester(this.currentSemester);
@@ -2,6 +2,9 @@ import NotenZeugnis from './Noten/Zeugnis.js';
import NotenTeacher from './Noten/Teacher.js';
import NotenRepeater from './Noten/Repeater.js';
import ApiStvGrades from '../../../../api/factory/stv/grades.js';
import { highlightGesamtnote } from "../../../../helpers/DocumentHelper.js";
const LOCAL_STORAGE_ID = 'stv_details_noten_2024-11-25_stdsem_all';
export default {
@@ -22,7 +25,10 @@ export default {
},
data() {
return {
stdsem: ''
stdsem: '',
endpoint: ApiStvGrades,
zeugnisLoaded: false,
teacherLoaded: false,
};
},
methods: {
@@ -33,6 +39,30 @@ export default {
},
saveStdsem(event) {
window.localStorage.setItem(LOCAL_STORAGE_ID, event.target.value);
},
onZeugnisLoaded() {
this.zeugnisLoaded = true;
this.checkHighlight();
},
onTeacherLoaded() {
this.teacherLoaded = true;
this.checkHighlight();
},
checkHighlight()
{
if (!this.zeugnisLoaded || !this.teacherLoaded)
return;
if (!this.$refs.zeugnis || !this.$refs.teacher)
return;
let zeugnisTable = this.$refs.zeugnis.$refs.table.tabulator;
let teacherTable = this.$refs.teacher.$refs.table.tabulator;
if (!zeugnisTable || !teacherTable)
return;
highlightGesamtnote(zeugnisTable, teacherTable);
}
},
created() {
@@ -49,10 +79,10 @@ export default {
</div>
<div class="row">
<div class="col-8">
<noten-zeugnis ref="zeugnis" :student="modelValue" :all-semester="!!stdsem"></noten-zeugnis>
<noten-zeugnis ref="zeugnis" :id="modelValue.prestudent_id" :all-semester="!!stdsem" :endpoint="endpoint" @loaded="onZeugnisLoaded"></noten-zeugnis>
</div>
<div class="col-4">
<noten-teacher ref="teacher" :student="modelValue" :all-semester="!!stdsem" @copied="reload"></noten-teacher>
<noten-teacher ref="teacher" :id="modelValue.prestudent_id" :endpoint="endpoint" :all-semester="!!stdsem" @copied="reload" @loaded="onTeacherLoaded"></noten-teacher>
<noten-repeater class="mt-4" ref="repeater" :student="modelValue" :all-semester="!!stdsem" @copied="reload"></noten-repeater>
</div>
</div>
@@ -1,13 +1,12 @@
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import ApiStvGrades from '../../../../../api/factory/stv/grades.js';
export default {
components: {
CoreFilterCmpt
},
emits: [
"copied"
"copied",
"loaded"
],
inject: {
currentSemester: {
@@ -16,20 +15,33 @@ export default {
}
},
props: {
student: Object,
allSemester: Boolean
endpoint: {
type: Object,
required: true
},
id: {
type: [Number, String],
required: true
},
allSemester: Boolean,
optionalTabulatorOptions: Object,
},
data() {
return {
tabulatorEvents: []
tabulatorEvents: [
{
event: "dataProcessed",
handler: () => this.$emit("loaded"),
},
]
};
},
computed: {
tabulatorOptions() {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiStvGrades.getTeacherProposal(
this.student.prestudent_id,
ajaxRequestFunc: () => this.$api.call(this.endpoint.getTeacherProposal(
this.id,
(!this.allSemester ? this.currentSemester : null)
)),
ajaxResponse: (url, params, response) => {
@@ -77,16 +89,19 @@ export default {
{ field: 'lehrveranstaltung_id', title: this.$p.t('lehre/lehrveranstaltung_id'), visible: false },
{ field: 'punkte', title: this.$p.t('stv/grades_points'), visible: false }
],
columnDefaults: {
headerFilter: this.optionalTabulatorOptions?.headerFilter ?? false,
},
layout: 'fitDataStretch',
height: '100%',
selectableRows: true,
selectableRowsRangeMode: 'click',
persistenceID: 'stv-details-noten-teacher-20260217'
persistenceID: this.optionalTabulatorOptions?.persistenceTeacherID ?? 'stv-details-noten-teacher-2025120401',
};
}
},
watch: {
student(n) {
id() {
this.$refs.table.reloadTable();
},
allSemester(n) {
@@ -97,7 +112,7 @@ export default {
copyGrades(selected) {
const promises = selected.map(
grade => this.$api
.call(ApiStvGrades.copyTeacherProposalToCertificate(grade), {
.call(this.endpoint.copyTeacherProposalToCertificate(grade), {
errorHeader: grade.lehrveranstaltung_bezeichnung
})
.then(() => {
@@ -2,8 +2,6 @@ import {CoreFilterCmpt} from "../../../../filter/Filter.js";
import ZeugnisActions from './Zeugnis/Actions.js';
import ZeugnisDocuments from './Zeugnis/Documents.js';
import ApiStvGrades from '../../../../../api/factory/stv/grades.js';
export default {
name: 'Zeugnis',
components: {
@@ -11,6 +9,7 @@ export default {
ZeugnisActions,
ZeugnisDocuments
},
emits: ['loaded'],
inject: {
config: {
from: 'config',
@@ -22,7 +21,15 @@ export default {
}
},
props: {
student: Object,
endpoint: {
type: Object,
required: true
},
id: {
type: [Number, String],
required: true
},
optionalTabulatorOptions: Object,
allSemester: Boolean
},
data() {
@@ -35,6 +42,10 @@ export default {
return item;
})
},
{
event: "dataProcessed",
handler: () => this.$emit("loaded")
},
{
event: "rowSelected",
handler: row => row.getElement().style.zIndex = 12
@@ -53,7 +64,7 @@ export default {
computed: {
tabulatorOptions() {
const listPromise = this.$api
.call(ApiStvGrades.list())
.call(this.endpoint.list())
.then(res => res.data.map(({bezeichnung: label, note: value}) => ({label, value})));
let gradeField = {
@@ -86,7 +97,7 @@ export default {
note_bezeichnung
}))
// send to backend
.then(data => this.$api.call(ApiStvGrades.updateCertificate(data)))
.then(data => this.$api.call(this.endpoint.updateCertificate(data)))
// get bezeichnung again
.then(() => listPromise)
.then(list => list.find(el => el.value == note))
@@ -109,7 +120,7 @@ export default {
if (filterTerm) {
return this.$api
.call(
ApiStvGrades.getGradeFromPoints(
this.endpoint.getGradeFromPoints(
filterTerm,
cell.getData().lehrveranstaltung_id,
this.currentSemester
@@ -142,6 +153,8 @@ export default {
.then(() => node.innerText = this.$p.t('ui/loading'))
.catch(this.$fhcAlert.handleSystemError);
gradeField.editorParams.placeholderLoading = node;
gradeField.visible = this.optionalTabulatorOptions?.visibleColumns?.note ?? true
gradeField.headerFiler = this.optionalTabulatorOptions?.headerFilter?.note || this.optionalTabulatorOptions?.note || false
}
const columns = [
@@ -238,8 +251,8 @@ export default {
return {
ajaxURL: 'dummy',
ajaxRequestFunc: () => this.$api.call(ApiStvGrades.getCertificate(
this.student.prestudent_id,
ajaxRequestFunc: () => this.$api.call(this.endpoint.getCertificate(
this.id,
(!this.allSemester ? this.currentSemester : null)
)),
ajaxResponse: (url, params, response) => {
@@ -248,9 +261,10 @@ export default {
columns,
height: '100%',
layout: 'fitDataStretchFrozen',
selectableRows: 1,
selectable: true,
selectableRowsRangeMode: 'click',
persistenceID: 'stv-details-noten-zeugnis-20260217',
selectableRows: true,
persistenceID: this.optionalTabulatorOptions?.persistenceZeugnisID ?? 'stv-details-noten-zeugnis-20260217',
persistence:{
columns: ["width", "visible", "frozen"]
}
@@ -258,7 +272,8 @@ export default {
}
},
watch: {
student(n) {
id()
{
this.$refs.table.reloadTable();
},
allSemester(n) {
@@ -269,7 +284,7 @@ export default {
setGrade(data) {
this.$api
.call(
ApiStvGrades.updateCertificate(data),
this.endpoint.updateCertificate(data),
{ errorHeader: data.lehrveranstaltung_bezeichnung }
)
.then(this.$refs.table.reloadTable)
@@ -282,7 +297,7 @@ export default {
.confirmDelete()
.then(result => result ? data : Promise.reject({handled:true}))
.then(data => this.$api.call(
ApiStvGrades.deleteCertificate(data),
this.endpoint.deleteCertificate(data),
{ errorHeader: data.lehrveranstaltung_bezeichnung }
))
.then(this.$refs.table.reloadTable)
@@ -291,7 +306,7 @@ export default {
}
},
created() {
this.$p.loadCategory(['global', 'stv', 'lehre'])
this.$p.loadCategory(['global', 'stv', 'lehre', 'person'])
.then(() => {
if (this.$refs.table.tableBuilt)
this.$refs.table.tabulator.columnManager.setColumns(this.tabulatorOptions.columns);
+44
View File
@@ -0,0 +1,44 @@
export function highlightGesamtnote(zeugnisTable, teacherTable)
{
let zeugnisData = zeugnisTable.getData();
let studentsWithNote = new Set();
zeugnisData.forEach(row => {
let student = row.uid;
let lv = row.lehrveranstaltung_id;
let note = row.note;
if (!student || !lv)
return;
if (note == null || note === "")
return;
let key = `${student}_${lv}`;
studentsWithNote.add(key);
});
teacherTable.deselectRow();
teacherTable.getRows().forEach(row => {
let data = row.getData();
let student = data.student_uid;
let lv = data.lehrveranstaltung_id;
let note = data.note;
if (!student || !lv)
return;
if (note == null || note === "")
return;
let key = `${student}_${lv}`;
if (!studentsWithNote.has(key))
{
row.select();
}
});
}
+4
View File
@@ -30,6 +30,10 @@ const TYPE_DEFINITION = {
prestudent: {
id: "prestudent_id",
dragIcon: "fa-solid fa-user-graduate text-muted"
},
verband: {
id: "gruppe_id",
dragIcon: "fa-solid fa-user-group text-muted"
}
// TODO: IMPLEMENT OTHER TYPES
};