createPruefung entry with "noch nicht eingetragen" note as default for a selection of students at given date; filterHeaders in noten cols; antrittCount col; pruefungsformatter with antritt highlighting;

This commit is contained in:
Johann Hoffmann
2025-07-29 17:32:07 +02:00
parent 52d9e0a195
commit 6ccbc95697
4 changed files with 681 additions and 275 deletions
+196 -152
View File
@@ -34,7 +34,8 @@ class Noten extends FHCAPI_Controller
'saveStudentenNoten' => self::PERM_LOGGED, // todo: berechtigungen!
'getNotenvorschlagStudent' => self::PERM_LOGGED,
'saveNotenvorschlag' => self::PERM_LOGGED,
'saveStudentPruefung' => self::PERM_LOGGED
'saveStudentPruefung' => self::PERM_LOGGED,
'createPruefungen' => self::PERM_LOGGED
]);
$this->load->library('AuthLib', null, 'AuthLib');
@@ -264,7 +265,6 @@ class Noten extends FHCAPI_Controller
$lva_id = $result->lva_id;
$lehreinheit_id = $result->lehreinheit_id;
// $lehreinheit_id_pr = $result->lehreinheit_id_pr; // todo: very rare, refactor?
$stsem = $result->sem_kurzbz;
$typ = $result->typ;
@@ -275,7 +275,7 @@ class Noten extends FHCAPI_Controller
// by retrieving it from students row in campus.vw_student_lehrveranstaltung earlier on
// $lehreinheit_id = getLehreinheit($db, $lvid, $student_uid, $stsem);
$lehreinheit_id = $result->lehreinheit_id;
// $lehreinheit_id = $result->lehreinheit_id;
$punkte = str_replace(',', '.', $punkte);
@@ -296,155 +296,18 @@ class Noten extends FHCAPI_Controller
$old_note = $note;
//Laden der Lehrveranstaltung
$lv_obj = new lehrveranstaltung();
if(!$lv_obj->load($lva_id))
die($lv_obj->errormsg);
//Studiengang laden
$stg_obj = new studiengang($lv_obj->studiengang_kz);
// TODO: notwendiger check?
// //Laden der Lehrveranstaltung
// $lv_obj = new lehrveranstaltung();
// if(!$lv_obj->load($lva_id))
// die($lv_obj->errormsg);
//
// //Studiengang laden
// $stg_obj = new studiengang($lv_obj->studiengang_kz);
//
// $response = savePruefung($lvid, $student_uid, $stsem, $lehreinheit_id, $datum, $typ, $note, $punkte);
$savedPruefung = null;
$extraPruefung = null;
if($typ == "Termin2") {
$pr = new Pruefung();
// Wenn eine Pruefung angelegt wird, wird zuerst eine Pruefung mit 1. Termin angelegt
// und dort die Zeugnisnote gespeichert
if($pr->getPruefungen($student_uid, "Termin1", $lva_id, $stsem))
{
if ($pr->result)
{
// TODO: is this filler if branch really necessary?
$termin1 = 1;
}
else
{
$lvnote = new lvgesamtnote();
// update Termin1 note
if ($lvnote->load($lva_id, $student_uid, $stsem))
{
$pr_note = $lvnote->note;
$pr_punkte = $lvnote->punkte;
$benotungsdatum = $lvnote->benotungsdatum;
}
else // set Termin1 note to "noch nicht eingetragen"
{
$pr_note = 9;
$pr_punkte = '';
$benotungsdatum = $jetzt;
}
$pr_1 = new Pruefung();
$pr_1->lehreinheit_id = $lehreinheit_id;
$pr_1->student_uid = $student_uid;
$pr_1->mitarbeiter_uid = getAuthUID();
$pr_1->note = $pr_note;
$pr_1->punkte = $pr_punkte;
$pr_1->pruefungstyp_kurzbz = "Termin1";
$pr_1->datum = $benotungsdatum;
$pr_1->anmerkung = "";
$pr_1->insertamum = $jetzt;
$pr_1->insertvon = getAuthUID();
$pr_1->updateamum = null;
$pr_1->updatevon = null;
$pr_1->ext_id = null;
$pr_1->new = true;
$pr_1->save();
$extraPruefung = $pr_1; //"neu T1";
}
$prTermin2 = new Pruefung();
$pr_2 = new Pruefung();
// Die Pruefung wird als Termin2 eingetragen
if ($prTermin2->getPruefungen($student_uid, 'Termin2', $lva_id, $stsem))
{
if ($prTermin2->result)
{
$pr_2->load($prTermin2->result[0]->pruefung_id);
$pr_2->new = null;
$pr_2->updateamum = $jetzt;
$pr_2->updatevon = getAuthUID();
$old_note = $pr_2->note;
$pr_2->note = $note;
$pr_2->punkte = $punkte;
$pr_2->datum = $datum;
$pr_2->anmerkung = "";
$savedPruefung = $pr_2;//"update T2";
}
else
{
$pr_2->lehreinheit_id = $lehreinheit_id;
$pr_2->student_uid = $student_uid;
$pr_2->mitarbeiter_uid = getAuthUID();
$pr_2->note = $note;
$pr_2->punkte = $punkte;
$pr_2->pruefungstyp_kurzbz = $typ;
$pr_2->datum = $datum;
$pr_2->anmerkung = "";
$pr_2->insertamum = $jetzt;
$pr_2->insertvon = getAuthUID();
$pr_2->updateamum = null;
$pr_2->updatevon = null;
$pr_2->ext_id = null;
$pr_2->new = true;
$old_note = -1;
$savedPruefung = $pr_2;//"new T2";
}
$pr_2->save();
}
}
} else if($typ == "Termin3") {
$prTermin3 = new Pruefung();
$pr_3 = new Pruefung();
if ($prTermin3->getPruefungen($student_uid, 'Termin3', $lva_id, $stsem))
{
if ($prTermin3->result)
{
$pr_3->load($prTermin3->result[0]->pruefung_id);
$pr_3->new = null;
$pr_3->updateamum = $jetzt;
$pr_3->updatevon = getAuthUID();
$old_note = $pr_3->note;
$pr_3->note = $note;
$pr_3->punkte = $punkte;
$pr_3->datum = $datum;
$pr_3->anmerkung = "";
$savedPruefung = $pr_3; //"update T3";
}
else
{
$pr_3->lehreinheit_id = $lehreinheit_id;
$pr_3->student_uid = $student_uid;
$pr_3->mitarbeiter_uid = getAuthUID();
$pr_3->note = $note;
$pr_3->punkte = $punkte;
$pr_3->pruefungstyp_kurzbz = $typ;
$pr_3->datum = $datum;
$pr_3->anmerkung = "";
$pr_3->insertamum = $jetzt;
$pr_3->insertvon = getAuthUID();
$pr_3->updateamum = null;
$pr_3->updatevon = null;
$pr_3->ext_id = null;
$pr_3->new = true;
$old_note = -1;
$savedPruefung = $pr_3; //"new T3";
}
$pr_3->save();
}
} else {
$this->terminateWithError("typ is not termin2 or termin3", 'general');
}
$pruefungenChanged = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum);
//Gesamtnote updaten
$lvgesamtnote = new lvgesamtnote();
if (!$lvgesamtnote->load($lva_id, $student_uid, $stsem))
@@ -480,9 +343,162 @@ class Noten extends FHCAPI_Controller
// $response = "update";
}
$saved = $lvgesamtnote->save($new);
$lvgesamtnote->save($new);
$savedPruefung = $pruefungenChanged['savedPruefung'] ?? null;
$extraPruefung = $pruefungenChanged['extraPruefung'] ?? null; // TODO: test
$this->terminateWithSuccess(array($savedPruefung, $lvgesamtnote, $extraPruefung));
}
private function savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum)
{
$jetzt = date("Y-m-d H:i:s");
$this->terminateWithSuccess(array($savedPruefung, $lvgesamtnote, $saved, $extraPruefung));
$pruefungenChanged = [];
if($typ == "Termin2") {
$pr = new Pruefung();
// Wenn eine Pruefung angelegt wird, wird zuerst eine Pruefung mit 1. Termin angelegt
// und dort die Zeugnisnote gespeichert
if($pr->getPruefungen($student_uid, "Termin1", $lva_id, $stsem))
{
if ($pr->result)
{
// TODO: is this filler if branch really necessary?
$termin1 = 1;
}
else
{
$lvnote = new lvgesamtnote();
// update Termin1 note
if ($lvnote->load($lva_id, $student_uid, $stsem))
{
$pr_note = $lvnote->note;
$pr_punkte = $lvnote->punkte;
$benotungsdatum = $lvnote->benotungsdatum;
}
else // set Termin1 note to "noch nicht eingetragen"
{
$pr_note = 9;
$pr_punkte = '';
$benotungsdatum = $jetzt;
}
$pr_1 = new Pruefung();
$pr_1->lehreinheit_id = $lehreinheit_id;
$pr_1->student_uid = $student_uid;
$pr_1->mitarbeiter_uid = getAuthUID();
$pr_1->note = $pr_note;
$pr_1->punkte = $pr_punkte;
$pr_1->pruefungstyp_kurzbz = "Termin1";
$pr_1->datum = $benotungsdatum;
$pr_1->anmerkung = "";
$pr_1->insertamum = $jetzt;
$pr_1->insertvon = getAuthUID();
$pr_1->updateamum = null;
$pr_1->updatevon = null;
$pr_1->ext_id = null;
$pr_1->new = true;
$pr_1->save();
$pruefungenChanged['extraPruefung'] = $pr_1; //"neu T1";
}
$prTermin2 = new Pruefung();
$pr_2 = new Pruefung();
// Die Pruefung wird als Termin2 eingetragen
if ($prTermin2->getPruefungen($student_uid, 'Termin2', $lva_id, $stsem))
{
if ($prTermin2->result)
{
$pr_2->load($prTermin2->result[0]->pruefung_id);
$pr_2->new = null;
$pr_2->updateamum = $jetzt;
$pr_2->updatevon = getAuthUID();
$old_note = $pr_2->note;
$pr_2->note = $note;
$pr_2->punkte = $punkte;
$pr_2->datum = $datum;
$pr_2->anmerkung = "";
$pruefungenChanged['savedPruefung'] = $pr_2;
// $savedPruefung = $pr_2;//"update T2";
}
else
{
$pr_2->lehreinheit_id = $lehreinheit_id;
$pr_2->student_uid = $student_uid;
$pr_2->mitarbeiter_uid = getAuthUID();
$pr_2->note = $note;
$pr_2->punkte = $punkte;
$pr_2->pruefungstyp_kurzbz = $typ;
$pr_2->datum = $datum;
$pr_2->anmerkung = "";
$pr_2->insertamum = $jetzt;
$pr_2->insertvon = getAuthUID();
$pr_2->updateamum = null;
$pr_2->updatevon = null;
$pr_2->ext_id = null;
$pr_2->new = true;
$old_note = -1;
$pruefungenChanged['savedPruefung'] = $pr_2;
// $savedPruefung = $pr_2;//"new T2";
}
$pr_2->save();
}
}
} else if($typ == "Termin3") {
$prTermin3 = new Pruefung();
$pr_3 = new Pruefung();
if ($prTermin3->getPruefungen($student_uid, 'Termin3', $lva_id, $stsem))
{
if ($prTermin3->result)
{
$pr_3->load($prTermin3->result[0]->pruefung_id);
$pr_3->new = null;
$pr_3->updateamum = $jetzt;
$pr_3->updatevon = getAuthUID();
$old_note = $pr_3->note;
$pr_3->note = $note;
$pr_3->punkte = $punkte;
$pr_3->datum = $datum;
$pr_3->anmerkung = "";
$pruefungenChanged['savedPruefung'] = $pr_3;
// $savedPruefung = $pr_3; //"update T3";
}
else
{
$pr_3->lehreinheit_id = $lehreinheit_id;
$pr_3->student_uid = $student_uid;
$pr_3->mitarbeiter_uid = getAuthUID();
$pr_3->note = $note;
$pr_3->punkte = $punkte;
$pr_3->pruefungstyp_kurzbz = $typ;
$pr_3->datum = $datum;
$pr_3->anmerkung = "";
$pr_3->insertamum = $jetzt;
$pr_3->insertvon = getAuthUID();
$pr_3->updateamum = null;
$pr_3->updatevon = null;
$pr_3->ext_id = null;
$pr_3->new = true;
$old_note = -1;
$pruefungenChanged['savedPruefung'] = $pr_3;
// $savedPruefung = $pr_3; //"new T3";
}
$pr_3->save();
}
} else {
// TODO: proper error phrase
$this->terminateWithError("Typ is not termin2 or termin3.", 'general');
}
return $pruefungenChanged;
}
public function saveNotenvorschlag() {
@@ -540,6 +556,34 @@ class Noten extends FHCAPI_Controller
$this->terminateWithSuccess(array($ret, $lvgesamtnote));
}
public function createPruefungen() {
$result = $this->getPostJSON();
if(!property_exists($result, 'uids') || !property_exists($result, 'datum')) {
$this->terminateWithError($this->p->t('global', 'missingParameters'), 'general');
}
$uids = $result->uids;
$datum = $result->datum;
$lva_id = $result->lva_id;
$stsem = $result->sem_kurzbz;
$ret = [];
foreach ($uids as $student) {
$student_uid = $student->uid;
$typ = $student->typ;
$note = 9; //$result->note; // TODO: parameterize for import maybe
$punkte = ''; // TODO: check punkte feature
$lehreinheit_id = $student->lehreinheit_id;
$ret[$student->uid] = $this->savePruefungstermin($typ, $student_uid, $lva_id, $stsem, $lehreinheit_id, $note, $punkte, $datum);
}
$this->terminateWithSuccess($ret);
}
}
+8 -1
View File
@@ -49,5 +49,12 @@ export default {
url: '/api/frontend/v1/Noten/saveStudentPruefung',
params: { student_uid, note, punkte, datum, lva_id, lehreinheit_id, sem_kurzbz, typ }
};
}
},
createPruefungen(uids, datum, lva_id, sem_kurzbz){
return {
method: 'post',
url: '/api/frontend/v1/Noten/createPruefungen',
params: { uids, datum, lva_id, sem_kurzbz }
};
},
};
@@ -35,12 +35,14 @@ export const Benotungstool = {
},
data() {
return {
loading: false,
selectedUids: [], // shared selection state
selectedLehreinheit: null,
lehreinheiten: null,
tabulatorCanBeBuilt: false,
selectedPruefungNote: null,
selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker
distinctPruefungsDates: null,
pruefungStudent: null,
pruefung: null,
password: '',
@@ -76,8 +78,6 @@ export const Benotungstool = {
{
event: "rowSelectionChanged",
handler: async (data, rows) => {
console.log("Selected Data:", data);
console.log("Selected Rows:", rows);
this.selectedUids = data;
}
},
@@ -107,6 +107,25 @@ export const Benotungstool = {
]};
},
methods: {
selectionArraysAreEqual(arr1, arr2) {
if(arr1.length !== arr2.length) return false
const sortFunc = (s1, s2) => {
if(s1.nachname > s2.nachname) {
return 1
} else if (s1.nachname < s2.nachname) {
return -1
} else {
return 0
}
}
const sortedArr1 = arr1.sort(sortFunc)
const sortedArr2 = arr2.sort(sortFunc)
const arrsREqual = sortedArr1.every((val, index) => val === sortedArr2[index]);
return arrsREqual
},
getNotenTableOptions() {
return {
height: 700,
@@ -116,6 +135,15 @@ export const Benotungstool = {
selectable: true,
selectableRangeMode: "click", // shift+click
selectablePersistence: false, // reset selection on table reload
selectableCheck: function(row){
const data = row.getData();
if(data['kommPruef']) return false
else if(data.hoechsterAntritt >= 3) return false // 3 pruefungen counted
return true; // student can be selected to add pruefung
},
rowFormatter: this.unselectableFormatter,
columns: [
{
formatter: "rowSelection",
@@ -128,6 +156,7 @@ export const Benotungstool = {
width: 50,
},
{title: Vue.computed(() => this.$p.t('benotungstool/c4mail')), field: 'email', formatter: this.mailFormatter, tooltip: false, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4antrittCount')), field: 'hoechsterAntritt', tooltip: false, widthGrow: 1},
{title: 'UID', field: 'uid', tooltip: false, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4vorname')), field: 'vorname', tooltip: false, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4nachname')), field: 'nachname', widthGrow: 1},
@@ -172,14 +201,30 @@ export const Benotungstool = {
{title: '', width: 50, hozAlign: 'center', formatter: this.arrowFormatter, cellClick: this.saveNote},
{title: Vue.computed(() => this.$p.t('benotungstool/c4lvnote')), field: 'lv_note',
formatter: this.notenFormatter,
headerFilter: 'list',
headerFilterParams: () => {
return { values: ["\u00A0",...this.notenOptions.map(opt => opt.bezeichnung)] } // TODO: fix option render height lmao...
},
headerFilterFunc: this.notenFilterFunc,
widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4freigabe')), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter},
{title: Vue.computed(() => this.$p.t('benotungstool/c4zeugnisnote')), field: 'note', formatter: this.notenFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4zeugnisnote')),
headerFilter: true,
field: 'note',
formatter: this.notenFormatter,
widthGrow: 1},
{title: Vue.computed(() => this.$p.t('benotungstool/c4kommPruef')), field: 'kommPruef', widthGrow: 1, formatter: this.pruefungFormatter, hozAlign:"center", minWidth: 150}
],
persistence: false,
}
},
notenFilterFunc(filterVal, rowVal) {
const opt = this.notenOptions.find(opt => opt.bezeichnung === filterVal)
if(opt.note == rowVal) return true
if(filterVal === "" || filterVal === null) return true
return false
},
parseDate(timestamp) {
if(!timestamp) return null
const [datePart, timePart] = timestamp.split(" ");
@@ -196,6 +241,9 @@ export const Benotungstool = {
} else {
return 'ok'
}
},
unselectableFormatter(row) {
},
notenFormatter(cell) {
const value = cell.getValue()
@@ -232,9 +280,6 @@ export const Benotungstool = {
return value
},
handlePasswordChanged(pw) {
// console.log('pw:', pw)
},
saveNote(e, cell) { // Notenvorschlag freigeben
const row = cell.getRow()
const data = row.getData()
@@ -271,19 +316,53 @@ export const Benotungstool = {
return ''
}
const colDef = cell.getColumn().getDefinition()
// column is just a date, student can have any of his antritte on this date, so we need to get
// student.pruefungen and look for a pruefung with this cols title as date
const field = cell.getColumn().getField()
const studentPruefung = field != 'kommPruef' ? data.pruefungen.find(p => p.datum === field) : data['kommPruef']
// is this column/cell allowed to have an add pruefung action
const canAdd = field !== 'kommPruef' && data.hoechsterAntritt < 4 && !colDef.originalNote
// TODO: check for some time limit maybe? old pruefungen can be changed/created
// TODO: it also looks ugly and unprofessional, should at some peoplt disable/hide the change action
// Create root row div
const rowDiv = document.createElement('div');
rowDiv.className = 'row';
rowDiv.className = 'row flex-nowrap';
rowDiv.style.display = 'flex';
rowDiv.style.justifyContent = 'center';
rowDiv.style.alignItems = 'center';
rowDiv.style.height = '100%';
function createCol(content) {
if(studentPruefung) {
let color = ''
switch(studentPruefung.pruefungstyp_kurzbz) {
case 'Termin1':
color = 'green'
break
case 'Termin2':
color = 'yellow'
break
case 'Termin3':
color = 'orange'
break
case 'kommPruef':
color = 'red'
break
}
rowDiv.style.borderLeft = `4px solid ${color}`;
rowDiv.style.marginLeft = "6px"; // small indent so text doesn't overlap
rowDiv.style.boxSizing = "border-box";
}
function createCol(content, classParam) {
const colDiv = document.createElement('div');
colDiv.className = 'col-4';
colDiv.className = classParam ?? 'col-4';
colDiv.style.justifyContent = 'center';
colDiv.style.alignItems = 'center';
colDiv.style.height = '100%';
@@ -297,19 +376,22 @@ export const Benotungstool = {
return colDiv;
}
const field = cell.getColumn().getField()
if(data[field]) {
const dateParts = data[field].datum.split('-')
const date = `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}`
// First column (date)
rowDiv.appendChild(createCol(date));
rowDiv.appendChild(createCol(date, 'col-4 d-flex align-items-center'));
const noteDefEntry = data.note ? this.notenOptions.find(n => n.note == data[field].note) : null
// Second column (note_bezeichnung)
rowDiv.appendChild(createCol(data[field].note_bezeichnung || ''));
if(field === 'kommPruef' || field === 'pruefungNr0') {// no actions on kommPruef allowed
rowDiv.appendChild(createCol('')); // append empty col4 to have formatting similar
rowDiv.appendChild(createCol(noteDefEntry.bezeichnung || '', 'col-auto ms-auto d-flex justify-content-center align-items-center'));
// no actions on kommPruef allowed
// no actions on termin1 aka pruefung 0 aka ursprüngliche note erlaubt
if(field === 'kommPruef' || colDef.originalNote) {
rowDiv.appendChild(createCol('', 'col-4 d-flex justify-content-center align-items-center')); // append empty col4 to have formatting similar
return rowDiv
}
@@ -318,44 +400,42 @@ export const Benotungstool = {
button.className = 'btn btn-outline-secondary';
button.textContent = 'Change'; // TODO: phrase
button.addEventListener('click', () => {
this.openPruefungModal(data, data[field]);
this.openPruefungModal(data, data[field], field);
});
rowDiv.appendChild(createCol(button));
rowDiv.appendChild(createCol(button, 'col-4 d-flex justify-content-center align-items-center'));
return rowDiv;
} else if (field !== 'kommPruef' && field !== 'pruefungNr0') { // return new btn action
} else if (canAdd) { // return new btn action
const button = document.createElement('button');
button.className = 'btn btn-outline-secondary';
button.textContent = 'Add'; // TODO: phrase
button.addEventListener('click', () => {
this.openPruefungModal(data)
this.openPruefungModal(data, null, field)
});
rowDiv.appendChild(createCol(button));
rowDiv.appendChild(createCol(button), 'col-4 d-flex justify-content-center align-items-center');
return rowDiv;
} else return ''
},
openPruefungModal(student, pruefung = null) {
openPruefungModal(student, pruefung = null, field) {
this.pruefungStudent = student
this.pruefung = pruefung
debugger
if(this.pruefung?.datum) {
const pruefungDateParts = this.pruefung?.datum?.split('-')
const dateStr = this.pruefung?.datum ?? field
const pruefungDateParts = dateStr.split('-')
// new date obj so datepicker picks ob the change by ref
const newDate = new Date()
newDate.setFullYear(pruefungDateParts[0])
newDate.setMonth(pruefungDateParts[1])
newDate.setDate(pruefungDateParts[2])
this.selectedPruefungDate = newDate
} else {
const newDate = new Date()
newDate.setTime(Date.now())
this.selectedPruefungDate = newDate
}
// new date obj so datepicker picks ob the change by ref
const newDate = new Date()
newDate.setFullYear(pruefungDateParts[0])
newDate.setMonth(pruefungDateParts[1])
newDate.setMonth(newDate.getMonth() - 1) // acount for js date month offset
newDate.setDate(pruefungDateParts[2])
this.selectedPruefungDate = newDate
if(this.pruefung?.note) {
this.selectedPruefungNote = this.notenOptions.find(n => n.note == this.pruefung.note)
@@ -366,9 +446,9 @@ export const Benotungstool = {
this.$refs.modalContainerPruefung.show()
},
pruefungTitleFormatter(cell) {
const col = cell.getColumn()
if(col.getField() === "pruefungNr0") return this.$p.t('benotungstool/c4originalZnote')
return col.getDefinition().title;
const def = cell.getColumn().getDefinition()
if(def.originalNote) return this.$p.t('benotungstool/c4originalZnote')
return def.title;
},
arrowFormatter(cell) {
const row = cell.getRow()
@@ -391,6 +471,17 @@ export const Benotungstool = {
buildMailToLink(student){
return 'mailto:' + student.uid +'@'+ this.domain
},
insertSortedDate(arr, dateStr) {
// Binary search to find insertion index
let left = 0, right = arr.length;
while (left < right) {
const mid = (left + right) >> 1;
if (arr[mid] < dateStr) left = mid + 1;
else right = mid;
}
arr.splice(left, 0, dateStr); // insert at index
return arr;
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
},
@@ -410,7 +501,7 @@ export const Benotungstool = {
this.teilnoten = data[3] ?? []
// let pruefungenRegularColCount = 0;
const distinctPruefungsDates = []
this.distinctPruefungsDates = []
const cols = [...this.notenTableOptions.columns.slice(0, -1)];
const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1];
@@ -421,7 +512,7 @@ export const Benotungstool = {
// TODO: filter kommPruef here? or change kommProf ColDefinition
if(!distinctPruefungsDates.includes(p.datum)) distinctPruefungsDates.push(p.datum)
if(p.pruefungstyp_kurzbz !== 'kommPruef' && !this.distinctPruefungsDates.includes(p.datum)) this.distinctPruefungsDates.push(p.datum)
// seperate kommPruefungen from previous pruefungen counts since the column count variability always ends with this
if(p.pruefungstyp_kurzbz == 'kommPruef') {
@@ -433,7 +524,6 @@ export const Benotungstool = {
// if(student.pruefungen.length > pruefungenRegularColCount) pruefungenRegularColCount = student.pruefungen.length
})
// TODO: check if note is überschreibbar!
this.studenten?.forEach(s => {
// sort students regular pruefungen by datum
s.pruefungen.sort((p1, p2) => {
@@ -449,7 +539,8 @@ export const Benotungstool = {
s.pruefungen.forEach((p, i) => {
s[p.datum] = p
})
s.hoechsterAntritt = this.getAntrittCountStudent(s)
s.email = this.buildMailToLink(s)
s.lv_note = this.teilnoten[s.uid].note_lv
s.freigabedatum = this.parseDate(this.teilnoten[s.uid]['freigabedatum'])
@@ -466,8 +557,8 @@ export const Benotungstool = {
})
})
distinctPruefungsDates.sort((d1, d2) => {
this.distinctPruefungsDates.sort((d1, d2) => {
if(d1 > d2) {
return 1
} else if (d1 < d2) {
@@ -476,32 +567,38 @@ export const Benotungstool = {
return 0
}
})
distinctPruefungsDates.forEach((date, index)=>{
// TODO date format dd.mm.yyyy
// const title =
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
cols.push({
title: date,//this.$p.t('benotungstool/pruefungNr', [index+1]),
title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]),
field: date,
formatter: this.pruefungFormatter,
titleFormatter: this.pruefungTitleFormatter,
hozAlign:"center",
widthGrow: 1,
minWidth: 150
minWidth: 200,
originalNote
})
})
console.log('distinctPruefungsDates', distinctPruefungsDates)
console.log('cols', cols)
cols.push(kommCol) // keep kommPruef Col as last
this.loading = false
this.$refs.notenTable.tabulator.clearSort()
this.$refs.notenTable.tabulator.setColumns(cols)
this.$refs.notenTable.tabulator.setData(this.studenten);
this.$refs.notenTable.tabulator.redraw(true);
},
loadNoten(lv_id, sem_kurzbz) {
this.loading = true
this.$api.call(ApiNoten.getStudentenNoten(lv_id, sem_kurzbz))
.then(res => {
if(res?.data) this.setupData(res.data)
@@ -520,6 +617,7 @@ export const Benotungstool = {
this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height)
},
setupCreated() {
this.loading = true
// fetch lva dropdown
this.$api.call(ApiLehre.getZugewieseneLv(this.viewData?.uid, this.sem_kurzbz)).then(res => {
this.lehrveranstaltungen = res.data
@@ -642,16 +740,32 @@ export const Benotungstool = {
getOptionLabelLe(option) {
return option.infoString
},
getPruefungstypForStudentByAntritt(student) {
// when adding new pruefungen, determine the next pruefungstyp by using the antritt counter
switch (student.hoechsterAntritt) {
case 0:
return "Termin2"
break
case 1:
return "Termin2"
break
case 2:
return "Termin3"
break
default:
return ""
}
},
savePruefungEingabe() {
const year = this.selectedPruefungDate.getFullYear();
const month = String(this.selectedPruefungDate.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const day = String(this.selectedPruefungDate.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
// TODO: test this hypothesis
// first pruefung is always "Termin2" since normal note counts as Termin1
const pOffset = this.pruefung === null && this.pruefungStudent.pruefungen.length === 0 ? 2 : 1
const typ = this.pruefung ? this.pruefung.pruefungstyp_kurzbz : ('Termin'+(this.pruefungStudent.pruefungen.length + pOffset))
// const pOffset = this.pruefung === null && this.pruefungStudent.pruefungen.length === 0 ? 2 : 1
const typ = this.pruefung ? this.pruefung.pruefungstyp_kurzbz : this.getPruefungstypForStudentByAntritt(this.pruefungStudent)
this.$api.call(ApiNoten.saveStudentPruefung(
this.pruefungStudent.uid,
@@ -664,54 +778,116 @@ export const Benotungstool = {
typ
)).then(res => {
if(res.meta.status === 'success') {
this.$fhcAlert.alertInfo('Prüfung für Student ' + this.pruefungStudent.uid + ' bearbeitet oder angelegt') // TODO: phrase
const s = this.studenten.find(s => s.uid === res.data[1]?.student_uid)
console.log('old student freigabedatum', s.freigabedatum)
console.log('old student benotungsdatum', s.benotungsdatum)
s.freigabedatum = this.parseDate(res.data[1]?.['freigabedatum'])
s.benotungsdatum = this.parseDate(res.data[1]?.['benotungsdatum'])
console.log('new student freigabedatum', s.freigabedatum)
console.log('new student benotungsdatum', s.benotungsdatum)
s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid);
// todo: update student subject grade/lv_note
s.lv_note = res.data[1]?.note
// find the exact student pruefung and update that
// add new pruefung to row
if(res.data[0]?.new) {
// TODO: if new pruefung was of typ "Termin2", "Termin1" shadow pruefung is not available
if(!this.pruefung) {
this.handleAddNewTermin(res.data, s)
} else { // update existing
const oldIndex = s.pruefungen.findIndex(p => p.pruefung_id == res.data[0]?.pruefung_id)
const oldIndex = s.pruefungen.findIndex(p => p.pruefung_id == this.pruefung.pruefung_id)
if(oldIndex !== -1) {
s.pruefungen.splice(oldIndex, 1, res.data[0])
s['pruefungNr'+oldIndex] = res.data[0]
s[res.data[0].datum] = res.data[0]
}
// antritte might have changed due to different benotung
s.hoechsterAntritt = this.getAntrittCountStudent(s)
}
// // TODO: manual row update!
// const row = this.$refs.notenTable.tabulator.getRow(s.uid)
// row.update()
this.$refs.notenTable.tabulator.redraw(true)
this.$fhcAlert.alertSuccess('Prüfung gespeichert') // TODO: phrase
this.$fhcAlert.alertInfo('Prüfung gespeichert') // TODO: phrase
}
})
this.$refs.modalContainerPruefung.hide()
},
handleAddNewTermin(data, student){
const savedPruefung = data[0]
const extra = data[2]
// check for extra pruefung (termin1) to add before
if(extra) {
extra.datum = extra.datum.split(' ')[0]
if(!this.distinctPruefungsDates.includes(extra.datum)) {
this.insertSortedDate(this.distinctPruefungsDates, extra.datum)
}
student.pruefungen.push(extra)
student[extra.datum] = extra
}
if(!this.distinctPruefungsDates.includes(savedPruefung.datum)) {
this.insertSortedDate(this.distinctPruefungsDates, savedPruefung.datum)
}
// add pruefung to pruefungen array
student.pruefungen.push(savedPruefung)
// add pruefung to student via its datum as a field
student[savedPruefung.datum] = savedPruefung
// usually should be in order naturally, just to be save
student.pruefungen.sort((p1, p2) => {
if(p1.datum > p2.datum) {
return 1
} else if (p1.datum < p2.datum) {
return -1
} else {
return 0
}
})
// recalculate student antritte
student.hoechsterAntritt = this.getAntrittCountStudent(student)
// add col to table
const cols = [...this.notenTableOptions.columns.slice(0, -1)];
const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1];
// TODO: could reuse cols instead of recreating all from a variable maybe
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
cols.push({
title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]),
field: date,
formatter: this.pruefungFormatter,
titleFormatter: this.pruefungTitleFormatter,
hozAlign:"center",
widthGrow: 1,
minWidth: 200,
originalNote
})
})
cols.push(kommCol) // keep kommPruef Col as last
// redraw table
},
saveNoteneingabe() {
this.$api.call(ApiNoten.saveStudentenNoten(this.password, this.changedNoten, this.lv_id, this.sem_kurzbz))
.then((res) => {
if(res.meta.status === 'success') {
this.$fhcAlert.alertSuccess('Noten gespeichert')
this.$fhcAlert.alertInfo('Noten gespeichert')
}
res.data.forEach(d => {
@@ -733,12 +909,6 @@ export const Benotungstool = {
openNewPruefungsdatumModal() {
this.$refs.modalContainerNeuesPruefungsdatum.show()
},
handleChangePruefungDatum(e) {
// console.log('handleChangePruefungDatum', e)
},
handleChangePruefungNote(e) {
// console.log(e)
},
getOptionLabelNotePruefung(option) {
return option.bezeichnung
},
@@ -746,7 +916,150 @@ export const Benotungstool = {
this.selectedLehreinheit = e.value
},
addPruefung(){
this.$refs.modalContainerNeuesPruefungsdatum.hide()
// filter students that already have a pruefung on datum
// TODO: save new pruefungs entry for all selected students on selected date with default note "noch nicht eingetragen" aka 9
const year = this.selectedPruefungDate.getFullYear();
const month = String(this.selectedPruefungDate.getMonth() + 1).padStart(2, '0'); // Months are 0-based
const day = String(this.selectedPruefungDate.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
const uids = this.selectedUids.map(student => {
return {
uid: student.uid,
lehreinheit_id: student.lehreinheit_id,
typ: this.getPruefungstypForStudentByAntritt(student)//student.hoechsterAntritt
}
})
this.loading = true;
this.$api.call(ApiNoten.createPruefungen(
uids,
dateStr,
this.lv_id,
this.sem_kurzbz,
)).then(res => {
if(res.meta.status === "success") {
this.$fhcAlert.alertInfo('Prüfung an ' + dateStr + ' angelegt') // TODO: phrase
const pruefungen = res.data
uids.forEach(entry => {
const saved = pruefungen[entry.uid].savedPruefung
const extra = pruefungen[entry.uid].extraPruefung
const student = this.studenten.find(s => s.uid == entry.uid)
if(!student) return
// check for extra pruefung (termin1) to add before
if(extra) {
extra.datum = extra.datum.split(' ')[0]
if(!this.distinctPruefungsDates.includes(extra.datum)) {
this.insertSortedDate(this.distinctPruefungsDates, extra.datum)
}
student.pruefungen.push(extra)
student[extra.datum] = extra
}
if(!this.distinctPruefungsDates.includes(saved.datum)) {
this.insertSortedDate(this.distinctPruefungsDates, saved.datum)
}
// add pruefung to pruefungen array
student.pruefungen.push(saved)
// add pruefung to student via its datum as a field
student[saved.datum] = saved
// usually should be in order naturally, just to be save
student.pruefungen.sort((p1, p2) => {
if(p1.datum > p2.datum) {
return 1
} else if (p1.datum < p2.datum) {
return -1
} else {
return 0
}
})
// recalculate student antritte
student.hoechsterAntritt = this.getAntrittCountStudent(student)
})
// add col to table
const cols = [...this.notenTableOptions.columns.slice(0, -1)];
const kommCol = this.notenTableOptions.columns[this.notenTableOptions.columns.length - 1];
// TODO: could reuse cols instead of recreating all from a variable maybe
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
cols.push({
title: titledate,//this.$p.t('benotungstool/pruefungNr', [index+1]),
field: date,
formatter: this.pruefungFormatter,
titleFormatter: this.pruefungTitleFormatter,
hozAlign:"center",
widthGrow: 1,
minWidth: 150,
originalNote
})
})
cols.push(kommCol) // keep kommPruef Col as last
// redraw table
this.loading = false
this.$refs.notenTable.tabulator.clearSort()
this.$refs.notenTable.tabulator.setColumns(cols)
this.$refs.notenTable.tabulator.setData(this.studenten);
this.$refs.notenTable.tabulator.redraw(true);
}
})
},
getAntrittCountStudent(student) {
// checks for existence of a prüfung with a note that resolves to a
// "angetretene Prüfung" -> anything except "entschuldigt" & "noch nicht eingetragen"
// and returns the next allowed pruefungstyp from the number of taken pruefungen
// 1 -> reguläre note
// 2 -> erste Nachprüfung / Termin2
// 3 -> 2te Nachprüfung / Termin3
// 4 -> kommPruef
if(student['kommPruef']) return 4
let pruefungsAntrittCount = 0
const pLen = student.pruefungen.length
for(let i = 0; i < pLen; i++) {
const p = student.pruefungen[i]
if(p.note != 9 && p.note != 17) pruefungsAntrittCount++
}
// when student never had to take an exam beyond the original benotung
// aka pruefungsantritt (even though it does not have to have pruefungscharacter)
// it still counts as an antritt, except it is coming from a notenOption like "angerechnet"
// which indicates no participation at all
if(pruefungsAntrittCount === 0 && student.note){
const noteOption = this.notenOptions.find(note => note.note == student.note)
if(noteOption.lehre) return 1
else return 0
}
return pruefungsAntrittCount
}
},
watch: {
@@ -754,19 +1067,18 @@ export const Benotungstool = {
const table = this.$refs.notenTable?.tabulator
if (!table) return;
console.log('selectedUids watcher newVal',newVal)
// Get all current rows
const allRows = table.getRows();
console.log('table allRows',allRows)
// allRows.forEach(row => {
// const rowData = row.getData();
//
// if (newVal.includes(rowData.uid)) {
// row.select(); // ensure row is selected
// } else {
// row.deselect(); // ensure row is deselected
// }
// });
allRows.forEach(row => {
const rowData = row.getData();
const found = newVal.find(stud => stud.uid == rowData.uid)
if (found) {
row.select(); // ensure row is selected
} else {
row.deselect(); // ensure row is deselected
}
});
},
selectedLehreinheit(newVal) {
if(!this.$refs.notenTable) return
@@ -794,7 +1106,7 @@ export const Benotungstool = {
},
getSaveBtnClass() {
// return "btn btn-primary ml-2"
return !this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2"
return this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2"
},
getNewBtnClass() {
return "btn btn-primary ml-2"
@@ -824,34 +1136,32 @@ export const Benotungstool = {
<template v-slot:title>{{$p.t('benotungstool/c4addNewPruefung')}}</template>
<template v-slot:default>
<div class="row justify-content-center">
<div class="col-auto">
<div class="col-1 text-center">{{$p.t('benotungstool/c4date')}}:</div>
<div class="col-6">
<datepicker
v-model="selectedPruefungDate"
@update:model-value="handleChangePruefungDatum"
:clearable="false"
:time-picker="false"
:text-input="true"
:auto-apply="true">
</datepicker>
</div>
<div class="col-auto text-center">{{$p.t('benotungstool/c4date')}}:</div>
<div class="col-6">
<datepicker
v-model="selectedPruefungDate"
@update:model-value="handleChangePruefungDatum"
:clearable="false"
:enableTimePicker="false"
:text-input="true"
:auto-apply="true">
</datepicker>
</div>
</div>
<div class="row justify-content-center">
<div class="col-auto">
<div class="col-1 text-center">{{$p.t('benotungstool/prueflingSelection')}}:</div>
<div class="col-6">
<Multiselect
v-model="selectedUids"
:options="getStudentenOptions"
optionLabel="infoString"
placeholder="Studenten auswählen"
:maxSelectedLabels="3"
showToggleAll
class="w-full md:w-20rem" />
</div>
<div class="row mt-4 justify-content-center">
<div class="col-auto text-center">{{$p.t('benotungstool/prueflingSelection')}}:</div>
<div class="col-6">
<Multiselect
v-model="selectedUids"
:options="getStudentenOptions"
optionLabel="infoString"
placeholder="Studenten auswählen"
:maxSelectedLabels="3"
showToggleAll
class="w-100" />
</div>
</div>
@@ -861,7 +1171,7 @@ export const Benotungstool = {
</template>
</bs-modal>
<bs-modal ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg">
<bs-modal ref="modalContainerNotenSpeichern" class="bootstrap-prompt" dialogClass="modal-lg">
<template v-slot:title>{{ $p.t('benotungstool/noteneingabeSpeichern') }}</template>
<template v-slot:default>
<div class="row justify-content-center">
@@ -873,10 +1183,10 @@ export const Benotungstool = {
<template v-slot:footer>
<button type="button" class="btn btn-primary" @click="saveNoteneingabe">{{ $p.t('benotungstool/noteneingabeBestätigen') }}</button>
</template>
</bs-modal>
</bs-modal>
<bs-modal ref="modalContainerPruefung" class="bootstrap-prompt" dialogClass="modal-lg">
<template v-slot:title>{{ $p.t('benotungstool/createPruefungFor') }} {{pruefungStudent?.vorname}} {{pruefungStudent?.nachname}}</template>
<template v-slot:title>{{ pruefung ? $p.t('benotungstool/editPruefungFor') : $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">{{$p.t('benotungstool/c4date')}}:</div>
@@ -885,7 +1195,7 @@ export const Benotungstool = {
v-model="selectedPruefungDate"
@update:model-value="handleChangePruefungDatum"
:clearable="false"
:time-picker="false"
:enableTimePicker="false"
:text-input="true"
:auto-apply="true">
</datepicker>
@@ -910,6 +1220,11 @@ export const Benotungstool = {
</template>
</bs-modal>
<div v-show="loading" style="position: absolute; width: 100vw; height: 100vh; background: rgba(255,255,255,0.5); z-index: 8500; display: flex; justify-content: center; align-items: center;">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<div class="row">
<div class="col-4">
<h2>{{$p.t('benotungstool/benotungstoolTitle')}}</h2>
+40
View File
@@ -41936,6 +41936,26 @@ and represent the current state of research on the topic. The prescribed citatio
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',
'phrase' => 'c4antrittCount',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Anzahl Prüfungsantritte',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Number of exam attempts',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',
@@ -42396,6 +42416,26 @@ and represent the current state of research on the topic. The prescribed citatio
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',
'phrase' => 'editPruefungFor',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Prüfung bearbeiten für',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Edit examination for',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'benotungstool',