diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php index f8bbc966b..261bf612b 100644 --- a/application/controllers/api/frontend/v1/Noten.php +++ b/application/controllers/api/frontend/v1/Noten.php @@ -242,7 +242,7 @@ class Noten extends FHCAPI_Controller $res = $this->LehrveranstaltungModel->load($lv_id); if(isError($res) || !hasData($res)) { - $this->terminateWithError('Keine gültige Lehrveranstaltung gefunden für ID: '.$lv_id); + $this->terminateWithError($this->p->t('benotungstool', 'noValidLvFoundForId', [$lv_id])); } $lv = getData($res)[0]; @@ -250,7 +250,7 @@ class Noten extends FHCAPI_Controller $studiengang_kz = $lv->studiengang_kz; $res = $this->StudiengangModel->load($studiengang_kz); if(isError($res) || !hasData($res)) { - $this->terminateWithError('Kein gültiger Studiengang gefunden für ID: '.$studiengang_kz); + $this->terminateWithError($this->p->t('benotungstool', 'noValidStudiengangFoundForId', [$studiengang_kz])); } $sg = getData($res)[0]; $lvaFullName = $sg->kurzbzlang . ' ' . $lv->semester . '.Semester @@ -261,7 +261,7 @@ class Noten extends FHCAPI_Controller $res = $this->PersonModel->load(getAuthPersonId()); if(isError($res) || !hasData($res)) { - $this->terminateWithError('Keine gültige Person gefunden für ID: '.getAuthPersonId()); + $this->terminateWithError($this->p->t('benotungstool', 'noValidPersonFoundForId', [getAuthPersonId()])); } $pers = getData($res)[0]; $lektorFullName = $pers->anrede.' '.$pers->vorname.' '.$pers->nachname; //.' ('.$pers->kurzbz.')'; @@ -273,7 +273,7 @@ class Noten extends FHCAPI_Controller foreach ($data as $row) { $studienplan_bezeichnung .= $row->bezeichnung . ' '; } - $betreff = 'Notenfreigabe ' . $lv->bezeichnung . ' ' . $lv->orgform_kurzbz . ' - ' . $studienplan_bezeichnung; + $betreff = $this->p->t('benotungstool','notenfreigabe').' ' . $lv->bezeichnung . ' ' . $lv->orgform_kurzbz . ' - ' . $studienplan_bezeichnung; $studlist = ""; @@ -493,7 +493,7 @@ class Noten extends FHCAPI_Controller $this->addMeta('$pruefungenChanged', $pruefungenChanged); $this->addMeta('$lvgesamtnote', $lvgesamtnote); $savedPruefung = $pruefungenChanged['savedPruefung'] ?? null; - $extraPruefung = $pruefungenChanged['extraPruefung'] ?? null; // TODO: test + $extraPruefung = $pruefungenChanged['extraPruefung'] ?? null; $savedPruefungData = count($savedPruefung) > 0 ? $savedPruefung[0] : null; $extraPruefungData = count($extraPruefung) > 0 ? $extraPruefung[0] : null; @@ -509,8 +509,8 @@ class Noten extends FHCAPI_Controller $this->load->model('education/Lvgesamtnote_model', 'LvgesamtnoteModel'); - - if($typ == "Termin2") { + if($typ == "Termin2") + { // Wenn eine Nachprüfung angelegt wird, wird zuerst eine Pruefung mit 1. Termin angelegt welche für die ursprüngliche Note // vor den Prüfungsantritten zählt @@ -537,7 +537,7 @@ class Noten extends FHCAPI_Controller } else if(!hasData($resultLV))// set Termin1 note to "noch nicht eingetragen" { - $pr_note = 9; + $pr_note = 9; $pr_punkte = null; $benotungsdatum = $jetzt; } @@ -550,7 +550,7 @@ class Noten extends FHCAPI_Controller 'student_uid' => $student_uid, 'mitarbeiter_uid' => getAuthUID(), 'note' => $pr_note, - 'punkte' => $pr_punkte, +// 'punkte' => $pr_punkte, 'pruefungstyp_kurzbz' => "Termin1", 'datum' => $benotungsdatum, 'anmerkung' => "", @@ -581,7 +581,7 @@ class Noten extends FHCAPI_Controller 'updateamum' => $jetzt, 'updatevon' => getAuthUID(), 'note' => $note, - 'punkte' => $punkte, +// 'punkte' => $punkte, 'datum' => $datum, 'anmerkung' => "" ) @@ -600,7 +600,7 @@ class Noten extends FHCAPI_Controller 'student_uid' => $student_uid, 'mitarbeiter_uid' => getAuthUID(), 'note' => $note, - 'punkte' => null,//$punkte, +// 'punkte' => null,//$punkte, 'pruefungstyp_kurzbz' => $typ, 'datum' => $datum, 'anmerkung' => "", @@ -617,7 +617,8 @@ class Noten extends FHCAPI_Controller } } - } else if($typ == "Termin3") { + } else if($typ == "Termin3") + { $result3 = $this->LePruefungModel->getPruefungenByUidTypLvStudiensemester($student_uid, "Termin3", $lva_id, $stsem); @@ -631,7 +632,7 @@ class Noten extends FHCAPI_Controller 'updateamum' => $jetzt, 'updatevon' => getAuthUID(), 'note' => $note, - 'punkte' => $punkte, +// 'punkte' => $punkte, 'datum' => $datum, 'anmerkung' => "" ) @@ -650,7 +651,7 @@ class Noten extends FHCAPI_Controller 'student_uid' => $student_uid, 'mitarbeiter_uid' => getAuthUID(), 'note' => $note, - 'punkte' => null,//$punkte, +// 'punkte' => null,//$punkte, 'pruefungstyp_kurzbz' => $typ, 'datum' => $datum, 'anmerkung' => "", @@ -668,8 +669,7 @@ class Noten extends FHCAPI_Controller } } else { - // TODO: proper error phrase that explains better why we terminated with error - $this->terminateWithError("Typ is not termin2 or termin3.", 'general'); + $this->terminateWithError($this->p->t('benotungstool', 'wrongPruefungType', [$student_uid, $typ]), 'general'); } return $pruefungenChanged; @@ -749,7 +749,6 @@ class Noten extends FHCAPI_Controller $noten = $result->noten; $retLvNoten = []; - $responseMsgs = []; foreach($noten as $note) { @@ -858,7 +857,6 @@ class Noten extends FHCAPI_Controller $ret[$student_uid] = $this->savePruefungstermin($typ, $student_uid, $lv_id, $sem_kurzbz, $lehreinheit_id, $note, $punkte, $datum); } - $this->terminateWithSuccess($ret); } diff --git a/application/models/organisation/Studienplan_model.php b/application/models/organisation/Studienplan_model.php index 569aebd0b..5a3f3498d 100644 --- a/application/models/organisation/Studienplan_model.php +++ b/application/models/organisation/Studienplan_model.php @@ -59,6 +59,37 @@ class Studienplan_model extends DB_Model 'tbl_studienplan_lehrveranstaltung.semester' => $semester )); } + + public function getStudienplanByLvaSemKurzbz($lehrveranstaltung_id, $studiensemester_kurzbz) { + $qry= " + SELECT + DISTINCT tbl_studienplan.* + FROM + lehre.tbl_studienplan + JOIN lehre.tbl_studienplan_lehrveranstaltung + USING(studienplan_id) + WHERE + tbl_studienplan_lehrveranstaltung.lehrveranstaltung_id IN ( + SELECT + lv.lehrveranstaltung_id + FROM + lehre.tbl_lehrveranstaltung AS lv + LEFT JOIN lehre.tbl_lehrveranstaltung AS t ON t.lehrveranstaltung_id=lv.lehrveranstaltung_template_id + WHERE + lv.lehrtyp_kurzbz<>'tpl' + AND (lv.lehrveranstaltung_id= ? OR (lv.lehrveranstaltung_template_id= ? AND t.lehrtyp_kurzbz='tpl')) + ) + AND EXISTS ( + SELECT 1 + FROM + lehre.tbl_studienplan_semester + WHERE studienplan_id=tbl_studienplan.studienplan_id + AND studiensemester_kurzbz= ? + AND semester = tbl_studienplan_lehrveranstaltung.semester) + ORDER BY bezeichnung"; + + return $this->execReadOnlyQuery($qry, array($lehrveranstaltung_id, $lehrveranstaltung_id, $studiensemester_kurzbz)); + } public function getStudienplanLehrveranstaltungForPrestudent($studienplan_id, $semester, $prestudent_id) { diff --git a/public/js/components/Cis/Benotungstool/Benotungstool.js b/public/js/components/Cis/Benotungstool/Benotungstool.js index 2cdfad4a2..a873e2613 100644 --- a/public/js/components/Cis/Benotungstool/Benotungstool.js +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -39,6 +39,9 @@ export const Benotungstool = { }, data() { return { + neuesPruefungsdatumModalVisible: false, + offsetLeft: 0, + headerEl: null, loading: false, selectedUids: [], // shared selection state selectedLehreinheit: null, @@ -76,7 +79,16 @@ export const Benotungstool = { { event: "rowSelectionChanged", handler: async (data, rows) => { - this.selectedUids = data.filter(d => d.selectable); + // avoid an expensive update loop if selection happens in modal + if(this.neuesPruefungsdatumModalVisible) return + + if(data.length == 1 && this.selectedUids.length == 1 && data[0].uid === this.selectedUids[0].uid){ + // special case to work around an internal tabulator selection quirk + this.selectedUids = [] + } else { + this.selectedUids = data.filter(d => d.selectable); + } + } }, { @@ -101,7 +113,7 @@ export const Benotungstool = { row.reformat() // trigger reformat of arrow } } - }, + } ]}; }, methods: { @@ -243,7 +255,7 @@ export const Benotungstool = { this.loading = true this.$api.call(ApiNoten.saveNotenvorschlagBulk(this.lv_id, this.sem_kurzbz, notenbulk)).then(res => { if(res.meta.status === 'success') { - this.$fhcAlert.alertWarning('Noten erfolgreich importiert') // TODO: phrase + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/notenImportSuccessAlert')) const lvNoten = res.data @@ -274,7 +286,7 @@ export const Benotungstool = { this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk)) .then((res)=> { if(res.meta.status === 'success') { - this.$fhcAlert.alertWarning('Prüfungen erfolgreich importiert und gespeichert') // TODO: phrase + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungImportSuccessAlert')) this.handleAddNewPruefungenResponse(res, pruefungenbulk) } }).finally(()=>{this.loading = false}) @@ -282,8 +294,8 @@ export const Benotungstool = { handleAddNewPruefungenResponse(res, uids) { const pruefungen = res.data uids.forEach(entry => { - const saved = pruefungen[entry.uid].savedPruefung - const extra = pruefungen[entry.uid].extraPruefung + const saved = pruefungen[entry.uid].savedPruefung?.[0] + const extra = pruefungen[entry.uid].extraPruefung?.[0] const student = this.studenten.find(s => s.uid == entry.uid) if(!student) return @@ -332,9 +344,7 @@ export const Benotungstool = { this.distinctPruefungsDates.forEach((date, index)=>{ const dateparts = date.split('-') const titledate = `${dateparts[2]}.${dateparts[1]}.${dateparts[0]}` - - // TODO: should studenten without shadow pruefung Termin have their "ursprüngliche Zeugnisnote" - // col filled for consistency reasons? + // TODO: test if this holds true const originalNote = index === 0 @@ -346,7 +356,8 @@ export const Benotungstool = { hozAlign:"center", widthGrow: 1, minWidth: 150, - originalNote + originalNote, + visible: true }) }) @@ -419,7 +430,7 @@ export const Benotungstool = { selectable: true, selectableRangeMode: "click", // shift+click selectablePersistence: false, // reset selection on table reload - selectableCheck: function(row){ + selectableCheck: function(row, e){ const data = row.getData(); if(data['kommPruef']) return false @@ -558,7 +569,7 @@ export const Benotungstool = { row.select(); } - // stop Tabulator’s built-in handler + // stop built-in handler e.stopPropagation(); return false; }, @@ -576,7 +587,7 @@ export const Benotungstool = { allowed.forEach(r => r.select()); } - // stop Tabulator’s built-in handler + // stop built-in handler e.stopPropagation(); return false; }, @@ -598,13 +609,11 @@ export const Benotungstool = { }, terminCalcFormatter(cell) { const cellval = cell.getValue() - // TODO: phrase - return 'Prüflinge: ' + cellval + return this.$p.t('benotungstool/prueflingSelection')+': ' + cellval }, negativeNotenCalcFormatter(cell) { const cellval = cell.getValue() - // TODO: phrase - return 'Negativ: ' + cellval + return this.$p.t('benotungstool/c4negativ')+': ' + cellval }, negativeNotenCalc(entries) { return entries.reduce((acc, cur) => { @@ -717,8 +726,8 @@ export const Benotungstool = { s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); - row.update({ lv_note: data.note_vorschlag }) - row.update({ freigegeben: 'changed' }) + row.update({ lv_note: data.note_vorschlag, freigegeben: 'changed' }) + // row.update({ freigegeben: 'changed' }) row.reformat() // trigger reformat of arrow this.changedNotenCounter++; } @@ -822,7 +831,7 @@ export const Benotungstool = { // Third column (button) const button = document.createElement('button'); button.className = 'btn btn-outline-secondary'; - button.textContent = 'Change'; // TODO: phrase + button.textContent = this.$p.t('benotungstool/changePruefungButtonText'); button.addEventListener('click', () => { this.openPruefungModal(data, data[field], field); }); @@ -839,7 +848,7 @@ export const Benotungstool = { const button = document.createElement('button'); button.className = 'btn btn-outline-secondary'; - button.textContent = 'Add'; // TODO: phrase + button.textContent = this.$p.t('benotungstool/addPruefungButtonText'); button.addEventListener('click', () => { this.openPruefungModal(data, null, field) }); @@ -998,6 +1007,9 @@ export const Benotungstool = { const kP = s.pruefungen?.find(p => p.pruefungstyp_kurzbz == 'kommPruef') return !(kP || s.hoechsterAntritt >= 3) }, + set() { + // empty setter so tabulator doesnt scream + }, enumerable: true, configurable: true }) @@ -1032,7 +1044,8 @@ export const Benotungstool = { hozAlign:"center", widthGrow: 1, minWidth: 200, - originalNote + originalNote, + visible: true }) }) @@ -1099,8 +1112,30 @@ export const Benotungstool = { async setupMounted() { this.tableBuiltPromise = new Promise(this.tableResolve) await this.tableBuiltPromise + + this.headerEl = document.getElementById('nav-main') + if(this.headerEl) { + this.headerEl.addEventListener("shown.bs.collapse", this.handleSidebar); + this.headerEl.addEventListener("hidden.bs.collapse", this.handleSidebar); + this.handleSidebar() + } + + + + this.loadNoten(this.lv_id, this.sem_kurzbz) - this.calcMaxTableHeight() + // this.calcMaxTableHeight() + + }, + handleSidebar() { + // this.$refs.notenTable.tabulator.blockRedraw(); // stop automatic resizes + + this.offsetLeft = this.headerEl.getBoundingClientRect().width; + + // setTimeout(() => { + // this.$refs.notenTable.tabulator.restoreRedraw(); // re-enable + // this.$refs.notenTable.tabulator.redraw(true); // manual recalculation + // }, 400); // after animation finishes }, lvChanged(e) { @@ -1188,8 +1223,8 @@ export const Benotungstool = { this.sem_kurzbz, typ )).then(res => { - if(res.meta.status === 'success') { - this.$fhcAlert.alertWarning('Prüfung für Student ' + this.pruefungStudent.uid + ' bearbeitet oder angelegt') // TODO: phrase + if(res.meta.status === 'success') { //'Prüfung für Student ' + this.pruefungStudent.uid + ' bearbeitet oder angelegt' + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungSaveForUid', [this.pruefungStudent.uid])) const s = this.studenten.find(s => s.uid === res.data[1]?.student_uid) @@ -1214,18 +1249,18 @@ export const Benotungstool = { s.hoechsterAntritt = this.getAntrittCountStudent(s) } - // TODO: maybe fake rerun the selectable check by just assigning - // css classes tabulator-selectable/tabulator-unselectable on row elements - - // const rows = this.$refs.notenTable.tabulator.getRows() - // const row = rows.find(r => { - // const data = r.getData() - // return data.uid == this.pruefungStudent.uid - // }) - // row.reformat() + const oldScrollLeft = this.$refs.notenTable.tabulator.rowManager.scrollLeft + const oldScrollTop = this.$refs.notenTable.tabulator.rowManager.scrollTop this.$refs.notenTable.tabulator.redraw(true) - - this.$fhcAlert.alertWarning('Prüfung gespeichert') // TODO: phrase + + Vue.nextTick(()=> { + const table = this.$refs.notenTable.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = oldScrollLeft; + table.oldScrollTop = oldScrollTop; + } + }) + } }).finally(()=> { this.pruefungStudent = null @@ -1296,7 +1331,8 @@ export const Benotungstool = { hozAlign:"center", widthGrow: 1, minWidth: 200, - originalNote + originalNote, + visible: true }) }) @@ -1372,7 +1408,7 @@ export const Benotungstool = { this.sem_kurzbz, )).then(res => { if(res.meta.status === "success") { - this.$fhcAlert.alertWarning('Prüfung an ' + dateStr + ' angelegt') // TODO: phrase + this.$fhcAlert.alertSuccess(this.$p.t('benotungstool/pruefungAngelegtAn', [dateStr])) this.handleAddNewPruefungenResponse(res, uids) @@ -1492,7 +1528,7 @@ export const Benotungstool = { return acc }, []) : [] return cs - } + }, }, created() { this.setupCreated() @@ -1500,7 +1536,12 @@ export const Benotungstool = { mounted() { this.setupMounted() }, + unmounted() { + this.headerEl.removeEventlistener(this.handleSidebar) + this.headerEl.removeEventlistener(this.handleSidebar) + }, template: ` + - +