diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php index 86c56d5ad..68bd3f4fd 100644 --- a/application/controllers/api/frontend/v1/Noten.php +++ b/application/controllers/api/frontend/v1/Noten.php @@ -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); + } } diff --git a/public/js/api/factory/noten.js b/public/js/api/factory/noten.js index d26a1a884..b62ba1fe1 100644 --- a/public/js/api/factory/noten.js +++ b/public/js/api/factory/noten.js @@ -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 } + }; + }, }; \ No newline at end of file diff --git a/public/js/components/Cis/Benotungstool/Benotungstool.js b/public/js/components/Cis/Benotungstool/Benotungstool.js index 83144f4d7..fdbcf098d 100644 --- a/public/js/components/Cis/Benotungstool/Benotungstool.js +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -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 = { - +