From f6d2b55f75857d0c99acbfcf35f1c352dbef5e9a Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 16 Mar 2026 11:26:31 +0100 Subject: [PATCH 01/79] redrawTableScrollSave method instead of manual reset --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index bb5c6a710..b7db7e616 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -247,9 +247,23 @@ export const AbgabetoolAssistenz = { ]}; }, methods: { + redrawTableScrollSave() { + const table = this.$refs.abgabeTable.tabulator; + const scrollX = table.rowManager.scrollLeft; + const scrollY = table.rowManager.scrollTop; + this.$refs.abgabeTable.tabulator.redraw(true) + + Vue.nextTick(()=> { + const tableholder = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(tableholder) { + tableholder.scrollLeft = scrollX; + tableholder.scrollTop = scrollY; + } + }) + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) - this.$refs.abgabeTable.tabulator.redraw(true) + this.redrawTableScrollSave() }, getQGateStatusList() { return [ @@ -748,24 +762,9 @@ export const AbgabetoolAssistenz = { pa.abgabetermine.sort((a, b) => new Date(a.datum) - new Date(b.datum)) }) - // reset selection to empty - // this.$refs.abgabeTable.tabulator.deselectRow() - const table = this.$refs.abgabeTable.tabulator; - const scrollX = table.rowManager.scrollLeft; - const scrollY = table.rowManager.scrollTop; - - const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + this.projektarbeiten = this.mapProjekteToTableData(this.projektarbeiten) - table.setData(mappedData) - table.redraw(true) - - Vue.nextTick(()=> { - const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') - if(table) { - table.scrollLeft = scrollX; - table.scrollTop = scrollY; - } - }) + this.redrawTableScrollSave() }).finally(()=>{ this.saving = false @@ -1001,14 +1000,13 @@ export const AbgabetoolAssistenz = { return projekt.zweitbetreuer_full_name ?? '' }, async setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - - this.tableData = this.mapProjekteToTableData(this.projektarbeiten) + + this.projektarbeiten = this.mapProjekteToTableData(data[0]) await this.tableBuiltPromise - - this.$refs.abgabeTable.tabulator.setData(this.tableData); + + this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); }, loadProjektarbeiten(all = false, callback) { this.loading = true From aba4bc29096ae3884518383754411a10720a4e9d Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Thu, 19 Mar 2026 11:12:14 +0100 Subject: [PATCH 02/79] merge projektarbeit details & betreuer form and handle several UX changes regarding formData of both --- .../api/frontend/v1/stv/Projektarbeit.php | 17 +++- public/css/Studentenverwaltung.css | 4 + .../Details/Projektarbeit/Details.js | 8 +- .../Details/Projektarbeit/Projektarbeit.js | 96 ++++++++----------- .../Details/Projektarbeit/Projektbetreuer.js | 1 + 5 files changed, 63 insertions(+), 63 deletions(-) diff --git a/application/controllers/api/frontend/v1/stv/Projektarbeit.php b/application/controllers/api/frontend/v1/stv/Projektarbeit.php index 8740ef3d6..1f1035c98 100644 --- a/application/controllers/api/frontend/v1/stv/Projektarbeit.php +++ b/application/controllers/api/frontend/v1/stv/Projektarbeit.php @@ -90,6 +90,15 @@ class Projektarbeit extends FHCAPI_Controller if (!isset($projektarbeit_id) || !is_numeric($projektarbeit_id)) return $this->terminateWithError('Projektarbeit Id missing', self::ERROR_TYPE_GENERAL); + $result = $this->fetchProjektarbeitByID($projektarbeit_id); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess(current($data)); + } + + private function fetchProjektarbeitById($projektarbeit_id) { + $this->ProjektarbeitModel->resetQuery(); $this->ProjektarbeitModel->addSelect( 'lehre.tbl_projektarbeit.projektarbeit_id, titel, titel_english, themenbereich, projekttyp_kurzbz, lehrveranstaltung_id, lehreinheit_id, firma_id, beginn, ende, gesperrtbis, note, final, freigegeben, tbl_projektarbeit.anmerkung, fa.name AS firma_name' @@ -97,13 +106,10 @@ class Projektarbeit extends FHCAPI_Controller $this->ProjektarbeitModel->addJoin('lehre.tbl_lehreinheit le', 'lehreinheit_id'); $this->ProjektarbeitModel->addJoin('lehre.tbl_lehrveranstaltung lv', 'lehrveranstaltung_id'); $this->ProjektarbeitModel->addJoin('public.tbl_firma fa', 'firma_id', 'LEFT'); - $result = $this->ProjektarbeitModel->loadWhere( + return $this->ProjektarbeitModel->loadWhere( array('projektarbeit_id' => $projektarbeit_id) ); - $data = $this->getDataOrTerminateWithError($result); - - $this->terminateWithSuccess(current($data)); } /** @@ -132,7 +138,8 @@ class Projektarbeit extends FHCAPI_Controller ); $data = $this->getDataOrTerminateWithError($result); - + $data = $this->getDataOrTerminateWithError($this->fetchProjektarbeitById($data)); + $this->terminateWithSuccess($data); } diff --git a/public/css/Studentenverwaltung.css b/public/css/Studentenverwaltung.css index cb4b8f2e9..33d6a77f2 100644 --- a/public/css/Studentenverwaltung.css +++ b/public/css/Studentenverwaltung.css @@ -281,3 +281,7 @@ html.fs_huge { } */ /* slim ende */ + +.fhc-xxl-modal { + min-width: 80vw; +} \ No newline at end of file diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Details.js b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Details.js index 4ff1e236a..ea5a79a9d 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Details.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Details.js @@ -5,6 +5,7 @@ import PvAutoComplete from "../../../../../../../index.ci.php/public/js/componen import ApiStvProjektarbeit from '../../../../../api/factory/stv/projektarbeit.js'; export default { + name: 'ProjektarbeitDetails', components: { FormForm, FormInput, @@ -110,6 +111,10 @@ export default { this.formData.anmerkung = null; this.$refs.formDetails.clearValidation(); }, + setFormData(projektarbeit) { + this.formData = projektarbeit; + if (this.formData.firma_id) this.formData.firma = {firma_id: this.formData.firma_id, name: this.formData.firma_name}; + }, getFormData(newProjektarbeit, studiensemester_kurzbz, additional_lehrveranstaltung_id) { this.additional_lehrveranstaltung_id = additional_lehrveranstaltung_id; @@ -148,8 +153,7 @@ export default { return this.$api .call(ApiStvProjektarbeit.loadProjektarbeit(projektarbeit_id)) .then(result => { - this.formData = result.data; - if (this.formData.firma_id) this.formData.firma = {firma_id: this.formData.firma_id, name: this.formData.firma_name}; + this.setFormData(result.data) return result; }) .catch(this.$fhcAlert.handleSystemError) diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektarbeit.js b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektarbeit.js index 278ca7d16..6a0c4bb98 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektarbeit.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektarbeit.js @@ -9,6 +9,7 @@ import ProjektarbeitDetails from "./Details.js"; import Projektbetreuer from "./Projektbetreuer.js"; export default { + name: 'Projektarbeit', components: { CoreFilterCmpt, BsModal, @@ -213,17 +214,6 @@ export default { }); container.append(button); - button = document.createElement('button'); - button.className = 'btn btn-outline-secondary btn-action'; - button.innerHTML = ''; - button.title = this.$p.t('projektarbeit', 'betreuerBearbeiten'); - button.addEventListener('click', (event) => { - let data = cell.getData(); - this.editedProjektarbeit = data; - this.actionEditBetreuer(); - }); - container.append(button); - button = document.createElement('button'); button.className = 'btn btn-outline-secondary btn-action'; button.innerHTML = ''; @@ -264,6 +254,7 @@ export default { actionEditProjektarbeit() { this.statusNew = false; this.toggleMenu('details'); + this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz); this.$refs.projektarbeitModal.show(); }, actionEditBetreuer() { @@ -280,9 +271,18 @@ export default { .then(this.deleteProjektarbeit) .catch(this.$fhcAlert.handleSystemError); }, + saveProjektarbeit() { + if(this.statusNew) this.addNewProjektarbeit() + else this.updateProjektarbeit() + }, addNewProjektarbeit() { this.$refs.projektarbeitDetails.addNewProjektarbeit() .then((result) => { + if(result?.data?.length) { + this.editedProjektarbeit = result.data[0] + this.$refs.projektarbeitDetails.setFormData(this.editedProjektarbeit) + this.toggleMenu('betreuer'); + } this.projektarbeitSaved(); }) .catch(this.$fhcAlert.handleSystemError); @@ -308,7 +308,8 @@ export default { projektarbeitSaved() { this.reload(); this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); - this.hideModal('projektarbeitModal'); + if(!this.statusNew) this.hideModal('projektarbeitModal'); + else this.statusNew = false }, setDefaultStunden(projekttyp_kurzbz) { this.$refs.projektbetreuer.setDefaultStunden(projekttyp_kurzbz); @@ -321,22 +322,22 @@ export default { }, toggleMenu(tabId) { this.activeTab = tabId; - if (this.statusNew == false) { - switch(tabId) { - case 'details': - this.$refs.projektarbeitDetails.getFormData( - this.statusNew, this.editedProjektarbeit?.studiensemester_kurzbz, this.editedProjektarbeit?.lehrveranstaltung_id - ); - this.$refs.projektarbeitDetails.loadProjektarbeit(this.editedProjektarbeit?.projektarbeit_id); - break; - case 'betreuer': - this.$refs.projektbetreuer.getFormData( - this.editedProjektarbeit ? this.editedProjektarbeit.projekttyp_kurzbz : null - ); - this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz); - break; - } + if (this.statusNew == false && tabId == 'details') { + + this.$refs.projektarbeitDetails.getFormData( + this.statusNew, this.editedProjektarbeit?.studiensemester_kurzbz, this.editedProjektarbeit?.lehrveranstaltung_id + ); + this.$refs.projektarbeitDetails.loadProjektarbeit(this.editedProjektarbeit?.projektarbeit_id); + } else if(tabId == 'betreuer') { + this.$refs.projektbetreuer.getFormData( + this.editedProjektarbeit ? this.editedProjektarbeit.projekttyp_kurzbz : null + ); + this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz); } + }, + resetFormData() { + this.$refs.projektarbeitDetails.resetForm() + this.$refs.projektbetreuer.resetForm() } }, template: ` @@ -358,46 +359,29 @@ export default { - + - - -
-
-
-
- - -
-
+
+
+ +
-
-
-
- -
-
+
+
diff --git a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektbetreuer.js b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektbetreuer.js index a68a06b93..90355b3bd 100644 --- a/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektbetreuer.js +++ b/public/js/components/Stv/Studentenverwaltung/Details/Projektarbeit/Projektbetreuer.js @@ -10,6 +10,7 @@ import Vertrag from "./Vertrag.js"; import ApiStvProjektbetreuer from '../../../../../api/factory/stv/projektbetreuer.js'; export default { + name: 'Projektbetreuer', components: { CoreFilterCmpt, BsModal, From 9b8b1b25326dec1a7f6ec7b1c06e8f21b85191d8 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 20 Mar 2026 12:21:47 +0100 Subject: [PATCH 03/79] cellFormatter & TitleFormatter breakpoints at 100px to display longform or shortform text; phrasenupdate; lefthandside formatting per default; curated tabulator tooltips for complex cells WIP --- public/css/components/abgabetool/abgabe.css | 19 ++ .../Cis/Abgabetool/AbgabetoolAssistenz.js | 165 ++++++++++++++---- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 150 +++++++++++++--- system/phrasesupdate.php | 6 +- 4 files changed, 275 insertions(+), 65 deletions(-) diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index d70481805..7c22d8e7f 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -305,4 +305,23 @@ /* If you use hover rows, you need to ensure the sticky cell matches the hover color */ #abgabetable .tabulator-row:hover .tabulator-cell.sticky-col { background-color: #ccc; /* Match your existing hover color */ +} + +.tabulator-cell { + container-type: inline-size; +} + +.tabulator-col-title { + container-type: inline-size; +} + +@container (max-width: 100px) { + .full-text { + display: none !important; + } + + .short-text { + display: inline-block !important; + width: 100%; + } } \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index bb5c6a710..72b764931 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -53,7 +53,6 @@ export const AbgabetoolAssistenz = { }, data() { return { - tableData: null, studiensemesterOptions: null, allSem: null, curSem: null, @@ -185,7 +184,7 @@ export const AbgabetoolAssistenz = { {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgform'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, @@ -210,20 +209,34 @@ export const AbgabetoolAssistenz = { headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), headerFilter: 'list', headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + field: 'qgate1Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, + widthGrow: 1, width: 220, tooltip: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: 'list', headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + field: 'qgate2Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, + widthGrow: 1, width: 220, tooltip: false}, ], persistence: false, persistenceID: "abgabetool_2026_03_16" @@ -247,6 +260,61 @@ export const AbgabetoolAssistenz = { ]}; }, methods: { + shortLongTitleFormatter(cell, formatterParams, onRendered) { + const longForm = cell.getValue() + const shortForm = formatterParams?.shortForm + + if(longForm && shortForm) { + return ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + toolTipFuncPrevTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle); + }, + toolTipFuncNextTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle); + }, + mapDateStyleToTabulatorTooltip(dateStyleString) { + switch(dateStyleString) { + case 'bestanden': + return this.$p.t('abgabetool/c4tooltipBestanden') + break; + case 'nichtbestanden': + return this.$p.t('abgabetool/c4tooltipNichtBestanden') + break; + case 'beurteilungerforderlich': + return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich') + break; + case 'verspaetet': + return this.$p.t('abgabetool/c4tooltipVerspaetet') + break; + case 'abgegeben': + return this.$p.t('abgabetool/c4tooltipAbgegeben') + break; + case 'verpasst': + return this.$p.t('abgabetool/c4tooltipVerpasst') + break; + case 'abzugeben': + return this.$p.t('abgabetool/c4tooltipAbzugeben') + break; + case 'standard': + return this.$p.t('abgabetool/c4tooltipStandardv2') + break; + default: return '' + } + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) this.$refs.abgabeTable.tabulator.redraw(true) @@ -428,6 +496,32 @@ export const AbgabetoolAssistenz = { projekt.qgate2StatusRank = 1 } }) + + // set shorthand statuscode once real status has been determined + projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank) + projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank) + }, + mapRankToShortStatus(rank) { + switch(rank){ + case 0: // kein termin vorhanden + return '--' + break; + case 1: // noch nicht stattgefunden + return 'o' + break; + case 2: // noch nicht abgegeben + return '?' + break; + case 3: // noch nicht benotet + return '~' + break; + case 4: // negativ benotet + return '-' + break; + case 5: // positiv benotet + return '+' + break; + } }, getItemBezeichnung(item){ if(!item.bezeichnung) return '' @@ -901,37 +995,37 @@ export const AbgabetoolAssistenz = { this.$refs.drawer.show() }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return + const longForm = cell.getValue() + if(!longForm) return + const data = cell.getData() + const entry = Object.entries(data).find(entry => entry[1] == longForm) - return '
' + - '

'+val+'

' + // shortFormKey must have same keyname as longForm but with 'Short' appended + const shortForm = data[entry[0]+'Short'] + + if(shortForm && longForm) { + return `
+ + ${longForm} + + +
`; + } else { + return '
' + + '

'+longForm+'

' + } }, detailFormatter(cell) { - return '
' + + return '
' + '
' }, - mailFormatter(cell) { - const val = cell.getValue() - return '
' + - '
' - }, - timelineFormatter() { - return '
' + - '
' - }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' - }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, abgabterminFormatter(cell) { const val = cell.getValue() @@ -968,14 +1062,14 @@ export const AbgabetoolAssistenz = { const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung return '
' + - '
' + + '
' + icon + '
' + '
' + - '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + + '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + '
'+ '
' - + } else { return '' } @@ -1001,14 +1095,13 @@ export const AbgabetoolAssistenz = { return projekt.zweitbetreuer_full_name ?? '' }, async setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - - this.tableData = this.mapProjekteToTableData(this.projektarbeiten) + + this.projektarbeiten = this.mapProjekteToTableData(data[0]) await this.tableBuiltPromise - this.$refs.abgabeTable.tabulator.setData(this.tableData); + this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); }, loadProjektarbeiten(all = false, callback) { this.loading = true diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 6791d31aa..8f8a224d0 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -43,7 +43,6 @@ export const AbgabetoolMitarbeiter = { }, data() { return { - tableData: null, abgabetypenBetreuer: null, detailIsFullscreen: false, phrasenPromise: null, @@ -153,19 +152,29 @@ export const AbgabetoolMitarbeiter = { headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, + headerFilterParams: { valuesLookup: this.getQGateStatusList }, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, + headerFilterParams: { valuesLookup: this.getQGateStatusList }, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} ], persistence: false, @@ -205,6 +214,61 @@ export const AbgabetoolMitarbeiter = { ]}; }, methods: { + shortLongTitleFormatter(cell, formatterParams, onRendered) { + const longForm = cell.getValue() + const shortForm = formatterParams?.shortForm + + if(longForm && shortForm) { + return ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + toolTipFuncPrevTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle); + }, + toolTipFuncNextTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle); + }, + mapDateStyleToTabulatorTooltip(dateStyleString) { + switch(dateStyleString) { + case 'bestanden': + return this.$p.t('abgabetool/c4tooltipBestanden') + break; + case 'nichtbestanden': + return this.$p.t('abgabetool/c4tooltipNichtBestanden') + break; + case 'beurteilungerforderlich': + return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich') + break; + case 'verspaetet': + return this.$p.t('abgabetool/c4tooltipVerspaetet') + break; + case 'abgegeben': + return this.$p.t('abgabetool/c4tooltipAbgegeben') + break; + case 'verpasst': + return this.$p.t('abgabetool/c4tooltipVerpasst') + break; + case 'abzugeben': + return this.$p.t('abgabetool/c4tooltipAbzugeben') + break; + case 'standard': + return this.$p.t('abgabetool/c4tooltipStandardv2') + break; + default: return '' + } + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) this.$refs.abgabeTable.tabulator.redraw(true) @@ -451,6 +515,32 @@ export const AbgabetoolMitarbeiter = { projekt.qgate2StatusRank = 1 } }) + + // set shorthand statuscode once real status has been determined + projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank) + projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank) + }, + mapRankToShortStatus(rank) { + switch(rank){ + case 0: // kein termin vorhanden + return '--' + break; + case 1: // noch nicht stattgefunden + return 'o' + break; + case 2: // noch nicht abgegeben + return '?' + break; + case 3: // noch nicht benotet + return '~' + break; + case 4: // negativ benotet + return '-' + break; + case 5: // positiv benotet + return '+' + break; + } }, checkAbgabetermineProjektarbeit(projekt) { const now = luxon.DateTime.now() @@ -517,11 +607,11 @@ export const AbgabetoolMitarbeiter = { const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung return '
' + - '
' + - icon + + '
' + + icon + '
' + '
' + - '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + + '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + '
'+ '
' @@ -727,28 +817,37 @@ export const AbgabetoolMitarbeiter = { }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return - - return '
' + - '

'+val+'

' + const longForm = cell.getValue() + if(!longForm) return + const data = cell.getData() + const entry = Object.entries(data).find(entry => entry[1] == longForm) + + // shortFormKey must have same keyname as longForm but with 'Short' appended + const shortForm = data[entry[0]+'Short'] + + if(shortForm && longForm) { + return `
+ + ${longForm} + + +
`; + } else { + return '
' + + '

'+longForm+'

' + } }, detailFormatter(cell) { - return '
' + + return '
' + '
' }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' - }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, tableResolve(resolve) { this.tableBuiltResolve = resolve @@ -765,10 +864,9 @@ export const AbgabetoolMitarbeiter = { setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - this.tableData = data[0]?.retval?.map(projekt => { + this.projektarbeiten = data[0]?.retval?.map(projekt => { this.checkAbgabetermineProjektarbeit(projekt) projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' @@ -789,7 +887,7 @@ export const AbgabetoolMitarbeiter = { }) this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns) - this.$refs.abgabeTable.tabulator.setData(this.tableData); + this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); }, loadProjektarbeiten(all = false, callback) { this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all)) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 3b3b6f995..0903e7fe0 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -43767,18 +43767,18 @@ array( array( 'app' => 'core', 'category' => 'abgabetool', - 'phrase' => 'c4orgform', + 'phrase' => 'c4orgformv2', 'insertvon' => 'system', 'phrases' => array( array( 'sprache' => 'German', - 'text' => "Organisationseinheit", + 'text' => "Organisationsform", 'description' => '', 'insertvon' => 'system' ), array( 'sprache' => 'English', - 'text' => "organization unit", + 'text' => "organization form", 'description' => '', 'insertvon' => 'system' ) From 6ddb064c9efca342a9f7a050593df275fc6a682f Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 23 Mar 2026 10:09:46 +0100 Subject: [PATCH 04/79] qgateStatus tooltips --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 26 +++++++++++++------ .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 14 ++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 72b764931..aa9b80491 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -191,18 +191,18 @@ export const AbgabetoolAssistenz = { formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), @@ -226,7 +226,12 @@ export const AbgabetoolAssistenz = { titleFormatterParams: { shortForm: 'QG1' }, - widthGrow: 1, width: 220, tooltip: false}, + widthGrow: 1, width: 220, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: 'list', headerFilterParams: { valuesLookup: this.getQGateStatusList }, @@ -236,7 +241,12 @@ export const AbgabetoolAssistenz = { titleFormatterParams: { shortForm: 'QG2' }, - widthGrow: 1, width: 220, tooltip: false}, + widthGrow: 1, width: 220, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + }, ], persistence: false, persistenceID: "abgabetool_2026_03_16" diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 8f8a224d0..05feb187f 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -167,7 +167,12 @@ export const AbgabetoolMitarbeiter = { titleFormatterParams: { shortForm: 'QG1' }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: 'list', headerFilterParams: { valuesLookup: this.getQGateStatusList }, @@ -175,7 +180,12 @@ export const AbgabetoolMitarbeiter = { titleFormatterParams: { shortForm: 'QG2' }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} + field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + } ], persistence: false, persistenceID: 'abgabeTableBetreuer2026-02-26' From 079a8e3ec1b3bcf8218a38e96ececb2bd4e5e7db Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 24 Mar 2026 15:19:36 +0100 Subject: [PATCH 05/79] added selected: x | filtered: y | total: z count to assistenz table --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index aa9b80491..6d93946e8 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -53,6 +53,9 @@ export const AbgabetoolAssistenz = { }, data() { return { + count: 0, + filteredcount: 0, + selectedcount: 0, studiensemesterOptions: null, allSem: null, curSem: null, @@ -265,7 +268,12 @@ export const AbgabetoolAssistenz = { }) this.selectedData = data + this.selectedcount = data.length; } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => this.filteredcount = rows.length } ]}; }, @@ -1108,7 +1116,8 @@ export const AbgabetoolAssistenz = { this.domain = data[1] this.projektarbeiten = this.mapProjekteToTableData(data[0]) - + this.count = this.projektarbeiten.length + await this.tableBuiltPromise this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); @@ -1165,6 +1174,17 @@ export const AbgabetoolAssistenz = { }, }, computed: { + countsToHTML() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, emailItems() { const menu = [] @@ -1583,6 +1603,7 @@ export const AbgabetoolAssistenz = { :title="''" @uuidDefined="handleUuidDefined" ref="abgabeTable" + :description="countsToHTML" :newBtnShow="true" :newBtnLabel="$p.t('abgabetool/neueTerminserie')" :newBtnDisabled="!selectedData.length" From 44e43d9479b97519f1d67ab8894dc301b14521c7 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 8 Apr 2026 15:47:17 +0200 Subject: [PATCH 06/79] phrasenkey fix 'c4fehlerAktualitaetProjektarbeit ' => 'c4fehlerAktualitaetProjektarbeit' --- system/phrasesupdate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 0903e7fe0..73bb55b64 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -41761,7 +41761,7 @@ array( array( 'app' => 'core', 'category' => 'abgabetool', - 'phrase' => 'c4fehlerAktualitaetProjektarbeit ', + 'phrase' => 'c4fehlerAktualitaetProjektarbeit', 'insertvon' => 'system', 'phrases' => array( array( From 6868e6814581968b12db09bfdf89274d6c57d8c4 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Thu, 9 Apr 2026 15:42:05 +0200 Subject: [PATCH 07/79] define datepickers to use ISO strings internally with model-type="yyyy-MM-dd" to avoid construction of new js dateobjects when deleting the value via text input, which could lead to a off by one day/timezone error when saving the date --- .../js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 3 +++ public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js | 5 +++-- public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 011e75145..9132bcfba 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -656,6 +656,7 @@ export const AbgabeMitarbeiterDetail = { :enable-time-picker="false" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" :text-input="true" auto-apply> @@ -806,6 +807,7 @@ export const AbgabeMitarbeiterDetail = { :enable-time-picker="false" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" :text-input="true" auto-apply> @@ -874,6 +876,7 @@ export const AbgabeMitarbeiterDetail = { :disabled="true" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" >
diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 6d93946e8..1a6e626bb 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -85,7 +85,7 @@ export const AbgabetoolAssistenz = { allowedNotenOptions: null, notenOptionsNonFinal: null, serienTermin: Vue.reactive({ - datum: new Date(), + datum: new Date().toISOString().split('T')[0], bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -832,7 +832,7 @@ export const AbgabetoolAssistenz = { this.saving = true this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.$api.call(ApiAbgabe.postSerientermin( - this.serienTermin.datum.toISOString(), + this.serienTermin.datum, this.serienTermin.bezeichnung.paabgabetyp_kurzbz, this.serienTermin.bezeichnung.bezeichnung, this.serienTermin.kurzbz, @@ -1385,6 +1385,7 @@ export const AbgabetoolAssistenz = { :clearable="false" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" :enable-time-picker="false" :text-input="true" auto-apply> diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 05feb187f..a2e88854f 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -57,7 +57,7 @@ export const AbgabetoolMitarbeiter = { allowedNotenOptions: null, notenOptionsNonFinal: null, serienTermin: Vue.reactive({ - datum: new Date(), + datum: new Date().toISOString().split('T')[0], bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -712,7 +712,7 @@ export const AbgabetoolMitarbeiter = { addSeries() { this.saving = true this.$api.call(ApiAbgabe.postSerientermin( - this.serienTermin.datum.toISOString(), + this.serienTermin.datum, this.serienTermin.bezeichnung.paabgabetyp_kurzbz, this.serienTermin.bezeichnung.bezeichnung, this.serienTermin.kurzbz, @@ -1043,6 +1043,7 @@ export const AbgabetoolMitarbeiter = { :enable-time-picker="false" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" :text-input="true" auto-apply> From b18ce08578a59b0b74ffe50b699fe51e872abe83 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 27 Apr 2026 11:15:59 +0200 Subject: [PATCH 08/79] c4noZuordnungBetreuerStudent more elaborate statement that maybe the login has expired and to please log in again. happens if getAuthUid returns null but the user is still on the site; --- system/phrasesupdate.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 0476db2c9..24c1f0e7e 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -46909,13 +46909,21 @@ array( 'phrases' => array( array( 'sprache' => 'German', - 'text' => 'Keine Zuordnung oder Berechtigung für die Projektarbeit gefunden!', + 'text' => 'Keine Zuordnung oder Berechtigung für die Projektarbeit gefunden! + + Der eingeloggte User konnte nicht als StudentIn identifiziert werden. + + Falls Sie diese Fehlermeldung als StudentIn erhalten loggen Sie sich bitte neu ein, indem Sie den Browser schließen und erneut die CIS Seite aufrufen!', 'description' => '', 'insertvon' => 'system' ), array( 'sprache' => 'English', - 'text' => 'No assignment or authorization found for the project!', + 'text' => 'No assignment or authorization found for the project! + + The logged-in user could not be identified as a student. + + If you receive this error message as a student, please log in again by closing your browser and reopening the CIS page!', 'description' => '', 'insertvon' => 'system' ) From b16194701c5664b2d3195efd8df07bf33a1825e7 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 27 Apr 2026 16:00:55 +0200 Subject: [PATCH 09/79] c4noZuordnungBetreuerStudent more elaborate statement that maybe the login has expired and to please log in again. happens if getAuthUid returns null but the user is still on the site; error message now also displays the uid values used to check for student zuordnung; removed the $student_uid post/get parameter from every method to isolate the issue; --- .../controllers/api/frontend/v1/Abgabe.php | 370 +++++++++++------- system/phrasesupdate.php | 4 +- 2 files changed, 221 insertions(+), 153 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 43dc18d1c..9bc4c64b8 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -146,23 +146,26 @@ class Abgabe extends FHCAPI_Controller } $data = getData($res)[0]; $student_uid = $data->uid; - - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - $projektarbeitIsCurrent = false; - $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { - $projektarbeitIsCurrent = $result; - }; - Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); - - $ret = $this->ProjektarbeitModel->getProjektarbeitAbgabetermine($projektarbeit_id); - - foreach ($ret->retval as $termin) { - $this->checkAbgabeSignatur($termin, $projektarbeit->student_uid); - } - - $this->terminateWithSuccess(array($ret, $projektarbeitIsCurrent)); + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); } + + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); + + $ret = $this->ProjektarbeitModel->getProjektarbeitAbgabetermine($projektarbeit_id); + + foreach ($ret->retval as $termin) { + $this->checkAbgabeSignatur($termin, $projektarbeit->student_uid); + } + + $this->terminateWithSuccess(array($ret, $projektarbeitIsCurrent)); + } /** @@ -293,55 +296,62 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->post('projektarbeit_id'); $paabgabe_id = $this->input->post('paabgabe_id'); - $student_uid = $this->input->post('student_uid'); - $bperson_id = $this->input->post('bperson_id'); $paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz'); if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' || $paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '' || $paabgabetyp_kurzbz === NULL || trim((string)$paabgabetyp_kurzbz) === '') { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); + } + $path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - $path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - - if ((isset($_FILES) and isset($_FILES['file']) and ! $_FILES['file']['error'])) { - move_uploaded_file($_FILES['file']['tmp_name'], $path); - - if(file_exists($path)) { - - chmod($path, 0640); - - $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); - $res = $this->PaabgabeModel->update($paabgabe_id, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - )); - - $this->logLib->logInfoDB(array('zwischenupload',$res, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - ), getAuthUID(), getAuthPersonId(), $student_uid)); - - $this->terminateWithSuccess($res); - } else { - $this->terminateWithError('Error moving File', 'general'); - } - + if ((isset($_FILES) and isset($_FILES['file']) and ! $_FILES['file']['error'])) { + move_uploaded_file($_FILES['file']['tmp_name'], $path); + + if(file_exists($path)) { + + chmod($path, 0640); + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + $res = $this->PaabgabeModel->update($paabgabe_id, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + )); + + $this->logLib->logInfoDB(array('zwischenupload',$res, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), $student_uid)); + + $this->terminateWithSuccess($res); } else { - $this->terminateWithError('File missing', 'general'); + $this->terminateWithError('Error moving File', 'general'); } + } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError('File missing', 'general'); } } @@ -355,7 +365,6 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->post('projektarbeit_id'); $paabgabe_id = $this->input->post('paabgabe_id'); - $student_uid = $this->input->post('student_uid'); $sprache = $this->input->post('sprache'); $abstract = $this->input->post('abstract'); $abstract_en = $this->input->post('abstract_en'); @@ -367,7 +376,6 @@ class Abgabe extends FHCAPI_Controller if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' || $paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '' || $paabgabetyp_kurzbz === NULL || trim((string)$paabgabetyp_kurzbz) === '' || $abstract === NULL || $abstract_en === NULL || $schlagwoerter === NULL || $schlagwoerter_en === NULL @@ -379,72 +387,84 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - if ((isset($_FILES) and isset($_FILES['file']) and !$_FILES['file']['error'])) { - move_uploaded_file($_FILES['file']['tmp_name'], PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf'); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); + } - if (file_exists(PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf')) { + if ((isset($_FILES) and isset($_FILES['file']) and !$_FILES['file']['error'])) { + move_uploaded_file($_FILES['file']['tmp_name'], PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf'); - $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + if (file_exists(PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf')) { - $result = $this->ProjektarbeitModel->load($projektarbeit_id); - $projektarbeitArr = $this->getDataOrTerminateWithError($result, 'general'); + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - if (count($projektarbeitArr) > 0) { - $projektarbeit = $projektarbeitArr[0]; - } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); - } + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $projektarbeitArr = $this->getDataOrTerminateWithError($result, 'general'); - $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); - $result = $this->PaabgabeModel->load($paabgabe_id); - $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); - - if (count($paabgabeArr) > 0) { - $paabgabe = $paabgabeArr[0]; - } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); - } - - $this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid); - $signaturstatus = $paabgabe->signatur; - - // update projektarbeit cols - $this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id, $sprache, $abstract, $abstract_en - , $schlagwoerter, $schlagwoerter_en, $seitenanzahl); - - - // update paabgabe datum - $res = $this->PaabgabeModel->update($paabgabe_id, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - )); - - $res = $this->PaabgabeModel->load($res->retval); - $abgabe = getData($res)[0]; - $abgabe->signatur = $signaturstatus; - - $this->sendUploadEmail($bperson_id, $projektarbeit_id, $paabgabetyp_kurzbz, $student_uid); - - $this->logLib->logInfoDB(array('endupload', $res, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - ), getAuthUID(), getAuthPersonId(), array($projektarbeit_id, $sprache, $abstract, $abstract_en - , $schlagwoerter, $schlagwoerter_en, $seitenanzahl))); - - $this->terminateWithSuccess($abgabe); + if (count($projektarbeitArr) > 0) { + $projektarbeit = $projektarbeitArr[0]; } else { - $this->terminateWithError('Error moving File', 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); } + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + $result = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); + + if (count($paabgabeArr) > 0) { + $paabgabe = $paabgabeArr[0]; + } else { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); + } + + $this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid); + $signaturstatus = $paabgabe->signatur; + + // update projektarbeit cols + $this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id, $sprache, $abstract, $abstract_en + , $schlagwoerter, $schlagwoerter_en, $seitenanzahl); + + + // update paabgabe datum + $res = $this->PaabgabeModel->update($paabgabe_id, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + )); + + $res = $this->PaabgabeModel->load($res->retval); + $abgabe = getData($res)[0]; + $abgabe->signatur = $signaturstatus; + + $this->sendUploadEmail($bperson_id, $projektarbeit_id, $paabgabetyp_kurzbz, $student_uid); + + $this->logLib->logInfoDB(array('endupload', $res, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), array($projektarbeit_id, $sprache, $abstract, $abstract_en + , $schlagwoerter, $schlagwoerter_en, $seitenanzahl))); + + $this->terminateWithSuccess($abgabe); } else { - $this->terminateWithError('File missing', 'general'); + $this->terminateWithError('Error moving File', 'general'); } + } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError('File missing', 'general'); } } @@ -579,9 +599,22 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); } $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); @@ -699,12 +732,28 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } - $this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id)); + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID()); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); @@ -757,16 +806,27 @@ class Abgabe extends FHCAPI_Controller // and should never unintentionally happen $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - - $res = []; + $abgaben = []; foreach ($projektarbeit_ids as $projektarbeit_id) { $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } $result = $this->PaabgabeModel->insert( @@ -1034,30 +1094,34 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->get('projektarbeit_id'); $paabgabe_id = $this->input->get('paabgabe_id'); - $student_uid = $this->input->get('student_uid'); if ($paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '') { + || $projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '') { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - - // zuordnung function is supposed for mitarbeiter_uids, students should be allowed to download their own files - // without adapting zuordnung logic - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - - - if(file_exists($file_path)) { - $this->terminateWithFileOutput('application/octet-stream', file_get_contents($file_path), basename($file_path)); - } else { - $this->terminateWithError('File not found', 'general'); - } + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)){ + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); + } + + $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; + + if(file_exists($file_path)) { + $this->terminateWithFileOutput('application/octet-stream', file_get_contents($file_path), basename($file_path)); } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError('File not found', 'general'); } } @@ -1101,9 +1165,22 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } // update projektarbeit cols @@ -1329,25 +1406,16 @@ class Abgabe extends FHCAPI_Controller } } - private function checkZuordnung($projektarbeit_id, $betreuer_uid) { + private function checkZuordnung($projektarbeit_id, $auth_uid, $student_uid, $studiengang_kz) { // check if authenticated user is zugewiesen as betreuer to projektarbeit or has admin/assistenz berechtigung // over the studiengang of the student working on that projektarbeit_id - $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); - if(isError($res)) { - $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); - } + // student itself is always considered zugeordnet towards their own projektarbeit + if($auth_uid == $student_uid) return true; - if(!hasData($res)) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); - } - $data = getData($res)[0]; - $student_uid = $data->uid; - $studiengang_kz = $data->studiengang_kz; - - $res = $this->ProjektarbeitModel->checkZuordnung($student_uid, $betreuer_uid); + // if auth_uid is of a betreuer, look for actual betreuungszuordnung between those ids + $res = $this->ProjektarbeitModel->checkZuordnung($student_uid, $auth_uid); if(isError($res)) { $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingBetreuerStudentZuordnung'), 'general'); } @@ -1394,9 +1462,9 @@ class Abgabe extends FHCAPI_Controller return $data->projektarbeit_id; } - private function checkZuordnungByPaabgabe($paabgabe_id, $betreuer_uid) { + private function checkZuordnungByPaabgabe($paabgabe_id, $auth_uid, $student_uid, $studiengang_kz) { $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); - return $this->checkZuordnung($projektarbeit_id, $betreuer_uid); + return $this->checkZuordnung($projektarbeit_id, $auth_uid, $student_uid, $studiengang_kz); } // loads a projektarbeit table row by id and looks if a note has been set. A non null note field diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 24c1f0e7e..3f5956811 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -46911,7 +46911,7 @@ array( 'sprache' => 'German', 'text' => 'Keine Zuordnung oder Berechtigung für die Projektarbeit gefunden! - Der eingeloggte User konnte nicht als StudentIn identifiziert werden. + Der eingeloggte User "{0}" konnte nicht als StudentIn "{1}" identifiziert werden, hat keine Betreuungszuordnung und keine Administrationsrechte. Falls Sie diese Fehlermeldung als StudentIn erhalten loggen Sie sich bitte neu ein, indem Sie den Browser schließen und erneut die CIS Seite aufrufen!', 'description' => '', @@ -46921,7 +46921,7 @@ array( 'sprache' => 'English', 'text' => 'No assignment or authorization found for the project! - The logged-in user could not be identified as a student. + The logged-in user "{0}" could not be identified as student "{1}", has no reviewer assignment and no administrative rights. If you receive this error message as a student, please log in again by closing your browser and reopening the CIS page!', 'description' => '', From f9550c99bb926d9466f3a7f678579d9d3d571082 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 27 Apr 2026 16:13:31 +0200 Subject: [PATCH 10/79] remove newAbgabetermin from SpeedDial Items --- .../components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 9132bcfba..5900875d7 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -46,12 +46,7 @@ export const AbgabeMitarbeiterDetail = { eidAkzeptiert: false, enduploadTermin: null, allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages, - speedDialItems: [{ - label: Vue.computed(() => this.$p.t('abgabetool/c4newAbgabetermin')), - icon: "fa fa-plus", - command: this.openCreateNewAbgabeModal, - disabled: Vue.computed(() => !this.getAllowedToCreateNewTermin) - }, + speedDialItems: [ { label: Vue.computed(() => this.$p.t('abgabetool/c4benoten')), icon: "fa fa-user-check", From c62c3c23f64d90f7a27a6e6112a0f7dd0124132d Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Mon, 27 Apr 2026 16:37:36 +0200 Subject: [PATCH 11/79] minimal column setup by default, added sensible minWidths --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 4bc918ba8..398a163ed 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -182,44 +182,44 @@ export const AbgabetoolAssistenz = { // frozen: true, // width: 40 // }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 100, cssClass: 'sticky-col'}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 130, cssClass: 'sticky-col'}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, minWidth: 140, tooltip: false, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 50, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 50, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, minWidth: 200, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, visible: false, formatter: this.centeredTextFormatter, minWidth: 100, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, tooltip: this.toolTipFuncPrevTermin, - field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, + field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, tooltip: this.toolTipFuncNextTermin, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250}, + formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), headerFilter: 'list', headerFilterParams: { valuesLookup: this.getQGateStatusList }, @@ -229,7 +229,7 @@ export const AbgabetoolAssistenz = { titleFormatterParams: { shortForm: 'QG1' }, - widthGrow: 1, width: 220, + widthGrow: 1, width: 50, tooltip: (e, cell) => { const data = cell.getData(); return data.qgate1Status @@ -244,7 +244,7 @@ export const AbgabetoolAssistenz = { titleFormatterParams: { shortForm: 'QG2' }, - widthGrow: 1, width: 220, + widthGrow: 1, width: 50, tooltip: (e, cell) => { const data = cell.getData(); return data.qgate2Status From f51d0f72c63ff1534fc27d153914fd1587ba200b Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 28 Apr 2026 17:27:19 +0200 Subject: [PATCH 12/79] setSelectedFields() method in filter cmpt to keep custom persistence with visible fields; distinct titles assistenz/betreuer page; fix select logic to respect the possibility of dataset being filtered; beurteilungsnotiz required if note of that termin is negative; multiselect filter on qgate status columns + persistence of those; WIP switching mode to flat table showing all abgabetermine of all projektarbeiten to multi edit/delete/whatever with them selected --- public/css/components/abgabetool/abgabe.css | 5 + .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 17 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 505 +++++++++++++++++- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 2 +- public/js/components/filter/Filter.js | 6 +- system/phrasesupdate.php | 60 +++ 6 files changed, 577 insertions(+), 18 deletions(-) diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index 7c22d8e7f..c7e18f728 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -324,4 +324,9 @@ display: inline-block !important; width: 100%; } +} + +/*conditional tooltips fix*/ +.p-tooltip.custom-tooltip { + z-index: 8001 !important; } \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 5900875d7..3c5855cad 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -76,6 +76,9 @@ export const AbgabeMitarbeiterDetail = { } }, methods: { + terminIsInvalid(termin) { + return termin.note?.positiv == false && !termin.beurteilungsnotiz + }, getNoteBezeichnung(termin){ if(termin.noteBackend?.bezeichnung) { return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌' @@ -556,6 +559,12 @@ export const AbgabeMitarbeiterDetail = { class: "custom-tooltip" } }, + getTooltipBeurteilungsnotiz() { + return { + value: this.$p.t('abgabetool/c4beurteilungsnotizBeiNegNote'), + class: "custom-tooltip" + } + }, getProjektarbeitTitel() { if(this.projektarbeit?.titel) return this.$capitalize(this.$p.t('abgabetool/c4titel')) + ': ' + this.projektarbeit.titel @@ -848,8 +857,10 @@ export const AbgabeMitarbeiterDetail = {
{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}
-
- +
+
@@ -905,7 +916,7 @@ export const AbgabeMitarbeiterDetail = {
- diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 398a163ed..9df53d106 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -53,9 +53,16 @@ export const AbgabetoolAssistenz = { }, data() { return { + mode: 'perProjectView', + qgate1FilterSelected: [], + qgate2FilterSelected: [], count: 0, filteredcount: 0, selectedcount: 0, + countFlat: 0, + filteredcountFlat: 0, + selectedcountFlat: 0, + filteredRows: null, studiensemesterOptions: null, allSem: null, curSem: null, @@ -97,7 +104,9 @@ export const AbgabetoolAssistenz = { }), showAll: false, tabulatorUuid: Vue.ref(0), + tabulatorUuidFlat: Vue.ref(0), selectedData: [], + selectedDataFlat: [], domain: '', student_uid: null, detail: null, @@ -106,6 +115,8 @@ export const AbgabetoolAssistenz = { selectedProjektarbeit: null, tableBuiltResolve: null, tableBuiltPromise: null, + tableBuiltResolveFlat: null, + tableBuiltPromiseFlat: null, abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', @@ -221,8 +232,9 @@ export const AbgabetoolAssistenz = { tooltip: this.toolTipFuncNextTermin, formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, field: 'qgate1Status', formatter: this.centeredTextFormatter, titleFormatter: this.shortLongTitleFormatter, @@ -236,8 +248,9 @@ export const AbgabetoolAssistenz = { } }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, field: 'qgate2Status', formatter: this.centeredTextFormatter, titleFormatter: this.shortLongTitleFormatter, @@ -273,11 +286,299 @@ export const AbgabetoolAssistenz = { }, { event: 'dataFiltered', - handler: (filters, rows) => this.filteredcount = rows.length + handler: (filters, rows) => { + this.filteredRows = rows + this.filteredcount = rows.length + } } - ]}; + ], + abgabeTableOptionsFlat: { + minHeight: 250, + index: 'projektarbeit_id', + layout: 'fitData', + placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), + selectable: true, + rowHeight: 40, + renderVerticalBuffer: 2000, + columns: [ + { + formatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + if(!cell.getRow().getData().selectable) return + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle select manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + cell.getRow().getData().checkbox = checkbox + + let wrapper = document.createElement("div"); + wrapper.style.cssText = "display: flex; justify-content: center; align-items: center; height: 100%; width: 100%;"; + + wrapper.appendChild(checkbox); + + return wrapper; + }, + titleFormatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle "select all" manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + return checkbox; + }, + hozAlign: "center", + headerSort: false, + formatterParams: { + handleClick: this.selectHandlerFlat + }, + titleFormatterParams: { + handleClick: this.selectAllHandlerFlat + }, + width: 50, + cssClass: 'sticky-col' + }, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, minWidth: 140, tooltip: false, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 50, visible: false}, + + // --- termin data --- + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), + field: 'paabgabetyp_kurzbz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 120, widthGrow: 1 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), + field: 'kurzbz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 120, widthGrow: 1 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), + field: 'datum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminCol, + sorter: (a, b) => new Date(a) - new Date(b), + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 120, widthGrow: 1 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabedatum'))), + field: 'abgabedatum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminCol, + sorter: (a, b) => new Date(a) - new Date(b), + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 120, widthGrow: 1 + }, + + // --- status --- + { + title: '', + field: 'dateStyle', + headerSort: false, + formatter: (cell) => this.abgabterminFormatter(cell, true), // icon only mode + width: 48, + tooltip: (e, cell) => this.mapDateStyleToTabulatorTooltip(cell.getValue()) + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), + field: 'note', + formatter: (cell) => { + const val = cell.getValue(); + if (!val) return ''; + return val?.bezeichnung ?? this.notenOptions?.find(n => n.note == val)?.bezeichnung ?? val; + }, + minWidth: 100, widthGrow: 1 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notizQualGatev2'))), + field: 'beurteilungsnotiz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 150, widthGrow: 2, visible: false + }, + + // --- flags --- + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), + field: 'fixtermin', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4upload_allowed'))), + field: 'upload_allowed', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + ], + persistence: false, + persistenceID: "abgabetool_2026_03_16" + }, + abgabeTableEventHandlersFlat: [ + { + event: "rowSelectionChanged", + handler: async(data) => + { + this.selectedDataFlat.filter(sd => !data.includes(sd)).forEach(fsd => { + if(fsd.checkbox) fsd.checkbox.checked = false + }) + + data.forEach(d => { + if(d.checkbox) d.checkbox.checked = true + }) + + this.selectedDataFlat = data + this.selectedcountFlat = data.length; + } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredRowsFlat = rows + this.filteredcountFlat = rows.length + } + } + ] + }; }, methods: { + async switchMode() { + if(this.mode == 'perProjectView') { + this.mode = 'flatView' + + await this.tableBuiltPromiseFlat; + + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine); + } else { + this.mode = 'perProjectView' + } + }, + qgateHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { + + const options = [ + { label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' }, + { label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' }, + { label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' }, + { label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' }, + { label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' }, + { label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' }, + ]; + + const field = cell.getField(); + const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected'; + let selected = [...(this[stateKey] || [])]; // restore persistence state + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; width: 100%;'; + + const display = document.createElement('input'); + display.readOnly = true; + display.placeholder = ''; + display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;'; + + const dropdown = document.createElement('div'); + dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + options.forEach(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.value; + cb.checked = selected.includes(opt.value); // sync with persistence + cb.addEventListener('change', () => { + if (cb.checked) { + selected.push(opt.value); + } else { + selected = selected.filter(v => v !== opt.value); + } + this[stateKey] = [...selected]; // sync with persistence + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + success([...selected]); + }); + + row.appendChild(cb); + row.appendChild(document.createTextNode(opt.label)); + dropdown.appendChild(row); + }); + + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + + display.addEventListener('click', () => { + if (dropdown.style.display === 'none') { + const rect = display.getBoundingClientRect(); + dropdown.style.top = rect.bottom + 'px'; + dropdown.style.left = rect.left + 'px'; + dropdown.style.display = 'block'; + } else { + dropdown.style.display = 'none'; + } + }); + + display.addEventListener('blur', () => { + setTimeout(() => { dropdown.style.display = 'none'; }, 150); + }); + + document.body.appendChild(dropdown); + wrapper.appendChild(display); + + cell.getElement().addEventListener('remove', () => dropdown.remove()); + + onRendered(() => display.focus()); + + return wrapper; + }, + qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + + const matches = (val) => { + switch (val) { + case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet'); + case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet'); + case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded'); + case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted'); + case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet'); + case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden'); + default: return true; + } + }; + + // OR logic — row passes if it matches any selected filter + return filterVal.some(val => matches(val)); + }, redrawTableScrollSave() { const table = this.$refs.abgabeTable.tabulator; const scrollX = table.rowManager.scrollLeft; @@ -451,11 +752,11 @@ export const AbgabetoolAssistenz = { }, selectAllHandler(e, cell) { const table = cell.getTable(); - const rows = table.getRows(); + const rows = this.filteredRows ?? table.getRows(); // custom select all logic const allowed = rows.filter(r => r.getData().selectable); - const selected = allowed.every(r => r.isSelected()); + const selected = table.getRows().every(r => r.isSelected()); if(selected){ allowed.forEach(r => r.deselect()); @@ -714,6 +1015,8 @@ export const AbgabetoolAssistenz = { if (saved?.headerFilters && !this.headerFiltersRestored) { this.headerFiltersRestored = true // instantly avoid retriggers for (let hf of saved.headerFilters) { + if (hf.field === 'qgate1Status') this.qgate1FilterSelected = hf.value || []; + if (hf.field === 'qgate2Status') this.qgate2FilterSelected = hf.value || []; table.setHeaderFilterValue(hf.field, hf.value); } } @@ -734,6 +1037,99 @@ export const AbgabetoolAssistenz = { }, 100); } this.stateRestored = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); + + } + + }); + }, + handleTableBuiltFlat() { + const table = this.$refs.abgabeTableFlat.tabulator + + this.tableBuiltResolveFlat() + + table.on("columnMoved", () => { + this.saveState(table); + }); + + table.on("columnResized", () => { + this.saveState(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveState(table); + }); + + table.on("filterChanged", () => { + this.saveState(table); + }); + + table.on("headerFilterChanged", () => { + this.saveState(table); + }); + + table.on("dataSorted", () => { + this.saveState(table); + }); + + table.on("columnSorted", () => { + this.saveState(table); + }); + + table.on("sortersChanged", () => { + this.saveState(table); + }); + + const saved = this.loadState(); + + table.on("renderComplete", () => { + if(!this.stateRestored) { + + if (saved?.columns && !this.colLayoutRestored) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestored = true; + } + + if (saved?.filters && !this.filtersRestored) { + this.filtersRestored = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestored) { + this.headerFiltersRestored = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestored) { + this.sortRestored = true; + + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + if (!col) { + return null; + } + return { column: col, dir: s.dir }; + }).filter(Boolean); + + table.setSort(sortList); + }, 100); + } + this.stateRestored = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTableFlat.setSelectedFields(); } @@ -768,11 +1164,11 @@ export const AbgabetoolAssistenz = { }, formAction(cell) { const actionButtons = document.createElement('div'); - actionButtons.className = "d-flex gap-3"; // you can keep Bootstrap gap if loaded + actionButtons.className = "d-flex gap-3"; actionButtons.style.display = "flex"; - actionButtons.style.alignItems = "stretch"; // buttons stretch to full height + actionButtons.style.alignItems = "stretch"; actionButtons.style.justifyContent = "start"; - actionButtons.style.height = "100%"; // full grid cell height + actionButtons.style.height = "100%"; const val = cell.getValue(); @@ -1095,6 +1491,9 @@ export const AbgabetoolAssistenz = { tableResolve(resolve) { this.tableBuiltResolve = resolve }, + tableResolveFlat(resolve) { + this.tableBuiltResolveFlat = resolve + }, buildMailToLink(projekt) { return 'mailto:' + projekt.student_uid +'@'+ this.domain }, @@ -1148,6 +1547,9 @@ export const AbgabetoolAssistenz = { handleUuidDefined(uuid) { this.tabulatorUuid = uuid }, + handleUuidDefinedFlat(uuid) { + this.tabulatorUuidFlat = uuid + }, calcMaxTableHeight() { const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' const tableDataSet = document.getElementById('filterTableDataset' + tableID); @@ -1159,6 +1561,7 @@ export const AbgabetoolAssistenz = { }, async setupMounted() { this.tableBuiltPromise = new Promise(this.tableResolve) + this.tableBuiltPromiseFlat = new Promise(this.tableResolveFlat); await this.tableBuiltPromise await this.allConfigPromise @@ -1173,6 +1576,31 @@ export const AbgabetoolAssistenz = { }, }, computed: { + getAllTermine() { + if (!this.projektarbeiten) return []; + return this.projektarbeiten.flatMap(pa => + pa.abgabetermine.map(termin => ({ + ...termin, + student_uid: pa.student_uid, + student_vorname: pa.student_vorname, + student_nachname: pa.student_nachname, + titel: pa.titel, + projektarbeit_id: pa.projektarbeit_id, + stg: pa.stg, + })) + ); + }, + countsToHTMLFlat() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.countFlat || 0) + ''; + }, countsToHTML() { return this.$p.t('global/ausgewaehlt') + ': ' + (this.selectedcount || 0) + '' @@ -1561,7 +1989,7 @@ export const AbgabetoolAssistenz = {
-

{{$p.t('abgabetool/abgabetoolTitle')}}

+

{{$p.t('abgabetool/abgabetoolTitleAdmin')}}

@@ -1599,8 +2027,9 @@ export const AbgabetoolAssistenz = {

+
+ + +
+ +
+ + + +
`, diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index a2e88854f..2b9090d89 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -1110,7 +1110,7 @@ export const AbgabetoolMitarbeiter = {
-

{{$p.t('abgabetool/abgabetoolTitle')}}

+

{{$p.t('abgabetool/abgabetoolTitleBetreuer')}}


col.isVisible()).map(col => col.getField()); + }, async initTabulator() { let placeholder = '< Phrasen Plugin not loaded! >'; if (this.$p) { @@ -337,7 +341,7 @@ export const CoreFilterCmpt = { this.tabulator.on('tableBuilt', () => { const cols = this.tabulator.getColumns(); this.fields = cols.map(col => col.getField()); - this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); + this.setSelectedFields(); if (this.tabulator.options.persistence.headerFilter) this._setHeaderFilter(); }); diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index d0e23da66..f260d84fc 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -43544,6 +43544,46 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'abgabetoolTitleBetreuer', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Bachelor-/Masterarbeiten Betreuungen", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Supervision of Bachelor's/Master's theses", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'abgabetoolTitleAdmin', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Bachelor-/Masterarbeiten Administration", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => "Administration of Bachelor's/Master's theses", + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'abgabetool', @@ -44929,6 +44969,26 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4beurteilungsnotizBeiNegNote', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => "Wenn Sie eine negative Note erteilen, müssen Sie eine Beurteilungsnotiz ausfüllen!", + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'If you give a failing grade, you must fill out an evaluation form!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), array( 'app' => 'core', 'category' => 'abgabetool', From b6e1bb06f3339da13c5d9a349b2cf1f204826a85 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 6 May 2026 10:54:18 +0200 Subject: [PATCH 13/79] implemented tabulator localization --- .../controllers/api/frontend/v1/Phrasen.php | 27 ++ public/js/api/factory/phrasen.js | 12 +- .../Cis/Profil/MitarbeiterProfil.js | 33 +- .../Cis/Profil/MitarbeiterViewProfil.js | 27 +- public/js/components/Cis/Profil/Profil.js | 2 +- .../js/components/Cis/Profil/StudentProfil.js | 20 +- public/js/components/filter/Filter.js | 163 +++++++- system/phrasesupdate.php | 382 ++++++++++++++++++ 8 files changed, 603 insertions(+), 63 deletions(-) diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index 7cc652c71..9bf68a989 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -32,6 +32,7 @@ class Phrasen extends FHCAPI_Controller 'setLanguage' => self::PERM_ANONYMOUS, 'getLanguage' => self::PERM_ANONYMOUS, 'getAllLanguages' => self::PERM_ANONYMOUS, + 'getPhrases' => self::PERM_ANONYMOUS ]); $this->load->helper('hlp_language'); @@ -92,4 +93,30 @@ class Phrasen extends FHCAPI_Controller $this->terminateWithSuccess($langs); } + public function getPhrases() + { + $languages = json_decode($this->input->get('languages')); + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $phrasesGroupedByCategory = json_decode($this->input->get('phrasesGroupedByCategory')); + $result = []; + foreach ($languages as $language) { + $phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language); + $result[$language] = $this->getDataOrTerminateWithError($phrases); + } + + $this->terminateWithSuccess($result); + } + } \ No newline at end of file diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index a87e4f4be..127e64826 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -41,5 +41,15 @@ export default { method: 'get', url: '/api/frontend/v1/phrasen/getAllLanguages' }; - } + }, + getPhrases(phrasesGroupedByCategory, languages = []) { + return { + method: 'get', + url: '/api/frontend/v1/phrasen/getPhrases', + params: { + phrasesGroupedByCategory: JSON.stringify(phrasesGroupedByCategory), + languages: JSON.stringify(languages), + }, + }; + }, }; \ No newline at end of file diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 559006a1b..cd40d0741 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -34,7 +34,6 @@ export default { return { showModal: false, editDataFilter: null, - preloadedPhrasen:{}, // tabulator options funktionen_table_options: { persistenceID: "filterTableMaProfilFunktionen", @@ -46,6 +45,7 @@ export default { responsiveLayout: "collapse", responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, + locale: true, columns: [ { title: @@ -59,21 +59,21 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.bezeichnungPhrase), + title: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.organisationseinheitPhrase), + title: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.gueltigVonPhrase), + title: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -84,7 +84,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.preloadedPhrasen.gueltigBisPhrase), + title: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -95,7 +95,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.preloadedPhrasen.wochenstundenPhrase), + title: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -115,6 +115,7 @@ export default { responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}], + locale: true, columns: [ { title: @@ -128,14 +129,14 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.entlehnteBetriebsmittelPhrase), + title: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.inventarnummerPhrase), + title: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -143,7 +144,7 @@ export default { visible: true }, { - title: Vue.computed(() => this.preloadedPhrasen.ausgabedatumPhrase), + title: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -299,18 +300,6 @@ export default { }, created() { - // preload phrasen - this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => { - this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung'); - this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit'); - this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon'); - this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis'); - this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden'); - this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel'); - this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer'); - this.preloadedPhrasen.ausgabedatumPhrase = this.$p.t('profil/ausgabedatum'); - this.preloadedPhrasen.loaded=true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); @@ -440,7 +429,6 @@ export default {
this.$p.t('ui/bezeichnung')), + title: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')), + title: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() => this.$p.t('global/gueltigVon')), + title: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -71,7 +71,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.$p.t('global/gueltigBis')), + title: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -82,7 +82,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: Vue.computed(() => this.$p.t('profil/wochenstunden')), + title: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, @@ -113,9 +113,6 @@ export default { 'data.funktionen'(newVal) { if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal); }, - 'language.value'(newVal) { // reevaluates computed phrasen - if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns) - } }, computed: { getTelefonValue() { @@ -180,16 +177,6 @@ export default { }; }, }, - created(){ - this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => { - this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung'); - this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit'); - this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon'); - this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis'); - this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden'); - this.preloadedPhrasen.loaded = true; - }); - }, template: /*html*/ ` @@ -247,7 +234,7 @@ export default {
- +
diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index da205816d..37f3a8edb 100644 --- a/public/js/components/Cis/Profil/Profil.js +++ b/public/js/components/Cis/Profil/Profil.js @@ -24,7 +24,7 @@ Vue.$collapseFormatter = function (data) { let item2 = document.createElement("div"); item2.classList.add("col-6"); - item.innerHTML = "" + col.title + ""; + item.innerHTML = "placeholder"; item2.innerHTML = col.value ? col.value : "-"; list.appendChild(item); diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index 3f8e8380d..b4debb3c3 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -33,7 +33,6 @@ export default { showModal: false, collapseIconBetriebsmittel: true, editDataFilter: null, - preloadedPhrasen:{}, // tabulator options zutrittsgruppen_table_options: { persistenceID: "filterTableStudentProfilZutrittsgruppen", @@ -43,7 +42,7 @@ export default { minHeight: 200, layout: "fitColumns", columns: [{ - title: Vue.computed(() => this.preloadedPhrasen.zutrittsGruppenPhrase), + title: "profil/zutrittsGruppen", field: "bezeichnung" }], }, @@ -57,6 +56,7 @@ export default { responsiveLayout: "collapse", responsiveLayoutCollapseUseFormatters: false, responsiveLayoutCollapseFormatter: Vue.$collapseFormatter, + locale: true, columns: [ { title: @@ -69,14 +69,14 @@ export default { headerClick: this.collapseFunction, }, { - title: Vue.computed(()=>this.preloadedPhrasen.entlehnteBetriebsmittelPhrase), + title: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: Vue.computed(() =>this.preloadedPhrasen.inventarnummerPhrase) , + title: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -84,7 +84,7 @@ export default { visible: true }, { - title: Vue.computed(() =>this.preloadedPhrasen.ausgabedatum) , + title: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -242,14 +242,6 @@ export default { }, }, created() { - // preload phrasen - this.$p.loadCategory('profil').then(() => { - this.preloadedPhrasen.zutrittsGruppenPhrase = this.$p.t('profil/zutrittsGruppen'); - this.preloadedPhrasen.entlehnteBetriebsmittelPhrase = this.$p.t('profil/entlehnteBetriebsmittel'); - this.preloadedPhrasen.inventarnummerPhrase = this.$p.t('profil/inventarnummer'); - this.preloadedPhrasen.ausgabedatum = this.$p.t('profil/ausgabedatum'); - this.preloadedPhrasen.loaded = true; - }); //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, @@ -379,7 +371,6 @@ export default {
({...el, ...{title: filterTitles[el.name]}})); } }, + watch: { + "language.value": { + handler(newSelectedLanguage) { + if (!this.$props.tabulatorOptions.locale) return; + + this.tabulator.setLocale(newSelectedLanguage); + + if (this.$props.tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(newSelectedLanguage); + } + }, + }, + }, methods: { reloadTable() { if (this.tableOnly) @@ -263,6 +278,11 @@ export const CoreFilterCmpt = { persistence: this.persistence, }, ...(this.tabulatorOptions || {})}; + // set up localizations if required + if (tabulatorOptions.locale) { + tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions); + } + // set default height if no height property is set if (tabulatorOptions.height === undefined && tabulatorOptions.minHeight === undefined && @@ -302,7 +322,13 @@ export const CoreFilterCmpt = { for (let evt of this.tabulatorEvents) this.tabulator.on(evt.event, evt.handler); } - this.tabulator.on('tableBuilt', () => {this.tableBuilt = true; this.$emit('tableBuilt');}); + this.tabulator.on('tableBuilt', () => { + this.tableBuilt = true; + this.$emit('tableBuilt'); + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(); + } + }); this.tabulator.on("rowSelectionChanged", data => { this.selectedData = data; }); @@ -340,6 +366,9 @@ export const CoreFilterCmpt = { this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField()); if (this.tabulator.options.persistence.headerFilter) this._setHeaderFilter(); + if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { + this.localizeCollapsedColumnHeadings(); + } }); } @@ -349,6 +378,134 @@ export const CoreFilterCmpt = { this.$emit("headerFilterOn", this.filterActive); }); }, + async configureTabulatorLocalizations(tabulatorOptions) { + tabulatorOptions.locale = this.$p.user_language.value; + tabulatorOptions.langs = {}; + + let phrasesGroupedByCategoryRequestParam = { + tabulator: [ + "loading", + "error", + "item", + "items", + "page_size", + "page_title", + "first", + "first_title", + "last", + "last_title", + "prev", + "prev_title", + "next", + "next_title", + "all", + "showing", + "of", + "rows", + "pages", + ], + }; + tabulatorOptions.columns.forEach((column) => { + if (column.field === "collapse") return; + + let [category, phrase] = column.title.split("/"); + if (phrasesGroupedByCategoryRequestParam[category]) { + phrasesGroupedByCategoryRequestParam[category].push(phrase); + } else { + phrasesGroupedByCategoryRequestParam[category] = [phrase]; + } + }); + + const phrasesResponse = await this.$api.call( + ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), + ); + const phrasesData = phrasesResponse.data; + + Object.keys(phrasesData).forEach((language) => { + let genericTabulatorPhrases = phrasesData[language].filter( + (phrase) => phrase.category === "tabulator", + ); + + let tabulatorPhraseToTranslationMapper = {}; + genericTabulatorPhrases.forEach((phrase) => { + tabulatorPhraseToTranslationMapper[phrase.phrase] = + phrase.text; + }); + + tabulatorOptions.langs[language] = { + data: { + loading: tabulatorPhraseToTranslationMapper["loading"], + error: tabulatorPhraseToTranslationMapper["error"], + }, + groups: { + item: tabulatorPhraseToTranslationMapper["item"], + items: tabulatorPhraseToTranslationMapper["items"], + }, + pagination: { + page_size: + tabulatorPhraseToTranslationMapper["page_size"], + page_title: + tabulatorPhraseToTranslationMapper["page_title"], + first: tabulatorPhraseToTranslationMapper["first"], + first_title: + tabulatorPhraseToTranslationMapper["first_title"], + last: tabulatorPhraseToTranslationMapper["last"], + last_title: + tabulatorPhraseToTranslationMapper["last_title"], + prev: tabulatorPhraseToTranslationMapper["prev"], + prev_title: + tabulatorPhraseToTranslationMapper["prev_title"], + next: tabulatorPhraseToTranslationMapper["next"], + next_title: + tabulatorPhraseToTranslationMapper["next_title"], + all: tabulatorPhraseToTranslationMapper["all"], + counter: { + showing: + tabulatorPhraseToTranslationMapper["showing"], + of: tabulatorPhraseToTranslationMapper["of"], + rows: tabulatorPhraseToTranslationMapper["rows"], + pages: tabulatorPhraseToTranslationMapper["pages"], + }, + }, + }; + + let columnHeadingPhrases = phrasesData[language].filter( + (phrase) => phrase.category !== "tabulator", + ); + + let columnTitleToTranslationMapper = {}; + columnHeadingPhrases.forEach((phrase) => { + columnTitleToTranslationMapper[ + phrase.category + "/" + phrase.phrase + ] = phrase.text; + }); + + let columnFieldToTranslationMapper = {}; + tabulatorOptions.columns.forEach((column) => { + if (column.field === "collapse") return; + columnFieldToTranslationMapper[column.field] = + columnTitleToTranslationMapper[column.title]; + }); + + tabulatorOptions.langs[language].columns = + columnFieldToTranslationMapper; + }); + + return tabulatorOptions; + }, + async localizeCollapsedColumnHeadings() { + const columnHeadings = this.tabulator.getLang().columns; + + this.$refs.table + .querySelectorAll(".collapsedColumnHeading") + .forEach((collapsedColumnHeadingElement) => { + const field = collapsedColumnHeadingElement.getAttribute("field"); + if (!field?.length) return; + + collapsedColumnHeadingElement.innerHTML = + columnHeadings[field]; + }); + }, updateTabulator() { if (this.tabulator) { if (this.tableBuilt) diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 0476db2c9..fd3de09b8 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -58177,6 +58177,388 @@ I have been informed that I am under no obligation to consent to the transmissio ) ), // ### Phrases Dashboard Admin END + // generic tabulator phrases START + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'loading', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Loading', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'error', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Error', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'item', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'item', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'items', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'items', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_size', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Page Size', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'page_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Show Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'first_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'last_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Last Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Prev', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'prev_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Prev Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'next_title', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next Page', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'all', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'All', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'showing', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Showing', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'of', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'of', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'rows', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'rows', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'tabulator', + 'phrase' => 'pages', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => '???', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'pages', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // generic tabulator phrases END ); From 40a0f6a7e9b684dddf2559b99966db07cad88391 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Wed, 6 May 2026 10:56:24 +0200 Subject: [PATCH 14/79] implemented tabulator localization --- public/js/components/Cis/Profil/MitarbeiterProfil.js | 7 ------- public/js/components/Cis/Profil/StudentProfil.js | 8 +------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index cd40d0741..2332550b5 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -219,10 +219,6 @@ export default { } }); }, - setTableColumnTitles() { // reevaluates computed phrasen - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns) - if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns) - }, datetimeFormatterParams: function() { const params = { inputFormat:"yyyy-MM-dd", @@ -311,9 +307,6 @@ export default { 'data.mittel'(newVal) { if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal); }, - 'language.value'(newVal) { - this.setTableColumnTitles() - } }, template: /*html*/ `
diff --git a/public/js/components/Cis/Profil/StudentProfil.js b/public/js/components/Cis/Profil/StudentProfil.js index b4debb3c3..ee7654b86 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -27,7 +27,7 @@ export default { FetchProfilUpdates, EditProfil, }, - inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"], + inject: ["sortProfilUpdates", "collapseFunction", "isEditable"], data() { return { showModal: false, @@ -245,12 +245,6 @@ export default { //? sorts the profil Updates: pending -> accepted -> rejected this.data.profilUpdates?.sort(this.sortProfilUpdates); }, - watch: { - 'language.value'(newVal) { - if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns) - if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns) - } - }, template: /*html*/ `
Date: Wed, 6 May 2026 13:46:26 +0200 Subject: [PATCH 15/79] improved assistenz subqueries for zweitbetreuer infos by using common table expression instead of 8 subqueries; adapted splitMailsHelper function to take in body parameter to set default email text by parameter; dateStyles adapted so "in 12 days" also applies to termine without uploads; titleEdit modal in student & studentDetail component; send email to relevant assistenzen & projektarbeit betreuer about change from old to new title; 2nd flat table in AbgabetoolAssistenz that provides a list of all projektarbeittermine so it can be filtered & multiselected; multi delete & multi edit on these selected termine; tried to introduce a media query for zoomed in desktop users that shrinks fontsize and tabulator rows/cells; standard assistenz table column definitions have sensible minWidths & most columns are default invisible; fancy multiselect headerfilter on qualitygate 1/2 status column; actually figured out a vue watcher race codnition triggering loadProjektarbeiten twice unnecessarily; added a reload Button in case one observes a faulty reactivity somewhere in the table; fancy multiselect headerfilter on termin status column for flat table; Preselect current Semester & autoapply Filter for it; WIP refine new table & hunt for bugz; WIP working on the exact custom select handler Handling with filtered datasets; fixed root element style on legacy php view for abgabetool in old cis; WIP define more accurate allowed to delete & allowed to Edit conditions for abgabetermine -> currently benotet quality gates can be edited/deleted!!!!! --- .../controllers/api/frontend/v1/Abgabe.php | 339 ++++++++- .../models/education/Projektarbeit_model.php | 288 ++++---- application/views/Cis/Abgabetool.php | 2 +- public/css/components/abgabetool/abgabe.css | 18 +- public/js/api/factory/abgabe.js | 22 + .../Cis/Abgabetool/AbgabeStudentDetail.js | 246 ++++--- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 694 +++++++++++++++--- .../Cis/Abgabetool/AbgabetoolStudent.js | 180 +++-- .../Cis/Abgabetool/getDateStyleClass.js | 9 +- .../Details/Kontaktieren.js | 4 +- public/js/helpers/EmailHelpers.js | 27 +- system/phrasesupdate.php | 140 ++++ 12 files changed, 1535 insertions(+), 434 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 43dc18d1c..d6f3c29a8 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -35,9 +35,12 @@ class Abgabe extends FHCAPI_Controller 'getStudentProjektabgaben' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw', 'basis/abgabe_lektor:rw'), 'postStudentProjektarbeitZwischenabgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'postStudentProjektarbeitEndupload' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), + 'postStudentProjektarbeitTitel' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'getMitarbeiterProjektarbeiten' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'postProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'patchProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'deleteProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'deleteProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'postSerientermin' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'fetchDeadlines' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'getPaAbgabetypen' => self::PERM_LOGGED, @@ -448,7 +451,194 @@ class Abgabe extends FHCAPI_Controller } } - + + /** + * POST METHOD + * allows a student (or assistenz on their behalf) to update the titel of their own projektarbeit. + * blocked once the projektarbeit has been graded (checkProjektarbeitForFinishedStatus). + */ + public function postStudentProjektarbeitTitel() + { + $projektarbeit_id = $this->input->post('projektarbeit_id'); + $titel = $this->input->post('titel'); + + if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' + || $titel === NULL || trim((string)$titel) === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + + // Verify the projektarbeit actually belongs to the supplied student_uid + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); + } + $assignedStudentUid = getData($res)[0]->uid; + + // A student may only update their own title; assistenz is covered by checkZuordnung + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + if (getAuthUID() !== $assignedStudentUid && !$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $data = getData($result); + + $oldTitle = $data[0]->titel ?? ''; + + $result = $this->ProjektarbeitModel->update( + $projektarbeit_id, + array( + 'titel' => trim($titel), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + + $this->logLib->logInfoDB(array( + 'titelUpdate', + array( + 'projektarbeit_id' => $projektarbeit_id, + 'titel' => trim($titel), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), + getAuthUID(), + getAuthPersonId() + )); + + $this->sendTitelChangedEmail( + $projektarbeit_id, + trim($titel), + $oldTitle, + $assignedStudentUid + ); + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $this->terminateWithSuccess($result); + } + + /** + * Notifies all betreuer of a projektarbeit and all assistenzen responsible for its studiengang + * when a student updates the titel of their projektarbeit. + * + * Betreuer retrieval mirrors AbgabetoolJob->notifyBetreuerAboutChangedAbgaben. + * Assistenz retrieval mirrors AbgabetoolJob->notifyAssistenzAboutChangedAbgaben. + * + * @param int $projektarbeit_id + * @param string $new_titel The titel as it was saved + * @param string $student_uid + */ + private function sendTitelChangedEmail($projektarbeit_id, $new_titel, $old_titel, $student_uid) + { + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('person/Person_model', 'PersonModel'); + + $this->load->model('person/Person_model', 'PersonModel'); + $studentNameResult = $this->PersonModel->getFullName($student_uid); + $studentFullName = hasData($studentNameResult) ? getData($studentNameResult) : $student_uid; + + $studentInfoResult = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($studentInfoResult) || !hasData($studentInfoResult)) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: student info not found', $projektarbeit_id)); + return; + } + $studentInfo = getData($studentInfoResult)[0]; + $studiengang_kz = $studentInfo->studiengang_kz; + + $stgResult = $this->StudiengangModel->load($studiengang_kz); + $oe_kurzbz = null; + if (!isError($stgResult) && hasData($stgResult)) { + $oe_kurzbz = getData($stgResult)[0]->oe_kurzbz ?? null; + } + + // build shared mail data + $base_mail_data = array( + 'studentFullName' => $studentFullName, + 'new_titel' => $new_titel, + 'old_titel' => $old_titel + ); + + // notify all betreuer + $betreuerResult = $this->ProjektbetreuerModel->getAllBetreuerOfProjektarbeit($projektarbeit_id); + if (!isError($betreuerResult) && hasData($betreuerResult)) { + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_MITARBEITER'); + + foreach (getData($betreuerResult) as $betreuer) { + $email = $betreuer->uid ? $betreuer->uid . '@' . DOMAIN : ($betreuer->private_email ?? null); + if (!$email) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no email for betreuer', $betreuer->person_id)); + continue; + } + + $anredeResult = $this->ProjektarbeitModel->getProjektbetreuerAnrede($betreuer->person_id); + $anrede = (!isError($anredeResult) && hasData($anredeResult)) ? getData($anredeResult)[0] : null; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => ($anrede->anrede ?? '') === 'Herr' ? 'r' : '', + 'anrede' => $anrede->anrede ?? '', + 'fullFormattedNameString' => $anrede->first ?? $email, + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // notify assistenz for the studiengang OE + if (!$oe_kurzbz) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no oe_kurzbz resolved, skipping assistenz', $studiengang_kz)); + return; + } + + $assistenzResult = $this->OrganisationseinheitModel->getAssistenzForOE($oe_kurzbz); + if (isError($assistenzResult) || !hasData($assistenzResult)) { + return; + } + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_ASSISTENZ'); + + // similar pattern as job uses via the assistenzMap + $sentTo = []; + foreach (getData($assistenzResult) as $assistenz) { + if (in_array($assistenz->person_id, $sentTo)) { + continue; + } + $sentTo[] = $assistenz->person_id; + + $email = $assistenz->uid . '@' . DOMAIN; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => $assistenz->anrede === 'Herr' ? 'r' : '', + 'anrede' => $assistenz->anrede ?? '', + 'fullFormattedNameString' => $assistenz->first ?? ($assistenz->uid . '@' . DOMAIN), + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // validate paabgabe deadline against servertime just in case a student spoofs their local clock and thus // unlocks the upload ui private function checkPaabgabeDeadline($paabgabe_id) { @@ -687,6 +877,99 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithSuccess([$paabgabe, $existingPaabgabe]); } + /** + * called by abgabetool/assistenz when bulk-editing multiple abgabetermine via the flat termine table view + * only fields present in the payload are updated - absent fields are left untouched + */ + public function patchProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + // collect only fields that were actually sent + $updateFields = []; + + $datum = $this->input->post('datum'); + if ($datum !== NULL && trim((string)$datum) !== '') { + $updateFields['datum'] = $datum; + } + + $paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz'); + if ($paabgabetyp_kurzbz !== NULL && trim((string)$paabgabetyp_kurzbz) !== '') { + $updateFields['paabgabetyp_kurzbz'] = $paabgabetyp_kurzbz; + } + + $kurzbz = $this->input->post('kurzbz'); + if ($kurzbz !== NULL) { + $updateFields['kurzbz'] = $kurzbz; + } + + // booleans: only include if explicitly posted + $upload_allowed = $this->input->post('upload_allowed'); + if ($upload_allowed !== NULL) { + $updateFields['upload_allowed'] = filter_var($upload_allowed, FILTER_VALIDATE_BOOLEAN); + } + + $fixtermin = $this->input->post('fixtermin'); + if ($fixtermin !== NULL) { + $updateFields['fixtermin'] = filter_var($fixtermin, FILTER_VALIDATE_BOOLEAN); + } + + if (empty($updateFields)) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->update( + $paabgabe_id, + array_merge($updateFields, [ + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ]) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + $results[] = getData($this->PaabgabeModel->load($paabgabe_id))[0]; + + $this->logLib->logInfoDB(array( + 'paabgabe bulk updated', + $paabgabe_id, + $updateFields, + getAuthUID(), + getAuthPersonId() + )); + } + + $this->terminateWithSuccess($results); + } + /** * called by abgabetool/mitarbeiter in mitarbeiterdetail.js when deleting an abgabetermin * deletion is only possible if user is assistenz OR betreuer deletes their own custom termin @@ -719,11 +1002,55 @@ class Abgabe extends FHCAPI_Controller $result = $this->PaabgabeModel->delete($paabgabe_id); $result = $this->getDataOrTerminateWithError($result, 'general'); - // TODO: consider this in nightly email job $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($result); } + /** + * called by abgabetool/assistenz when deleting multiple abgabetermine via the flat termine table view + */ + public function deleteProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id)); + + $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID()); + + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) == 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->delete($paabgabe_id); + $result = $this->getDataOrTerminateWithError($result, 'general'); + $results[] = $result; + + $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); + } + + $this->terminateWithSuccess($results); + } + /** * endpoint for adding the same paabgabe for multiple projektarbeiten * can be slow for large n since it queries twice per projektarbeit_id @@ -1411,7 +1738,13 @@ class Abgabe extends FHCAPI_Controller $data = getData($res)[0]; if($data->note !== NULL) { - $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general'); + // hardcode this error msg cause phrasen arent reliable and people keep bugging why the cant edit old entries they definitely shouldnt update + $message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'); + if(strpos($message, "<<") === 0) { // phrase could not be loaded + $this->terminateWithError('Die Projektarbeit wurde bereits benotet, Sie dürfen deshalb keine weiteren Termine anlegen oder bearbeiten.', 'general'); + } else { + $this->terminateWithError($message); + } } } diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 3b1ea55e5..ef1759154 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -341,172 +341,130 @@ class Projektarbeit_model extends DB_Model public function getProjektarbeitenForStudiengang($studiengang_kz, $benotet) { - $new_qry = "SELECT DISTINCT ON(tmp.projektarbeit_id) *, campus.get_betreuer_details(tmp.zweitbetreuer_person_id) as zweitbetreuer_full_name, campus.get_betreuer_details(tmp.betreuer_person_id) as erstbetreuer_full_name - FROM( - SELECT - DISTINCT ON(tbl_projektarbeit.projektarbeit_id) - tbl_projektarbeit.projekttyp_kurzbz, - tbl_projektarbeit.titel, - tbl_projektarbeit.projektarbeit_id, - tbl_studiengang.typ, tbl_studiengang.kurzbz, - student_benutzer.uid as student_uid, - student_person.vorname as student_vorname, - student_person.nachname as student_nachname, - tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, - betreuer_benutzer.uid as betreuer_benutzer_uid, - betreuer_person.titelpre as betreuer_titelpre, - betreuer_person.vorname as betreuer_vorname, - betreuer_person.nachname as betreuer_nachname, - betreuer_person.titelpost as betreuer_titelpost, - lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart, - lehre.tbl_projektbetreuer.person_id as betreuer_person_id, - lehre.tbl_projektarbeit.sprache as sprache, - lehre.tbl_projektarbeit.seitenanzahl as seitenanzahl, - lehre.tbl_projektarbeit.kontrollschlagwoerter as kontrollschlagwoerter, - lehre.tbl_projektarbeit.schlagwoerter as schlagwoerter, - lehre.tbl_projektarbeit.schlagwoerter_en as schlagwoerter_en, - lehre.tbl_projektarbeit.abstract as abstract, - lehre.tbl_projektarbeit.abstract_en as abstract_en, - lehre.tbl_projektarbeit.insertamum as insertamum, - lehre.tbl_projektarbeit.note as note, - ( - SELECT orgform_kurzbz - FROM tbl_prestudentstatus - WHERE prestudent_id = (SELECT prestudent_id - FROM tbl_student - WHERE student_uid = student_benutzer.uid - LIMIT 1) - ORDER BY datum DESC, insertamum DESC, ext_id DESC - LIMIT 1 - ) - as organisationsform, - ( - SELECT person_id - FROM lehre.tbl_projektbetreuer - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_person_id, - ( - SELECT betreuerart_kurzbz - FROM lehre.tbl_projektbetreuer - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_betreuerart_kurzbz, - ( - SELECT tbl_betreuerart.beschreibung - FROM lehre.tbl_projektbetreuer - JOIN lehre.tbl_betreuerart USING (betreuerart_kurzbz) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_betreuerart_beschreibung, - ( - SELECT trim(COALESCE(titelpre, '') || ' ' || COALESCE(vorname, '') || ' ' || COALESCE(nachname, '') || ' ' || - COALESCE(titelpost, '')) - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_full_name, - ( - SELECT titelpre - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_titelpre, - ( - SELECT vorname - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_vorname, - ( - SELECT nachname - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_nachname, - ( - SELECT titelpost - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_titelpost, - ( - SELECT - COALESCE(tbl_studienplan.orgform_kurzbz, - tbl_prestudentstatus.orgform_kurzbz, tbl_studiengang.orgform_kurzbz) as - orgform - FROM - public.tbl_prestudent - JOIN public.tbl_prestudentstatus USING(prestudent_id) - JOIN public.tbl_studiensemester USING(studiensemester_kurzbz) - JOIN public.tbl_studiengang USING(studiengang_kz) - LEFT JOIN lehre.tbl_studienplan USING(studienplan_id) - WHERE - prestudent_id=tbl_student.prestudent_id - ORDER BY tbl_prestudentstatus.datum DESC LIMIT 1 - ) as orgform, - (SELECT status_kurzbz FROM public.tbl_prestudentstatus - WHERE prestudent_id=tbl_student.prestudent_id - ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus - FROM lehre.tbl_projektarbeit - LEFT JOIN public.tbl_benutzer student_benutzer ON (student_benutzer.uid = lehre.tbl_projektarbeit.student_uid) - LEFT JOIN public.tbl_person student_person ON (student_benutzer.person_id = student_person.person_id) - LEFT JOIN public.tbl_student on(student_benutzer.uid = public.tbl_student.student_uid) - LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id) - LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) - LEFT JOIN public.tbl_studiengang ON (public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz) - LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) - LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) - LEFT JOIN public.tbl_person betreuer_person ON (betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id) - LEFT JOIN public.tbl_benutzer betreuer_benutzer ON (betreuer_person.person_id = betreuer_benutzer.person_id) - WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom') - AND student_benutzer.aktiv AND ( - lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbegutachter' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Begutachter' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Betreuer' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbetreuer' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Senatsvorsitz' - ) - AND public.tbl_studiengang.studiengang_kz = ?"; + $new_qry = "WITH secondary_betreuer AS ( + SELECT DISTINCT ON (pb.projektarbeit_id) + pb.projektarbeit_id, + pb.person_id AS zweitbetreuer_person_id, + pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz, + ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung, + p.titelpre AS zweitbetreuer_titelpre, + p.vorname AS zweitbetreuer_vorname, + p.nachname AS zweitbetreuer_nachname, + p.titelpost AS zweitbetreuer_titelpost, + trim( + COALESCE(p.titelpre, '') || ' ' || + COALESCE(p.vorname, '') || ' ' || + COALESCE(p.nachname, '') || ' ' || + COALESCE(p.titelpost, '') + ) AS zweitbetreuer_full_name + FROM lehre.tbl_projektbetreuer pb + JOIN public.tbl_person p ON p.person_id = pb.person_id + LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id + LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz + WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}') + ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic + ) - if($benotet == 0) { - $new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL "; - } else if ($benotet == 1) { - $new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL "; - } - - $new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC + SELECT DISTINCT ON (tmp.projektarbeit_id) + *, + campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name, + campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name + FROM ( + SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id) + tbl_projektarbeit.projekttyp_kurzbz, + tbl_projektarbeit.titel, + tbl_projektarbeit.projektarbeit_id, + tbl_studiengang.typ, + tbl_studiengang.kurzbz, + student_benutzer.uid AS student_uid, + student_person.vorname AS student_vorname, + student_person.nachname AS student_nachname, + public.tbl_student.matrikelnr, + tbl_lehreinheit.studiensemester_kurzbz, + betreuer_benutzer.uid AS betreuer_benutzer_uid, + betreuer_person.titelpre AS betreuer_titelpre, + betreuer_person.vorname AS betreuer_vorname, + betreuer_person.nachname AS betreuer_nachname, + betreuer_person.titelpost AS betreuer_titelpost, + lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart, + lehre.tbl_projektbetreuer.person_id AS betreuer_person_id, + lehre.tbl_projektarbeit.sprache, + lehre.tbl_projektarbeit.seitenanzahl, + lehre.tbl_projektarbeit.kontrollschlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter_en, + lehre.tbl_projektarbeit.abstract, + lehre.tbl_projektarbeit.abstract_en, + lehre.tbl_projektarbeit.insertamum, + lehre.tbl_projektarbeit.note, + + sb.zweitbetreuer_person_id, + sb.zweitbetreuer_betreuerart_kurzbz, + sb.zweitbetreuer_betreuerart_beschreibung, + sb.zweitbetreuer_full_name, + sb.zweitbetreuer_titelpre, + sb.zweitbetreuer_vorname, + sb.zweitbetreuer_nachname, + sb.zweitbetreuer_titelpost, + + ( + SELECT orgform_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = ( + SELECT prestudent_id FROM public.tbl_student + WHERE student_uid = student_benutzer.uid LIMIT 1 + ) + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS organisationsform, + ( + SELECT COALESCE(tbl_studienplan.orgform_kurzbz, + tbl_prestudentstatus.orgform_kurzbz, + tbl_studiengang.orgform_kurzbz) + FROM public.tbl_prestudent + JOIN public.tbl_prestudentstatus USING (prestudent_id) + JOIN public.tbl_studiensemester USING (studiensemester_kurzbz) + JOIN public.tbl_studiengang USING (studiengang_kz) + LEFT JOIN lehre.tbl_studienplan USING (studienplan_id) + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY tbl_prestudentstatus.datum DESC + LIMIT 1 + ) AS orgform, + ( + SELECT status_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS studienstatus + + FROM lehre.tbl_projektarbeit + LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid + LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id + LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid + LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id) + LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) + LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz + LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) + LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) + LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id + LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id + LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id -- ← THE NEW LINE + + WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom') + AND student_benutzer.aktiv + AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN ( + 'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz' + ) + AND public.tbl_studiengang.studiengang_kz = ?"; + + if($benotet == 0) { + $new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL "; + } else if ($benotet == 1) { + $new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL "; + } + + $new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC ) as tmp"; return $this->execReadOnlyQuery($new_qry, array($studiengang_kz)); diff --git a/application/views/Cis/Abgabetool.php b/application/views/Cis/Abgabetool.php index 86e8721f2..a0621b1f9 100644 --- a/application/views/Cis/Abgabetool.php +++ b/application/views/Cis/Abgabetool.php @@ -38,7 +38,7 @@ $includesArray = array( $this->load->view('templates/FHC-Header', $includesArray); ?> -
+
uid= student_uid_prop="" stg_kz_prop="" diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index c7e18f728..8b25cbae6 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -329,4 +329,20 @@ /*conditional tooltips fix*/ .p-tooltip.custom-tooltip { z-index: 8001 !important; -} \ No newline at end of file +} + + /* Shrinks font and table rows for desktop users who have zoomed in their browser (150%+). + Does not affect mobile/touchscreen devices, which use touch input instead of a mouse. */ +@media (pointer: fine) and (min-resolution: 1.5dppx) { + + html { + font-size: 0.5rem; + } + + .tabulator-cell, .tabulator-row { + height: 20px; + max-height: 20px; + } + +} + diff --git a/public/js/api/factory/abgabe.js b/public/js/api/factory/abgabe.js index c6f229973..7621b548b 100644 --- a/public/js/api/factory/abgabe.js +++ b/public/js/api/factory/abgabe.js @@ -77,6 +77,13 @@ export default { } }; }, + patchProjektarbeitAbgabeMultiple(payload) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/patchProjektarbeitAbgabeMultiple', + params: payload + }; + }, deleteProjektarbeitAbgabe(paabgabe_id) { return { method: 'post', @@ -84,6 +91,13 @@ export default { params: { paabgabe_id } }; }, + deleteProjektarbeitAbgabeMultiple(paabgabe_ids) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabeMultiple', + params: { paabgabe_ids } + }; + }, postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) { return { method: 'post', @@ -139,6 +153,14 @@ export default { url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben', params: {paabgabe_ids, student_uid}, + }; + }, + postStudentProjektarbeitTitel(projektarbeit_id, titel) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitTitel', + params: {projektarbeit_id, titel}, + }; } }; \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index a7931f307..04664c0ec 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -31,12 +31,14 @@ export const AbgabeStudentDetail = { default: false } }, + emits: ['titel-updated'], data() { return { loading: false, eidAkzeptiert: false, enduploadTermin: null, allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages, + editingTitel: '', form: Vue.reactive({ sprache: '', abstract: '', @@ -49,9 +51,52 @@ export const AbgabeStudentDetail = { } }, methods: { + openTitelEdit() { + this.editingTitel = this.projektarbeit.titel ?? ''; + this.$refs.modalTitelEdit.show(); + }, + async saveTitel() { + const trimmed = this.editingTitel.trim(); + if (!trimmed) { + this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningEmptyField'))); + return; + } + + const confirmed = await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4confirmTitelSpeichern'), + acceptLabel: this.$capitalize(this.$p.t('ui/speichern')), + acceptClass: 'p-button-primary', + rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')), + rejectClass: 'p-button-secondary' + }); + + if (confirmed === false) return; + + this.loading = true; + this.$api.call( + ApiAbgabe.postStudentProjektarbeitTitel( + this.projektarbeit.projektarbeit_id, + trimmed + ) + ).then(res => { + if (res.meta.status === 'success') { + this.projektarbeit.titel = trimmed; + this.$emit('titel-updated', { + projektarbeit_id: this.projektarbeit.projektarbeit_id, + titel: trimmed + }); + this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess'))); + this.$refs.modalTitelEdit.hide(); + } else { + this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError'))); + } + }).finally(() => { + this.loading = false; + }); + }, getNoteBezeichnung(termin){ const noteOpt = this.notenOptions.find(opt => opt.note == termin.note) - + if(noteOpt?.bezeichnung) { return noteOpt?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌' } else if(noteOpt?.benotbar === true && !termin.note) { @@ -65,7 +110,7 @@ export const AbgabeStudentDetail = { this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile'))); return false } - + if(endupload) { if(await this.$fhcAlert.confirm({ message: this.$p.t('abgabetool/confirmEnduploadSpeichern'), @@ -77,16 +122,16 @@ export const AbgabeStudentDetail = { return false } } - + return true; }, async triggerEndupload() { - + if (!await this.validate(this.enduploadTermin, true)) { return false; } - + // post endabgabe const formData = new FormData(); formData.append('paabgabetyp_kurzbz', this.enduploadTermin.paabgabetyp_kurzbz) @@ -94,14 +139,14 @@ export const AbgabeStudentDetail = { formData.append('paabgabe_id', this.enduploadTermin.paabgabe_id) formData.append('student_uid', this.projektarbeit.student_uid) formData.append('bperson_id', this.projektarbeit.bperson_id) - + formData.append('sprache', this.form['sprache'].sprache) formData.append('abstract', this.form['abstract']) formData.append('abstract_en', this.form['abstract_en']) formData.append('schlagwoerter', this.form['schlagwoerter']) formData.append('schlagwoerter_en', this.form['schlagwoerter_en']) formData.append('seitenanzahl', this.form['seitenanzahl']) - + for (let i = 0; i < this.enduploadTermin.file.length; i++) { formData.append('file', this.enduploadTermin.file[i]); } @@ -110,23 +155,21 @@ export const AbgabeStudentDetail = { .then(res => { this.handleUploadRes(res, this.enduploadTermin) }).finally(()=> { - this.loading = false + this.loading = false }) - + this.$refs.modalContainerEnduploadZusatzdaten.hide() }, downloadAbgabe(termin) { const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${termin.paabgabe_id}&student_uid=${this.projektarbeit.student_uid}&projektarbeit_id=${this.projektarbeit.projektarbeit_id}`; window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url) - // this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)) }, formatDate(dateParam) { const date = new Date(dateParam) - // handle missing leading 0 const padZero = (num) => String(num).padStart(2, '0'); - const month = padZero(date.getMonth() + 1); // Months are zero-based + const month = padZero(date.getMonth() + 1); const day = padZero(date.getDate()); const year = date.getFullYear(); @@ -134,14 +177,12 @@ export const AbgabeStudentDetail = { }, async upload(termin) { - // only do this on endupload if (! await this.validate(termin)) { return false; } - + if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') { - // open endupload form modal for further inputs this.enduploadTermin = termin this.$refs.modalContainerEnduploadZusatzdaten.show() } else { @@ -151,7 +192,7 @@ export const AbgabeStudentDetail = { formData.append('paabgabe_id', termin.paabgabe_id) formData.append('student_uid', this.projektarbeit.student_uid) formData.append('bperson_id', this.projektarbeit.bperson_id) - + for (let i = 0; i < termin.file.length; i++) { formData.append('file', termin.file[i]); } @@ -161,7 +202,7 @@ export const AbgabeStudentDetail = { .then(res => { this.handleUploadRes(res, termin) }).finally(()=> { - this.loading = false + this.loading = false }) } }, @@ -169,21 +210,18 @@ export const AbgabeStudentDetail = { if(res.meta.status == "success") { this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3'))) - // update 'abgabedatum' for successful upload -> shows the pdf icon and date once set termin.abgabedatum = new Date().toISOString().split('T')[0]; if(res?.data?.signatur !== undefined) { termin.signatur = res.data.signatur } - + } else { this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3'))) } - + if(res.meta.signaturInfo) { this.$fhcAlert.alertInfo(res.meta.signaturInfo) } - - }, getOptionLabel(option) { return option.sprache @@ -195,7 +233,6 @@ export const AbgabeStudentDetail = { }, watch: { projektarbeit(newVal) { - // default select german if projektarbeit sprache was null this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German') this.form.abstract = newVal.abstract ?? '' this.form.abstract_en = newVal.abstract_en ?? '' @@ -203,15 +240,13 @@ export const AbgabeStudentDetail = { this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.seitenanzahl = newVal.seitenanzahl ?? 1 - } }, computed: { getMoodleLink() { - return this.moodle_link + this.projektarbeit.studiengang_kz + return this.moodle_link + this.projektarbeit.studiengang_kz }, getMessagePtStyle() { - // adjust outer spacing and internal padding to appear similar to doenload button in size return { root: { style: { @@ -244,85 +279,47 @@ export const AbgabeStudentDetail = { }) return qgatefound }, + isTitelEditAllowed() { + // blocked once the projektarbeit has a note (finished) - mirrors backend guard + return !this.isViewMode && !this.projektarbeit?.note; + }, getTooltipVerspaetet() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" } }, getTooltipVerpasst() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), class: "custom-tooltip" } }, getTooltipAbzugeben() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), class: "custom-tooltip" } }, getTooltipStandard() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), class: "custom-tooltip" } }, getTooltipAbgegeben() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), class: "custom-tooltip" } }, getTooltipFixtermin() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), class: "custom-tooltip" } }, getTooltipAbgabeDetected() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), class: "custom-tooltip" } }, getTooltipNotAllowedToUpload() { if(this.isViewMode) { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" } } else { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')), class: "custom-tooltip" } } }, getTooltipBeurteilungerforderlich() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), class: "custom-tooltip" } }, getTooltipBestanden() { - return { - value: this.$p.t('abgabetool/c4tooltipBestanden'), - class: "custom-tooltip" - } + return { value: this.$p.t('abgabetool/c4tooltipBestanden'), class: "custom-tooltip" } }, getTooltipNichtBestanden() { - return { - value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), - class: "custom-tooltip" - } + return { value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), class: "custom-tooltip" } }, - }, - created() { - - }, - mounted() { - }, template: ` @@ -332,9 +329,24 @@ export const AbgabeStudentDetail = {
{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}
-

{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}

-

{{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}

-

{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}

+

{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}

+ +

+ {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}} + +

+ +

{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}

{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }} @@ -357,7 +369,6 @@ export const AbgabeStudentDetail = { -

{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }} @@ -408,8 +419,6 @@ export const AbgabeStudentDetail = {
- -
@@ -481,9 +490,6 @@ export const AbgabeStudentDetail = { {{ $p.t('abgabetool/c4keineSignatur') }} {{ $p.t('abgabetool/c4signaturServerError') }}
- - -
@@ -542,8 +548,49 @@ export const AbgabeStudentDetail = {
{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}
-
- +
+ + + + - `, }; -export default AbgabeStudentDetail; +export default AbgabeStudentDetail; \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 9df53d106..32a641096 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -53,6 +53,7 @@ export const AbgabetoolAssistenz = { }, data() { return { + flatDataDirty: true, mode: 'perProjectView', qgate1FilterSelected: [], qgate2FilterSelected: [], @@ -73,6 +74,11 @@ export const AbgabetoolAssistenz = { colLayoutRestored: false, sortRestored: false, stateRestored: false, + headerFiltersRestoredFlat: false, + filtersRestoredFlat: false, + colLayoutRestoredFlat: false, + sortRestoredFlat: false, + stateRestoredFlat: false, timelineProjekt: null, selectedStudiengangOption: null, studiengaengeOptions: null, @@ -91,6 +97,22 @@ export const AbgabetoolAssistenz = { allowedNotenFilterOptions: null, allowedNotenOptions: null, notenOptionsNonFinal: null, + serienEdit: Vue.reactive({ + datum: null, + bezeichnung: null, + kurzbz: null, + upload_allowed: null, + fixtermin: null, + invertedFixtermin: null, + }), + // track which fields should actually be applied + serienEditFields: { + datum: false, + bezeichnung: false, + kurzbz: false, + upload_allowed: false, + fixtermin: false, + }, serienTermin: Vue.reactive({ datum: new Date().toISOString().split('T')[0], bezeichnung: { @@ -124,7 +146,6 @@ export const AbgabetoolAssistenz = { placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), selectable: true, selectableCheck: this.selectionCheck, - rowHeight: 40, renderVerticalBuffer: 2000, columns: [ { @@ -294,12 +315,12 @@ export const AbgabetoolAssistenz = { ], abgabeTableOptionsFlat: { minHeight: 250, + height: 700, index: 'projektarbeit_id', - layout: 'fitData', + layout: 'fitColumns', placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), selectable: true, - rowHeight: 40, - renderVerticalBuffer: 2000, + renderVerticalBuffer: 400, columns: [ { formatter: function (cell, formatterParams, onRendered) { @@ -347,7 +368,7 @@ export const AbgabetoolAssistenz = { hozAlign: "center", headerSort: false, formatterParams: { - handleClick: this.selectHandlerFlat + handleClick: this.selectHandler }, titleFormatterParams: { handleClick: this.selectAllHandlerFlat @@ -355,26 +376,25 @@ export const AbgabetoolAssistenz = { width: 50, cssClass: 'sticky-col' }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, minWidth: 140, tooltip: false, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, minWidth: 100, visible: true}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 150, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1, minWidth: 50, visible: false}, - - // --- termin data --- + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, tooltip: false, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), field: 'paabgabetyp_kurzbz', headerFilter: true, - formatter: this.centeredTextFormatter, - minWidth: 120, widthGrow: 1 + formatter: this.paabgabetypFormatter, + + minWidth: 120, }, { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), field: 'kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, - minWidth: 120, widthGrow: 1 + minWidth: 120 }, { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), @@ -383,7 +403,7 @@ export const AbgabetoolAssistenz = { headerFilterFunc: this.headerFilterTerminCol, sorter: (a, b) => new Date(a) - new Date(b), formatter: (cell) => this.formatDate(cell.getValue()), - minWidth: 120, widthGrow: 1 + minWidth: 100 }, { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabedatum'))), @@ -392,37 +412,47 @@ export const AbgabetoolAssistenz = { headerFilterFunc: this.headerFilterTerminCol, sorter: (a, b) => new Date(a) - new Date(b), formatter: (cell) => this.formatDate(cell.getValue()), - minWidth: 120, widthGrow: 1 + minWidth: 100 }, - - // --- status --- { - title: '', + title: 'Status', field: 'dateStyle', headerSort: false, - formatter: (cell) => this.abgabterminFormatter(cell, true), // icon only mode - width: 48, + headerFilter: this.statusHeaderFilterEditor, + headerFilterFunc: this.statusHeaderFilterFunc, + headerFilterParams: {}, + formatter: this.abgabterminFormatter, + formatterParams: { iconOnly: true }, + width: 70, tooltip: (e, cell) => this.mapDateStyleToTabulatorTooltip(cell.getValue()) }, { - title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4noteprojektarbeit'))), + field: 'pa_note', + formatter: (cell) => { + const val = cell.getValue(); + if (!val) return ''; + return val?.bezeichnung ?? this.notenOptions?.find(n => n.note == val)?.bezeichnung ?? val; + }, + minWidth: 100 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notetermin'))), field: 'note', formatter: (cell) => { const val = cell.getValue(); if (!val) return ''; return val?.bezeichnung ?? this.notenOptions?.find(n => n.note == val)?.bezeichnung ?? val; }, - minWidth: 100, widthGrow: 1 + minWidth: 100 }, { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notizQualGatev2'))), field: 'beurteilungsnotiz', headerFilter: true, formatter: this.centeredTextFormatter, - minWidth: 150, widthGrow: 2, visible: false + minWidth: 150, visible: false }, - - // --- flags --- { title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), field: 'fixtermin', @@ -443,7 +473,7 @@ export const AbgabetoolAssistenz = { }, ], persistence: false, - persistenceID: "abgabetool_2026_03_16" + persistenceID: "abgabetoolflat_2026_03_16" }, abgabeTableEventHandlersFlat: [ { @@ -473,17 +503,266 @@ export const AbgabetoolAssistenz = { }; }, methods: { + reloadData() { + this.loadProjektarbeiten() + }, + openEditModal() { + // reset + this.serienEditFields = { + datum: false, + bezeichnung: false, + kurzbz: false, + upload_allowed: false, + fixtermin: false, + } + this.serienEdit.datum = new Date().toISOString().split('T')[0] + this.serienEdit.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === 'zwischen') + this.serienEdit.kurzbz = '' + this.serienEdit.upload_allowed = false + this.serienEdit.invertedFixtermin = true + + this.$refs.modalContainerEditSeries.show() + }, + async handleEditSelectedTermine() { + const activeFields = Object.keys(this.serienEditFields).filter(k => this.serienEditFields[k]) + if (!activeFields.length) { + this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4noFieldsSelected')) + return + } + + if (await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4confirm_edit_n_termine', [this.selectedDataFlat.length]), + acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')), + acceptClass: 'p-button-primary', + rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')), + rejectClass: 'p-button-secondary' + }) === false) return + + this.$refs.modalContainerEditSeries.hide() + this.editSelectedTermine(this.selectedDataFlat) + }, + editSelectedTermine(termine) { + const paabgabeIDS = termine.map(t => t.paabgabe_id) + + // only send fields that were checked + const payload = { paabgabe_ids: paabgabeIDS } + if (this.serienEditFields.datum) payload.datum = this.serienEdit.datum + if (this.serienEditFields.bezeichnung) payload.paabgabetyp_kurzbz = this.serienEdit.bezeichnung.paabgabetyp_kurzbz + if (this.serienEditFields.kurzbz) payload.kurzbz = this.serienEdit.kurzbz + if (this.serienEditFields.upload_allowed) payload.upload_allowed = this.serienEdit.upload_allowed + if (this.serienEditFields.fixtermin) payload.fixtermin = !this.serienEdit.invertedFixtermin + + this.saving = true + this.$api.call(ApiAbgabe.patchProjektarbeitAbgabeMultiple(payload)).then(res => { + if (res?.meta?.status == 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('ui/gespeichert')) + + // patch local data structure + termine.forEach(t => { + const pa = this.projektarbeiten.find(pa => pa.projektarbeit_id == t.projektarbeit_id) + const termin = pa.abgabetermine.find(termin => termin.paabgabe_id === t.paabgabe_id) + if (!termin) return + + if (this.serienEditFields.datum) termin.datum = this.serienEdit.datum + if (this.serienEditFields.bezeichnung) termin.paabgabetyp_kurzbz = this.serienEdit.bezeichnung.paabgabetyp_kurzbz + if (this.serienEditFields.kurzbz) termin.kurzbz = this.serienEdit.kurzbz + if (this.serienEditFields.upload_allowed) termin.upload_allowed = this.serienEdit.upload_allowed + if (this.serienEditFields.fixtermin) termin.fixtermin = !this.serienEdit.invertedFixtermin + }) + + const updatedProjektarbeiten = new Set(termine.map(t => t.projektarbeit_id)) + updatedProjektarbeiten.forEach(pa_id => { + const projektarbeit = this.projektarbeiten.find(pa => pa.projektarbeit_id == pa_id) + this.checkAbgabetermineProjektarbeit(projektarbeit) + }) + + this.redrawTableScrollSave() + this.selectedDataFlat = [] + this.selectedcountFlat = 0 + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + + } else if (res?.meta?.status == 'error') { + this.$fhcAlert.alertError() + } + }).finally(() => { + this.saving = false + }) + }, + deleteSelectedTermine(termine) { + const paabgabeIDS = termine.map(t => t.paabgabe_id) + this.$api.call(ApiAbgabe.deleteProjektarbeitAbgabeMultiple(paabgabeIDS)).then( (res) => { + if(res?.meta?.status == 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('ui/genericDeleted', [this.$p.t('abgabetool/c4abgaben_n', [paabgabeIDS.length])])) + + termine.forEach(t => { + const pa = this.projektarbeiten.find(pa => pa.projektarbeit_id == t.projektarbeit_id) + const deletedTerminIndex = pa.abgabetermine.findIndex(termin => t.paabgabe_id === termin.paabgabe_id) + pa.abgabetermine.splice(deletedTerminIndex, 1) + }) + + const updatedProjektarbeiten = new Set(termine.map(t => t.projektarbeit_id)) + + updatedProjektarbeiten.forEach(pa_id => { + const projektarbeit = this.projektarbeiten.find(pa => pa.projektarbeit_id == pa_id) + this.checkAbgabetermineProjektarbeit(projektarbeit) + }) + + this.redrawTableScrollSave() + + // update flat table with fresh computed data and clear selection + this.selectedDataFlat = [] + this.selectedcountFlat = 0 + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + + } else if(res?.meta?.status == 'error'){ + this.$fhcAlert.alertError() + } + }) + }, + async handleDeleteSelectedTermine() { + // TODO: check if every selected termin is actually "allowed to delete" + + + if(await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4confirm_delete_n_termine', [this.selectedDataFlat.length]), + acceptLabel: 'Löschen', + acceptClass: 'p-button-danger', + rejectLabel: 'Zurück', + rejectClass: 'p-button-secondary' + }) === false) { + return false + } else { + this.deleteSelectedTermine(this.selectedDataFlat) + } + + }, async switchMode() { if(this.mode == 'perProjectView') { this.mode = 'flatView' await this.tableBuiltPromiseFlat; - - this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine); + + if(this.flatDataDirty) { + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine); + this.flatDataDirty = false + } + } else { this.mode = 'perProjectView' } }, + getDateStyleHtml(dateStyle) { + const iconMap = { + 'verspaetet': '', + 'verpasst': '', + 'abzugeben': '', + 'standard': '', + 'abgegeben': '', + 'beurteilungerforderlich': '', + 'bestanden': '', + 'nichtbestanden': '', + }; + return iconMap[dateStyle] ?? ''; + }, + statusHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { + const options = [ + { label: this.$p.t('abgabetool/c4positivBenotet'), value: 'bestanden', dateStyle: 'bestanden' }, + { label: this.$p.t('abgabetool/c4negativBenotet'), value: 'nichtbestanden', dateStyle: 'nichtbestanden' }, + { label: this.$p.t('abgabetool/c4tooltipVerspaetet'), value: 'verspaetet', dateStyle: 'verspaetet' }, + { label: this.$p.t('abgabetool/c4tooltipVerpasst'), value: 'verpasst', dateStyle: 'verpasst' }, + { label: this.$p.t('abgabetool/c4tooltipAbzugeben'), value: 'abzugeben', dateStyle: 'abzugeben' }, + { label: this.$p.t('abgabetool/c4tooltipAbgegeben'), value: 'abgegeben', dateStyle: 'abgegeben' }, + { label: this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich'), value: 'beurteilungerforderlich', dateStyle: 'beurteilungerforderlich' }, + { label: this.$p.t('abgabetool/c4tooltipStandardv2'), value: 'standard', dateStyle: 'standard' }, + ]; + + const field = cell.getField(); + const stateKey = field + 'FilterSelected'; // e.g. dateStyleFilterSelected + let selected = [...(this[stateKey] || [])]; + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; width: 100%;'; + + const display = document.createElement('input'); + display.readOnly = true; + display.placeholder = ''; + display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;'; + + const dropdown = document.createElement('div'); + dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 220px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + const updateDisplay = () => { + display.value = options + .filter(o => selected.includes(o.value)) + .map(o => o.label) + .join(', '); + }; + + options.forEach(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 0; cursor: pointer; white-space: nowrap; padding-right: 8px;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.value; + cb.checked = selected.includes(opt.value); + cb.style.cssText = 'margin: 0 6px;'; + cb.addEventListener('change', () => { + selected = cb.checked + ? [...selected, opt.value] + : selected.filter(v => v !== opt.value); + this[stateKey] = [...selected]; + updateDisplay(); + success([...selected]); + }); + + // icon badge — same look as cell + const badge = document.createElement('div'); + badge.className = opt.dateStyle + '-header'; + badge.style.cssText = `min-width: 36px; height: 36px; display: flex; align-items: center; + justify-content: center; flex-shrink: 0; padding: 0px 17px 0px 17px;`; + badge.innerHTML = this.getDateStyleHtml(opt.dateStyle); + + const labelText = document.createElement('span'); + labelText.textContent = opt.label; + labelText.style.cssText = 'margin-left: 6px;'; + + row.appendChild(cb); + row.appendChild(badge); + row.appendChild(labelText); + dropdown.appendChild(row); + }); + + updateDisplay(); + + display.addEventListener('click', () => { + if (dropdown.style.display === 'none') { + const rect = display.getBoundingClientRect(); + dropdown.style.top = rect.bottom + 'px'; + dropdown.style.left = rect.left + 'px'; + dropdown.style.display = 'block'; + } else { + dropdown.style.display = 'none'; + } + }); + + display.addEventListener('blur', () => { + setTimeout(() => { dropdown.style.display = 'none'; }, 150); + }); + + document.body.appendChild(dropdown); + wrapper.appendChild(display); + cell.getElement().addEventListener('remove', () => dropdown.remove()); + onRendered(() => display.focus()); + + return wrapper; + }, + statusHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + // rowVal is the raw dateStyle string on the flat table + return filterVal.some(val => val === rowVal); + }, qgateHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { const options = [ @@ -613,10 +892,12 @@ export const AbgabetoolAssistenz = { }, toolTipFuncPrevTermin(e, cell, onRendered) { const data = cell.getData(); + if(!data.prevTermin) return '' return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle); }, toolTipFuncNextTermin(e, cell, onRendered) { const data = cell.getData(); + if(!data.nextTermin) return '' return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle); }, mapDateStyleToTabulatorTooltip(dateStyleString) { @@ -722,20 +1003,31 @@ export const AbgabetoolAssistenz = { const uniqueRecipients = [...new Set(recipientList)]; const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]); - splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p) }, sammelMailBetreuer(param) { - const recipientList = []; this.selectedData.forEach(row => { if (row.betreuer_mail) recipientList.push(row.betreuer_mail); if (row.zweitbetreuer_mail) recipientList.push(row.zweitbetreuer_mail); }); - // actually not necessary for email clients but looks better for assistenz if we avoid duplicates here const uniqueRecipients = [...new Set(recipientList)]; const subject = this.$p.t('abgabetool/c4sammelmailBetreuerBetreff', [this.selectedStudiengangOption?.bezeichnung]); - splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) + + // dedupe by student_uid, then build one line per student + const seenUids = new Set(); + const bodyLines = []; + this.selectedData.forEach(row => { + if (seenUids.has(row.student_uid)) return; + seenUids.add(row.student_uid); + const name = `${row.student_vorname ?? ''} ${row.student_nachname ?? ''}`.trim(); + const titel = row.titel ? ` - ${row.titel}` : ''; + bodyLines.push(`${name}${titel}`); + }); + + const body = bodyLines.join('\n'); + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, body, this.$fhcAlert, this.$p) }, selectHandler(e, cell) { const row = cell.getRow(); @@ -768,6 +1060,24 @@ export const AbgabetoolAssistenz = { e.stopPropagation(); return false; }, + selectAllHandlerFlat(e, cell) { + const table = cell.getTable(); + const rows = this.filteredRowsFlat ?? table.getRows(); + + // custom select all logic + const allowed = rows.filter(r => r.getData().selectable); + const selected = table.getRows().every(r => r.isSelected()); + + if(selected){ + allowed.forEach(r => r.deselect()); + } else { + allowed.forEach(r => r.select()); + } + + // stop built-in handler + e.stopPropagation(); + return false; + }, checkQualityGateStatus(projekt) { const qgate1Termine = [] const qgate2Termine = [] @@ -994,7 +1304,7 @@ export const AbgabetoolAssistenz = { table.on("renderComplete", () => { if(!this.stateRestored) { - + if (saved?.columns && !this.colLayoutRestored) { const layout = saved.columns.map(col => ({ field: col.field, @@ -1051,43 +1361,43 @@ export const AbgabetoolAssistenz = { this.tableBuiltResolveFlat() table.on("columnMoved", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("columnResized", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("columnVisibilityChanged", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("filterChanged", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("headerFilterChanged", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("dataSorted", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("columnSorted", () => { - this.saveState(table); + this.saveStateFlat(table); }); table.on("sortersChanged", () => { - this.saveState(table); + this.saveStateFlat(table); }); - const saved = this.loadState(); + const saved = this.loadStateFlat(); table.on("renderComplete", () => { - if(!this.stateRestored) { - - if (saved?.columns && !this.colLayoutRestored) { + if(!this.stateRestoredFlat) { + + if (saved?.columns && !this.colLayoutRestoredFlat) { const layout = saved.columns.map(col => ({ field: col.field, width: col.width, @@ -1097,22 +1407,22 @@ export const AbgabetoolAssistenz = { table.setColumnLayout(layout); - this.colLayoutRestored = true; + this.colLayoutRestoredFlat = true; } - if (saved?.filters && !this.filtersRestored) { - this.filtersRestored = true // instantly avoid retriggers + if (saved?.filters && !this.filtersRestoredFlat) { + this.filtersRestoredFlat = true // instantly avoid retriggers table.setFilter(saved.filters); } - if (saved?.headerFilters && !this.headerFiltersRestored) { - this.headerFiltersRestored = true // instantly avoid retriggers + if (saved?.headerFilters && !this.headerFiltersRestoredFlat) { + this.headerFiltersRestoredFlat = true // instantly avoid retriggers for (let hf of saved.headerFilters) { table.setHeaderFilterValue(hf.field, hf.value); } } - if (saved?.sort?.length && !this.sortRestored) { - this.sortRestored = true; + if (saved?.sort?.length && !this.sortRestoredFlat) { + this.sortRestoredFlat = true; setTimeout(() => { const sortList = saved.sort.map(s => { @@ -1126,7 +1436,7 @@ export const AbgabetoolAssistenz = { table.setSort(sortList); }, 100); } - this.stateRestored = true + this.stateRestoredFlat = true // ensure that the filterCollapseables thingy has the correct values this.$refs.abgabeTableFlat.setSelectedFields(); @@ -1135,6 +1445,29 @@ export const AbgabetoolAssistenz = { }); }, + loadStateFlat() { + return JSON.parse(localStorage.getItem(this.abgabeTableOptionsFlat.persistenceID) || "null"); + }, + saveStateFlat(table) { + // avoid storing state after first restore part happened + if(!this.stateRestoredFlat) return + const rawLayout = table.getColumnLayout(); + const state = { + columns: rawLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: table.getSorters().map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: table.getFilters(), + headerFilters: table.getHeaderFilters() + }; + + localStorage.setItem(this.abgabeTableOptionsFlat.persistenceID, JSON.stringify(state)); + }, handleToggleFullscreenDetail() { this.detailIsFullscreen = !this.detailIsFullscreen }, @@ -1273,6 +1606,12 @@ export const AbgabetoolAssistenz = { this.projektarbeiten = this.mapProjekteToTableData(this.projektarbeiten) this.redrawTableScrollSave() + + // in case pesky user creates a series and instantly switches viewmode + this.flatDataDirty = true + if (this.mode === 'flatView') { + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + } }).finally(()=>{ this.saving = false @@ -1407,6 +1746,10 @@ export const AbgabetoolAssistenz = { this.timelineProjekt = projekt this.$refs.drawer.show() }, + paabgabetypFormatter(cell) { + const key = cell.getValue() + return this.$p.t('abgabetool/c4paatyp' + key) + }, centeredTextFormatter(cell) { const longForm = cell.getValue() if(!longForm) return @@ -1440,12 +1783,14 @@ export const AbgabetoolAssistenz = { return '
' + ''+val+'
' }, - abgabterminFormatter(cell) { + abgabterminFormatter(cell, formatterParams, onRendered,) { const val = cell.getValue() - + const dateStyle = val?.dateStyle ?? val + + if(val) { let icon = '' - switch(val.dateStyle) { + switch(dateStyle) { case 'verspaetet': icon = '' break @@ -1474,8 +1819,16 @@ export const AbgabetoolAssistenz = { const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung + if(formatterParams?.iconOnly) { + return '
' + + '
' + + icon + + '
' + + '
' + } + return '
' + - '
' + + '
' + icon + '
' + '
' + @@ -1519,6 +1872,21 @@ export const AbgabetoolAssistenz = { await this.tableBuiltPromise this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); + + // apply preselected semester filter now that table + data are ready + this.semesterChanged({ value: this.curSem }) + + // keep flat table in sync + this.flatDataDirty = true + if (this.mode === 'flatView') { + await this.tableBuiltPromiseFlat + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + this.flatDataDirty = false + } + + // reset flat selection since the underlying data changed entirely + this.selectedDataFlat = [] + this.selectedcountFlat = 0 }, loadProjektarbeiten(all = false, callback) { this.loading = true @@ -1533,7 +1901,7 @@ export const AbgabetoolAssistenz = { callback() } }).finally(()=>{ - this.loading=false + this.loading = false }) }, loadAbgaben(details) { @@ -1556,8 +1924,13 @@ export const AbgabetoolAssistenz = { if(!tableDataSet) return const rect = tableDataSet.getBoundingClientRect(); - this.abgabeTableOptions.height = window.visualViewport.height - rect.top - 80 + + this.abgabeTableOptions.height = Math.round(window.visualViewport.height - rect.top) this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height) + + // same thing for 2nd tabulator which would exceed in size massively + this.abgabeTableOptionsFlat.height = Math.round(window.visualViewport.height - rect.top) + }, async setupMounted() { this.tableBuiltPromise = new Promise(this.tableResolve) @@ -1567,7 +1940,7 @@ export const AbgabetoolAssistenz = { await this.allConfigPromise // called through notenOptionFilter/selectedStudiengangOption watcher on startup - // this.loadProjektarbeiten() + this.loadProjektarbeiten() this.calcMaxTableHeight() }, @@ -1579,15 +1952,27 @@ export const AbgabetoolAssistenz = { getAllTermine() { if (!this.projektarbeiten) return []; return this.projektarbeiten.flatMap(pa => - pa.abgabetermine.map(termin => ({ - ...termin, - student_uid: pa.student_uid, - student_vorname: pa.student_vorname, - student_nachname: pa.student_nachname, - titel: pa.titel, - projektarbeit_id: pa.projektarbeit_id, - stg: pa.stg, - })) + pa.abgabetermine.map(termin => { + // TODO: allowedToDelete prüfung auf terminnote? benotete qgate termine sollten "sicher" sein? + + const allowedToSave = pa.note !== null ? false : true + const allowedToDelete = pa.note !== null ? false : !termin.abgabedatum + return { + allowedToSave, + allowedToDelete, + selectable: allowedToSave || allowedToDelete, + ...termin, + student_uid: pa.student_uid, + student_vorname: pa.student_vorname, + student_nachname: pa.student_nachname, + titel: pa.titel, + pa_note: pa.note, + projektarbeit_id: pa.projektarbeit_id, + stg: pa.stg, + } + } + + ) ); }, countsToHTMLFlat() { @@ -1662,6 +2047,8 @@ export const AbgabetoolAssistenz = { this.serienTermin.upload_allowed = newVal.upload_allowed_default }, selectedStudiengangOption(newVal, oldVal) { + if(this.loading == true) return + // implicitely avoids juggling around promises for created api calls, // since we need note & stg flags for loadProjektarbeiten if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { @@ -1669,6 +2056,8 @@ export const AbgabetoolAssistenz = { } }, notenOptionFilter(newVal) { + if(this.loading == true) return + // that single where clause is worth a decent load time so rather not filter tabulator but just // adapt the qry if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { @@ -1691,7 +2080,12 @@ export const AbgabetoolAssistenz = { const cb = row.getElement().children[0]?.children[0]?.children[0] if(cb) cb.checked = true }) - + }, + projektarbeiten: { + handler() { + this.flatDataDirty = true + }, + deep: true } }, created() { @@ -1737,8 +2131,17 @@ export const AbgabetoolAssistenz = { const res = results[2].value; this.allSem = res.data[0]; const all = { studiensemester_kurzbz: this.$p.t('abgabetool/c4all') }; - this.curSem = all; + this.studiensemesterOptions = [all, ...this.allSem]; + + const currentSemObj = res.data[1]; + + // find the matching entry from studiensemesterOptions so PrimeVue + // can match by reference for the dropdown preselection + const foundRef = this.studiensemesterOptions.find( + s => s.studiensemester_kurzbz === currentSemObj.studiensemester_kurzbz + ) + this.curSem = foundRef ?? all; } // 4. Noten @@ -1780,6 +2183,105 @@ export const AbgabetoolAssistenz = { + + + +