Merge branch 'cis40_2026-05_ma_rc' into demo-cis40

This commit is contained in:
Harald Bamberger
2026-06-24 15:52:23 +02:00
6 changed files with 304 additions and 206 deletions
@@ -1353,7 +1353,9 @@ class Lehrveranstaltung_model extends DB_Model
tbl_lehrveranstaltung.semester as lv_semester,
tbl_lehrveranstaltung.bezeichnung as lv_bezeichnung,
(SELECT kurzbz FROM public.tbl_mitarbeiter
WHERE mitarbeiter_uid=tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as lektor
WHERE mitarbeiter_uid=tbl_lehreinheitmitarbeiter.mitarbeiter_uid) as lektor,
lehre.tbl_lehrveranstaltung.orgform_kurzbz as orgform
FROM
lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
+63 -48
View File
@@ -95,37 +95,37 @@ foreach ($result_student as $row)
if ($uids == '')
die('Es befinden sich keine Studierende in diesem Semester');
$qry = "SELECT
lehrveranstaltung_id, bezeichnung, studiengang_kz, semester, ects
FROM
lehre.tbl_lehrveranstaltung
WHERE
lehrveranstaltung_id IN
(
SELECT
distinct lehrveranstaltung_id
FROM
campus.vw_student_lehrveranstaltung, public.tbl_studentlehrverband
WHERE
tbl_studentlehrverband.studiengang_kz=".$db->db_add_param($studiengang_kz, FHC_INTEGER)." AND
tbl_studentlehrverband.semester=".$db->db_add_param($semester, FHC_INTEGER)." AND
vw_student_lehrveranstaltung.studiensemester_kurzbz=".$db->db_add_param($semester_aktuell)." AND
uid=student_uid AND
vw_student_lehrveranstaltung.studiensemester_kurzbz=tbl_studentlehrverband.studiensemester_kurzbz
$qry = "SELECT
lehrveranstaltung_id, bezeichnung, studiengang_kz, semester, ects
FROM
lehre.tbl_lehrveranstaltung
WHERE
lehrveranstaltung_id IN (
SELECT
DISTINCT lehrveranstaltung_id
FROM
campus.vw_student_lehrveranstaltung
JOIN public.tbl_studentlehrverband ON(student_uid = uid)
WHERE
tbl_studentlehrverband.studiengang_kz = ".$db->db_add_param($studiengang_kz, FHC_INTEGER)."
AND tbl_studentlehrverband.semester = ".$db->db_add_param($semester, FHC_INTEGER)."
AND vw_student_lehrveranstaltung.studiensemester_kurzbz = ".$db->db_add_param($semester_aktuell)."
AND vw_student_lehrveranstaltung.studiensemester_kurzbz = tbl_studentlehrverband.studiensemester_kurzbz
)
AND studiengang_kz<>0
AND studiengang_kz != 0
AND zeugnis
UNION
SELECT
lehrveranstaltung_id, bezeichnung, studiengang_kz, semester, ects
FROM
lehre.tbl_lehrveranstaltung JOIN lehre.tbl_zeugnisnote USING(lehrveranstaltung_id)
WHERE
tbl_lehrveranstaltung.studiengang_kz=".$db->db_add_param($studiengang_kz, FHC_INTEGER)." AND
tbl_zeugnisnote.student_uid in($uids) AND
tbl_zeugnisnote.studiensemester_kurzbz=".$db->db_add_param($semester_aktuell)." AND
zeugnis
ORDER BY bezeichnung";
UNION
SELECT
lehrveranstaltung_id, bezeichnung, studiengang_kz, semester, ects
FROM
lehre.tbl_lehrveranstaltung
JOIN lehre.tbl_zeugnisnote USING(lehrveranstaltung_id)
WHERE
tbl_lehrveranstaltung.studiengang_kz = ".$db->db_add_param($studiengang_kz, FHC_INTEGER)."
AND tbl_zeugnisnote.student_uid IN ($uids)
AND tbl_zeugnisnote.studiensemester_kurzbz = ".$db->db_add_param($semester_aktuell)."
AND zeugnis
ORDER BY bezeichnung";
if (!$result_lva = $db->db_query($qry))
die('Fehler beim Ermitteln der Lehrveranstaltungen');
@@ -338,6 +338,10 @@ if ($typ == 'xls')
}
$anzahl_lvspalten = $spalte - 2;
$worksheet->write($zeile, ++$spalte, 'ECTS Summe zugeteilt', $format_bold);
$maxlength[$spalte] = 20;
$worksheet->write($zeile, ++$spalte, 'ECTS Summe gewichtet', $format_bold);
$maxlength[$spalte] = 20;
$worksheet->write($zeile, ++$spalte, 'Notendurchschnitt', $format_bold);
$maxlength[$spalte] = 15;
$worksheet->write($zeile, ++$spalte, "Gewichteter\nNotendurchschnitt", $format_bold_wrap);
@@ -372,9 +376,12 @@ if ($typ == 'xls')
$worksheet->write($zeile, ++$spalte, $row_student->gruppe, $format_bold_left);
$worksheet->write($zeile, ++$spalte, $row_student->matrikelnr, $format_bold);
//Alle Zeugnisnoten des Studierenden holen
// Alle Zeugnisnoten des Studierenden holen
$noten = array();
$qry = "SELECT * FROM lehre.tbl_zeugnisnote WHERE student_uid=".$db->db_add_param($row_student->uid)." AND studiensemester_kurzbz=".$db->db_add_param($semester_aktuell);
$qry = "SELECT lehrveranstaltung_id, note
FROM lehre.tbl_zeugnisnote
WHERE student_uid = ".$db->db_add_param($row_student->uid)."
AND studiensemester_kurzbz = ".$db->db_add_param($semester_aktuell);
if ($result = $db->db_query($qry))
while ($row = $db->db_fetch_object($result))
$noten[$row->lehrveranstaltung_id] = $row->note;
@@ -382,15 +389,16 @@ if ($typ == 'xls')
//Zu jeder Lehrveranstaltungsnote Prüfungstyp (Anzahl der Antritte) holen
$pruefungstypen = array();
$qry = "SELECT tbl_lehrveranstaltung.lehrveranstaltung_id, pruefungstyp_kurzbz, sort, datum
FROM
lehre.tbl_pruefung
JOIN
lehre.tbl_lehreinheit using(lehreinheit_id)
JOIN
lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id)
WHERE
student_uid=".$db->db_add_param($row_student->uid)." AND studiensemester_kurzbz=".$db->db_add_param($semester_aktuell)."
ORDER BY lehrveranstaltung_id, sort, datum";
FROM
lehre.tbl_pruefung
JOIN
lehre.tbl_lehreinheit USING(lehreinheit_id)
JOIN
lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
WHERE
student_uid = ".$db->db_add_param($row_student->uid)."
AND studiensemester_kurzbz = ".$db->db_add_param($semester_aktuell)."
ORDER BY lehrveranstaltung_id, sort, datum";
if ($result = $db->db_query($qry))
{
while ($row = $db->db_fetch_object($result))
@@ -399,15 +407,14 @@ if ($typ == 'xls')
}
}
//Alle LVs holen zu denen der Studierende zugeteilt ist
// Alle LVs holen zu denen der Studierende zugeteilt ist
$zugeteilte_lvs = array();
$qry = "SELECT distinct lehrveranstaltung_id
FROM
campus.vw_student_lehrveranstaltung
WHERE
uid=".$db->db_add_param($row_student->uid)." AND
studiensemester_kurzbz=".$db->db_add_param($semester_aktuell);
$qry = "SELECT DISTINCT lehrveranstaltung_id
FROM
campus.vw_student_lehrveranstaltung
WHERE
uid = ".$db->db_add_param($row_student->uid)."
AND studiensemester_kurzbz = ".$db->db_add_param($semester_aktuell);
if ($result = $db->db_query($qry))
while ($row = $db->db_fetch_object($result))
$zugeteilte_lvs[] = $row->lehrveranstaltung_id;
@@ -416,17 +423,20 @@ if ($typ == 'xls')
$summe = 0;
$rowcount = 0;
$summeects = 0;
$total_ects = 0;
$gewichtetenote = 0;
while ($rowcount < $db->db_num_rows($result_lva))
{
$row_lva = $db->db_fetch_object($result_lva, $rowcount);
$rowcount++;
//wenn es eine Note gibt
if (isset($noten[$row_lva->lehrveranstaltung_id]))
{
$note = $noten[$row_lva->lehrveranstaltung_id];
$format = 0;
$total_ects += $row_lva->ects;
//wenn für die LV der Studierende eine Nachprüfung hat (z.B. 2 Termin, kommissionelle...)
if (isset($pruefungstypen[$row_lva->lehrveranstaltung_id]))
@@ -472,6 +482,7 @@ if ($typ == 'xls')
//Keine Note fuer diese LV vorhanden
if (in_array($row_lva->lehrveranstaltung_id, $zugeteilte_lvs))
{
$total_ects += $row_lva->ects;
$worksheet->write($zeile, ++$spalte, '', $format_colored_nichteingetragen);
}
else
@@ -489,6 +500,8 @@ if ($typ == 'xls')
if ($summeects != 0)
$gewichtetenote /= $summeects;
$worksheet->write($zeile, ++$spalte, sprintf("%.2f", $total_ects), $format_number);
$worksheet->write($zeile, ++$spalte, sprintf("%.2f", $summeects), $format_number);
$worksheet->write($zeile, ++$spalte, sprintf("%.2f", $schnitt), $format_number);
$worksheet->write($zeile, ++$spalte, sprintf("%.2f", $gewichtetenote), $format_number);
if ($gewichtetenote != 0)
@@ -529,6 +542,8 @@ if ($typ == 'xls')
$schnitt = $summe_schnitt / $anzahl_schnitt;
else
$schnitt = 0;
$worksheet->write($zeile, ++$spalte, '-', $format_bold_center);
$worksheet->write($zeile, ++$spalte, '-', $format_bold_center);
$worksheet->write($zeile, ++$spalte, sprintf("%.2f", $schnitt), $format_number);
if ($anzahlgewichtet != 0)
$summegewichtet = $summegewichtet / $anzahlgewichtet;
+1 -1
View File
@@ -101,7 +101,7 @@ const router = VueRouter.createRouter({
props: true
},
{
path: `/Cis/Benotungstool/:lv_id/:sem_kurzbz`,
path: `/Cis/Benotungstool/:lv_id?/:sem_kurzbz?`,
name: 'Benotungstool',
component: Benotungstool,
props: true
@@ -1,6 +1,7 @@
import {CoreFilterCmpt} from "../../filter/Filter.js";
import ApiLehre from "../../../api/factory/lehre.js";
import ApiNoten from "../../../api/factory/noten.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
import ApiStudiensemester from "../../../api/factory/studiensemester.js";
import BsModal from '../../Bootstrap/Modal.js';
import BsOffcanvas from '../../Bootstrap/Offcanvas.js';
@@ -9,7 +10,7 @@ import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule.js';
import MobilityLegende from '../../Mobility/Legende.js';
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import {debounce} from "../../../helpers/debounce.js";
import {centeredFormatter} from "../../../tabulator/formatter/centered.js";
import {centeredTextFormatter} from "../../../tabulator/formatter/centered.js";
export const Benotungstool = {
name: "Benotungstool",
@@ -30,25 +31,20 @@ export const Benotungstool = {
props: {
lv_id: {
default: null,
required: true
required: false
},
sem_kurzbz: {
default: null,
required: true
},
viewData: {
type: Object,
required: true,
default: () => ({uid: ''}),
validator(value) {
return value && value.uid
}
required: false
}
},
data() {
return {
uid: null,
headerFiltersRestored: false,
filtersRestored: false,
filteredRows: null,
filteredcount: 0,
colLayoutRestored: false,
sortRestored: false,
stateRestored: false,
@@ -137,10 +133,32 @@ export const Benotungstool = {
this.undoSelection(cell)
}
}
},
{
event: 'dataFiltered',
handler: (filters, rows) => {
this.filteredRows = rows;
this.filteredcount = rows.length;
if (!this.selectedUids.length) return;
const visibleData = new Set(rows.map(r => r.getData()));
const filteredOut = this.selectedUids.filter(su => !visibleData.has(su));
if (!filteredOut.length) return;
const filteredOutSet = new Set(filteredOut);
this.$refs.notenTable.tabulator.getSelectedRows()
.filter(r => filteredOutSet.has(r.getData()))
.forEach(r => r.deselect());
}
}
]};
},
methods: {
async fetchAuthUID() {
const authIdResponse = await this.$api.call(ApiAuthinfo.getAuthUID());
this.uid = authIdResponse.data.uid;
},
loadState() {
return JSON.parse(localStorage.getItem(this.persistenceID) || "null");
},
@@ -724,12 +742,13 @@ export const Benotungstool = {
virtualDom: true,
virtualDomBuffer: 5000,
index: 'uid',
layout: 'fitDataStretch',
layout: 'fitData',
placeholder: this.$capitalize(this.$p.t('global/noDataAvailable')),
selectable: true,
selectableRangeMode: "click", // shift+click
selectablePersistence: false, // reset selection on table reload
selectableCheck: this.selectableCheck,
rowHeight: 30,
rowFormatter: this.fixTabulatorSelectionFormatter,
columns: this.getColumnsDefinition(),
persistence: false,
@@ -797,18 +816,18 @@ export const Benotungstool = {
field: 'selectCol',
title: ''
})
columns.push({title: 'UID', field: 'uid', tooltip: false, widthGrow: 1, topCalc: this.sumCalcFunc, formatter: centeredFormatter, cssClass: 'sticky-col'})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mail'))), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, widthGrow: 1, variableHeight: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))), field: 'hoechsterAntritt', formatter: centeredFormatter, tooltip: false, widthGrow: 1})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', formatter: centeredFormatter, headerFilter: true, tooltip: false, widthGrow: 1})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', formatter: centeredFormatter, headerFilter: true, widthGrow: 1})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4anwesenheitsquote'))), field: 'anwquote', widthGrow: 1, formatter: this.percentFormatter})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mobility'))), field: 'mobility_zusatz', formatter: centeredFormatter, headerFilter: true, widthGrow: 1, visible: false})
columns.push({title: 'UID', field: 'uid', tooltip: false, topCalc: this.sumCalcFunc, formatter: centeredTextFormatter, cssClass: 'sticky-col'})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mail'))), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, variableHeight: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))), field: 'hoechsterAntritt', formatter: centeredTextFormatter, tooltip: false})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', formatter: centeredTextFormatter, headerFilter: true, tooltip: false})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', formatter: centeredTextFormatter, headerFilter: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4anwesenheitsquote'))), field: 'anwquote', formatter: this.percentFormatter})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mobility'))), field: 'mobility_zusatz', formatter: centeredTextFormatter, headerFilter: true, visible: false})
if(this.config?.CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE) {
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4teilnoten'))), field: 'teilnote', widthGrow: 1, formatter: this.teilnotenFormatter, variableHeight: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4teilnoten'))), field: 'teilnote', formatter: this.teilnotenFormatter, variableHeight: true})
}
if(this.config?.CIS_GESAMTNOTE_PUNKTE) {
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4punkte'))), field: 'punkte', widthGrow: 1,
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4punkte'))), field: 'punkte',
editor: this.liveNumberEditor,
editable: (cell) => {
const rowData = cell.getRow().getData();
@@ -856,8 +875,7 @@ export const Benotungstool = {
if(val === undefined) return ''
if(p || !match?.lkt_ueberschreibbar) style = 'color: gray;font-style: italic; background-color: #f0f0f0;pointer-events: none;opacity: 0.6;user-select: none;cursor: not-allowed;'
return '<div style="'+style+'">' + val + '</div>'
},
widthGrow: 1
}
})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4notenvorschlagUebernehmen'))), field: 'übernehmen', width: 150, hozAlign: 'center', formatter: this.arrowFormatter,
cellClick: this.saveNote,
@@ -868,10 +886,9 @@ export const Benotungstool = {
headerFilterParams: () => {
return { values: ["\u00A0",this.$p.t('benotungstool/c4noteEmpty') ,this.$p.t('benotungstool/c4positiv'), this.$p.t('benotungstool/c4negativ') ,...this.notenOptions.map(opt => opt.bezeichnung)] }
},
headerFilterFunc: this.notenFilterFunc,
widthGrow: 1
headerFilterFunc: this.notenFilterFunc
})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4freigabe'))), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter, variableHeight: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4freigabe'))), field: 'freigegeben', formatter: this.freigabeFormatter, variableHeight: true})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4zeugnisnote'))),
field: 'note',
formatter: this.notenFormatter,
@@ -881,8 +898,7 @@ export const Benotungstool = {
headerFilterParams: () => {
return { values: ["\u00A0", this.$p.t('benotungstool/c4noteEmpty'),this.$p.t('benotungstool/c4positiv'), this.$p.t('benotungstool/c4negativ') ,...this.notenOptions.map(opt => opt.bezeichnung)] }
},
headerFilterFunc: this.notenFilterFunc,
widthGrow: 1
headerFilterFunc: this.notenFilterFunc
})
columns.push({title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4kommPruef'))),
field: 'kommPruef', widthGrow: 1,
@@ -918,19 +934,19 @@ export const Benotungstool = {
},
selectAllHandler(e, cell) {
const table = cell.getTable();
const rows = table.getRows();
const rows = this.filteredRows ?? table.getRows();
// custom select all logic
const allowed = rows.filter(r => r.getData().selectable);
const selected = allowed.every(r => r.isSelected());
if(selected){
if (selected) {
allowed.forEach(r => r.deselect());
e.target.checked = false;
} else {
allowed.forEach(r => r.select());
e.target.checked = true;
}
// stop built-in handler
e.stopPropagation();
return false;
},
@@ -1031,7 +1047,7 @@ export const Benotungstool = {
notenFormatter(cell) {
const value = cell.getValue()
const field = cell.getField()
let style = 'display: flex; justify-content: center; align-items: center; height: 100%;';
let style = 'display: flex; justify-content: start; align-items: center; height: 100%;';
// Wenn sich die Zeugnisnote von der von Ihnen freigegebenen Note unterscheidet,
// wird erstere rot umrandet markiert.
@@ -1494,6 +1510,7 @@ export const Benotungstool = {
this.$refs.notenTable.tabulator.redraw(true);
},
loadNoten(lv_id, sem_kurzbz) {
if (!lv_id || !sem_kurzbz) return
this.loading = true
this.$api.call(ApiNoten.getStudentenNoten(lv_id, sem_kurzbz))
.then(res => {
@@ -1515,8 +1532,21 @@ export const Benotungstool = {
this.notenTableOptions.height = window.visualViewport.height - rect.top - 50
this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height)
},
loadLehrveranstaltungen(sem_kurzbz, preselectLvId = null) {
return this.$api.call(ApiLehre.getZugewieseneLv(this.uid, sem_kurzbz)).then(res => {
this.lehrveranstaltungen = res.data
this.lehrveranstaltungen.forEach(lva => {
lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester} - ${lva.orgform}: ${lva.lv_bezeichnung}`
})
this.selectedLehrveranstaltung = preselectLvId
? this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == preselectLvId) ?? null
: null
})
},
async setupCreated() {
this.loading = true
await this.fetchAuthUID()
this.debouncedFetchPunkteForPruefung = debounce(this.fetchNoteForPunktePruefung, 500)
@@ -1524,28 +1554,33 @@ export const Benotungstool = {
const configPromise = this.$api.call(ApiNoten.getCisConfig()).then(res => {
this.config = res.data
})
// fetch lva dropdown
this.$api.call(ApiLehre.getZugewieseneLv(this.viewData?.uid, this.sem_kurzbz)).then(res => {
this.lehrveranstaltungen = res.data
this.$api.call(ApiStudiensemester.getAllStudiensemesterAndAktOrNext()).then(res => {
this.studiensemester = res.data[0]
// build dropdown option string
this.lehrveranstaltungen.forEach(lva => {
lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester}: ${lva.lv_bezeichnung}`
})
this.selectedLehrveranstaltung = this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == this.lv_id)
let defaultSem = this.sem_kurzbz
? this.studiensemester.find(s => s.studiensemester_kurzbz === this.sem_kurzbz)
: null
if (!defaultSem) {
const aktOrNext = res.data[1]
let aktKurzbz = null
if (typeof aktOrNext === 'string') aktKurzbz = aktOrNext
else if (Array.isArray(aktOrNext)) aktKurzbz = aktOrNext[0]?.studiensemester_kurzbz ?? aktOrNext[0]
else if (aktOrNext && typeof aktOrNext === 'object') aktKurzbz = aktOrNext.studiensemester_kurzbz
defaultSem = this.studiensemester.find(s => s.studiensemester_kurzbz === aktKurzbz)
}
this.selectedSemester = defaultSem ?? this.studiensemester[0] ?? null
const sem = this.selectedSemester?.studiensemester_kurzbz
if (sem) this.loadLehrveranstaltungen(sem, this.lv_id)
})
LehreinheitenModule.setupContext(this.$.appContext.config.globalProperties)
LehreinheitenModule.bindParams(Vue.ref(Vue.computed(() => this.LeDropdownParams)));
// fetch sem_kurzbz dropdown
this.$api.call(ApiStudiensemester.getStudiensemester()).then(res => {
this.studiensemester = res.data[0]
this.selectedSemester = this.studiensemester.find(sem => sem.studiensemester_kurzbz === this.sem_kurzbz)
})
// fetch noten dropdown
this.$api.call(ApiNoten.getNoten()).then(async res => {
this.notenOptions = res.data
@@ -1562,50 +1597,46 @@ export const Benotungstool = {
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
this.loadNoten(this.lv_id, this.sem_kurzbz)
if (this.lv_id && this.sem_kurzbz) {
this.loadNoten(this.lv_id, this.sem_kurzbz)
} else {
this.loading = false
}
this.calcMaxTableHeight()
},
lvChanged(e) {
const sem = this.selectedSemester?.studiensemester_kurzbz ?? this.sem_kurzbz
this.$router.push({
name: "Benotungstool",
params: {
sem_kurzbz: this.sem_kurzbz,
sem_kurzbz: sem,
lv_id: e.value.lehrveranstaltung_id
}
})
// reload data
this.loadNoten(e.value.lehrveranstaltung_id, this.sem_kurzbz)
this.loadNoten(e.value.lehrveranstaltung_id, sem)
},
ssChanged(e) {
// change url params & write history
this.$router.push({
name: "Benotungstool",
params: {
sem_kurzbz: e.value.studiensemester_kurzbz,
lv_id: this.lv_id
}
})
const sem = e.value.studiensemester_kurzbz
const keepLvId = this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? this.lv_id
this.loading = true
// diff lv_id -> reload zugewiesene lv
this.$api.call(ApiLehre.getZugewieseneLv(this.viewData?.uid, e.value.studiensemester_kurzbz)).then(res => {
this.lehrveranstaltungen = res.data
this.loadLehrveranstaltungen(sem, keepLvId).then(() => {
const lvId = this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? null
// build dropdown option string
this.lehrveranstaltungen.forEach(lva => {
lva.fullString = `${lva.stg_kurzbz} - ${lva.lv_semester}: ${lva.lv_bezeichnung}`
this.$router.push({
name: "Benotungstool",
params: { sem_kurzbz: sem, lv_id: lvId ?? undefined }
})
this.selectedLehrveranstaltung = this.lehrveranstaltungen.find(lva => lva.lehrveranstaltung_id == this.lv_id)
}).then(()=>{
// reload data
this.loadNoten(this.lv_id, e.value.studiensemester_kurzbz)
}).finally( () => this.loading = false)
if (lvId) {
this.loadNoten(lvId, sem)
} else if (this.$refs.notenTable?.tabulator) {
this.$refs.notenTable.tabulator.setData([]) // no matching LV -> clear
}
}).finally(() => this.loading = false)
},
getOptionLabel(option) {
return option.studiensemester_kurzbz
@@ -1644,6 +1675,8 @@ export const Benotungstool = {
const typ = this.pruefung ? this.pruefung.pruefungstyp_kurzbz : this.getPruefungstypForStudentByAntritt(this.pruefungStudent)
const note = this.selectedPruefungNote?.note ?? 9 // noch nicht eingetragen
const punkte = this.selectedPruefungPunkte ?? 0
this.loading = true
this.$api.call(ApiNoten.saveStudentPruefung(
this.pruefungStudent.uid,
note,
@@ -1701,6 +1734,7 @@ export const Benotungstool = {
}).finally(()=> {
this.pruefungStudent = null
this.pruefung = null
this.loading = false
})
this.$refs.modalContainerPruefung.hide()
@@ -1846,6 +1880,7 @@ export const Benotungstool = {
})
},
saveNoteneingabe() {
this.loading = true
this.$api.call(ApiNoten.saveStudentenNoten(this.password, this.changedNoten, this.lv_id, this.sem_kurzbz))
.then((res) => {
if(res.meta.status === 'success') {
@@ -1866,6 +1901,8 @@ export const Benotungstool = {
this.changedNotenCounter++;
this.$refs.notenTable.tabulator.redraw(true)
}).finally(() => {
this.loading = false
})
this.$refs.modalContainerNotenSpeichern.hide()
@@ -1882,9 +1919,6 @@ export const Benotungstool = {
getOptionLabelNotePruefung(option) {
return option.bezeichnung
},
leChanged(e) {
this.selectedLehreinheit = e.value
},
addPruefung(){
this.$refs.modalContainerNeuesPruefungsdatum.hide()
@@ -2041,9 +2075,16 @@ export const Benotungstool = {
},
selectedLehreinheit(newVal) {
if(!this.$refs.notenTable) return
this.$refs.notenTable.tabulator.clearFilter();
this.$refs.notenTable.tabulator.getFilters()
.filter(f => f.field === 'lehreinheit_id')
.forEach(() => table.removeFilter("lehreinheit_id", "=", undefined))
if(newVal) this.$refs.notenTable.tabulator.setFilter("lehreinheit_id", "=", newVal.lehreinheit_id);
},
selectedLehrveranstaltung(newVal, oldVal) {
if (this.selectedLehreinheit) {
this.selectedLehreinheit = null
}
},
getKommPruefCount(newVal) {
if(!this.config.CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF) return 0
if(this.$refs.notenTable?.tabulator && newVal > 0) {
@@ -2056,6 +2097,16 @@ export const Benotungstool = {
}
},
computed: {
countsToHTML() {
return this.$p.t('global/ausgewaehlt')
+ ': <strong>' + (this.selectedUids.length || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gefiltert')
+ ': <strong>' + (this.filteredcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gesamt')
+ ': <strong>' + (this.studenten?.length || 0) + '</strong>';
},
maxAntrittCount() {
let maxAntritte = 1;
@@ -2077,9 +2128,9 @@ export const Benotungstool = {
},
LeDropdownParams() {
return {
lv_id: this.lv_id,
sem_kurzbz: this.sem_kurzbz
}
lv_id: this.selectedLehrveranstaltung?.lehrveranstaltung_id ?? null,
sem_kurzbz: this.selectedSemester?.studiensemester_kurzbz ?? null
}
},
getStudentenOptions() {
return this.studenten ? this.studenten.filter(s => s.selectable) : []
@@ -2128,14 +2179,16 @@ export const Benotungstool = {
this.setupMounted()
},
template: `
<bs-modal ref="modalContainerNotenImport" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal ref="modalContainerNotenImport" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
<template v-slot:title>{{$capitalize($p.t('benotungstool/c4notenImportieren'))}}</template>
<template v-slot:default>
<div class="row mt-4 justify-content-center">
<div v-html="getNotenimportHinweistext"></div>
<div class="row justify-content-center">
<div class="col-12" v-html="getNotenimportHinweistext"></div>
</div>
<div class="row mt-4 justify-content-center">
<Textarea v-model="importString" rows="5"></Textarea>
<div class="row mt-3 justify-content-center">
<div class="col-12">
<Textarea v-model="importString" rows="5" class="w-100"></Textarea>
</div>
</div>
</template>
<template v-slot:footer>
@@ -2143,13 +2196,12 @@ export const Benotungstool = {
</template>
</bs-modal>
<bs-modal ref="modalContainerNeuesPruefungsdatum" class="bootstrap-prompt" dialogClass="modal-lg"
<bs-modal ref="modalContainerNeuesPruefungsdatum" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4"
@hideBsModal="neuesPruefungsdatumModalVisible = false"
@showBsModal="neuesPruefungsdatumModalVisible = true"
>
@showBsModal="neuesPruefungsdatumModalVisible = true">
<template v-slot:title>{{$capitalize($p.t('benotungstool/c4addNewPruefung'))}}</template>
<template v-slot:default>
<div class="row justify-content-center">
<div class="row align-items-center justify-content-center">
<div class="col-3 text-center">{{$capitalize($p.t('benotungstool/c4date'))}}:</div>
<div class="col-6">
<datepicker
@@ -2162,34 +2214,33 @@ export const Benotungstool = {
</datepicker>
</div>
</div>
<div class="row mt-4 justify-content-center">
<div class="row mt-3 align-items-center justify-content-center">
<div class="col-3 text-center">{{$capitalize($p.t('benotungstool/prueflingSelectionv2'))}}:</div>
<div class="col-6">
<Multiselect
v-model="selectedUids"
:options="getStudentenOptions"
optionLabel="infoString"
<Multiselect
v-model="selectedUids"
:options="getStudentenOptions"
optionLabel="infoString"
placeholder="Studenten auswählen"
:maxSelectedLabels="3"
showToggleAll
class="w-100" />
</div>
</div>
</template>
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="addPruefung">{{ $capitalize($p.t('benotungstool/c4addNewPruefung')) }}</button>
</template>
</bs-modal>
</bs-modal>
<bs-modal ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
<template v-slot:title>{{ $p.t('benotungstool/noteneingabeSpeichern') }}</template>
<template v-slot:default>
<div class="row mt-4 justify-content-center">
<div v-html="getNotenfreigabeHinweistext"> </div>
<div class="row justify-content-center">
<div class="col-12" v-html="getNotenfreigabeHinweistext"></div>
</div>
<div class="row mt-4 justify-content-center">
<div class="row mt-3 justify-content-center">
<div class="col-auto">
<Password v-model="password" :feedback="false" showIcon="fa fa-eye" :toggleMask="true" :promptLabel="$p.t('benotungstool/passwort')"></Password>
</div>
@@ -2200,11 +2251,11 @@ export const Benotungstool = {
</template>
</bs-modal>
<bs-modal ref="modalContainerPruefung" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal ref="modalContainerPruefung" class="bootstrap-prompt" dialogClass="modal-lg" bodyClass="px-4 py-4">
<template v-slot:title>{{ pruefung ? $capitalize($p.t('benotungstool/editPruefungFor')) : $capitalize($p.t('benotungstool/createPruefungFor')) }} {{pruefungStudent?.vorname}} {{pruefungStudent?.nachname}}</template>
<template v-slot:default>
<div class="row justify-content-center">
<div class="col-1 text-center">{{$capitalize($p.t('benotungstool/c4date'))}}:</div>
<div class="row align-items-center justify-content-center">
<div class="col-3 text-center">{{$capitalize($p.t('benotungstool/c4date'))}}:</div>
<div class="col-6">
<datepicker
v-model="selectedPruefungDate"
@@ -2216,22 +2267,23 @@ export const Benotungstool = {
</datepicker>
</div>
</div>
<div v-if="config?.CIS_GESAMTNOTE_PUNKTE == true" class="row justify-content-center mt-4">
<div class="col-1 text-center">{{$capitalize($p.t('benotungstool/c4punkte'))}}:</div>
<div v-if="config?.CIS_GESAMTNOTE_PUNKTE == true" class="row mt-3 align-items-center justify-content-center">
<div class="col-3 text-center">{{$capitalize($p.t('benotungstool/c4punkte'))}}:</div>
<div class="col-6">
<InputNumber
<InputNumber
v-model="selectedPruefungPunkte"
@input="debouncedFetchPunkteForPruefung"
inputId="selectedPruefungInput" :min="0" :max="100000">
inputId="selectedPruefungInput" :min="0" :max="100000"
class="w-100">
</InputNumber>
</div>
</div>
<div class="row justify-content-center mt-4">
<div class="col-1 text-center">{{$capitalize($p.t('lehre/note'))}}:</div>
<div class="row mt-3 align-items-center justify-content-center">
<div class="col-3 text-center">{{$capitalize($p.t('lehre/note'))}}:</div>
<div class="col-6">
<Dropdown :placeholder="$capitalize($p.t('lehre/note'))"
<Dropdown :placeholder="$capitalize($p.t('lehre/note'))"
:disabled="config?.CIS_GESAMTNOTE_PUNKTE == true"
:style="{'width': '100%'}" :optionLabel="getOptionLabelNotePruefung"
:style="{'width': '100%'}" :optionLabel="getOptionLabelNotePruefung"
v-model="selectedPruefungNote" :options="notenOptionsLehre" showClear>
<template #optionsgroup="slotProps">
<div> {{ option.bezeichnung }} </div>
@@ -2243,7 +2295,7 @@ export const Benotungstool = {
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="savePruefungEingabe">{{ $capitalize($p.t('global/speichern')) }}</button>
</template>
</bs-modal>
</bs-modal>
<BsOffcanvas
ref="drawer"
@@ -2264,49 +2316,53 @@ export const Benotungstool = {
<FhcOverlay :active="loading"></FhcOverlay>
<div class="row">
<div class="col-4">
<div class="row align-items-center">
<div class="col-auto me-auto">
<h2>{{$capitalize($p.t('benotungstool/benotungstoolTitle'))}}</h2>
<h4>{{ lv?.bezeichnung }}</h4>
<h4>{{ selectedLehrveranstaltung?.lv_bezeichnung }}</h4>
</div>
<div class="col-auto d-none d-xxl-flex">
<label class="col-form-label">{{$capitalize($p.t('lehre/lehrveranstaltung'))}}:</label>
</div>
<div class="col-2">
<div class="col-lg-auto">
<Dropdown @change="lvChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabelLv"
:placeholder="$capitalize($p.t('lehre/lehrveranstaltung'))"
v-model="selectedLehrveranstaltung" :options="lehrveranstaltungen" appendTo="self">
<template #optionsgroup="slotProps">
<div> {{ option.fullString }} </div>
</template>
</Dropdown>
</div>
<Dropdown @change="lvChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabelLv"
:placeholder="$capitalize($p.t('lehre/lehrveranstaltung'))"
v-model="selectedLehrveranstaltung" :options="lehrveranstaltungen" appendTo="self">
<template #optionsgroup="slotProps">
<div> {{ option.fullString }} </div>
</template>
</Dropdown>
</div>
<div class="col-2">
<div class="col-lg-auto">
<Dropdown @change="leChanged" :style="{'width': '100%'}" v-bind="LehreinheitenModule"
v-model="selectedLehreinheit" showClear appendTo="self">
<template #option="slotProps">
<div>
{{ slotProps.option.infoString }}
<i class="fa-solid fa-user"></i>
{{ slotProps.option.studentcount }}
<i class="fa-solid fa-calendar-days"></i>
{{ slotProps.option.termincount }}
</div>
</template>
</Dropdown>
</div>
<div class="col-auto d-none d-xxl-flex">
<label class="col-form-label">{{$capitalize($p.t('lehre/lehreinheit'))}}:</label>
</div>
<div class="col-2">
<div class="col-lg-auto">
<Dropdown @change="ssChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabel"
v-model="selectedSemester" :options="studiensemester" appendTo="self">
<template #optionsgroup="slotProps">
<div> {{ option.studiensemester_kurzbz }} </div>
</template>
</Dropdown>
</div>
<Dropdown @change="leChanged" :style="{'width': '100%'}" v-bind="LehreinheitenModule"
v-model="selectedLehreinheit" showClear appendTo="self">
<template #option="slotProps">
<div>
{{ slotProps.option.infoString }}
<i class="fa-solid fa-user"></i>
{{ slotProps.option.studentcount }}
<i class="fa-solid fa-calendar-days"></i>
{{ slotProps.option.termincount }}
</div>
</template>
</Dropdown>
</div>
<div class="col-auto d-none d-xxl-flex">
<label class="col-form-label">{{$capitalize($p.t('lehre/studiensemester'))}}:</label>
</div>
<div class="col-2">
<Dropdown @change="ssChanged" :style="{'width': '100%'}" :optionLabel="getOptionLabel"
v-model="selectedSemester" :options="studiensemester" appendTo="self">
<template #optionsgroup="slotProps">
<div> {{ option.studiensemester_kurzbz }} </div>
</template>
</Dropdown>
</div>
</div>
<hr>
@@ -2315,6 +2371,8 @@ export const Benotungstool = {
<core-filter-cmpt
v-if="tabulatorCanBeBuilt"
@uuidDefined="handleUuidDefined"
:description="countsToHTML"
:useSelectionSpan="false"
:title="''"
ref="notenTable"
:tabulator-options="notenTableOptions"
@@ -22,6 +22,11 @@ export function bindParams(paramsRef) {
}
async function fetchLehreinheiten(lv_id, sem_kurzbz) {
if (!lv_id || !sem_kurzbz) {
options.value = []
return
}
appContext.$api.call(ApiLehre.getLeForLv(lv_id, sem_kurzbz)).then(res => {
const data = []
+22 -4
View File
@@ -1,5 +1,23 @@
export function centeredFormatter (cell)
{
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">'+val+'</div>'
export function centeredTextFormatter(cell) {
const longForm = cell.getValue()
if(!longForm) return
const data = cell.getData()
const entry = Object.entries(data).find(entry => entry[1] == longForm)
// shortFormKey must have same keyname as longForm but with 'Short' appended
const shortForm = data[entry[0]+'Short']
if(shortForm && longForm) {
return `<div style="display: flex; justify-content: start; align-items: center; height: 100%; width: 100%;">
<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
${longForm}
</span>
<span class="short-text" style="font-weight: bold; display: none;">
${shortForm}
</span>
</div>`;
} else {
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">'+longForm+'</p></div>'
}
}