From e58bf3a8cfb9cb600f2eb06925e4acbe59c29fb9 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 1 Aug 2025 09:48:46 +0200 Subject: [PATCH] WIP noten/pruefung import --- application/controllers/Cis/Benotungstool.php | 10 +- .../controllers/api/frontend/v1/Noten.php | 71 ++++++++- .../views/CisRouterView/CisRouterView.php | 3 +- public/js/api/factory/noten.js | 10 ++ .../Cis/Benotungstool/Benotungstool.js | 143 ++++++++++++++++-- system/phrasesupdate.php | 40 +++++ 6 files changed, 264 insertions(+), 13 deletions(-) diff --git a/application/controllers/Cis/Benotungstool.php b/application/controllers/Cis/Benotungstool.php index 81379b587..18038bb39 100644 --- a/application/controllers/Cis/Benotungstool.php +++ b/application/controllers/Cis/Benotungstool.php @@ -31,9 +31,17 @@ class Benotungstool extends Auth_Controller // TODO: check if related CIS config is also loaded when being routed in Cis4 by vuerouter // TODO: check if new benotungstool should be configurable the exact same way? + + $viewData = array( 'uid'=>getAuthUID(), - 'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN + 'CIS_GESAMTNOTE_UEBERSCHREIBEN' => CIS_GESAMTNOTE_UEBERSCHREIBEN, + 'CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF' => CIS_GESAMTNOTE_PRUEFUNG_KOMMPRUEF, + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN3' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN3, + 'CIS_GESAMTNOTE_PRUEFUNG_TERMIN2' => CIS_GESAMTNOTE_PRUEFUNG_TERMIN2, + 'CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE' => CIS_GESAMTNOTE_PRUEFUNG_MOODLE_LE_NOTE, + 'CIS_GESAMTNOTE_PUNKTE' => CIS_GESAMTNOTE_PUNKTE, + 'CIS_GESAMTNOTE_GEWICHTUNG' => CIS_GESAMTNOTE_GEWICHTUNG ); $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Benotungstool']); diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php index 290105cdf..c106b204f 100644 --- a/application/controllers/api/frontend/v1/Noten.php +++ b/application/controllers/api/frontend/v1/Noten.php @@ -35,7 +35,8 @@ class Noten extends FHCAPI_Controller 'getNotenvorschlagStudent' => self::PERM_LOGGED, 'saveNotenvorschlag' => self::PERM_LOGGED, 'saveStudentPruefung' => self::PERM_LOGGED, - 'createPruefungen' => self::PERM_LOGGED + 'createPruefungen' => self::PERM_LOGGED, + 'saveNotenvorschlagBulk' => self::PERM_LOGGED ]); $this->load->library('AuthLib', null, 'AuthLib'); @@ -108,6 +109,8 @@ class Noten extends FHCAPI_Controller } // send $grades reference to moodle addon + + // TODO: event getExterneNoten Events::trigger( 'moodleGrades', function & () use (&$grades) @@ -556,6 +559,72 @@ class Noten extends FHCAPI_Controller $this->terminateWithSuccess(array($ret, $lvgesamtnote)); } + + public function saveNotenvorschlagBulk() { + $result = $this->getPostJSON(); + + if(!property_exists($result, 'lv_id') || !property_exists($result, 'sem_kurzbz') || + !property_exists($result, 'noten')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + $noten = $result->noten; + + $retLvNoten = []; + $responseMsgs = []; + + foreach($noten as $note) + { + $lvgesamtnote = new lvgesamtnote(); + if (!$lvgesamtnote->load($lv_id, $note->uid, $sem_kurzbz)) + { + $lvgesamtnote->student_uid = $note->uid; + $lvgesamtnote->lehrveranstaltung_id = $lv_id; + $lvgesamtnote->studiensemester_kurzbz = $sem_kurzbz; + $lvgesamtnote->note = trim($note->note); + $lvgesamtnote->mitarbeiter_uid = getAuthUID(); + $lvgesamtnote->benotungsdatum = date("Y-m-d H:i:s"); + $lvgesamtnote->freigabedatum = null; + $lvgesamtnote->freigabevon_uid = null; + $lvgesamtnote->bemerkung = null; + $lvgesamtnote->updateamum = null; + $lvgesamtnote->updatevon = null; + $lvgesamtnote->insertamum = date("Y-m-d H:i:s"); + $lvgesamtnote->insertvon = getAuthUID(); + $lvgesamtnote->punkte =// TODO: deprecated? + $new = true; + $response = "neu"; + } + else + { + $lvgesamtnote->note = trim($note->note); + $lvgesamtnote->punkte = null; // TODO: deprecated? + $lvgesamtnote->benotungsdatum = date("Y-m-d H:i:s"); + $lvgesamtnote->updateamum = date("Y-m-d H:i:s"); + $lvgesamtnote->updatevon = getAuthUID(); + $new = false; + if ($lvgesamtnote->freigabedatum) + $response = "update_f"; + else + $response = "update"; + } + + if (!$lvgesamtnote->save($new)) + $responseMsgs[] = $lvgesamtnote->errormsg; + else + $responseMsgs[] = $response; + + + $lvgesamtnote->load($lv_id, $note->uid, $sem_kurzbz); + + $retLvNoten[] = $lvgesamtnote; + } + + $this->terminateWithSuccess(array($retLvNoten, $responseMsgs)); + + } public function createPruefungen() { $result = $this->getPostJSON(); diff --git a/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index b58490094..d45ec0951 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -26,7 +26,8 @@ $includesArray = array( 'vendor/npm-asset/primevue/accordion/accordion.js', 'vendor/npm-asset/primevue/accordiontab/accordiontab.js', 'vendor/npm-asset/primevue/password/password.js', - 'vendor/npm-asset/primevue/multiselect/multiselect.js' + 'vendor/npm-asset/primevue/multiselect/multiselect.js', + 'vendor/npm-asset/primevue/textarea/textarea.js' ), 'customJSModules' => array( 'public/js/apps/Dashboard/Fhc.js' diff --git a/public/js/api/factory/noten.js b/public/js/api/factory/noten.js index b62ba1fe1..e472f9a98 100644 --- a/public/js/api/factory/noten.js +++ b/public/js/api/factory/noten.js @@ -57,4 +57,14 @@ export default { params: { uids, datum, lva_id, sem_kurzbz } }; }, + saveNotenvorschlagBulk(lv_id, sem_kurzbz, noten) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/saveNotenvorschlagBulk', + params: { lv_id, sem_kurzbz, noten } + }; + }, + saveStudentPruefungBulk(lv_id, sem_kurzbz, pruefungen) { + + } }; \ 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 cbc73415c..50b7ec676 100644 --- a/public/js/components/Cis/Benotungstool/Benotungstool.js +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -12,6 +12,7 @@ export const Benotungstool = { CoreFilterCmpt, Dropdown: primevue.dropdown, Password: primevue.password, + Textarea: primevue.textarea, Datepicker: VueDatePicker, Multiselect: primevue.multiselect }, @@ -49,6 +50,7 @@ export const Benotungstool = { changedNotenCounter: 0, tabulatorUuid: Vue.ref(0), domain: '', + importString: '', teilnoten: null, lv: null, studenten: null, @@ -107,6 +109,102 @@ export const Benotungstool = { ]}; }, methods: { + parseNote(rowParts, notenbulk) { + const uid = rowParts[0] + + const student = this.studenten.find(s => s.uid === uid) + if(!student) return + + const note = rowParts[1] + + // find notenoption and check if its allowed to use in lehre + const notenOption = this.notenOptions.find(n => n.note == note) + if(!notenOption.lehre) return + + notenbulk.push({uid, note}) + }, + parsePruefung(rowParts, notenbulk) { + const uid = rowParts[0] + + const student = this.studenten.find(s => s.uid === uid) + if(!student) return + + const datum = rowParts[1] // should be in 'YYYY.MM.DD' + const datumObj = datum + + const year = datumObj.getFullYear(); + const month = String(datumObj.getMonth() + 1).padStart(2, '0'); // Months are 0-based + const day = String(datumObj.getDate()).padStart(2, '0'); + const dateStr = `${year}-${month}-${day}` + + const note = rowParts[2] + + // find notenoption and check if its allowed to use in lehre + const notenOption = this.notenOptions.find(n => n.note == note) + if(!notenOption.lehre) return + }, + saveNotenBulk(notenbulk) { + this.$api.call(ApiNoten.saveNotenvorschlagBulk(this.lv_id, this.sem_kurzbz, notenbulk)).then(res => { + console.log(res) + if(res.meta.status === 'success') { + const lvNoten = res.data[0] + + lvNoten.forEach(lvn => { + // 1.) get relevant student row by uid + const s = this.studenten.find(s => s.uid === lvn.uid) + s.note_vorschlag = lvn.note // TODO: check if note_vorschlag should be changed by import + + this.teilnoten[s.uid].note_lv = lvn.note + s.freigabedatum = this.parseDate(lvn['freigabedatum']) + s.benotungsdatum = this.parseDate(lvn['benotungsdatum']) + + s.freigegeben = this.checkFreigabe(s.freigabedatum, s.benotungsdatum, s.uid); + }) + + } + + // 2.) set note_vorschlag field + + // 4.) update rows with note_lv = note_vorschlag & recalculate freigabestatus + }) + }, + savePruefungBulk(pruefungenbulk) { + this.$api.call(ApiNoten.saveStudentPruefungBulk(this.lv_id, this.sem_kurzbz, pruefungenbulk)) + .then((res)=> { + if(res.meta.status === 'success') { + + + + + this.$fhcAlert.alertInfo('Prüfungen gespeichert') // TODO: phrase + } + }) + }, + importNoten() { + console.log('importNoten', this.importString) + + // TODO: check for signs of notenimport or pruefung import + + const rows = this.importString.split('\n') + const bulk = [] + let mode = '' + // read the lines + rows.forEach(r => { + const rowParts = r.split('\t') + if(rowParts.length === 3) { + this.parsePruefung(rowParts, bulk) + mode = 'pruefung' // if line parts are not uniform we are in trouble + } else if(rowParts.length === 2) { + this.parseNote(rowParts, bulk) + mode = 'note' + } + }) + + if(mode === 'note') this.saveNotenBulk(bulk) + else if (mode === 'pruefung') this.savePruefungBulk(bulk) + + this.$refs.modalContainerNotenImport.hide() + }, selectionArraysAreEqual(arr1, arr2) { if(arr1.length !== arr2.length) return false @@ -408,7 +506,7 @@ export const Benotungstool = { const date = `${dateParts[2]}.${dateParts[1]}.${dateParts[0]}` // First column (date) - rowDiv.appendChild(createCol(date, 'col-4 d-flex align-items-center')); + rowDiv.appendChild(createCol(date, 'col-4 d-flex justify-content-center align-items-center')); const noteDefEntry = data.note ? this.notenOptions.find(n => n.note == data[field].note) : null @@ -418,7 +516,7 @@ export const Benotungstool = { // 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 + // rowDiv.appendChild(createCol('', 'col-4 d-flex justify-content-center align-items-center')); // append empty col4 to have formatting similar return rowDiv } @@ -450,17 +548,22 @@ export const Benotungstool = { openPruefungModal(student, pruefung = null, field) { this.pruefungStudent = student this.pruefung = pruefung - const dateStr = this.pruefung?.datum ?? field const pruefungDateParts = dateStr.split('-') + + // does not work correctly // 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]) + // 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]) + + // works correctly + const newDate = new Date(+pruefungDateParts[0], +pruefungDateParts[1], +pruefungDateParts[2]) + newDate.setMonth(newDate.getMonth() - 1) this.selectedPruefungDate = newDate @@ -1037,6 +1140,9 @@ export const Benotungstool = { openNewPruefungsdatumModal() { this.$refs.modalContainerNeuesPruefungsdatum.show() }, + openNotenImportModal() { + this.$refs.modalContainerNotenImport.show() + }, getOptionLabelNotePruefung(option) { return option.bezeichnung }, @@ -1233,12 +1339,13 @@ export const Benotungstool = { return counter }, getSaveBtnClass() { - // return "btn btn-primary ml-2" return this.changedNoten?.length ? "btn btn-primary ml-2" : "btn btn-secondary ml-2" }, getNewBtnClass() { return "btn btn-primary ml-2" - // return !this.changedData.length ? "btn btn-secondary ml-2" : "btn btn-primary ml-2" + }, + getNotenImportBtnClass() { + return "btn btn-primary ml-2" }, changedNoten() { const v = this.changedNotenCounter // hack to trigger computed @@ -1259,6 +1366,19 @@ export const Benotungstool = { this.setupMounted() }, template: ` + + + + + @@ -1408,6 +1528,9 @@ export const Benotungstool = { + diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 5cfa302cd..f85bded48 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -42036,6 +42036,46 @@ and represent the current state of research on the topic. The prescribed citatio ) ) ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4notenImportieren', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Noten Import', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Import Grades', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'benotungstool', + 'phrase' => 'c4import', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Importieren', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Import', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'benotungstool',