From 81eee814e9ffeed2efcb107eec6b2afa6c9f2c90 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 2 Feb 2026 17:07:16 +0100 Subject: [PATCH] yellow dropdown styling only on editable tabulator colums for note_vorschlag; fetch note for punkte for notenvorschlag and pruefungsnote if certain config is set; added debounce helper file/function; WIP persisting punkte in backend --- .../controllers/api/frontend/v1/Noten.php | 24 ++- public/css/Cis4/Benotungstool.css | 4 +- public/js/api/factory/noten.js | 9 +- .../Cis/Benotungstool/Benotungstool.js | 167 ++++++++++++------ public/js/helpers/debounce.js | 9 + system/phrasesupdate.php | 29 ++- 6 files changed, 179 insertions(+), 63 deletions(-) create mode 100644 public/js/helpers/debounce.js diff --git a/application/controllers/api/frontend/v1/Noten.php b/application/controllers/api/frontend/v1/Noten.php index 57fcc54be..5d0569eff 100644 --- a/application/controllers/api/frontend/v1/Noten.php +++ b/application/controllers/api/frontend/v1/Noten.php @@ -38,7 +38,8 @@ class Noten extends FHCAPI_Controller 'createPruefungen' => array('lehre/benotungstool:rw'), 'saveNotenvorschlagBulk' => array('lehre/benotungstool:rw'), 'savePruefungenBulk' => array('lehre/benotungstool:rw'), - 'getCisConfig' => array('lehre/benotungstool:rw') + 'getCisConfig' => array('lehre/benotungstool:rw'), + 'getNoteByPunkte' => array('lehre/benotungstool:rw') ]); $this->load->library('AuthLib', null, 'AuthLib'); @@ -1045,6 +1046,27 @@ class Noten extends FHCAPI_Controller return $anwesenheiten; } + + public function getNoteByPunkte() { + $result = $this->getPostJSON(); + + // TODO validate post properly + if(!property_exists($result, 'punkte') + || !property_exists($result, 'lv_id') + || !property_exists($result, 'sem_kurzbz')) { + $this->terminateWithError($this->p->t('global', 'missingParameters'), 'general'); + } + + $punkte = $result->punkte; + $lv_id = $result->lv_id; + $sem_kurzbz = $result->sem_kurzbz; + + $result = $this->NotenschluesselaufteilungModel->getNote($punkte, $lv_id, $sem_kurzbz); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + + } } diff --git a/public/css/Cis4/Benotungstool.css b/public/css/Cis4/Benotungstool.css index 45b17d019..e58b4b945 100644 --- a/public/css/Cis4/Benotungstool.css +++ b/public/css/Cis4/Benotungstool.css @@ -24,13 +24,13 @@ /* styling for editable dropdown column of notenvorschläge in benotungstool*/ -#notentable .tabulator-tableholder .tabulator-editable { +#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="note_vorschlag"] { position: relative; background-color: rgba(255, 255, 157, 0.73); cursor: pointer; } -#notentable .tabulator-tableholder .tabulator-editable::after { +#notentable .tabulator-tableholder .tabulator-editable[tabulator-field="note_vorschlag"]::after { content: "▾"; position: absolute; right: 6px; diff --git a/public/js/api/factory/noten.js b/public/js/api/factory/noten.js index 51e1900e6..bedaddab0 100644 --- a/public/js/api/factory/noten.js +++ b/public/js/api/factory/noten.js @@ -76,5 +76,12 @@ export default { url: '/api/frontend/v1/Noten/savePruefungenBulk', params: { lv_id, sem_kurzbz, pruefungen } }; + }, + getNoteByPunkte(punkte, lv_id, sem_kurzbz) { + return { + method: 'post', + url: '/api/frontend/v1/Noten/getNoteByPunkte', + params: { punkte, lv_id, sem_kurzbz } + }; } -}; \ No newline at end of file +} \ 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 cf0ed83e0..500540ca0 100644 --- a/public/js/components/Cis/Benotungstool/Benotungstool.js +++ b/public/js/components/Cis/Benotungstool/Benotungstool.js @@ -8,6 +8,7 @@ import VueDatePicker from '../../vueDatepicker.js.php'; import LehreinheitenModule from '../../DropdownModes/LehreinheitenModule'; import MobilityLegende from '../../Mobility/Legende.js'; import FhcOverlay from "../../Overlay/FhcOverlay.js"; +import {debounce} from "../../../helpers/debounce.js"; export const Benotungstool = { name: "Benotungstool", @@ -18,6 +19,7 @@ export const Benotungstool = { MobilityLegende, Dropdown: primevue.dropdown, Divider: primevue.divider, + InputNumber: primevue.inputnumber, Password: primevue.password, Textarea: primevue.textarea, Datepicker: VueDatePicker, @@ -44,6 +46,7 @@ export const Benotungstool = { }, data() { return { + debouncedFetchPunkteForPruefung: null, config: null, // cis config neuesPruefungsdatumModalVisible: false, loading: false, @@ -52,6 +55,7 @@ export const Benotungstool = { tabulatorCanBeBuilt: false, selectedPruefungNote: null, selectedPruefungDate: new Date(), // v-model for pruefung edit datepicker + selectedPruefungPunkte: null, distinctPruefungsDates: null, pruefungStudent: null, pruefung: null, @@ -115,6 +119,15 @@ export const Benotungstool = { const row = cell.getRow() row.reformat() // trigger reformat of arrow + } else if (field === 'punkte') { + const newValue = cell.getValue(); + if(newValue == '' || newValue == null) return + this.$api.call(ApiNoten.getNoteByPunkte(newValue, this.lv_id, this.sem_kurzbz)).then(res => { + if(res?.meta?.status === 'success' && res.data >= 0) { + const row = cell.getRow(); + row.update({note_vorschlag: res.data}) + } + }) } } }, @@ -132,6 +145,13 @@ export const Benotungstool = { ]}; }, methods: { + fetchNoteForPunktePruefung(event) { + this.$api.call(ApiNoten.getNoteByPunkte(event.value, this.lv_id, this.sem_kurzbz)).then(res => { + if(res?.meta?.status === 'success' && res.data >= 0) { + this.selectedPruefungNote = this.notenOptions.find(n => n.note == res.data) + } + }) + }, isValidDate_ddmmyyyy(str) { if (typeof str !== 'string') return false; @@ -273,7 +293,7 @@ export const Benotungstool = { this.$fhcAlert.alertDefault( 'success', 'Info', - this.$p.t('benotungstool/notenImportSuccessAlert'), + this.$capitalize(this.$p.t('benotungstool/notenImportSuccessAlert')), true ) const lvNoten = res.data @@ -309,7 +329,7 @@ export const Benotungstool = { this.$fhcAlert.alertDefault( 'success', 'Info', - this.$p.t('benotungstool/pruefungImportSuccessAlert'), + this.$capitalize(this.$p.t('benotungstool/pruefungImportSuccessAlert')), true ) this.handleAddNewPruefungenResponse(res, pruefungenbulk) @@ -453,7 +473,7 @@ export const Benotungstool = { virtualDom: false, index: 'uid', layout: 'fitDataStretch', - placeholder: this.$p.t('global/noDataAvailable'), + placeholder: this.$capitalize(this.$p.t('global/noDataAvailable')), selectable: true, selectableRangeMode: "click", // shift+click selectablePersistence: false, // reset selection on table reload @@ -515,14 +535,30 @@ export const Benotungstool = { cssClass: 'sticky-col' }, {title: 'UID', field: 'uid', tooltip: false, widthGrow: 1, topCalc: this.sumCalcFunc, cssClass: 'sticky-col'}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4mail')), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, widthGrow: 1, variableHeight: true}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4antrittCountv2')), field: 'hoechsterAntritt', tooltip: false, widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4vorname')), field: 'vorname', headerFilter: true, tooltip: false, widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4nachname')), field: 'nachname', headerFilter: true, widthGrow: 1}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4anwesenheitsquote')), field: 'anwquote', widthGrow: 1, formatter: this.percentFormatter}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4mobility')), field: 'mobility_zusatz', headerFilter: true, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4teilnoten')), field: 'teilnote', widthGrow: 1, formatter: this.teilnotenFormatter, variableHeight: true}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4note')), field: 'note_vorschlag', + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mail'))), field: 'email', formatter: this.mailFormatter, tooltip: false, visible: false, widthGrow: 1, variableHeight: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4antrittCountv2'))), field: 'hoechsterAntritt', tooltip: false, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4vorname'))), field: 'vorname', headerFilter: true, tooltip: false, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4nachname'))), field: 'nachname', headerFilter: true, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4anwesenheitsquote'))), field: 'anwquote', widthGrow: 1, formatter: this.percentFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4mobility'))), field: 'mobility_zusatz', headerFilter: true, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4teilnoten'))), field: 'teilnote', widthGrow: 1, formatter: this.teilnotenFormatter, variableHeight: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4punkte'))), field: 'punkte', widthGrow: 1, + editor: 'number', + editorParams: (cell) => { + return { + min: 0, + max: 9999, + step: 1, + elementAttributes: { + maxlength: "4" + }, + selectContents: true, + verticalNavigation: "table" + } + }, + variableHeight: true + }, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4notenvorschlag'))), field: 'note_vorschlag', editor: 'list', editorParams: (cell) => { // write original cell value into row to it can be retrieved if edit is cancelled without selection @@ -537,6 +573,9 @@ export const Benotungstool = { }; }, editable: (cell) => { + // TODO: css style this a bit + // punkte features enables mapping but unable to set note directly + if(this.config?.CIS_GESAMTNOTE_PUNKTE) return false const rowData = cell.getRow().getData(); const noteOption = this.notenOptions.find(opt => opt.note == rowData.note) if(!noteOption) return true @@ -561,7 +600,7 @@ export const Benotungstool = { widthGrow: 1 }, {title: '', width: 50, hozAlign: 'center', formatter: this.arrowFormatter, cellClick: this.saveNote, variableHeight: true}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4lvnote')), field: 'lv_note', + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4lvnote'))), field: 'lv_note', formatter: this.notenFormatter, headerFilter: 'list', headerFilterParams: () => { @@ -570,8 +609,8 @@ export const Benotungstool = { headerFilterFunc: this.notenFilterFunc, widthGrow: 1 }, - {title: Vue.computed(() => this.$p.t('benotungstool/c4freigabe')), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter, variableHeight: true}, - {title: Vue.computed(() => this.$p.t('benotungstool/c4zeugnisnote')), + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4freigabe'))), field: 'freigegeben', widthGrow: 1, formatter: this.freigabeFormatter, variableHeight: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4zeugnisnote'))), field: 'note', formatter: this.notenFormatter, topCalc: this.negativeNotenCalc, @@ -583,7 +622,7 @@ export const Benotungstool = { headerFilterFunc: this.notenFilterFunc, widthGrow: 1 }, - {title: Vue.computed(() => this.$p.t('benotungstool/c4kommPruef')), + {title: Vue.computed(() => this.$capitalize(this.$p.t('benotungstool/c4kommPruef'))), field: 'kommPruef', widthGrow: 1, formatter: this.pruefungFormatter, topCalc: this.terminCalcFunc, @@ -643,11 +682,11 @@ export const Benotungstool = { }, terminCalcFormatter(cell) { const cellval = cell.getValue() - return this.$p.t('benotungstool/prueflingSelectionv2')+': ' + cellval + return this.$capitalize(this.$p.t('benotungstool/prueflingSelectionv2'))+': ' + cellval }, negativeNotenCalcFormatter(cell) { const cellval = cell.getValue() - return this.$p.t('benotungstool/c4negativ')+': ' + cellval + return this.$capitalize(this.$p.t('benotungstool/c4negativ'))+': ' + cellval }, negativeNotenCalc(entries) { return entries.reduce((acc, cur) => { @@ -673,18 +712,18 @@ export const Benotungstool = { } // specific searchterm cases - if(filterVal === this.$p.t('benotungstool/c4positiv')) { + if(filterVal === this.$capitalize(this.$p.t('benotungstool/c4positiv'))) { // option of the rowValue const valOpt = this.notenOptions.find(opt => opt.note == rowVal) if(!valOpt) return false return valOpt.positiv } - if(filterVal === this.$p.t('benotungstool/c4negativ')) { + if(filterVal === this.$capitalize(this.$p.t('benotungstool/c4negativ'))) { const valOpt = this.notenOptions.find(opt => opt.note == rowVal) if(!valOpt) return false return !valOpt.positiv } - if(filterVal === this.$p.t('benotungstool/c4noteEmpty') && rowVal === null) { + if(filterVal === this.$capitalize(this.$p.t('benotungstool/c4noteEmpty')) && rowVal === null) { return true } @@ -771,6 +810,9 @@ export const Benotungstool = { }).finally(()=>this.loading = false) + }, + punkteFormatter(cell) { + }, teilnotenFormatter(cell) { const val = cell.getValue() @@ -871,7 +913,7 @@ export const Benotungstool = { // Third column (button) const button = document.createElement('button'); button.className = 'btn btn-outline-secondary'; - button.textContent = this.$p.t('benotungstool/changePruefungButtonText'); + button.textContent = this.$capitalize(this.$p.t('benotungstool/changePruefungButtonText')); button.addEventListener('click', () => { this.openPruefungModal(data, data[field], field); }); @@ -888,7 +930,7 @@ export const Benotungstool = { const button = document.createElement('button'); button.className = 'btn btn-outline-secondary'; - button.textContent = this.$p.t('benotungstool/addPruefungButtonText'); + button.textContent = this.$capitalize(this.$p.t('benotungstool/addPruefungButtonText')); button.addEventListener('click', () => { this.openPruefungModal(data, null, field) }); @@ -915,8 +957,7 @@ export const Benotungstool = { // newDate.setDate(+pruefungDateParts[2]) // works correctly - const newDate = new Date(+pruefungDateParts[0], +pruefungDateParts[1], +pruefungDateParts[2]) - newDate.setMonth(newDate.getMonth() - 1) + const newDate = new Date(Number(pruefungDateParts[0]), Number(pruefungDateParts[1]) - 1, Number(pruefungDateParts[2])) this.selectedPruefungDate = newDate @@ -926,11 +967,13 @@ export const Benotungstool = { this.selectedPruefungNote = null } + this.selectedPruefungPunkte = this.pruefung?.punkte ?? null + this.$refs.modalContainerPruefung.show() }, pruefungTitleFormatter(cell) { const def = cell.getColumn().getDefinition() - if(def.originalNote) return this.$p.t('benotungstool/c4originalZnote') + if(def.originalNote) return this.$capitalize(this.$p.t('benotungstool/c4originalZnote')) return def.title; }, arrowFormatter(cell) { @@ -1131,10 +1174,13 @@ export const Benotungstool = { this.notenTableOptions.height = window.visualViewport.height - rect.top - 50 this.$refs.notenTable.tabulator.setHeight(this.notenTableOptions.height) }, - setupCreated() { + async setupCreated() { this.loading = true - // fetch cis config regarding gesamtnoteneingabe - this.$api.call(ApiNoten.getCisConfig()).then(res => { + + this.debouncedFetchPunkteForPruefung = debounce(this.fetchNoteForPunktePruefung, 500) + + // fetch cis config regarding gesamtnoteneingabe, needs to be fetched before setup can finish + const configPromise = this.$api.call(ApiNoten.getCisConfig()).then(res => { this.config = res.data }) @@ -1160,9 +1206,11 @@ export const Benotungstool = { }) // fetch noten dropdown - this.$api.call(ApiNoten.getNoten()).then(res => { + this.$api.call(ApiNoten.getNoten()).then(async res => { this.notenOptions = res.data this.notenOptionsLehre = res.data.filter(n => n.lehre === true) + + await configPromise this.notenTableOptions = this.getNotenTableOptions() this.tabulatorCanBeBuilt = true // because promises would be more work and not much better here }).catch(e => { @@ -1254,10 +1302,12 @@ export const Benotungstool = { const typ = this.pruefung ? this.pruefung.pruefungstyp_kurzbz : this.getPruefungstypForStudentByAntritt(this.pruefungStudent) const note = this.selectedPruefungNote?.note ?? 9 // noch nicht eingetragen + // TODO: check if this is supposed to work this way + const punkte = this.selectedPruefungPunkte ?? 0 this.$api.call(ApiNoten.saveStudentPruefung( this.pruefungStudent.uid, note, - this.pruefung?.punkte ?? '', + punkte, dateStr, this.lv_id, this.pruefungStudent.lehreinheit_id, @@ -1268,7 +1318,7 @@ export const Benotungstool = { this.$fhcAlert.alertDefault( 'success', 'Info', - this.$p.t('benotungstool/pruefungSaveForUid', [this.pruefungStudent.uid]), + this.$capitalize(this.$p.t('benotungstool/pruefungSaveForUid', [this.pruefungStudent.uid])), true ) const s = this.studenten.find(s => s.uid === res.data[1]?.student_uid) @@ -1481,7 +1531,7 @@ export const Benotungstool = { if(uidListError != '') { this.$fhcAlert.alertError( - this.$p.t('benotungstool/c4pruefungAnlageError', [dateStrFront]) + ': ' + uidListError + ' ' + this.$capitalize(this.$p.t('benotungstool/c4pruefungAnlageError', [dateStrFront])) + ': ' + uidListError + ' ' ) } @@ -1489,7 +1539,7 @@ export const Benotungstool = { this.$fhcAlert.alertDefault( 'success', 'Info', - this.$p.t('benotungstool/pruefungAngelegtAn', [dateStrFront]) + ': ' + uidListSuccess, + this.$capitalize(this.$p.t('benotungstool/pruefungAngelegtAn', [dateStrFront])) + ': ' + uidListSuccess, true ) @@ -1622,10 +1672,10 @@ export const Benotungstool = { return cs }, getNotenfreigabeHinweistext() { - return this.$p.t('benotungstool/notenfreigabeHinweistextv3') + return this.$capitalize(this.$p.t('benotungstool/notenfreigabeHinweistextv3')) }, getNotenimportHinweistext() { - return this.$p.t('benotungstool/notenimportHinweistextv3') + return this.$capitalize(this.$p.t('benotungstool/notenimportHinweistextv3')) } }, created() { @@ -1636,7 +1686,7 @@ export const Benotungstool = { }, template: ` - + @@ -1654,10 +1704,10 @@ export const Benotungstool = { @hideBsModal="neuesPruefungsdatumModalVisible = false" @showBsModal="neuesPruefungsdatumModalVisible = true" > - +