From 1dba4be23f4e7da2e571c64f9dbe42a3c3a39b5b Mon Sep 17 00:00:00 2001 From: ma0048 Date: Mon, 17 Feb 2025 13:31:57 +0100 Subject: [PATCH 001/588] - studsatus ablaeufe einzelen aktivieren/deaktivieren - history absteigend sortiert --- application/config/studierendenantrag.php | 29 ++++++ application/controllers/jobs/AntragJob.php | 77 ++++++++++++---- .../controllers/lehre/Studierendenantrag.php | 89 ++++++++++++------- application/libraries/AntragLib.php | 1 + .../views/lehre/Antrag/Leitung/List.php | 1 + .../components/Studierendenantrag/Leitung.js | 4 +- .../Studierendenantrag/Leitung/Actions.js | 5 +- .../Studierendenantrag/Leitung/Table.js | 2 +- 8 files changed, 156 insertions(+), 52 deletions(-) diff --git a/application/config/studierendenantrag.php b/application/config/studierendenantrag.php index 4e25aef28..ef2098aca 100644 --- a/application/config/studierendenantrag.php +++ b/application/config/studierendenantrag.php @@ -168,3 +168,32 @@ $config['stgkz_blacklist_wiederholung'] = []; * @var array An array of noten ids */ $config['note_blacklist_wiederholung'] = []; + +/** + * Enable/disable the Abmeldung StG process + * + * @var bool + */ +$config['abmeldung_stg_enabled'] = true; + +/** + * Enable/disable the Abmeldung process + * + * @var bool + */ +$config['abmeldung_enabled'] = true; + +/** + * Enable/disable the Unterbrecher process + * + * @var bool + */ +$config['unterbrechung_enabled'] = true; + +/** + * Enable/disable the Wiederholer process + * + * @var bool + */ +$config['wiederholung_enabled'] = true; + diff --git a/application/controllers/jobs/AntragJob.php b/application/controllers/jobs/AntragJob.php index 11b950174..4419997ec 100644 --- a/application/controllers/jobs/AntragJob.php +++ b/application/controllers/jobs/AntragJob.php @@ -40,6 +40,13 @@ class AntragJob extends JOB_Controller */ public function sendStglSammelmail() { + + if ($this->config->item('abmeldung_enabled') !== true && + $this->config->item('abmeldung_stg_enabled') !== true && + $this->config->item('unterbrechung_enabled') !== true && + $this->config->item('wiederholung_enabled') !== true) + return $this->logError('Konnte Job nicht starten: Keine der Configs "abmeldung_stg_enabled", "abmeldung_enabled", "unterbrechung_enabled", "wiederholung_enabled" auf true gesetzt'); + $this->load->model('person/Person_model', 'PersonModel'); $this->logInfo('Start Job sendStglSammelmail'); @@ -47,25 +54,38 @@ class AntragJob extends JOB_Controller $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->StudierendenantragModel->addJoin('public.tbl_prestudent', 'prestudent_id'); - $this->db->group_start(); - $this->db->where('typ', Studierendenantrag_model::TYP_ABMELDUNG); - $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); - $this->db->group_end(); - $this->db->or_group_start(); - $this->db->where('typ', Studierendenantrag_model::TYP_ABMELDUNG_STGL); - $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); - $this->db->group_end(); + if ($this->config->item('abmeldung_enabled') === true) + { + $this->db->group_start(); + $this->db->where('typ', Studierendenantrag_model::TYP_ABMELDUNG); + $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); + $this->db->group_end(); + } - $this->db->or_group_start(); - $this->db->where('typ', Studierendenantrag_model::TYP_UNTERBRECHUNG); - $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); - $this->db->group_end(); + if ($this->config->item('abmeldung_stg_enabled') === true) + { + $this->db->or_group_start(); + $this->db->where('typ', Studierendenantrag_model::TYP_ABMELDUNG_STGL); + $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); + $this->db->group_end(); + } - $this->db->or_group_start(); - $this->db->where('typ', Studierendenantrag_model::TYP_WIEDERHOLUNG); - $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_LVSASSIGNED); - $this->db->group_end(); + if ($this->config->item('unterbrechung_enabled') === true) + { + $this->db->or_group_start(); + $this->db->where('typ', Studierendenantrag_model::TYP_UNTERBRECHUNG); + $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_CREATED); + $this->db->group_end(); + } + + if ($this->config->item('wiederholung_enabled') === true) + { + $this->db->or_group_start(); + $this->db->where('typ', Studierendenantrag_model::TYP_WIEDERHOLUNG); + $this->db->where('campus.get_status_studierendenantrag(studierendenantrag_id)', Studierendenantragstatus_model::STATUS_LVSASSIGNED); + $this->db->group_end(); + } $result = $this->StudierendenantragModel->load(); if(isError($result)) @@ -203,6 +223,10 @@ class AntragJob extends JOB_Controller public function sendReminderWiedereinstieg() { $now = new DateTime(); + + if ($this->config->item('unterbrechung_enabled') !== true) + return $this->logError('Konnte Job nicht starten: Config "unterbrechung_enabled" nicht auf true gesetzt'); + $modifier = $this->config->item('unterbrechung_job_remind_wiedereinstieg_date_modifier'); if (!$modifier) return $this->logError('Konnte Job nicht starten: Config "unterbrechung_job_remind_wiedereinstieg_date_modifiers" nicht gesetzt'); @@ -276,6 +300,13 @@ class AntragJob extends JOB_Controller { $this->logInfo('Start Job handleWiederholerDeadline'); + if ($this->config->item('wiederholung_enabled') !== true) + { + $this->logError('Config "wiederholung_enabled" nicht auf true gesetzt'); + $this->logInfo('Ende Job handleWiederholerDeadline'); + return; + } + $this->load->library('PrestudentLib'); $insertvon = $this->config->item('antrag_job_systemuser'); @@ -394,6 +425,13 @@ class AntragJob extends JOB_Controller { $this->logInfo('Start Job handleAbmeldungenStglDeadline'); + if ($this->config->item('abmeldung_stg_enabled') !== true) + { + $this->logError('Config "abmeldung_stg_enabled" nicht auf true gesetzt'); + $this->logInfo('Ende Job handleAbmeldungenStglDeadline'); + return; + } + $insertvon = $this->config->item('antrag_job_systemuser'); if (!$insertvon) { $this->logError('Config "antrag_job_systemuser" nicht gesetzt'); @@ -527,6 +565,13 @@ class AntragJob extends JOB_Controller { $this->logInfo('Start Job sendAufforderungWiederholer'); + if ($this->config->item('wiederholung_enabled') !== true) + { + $this->logError('Config "wiederholung_enabled" nicht auf true gesetzt'); + $this->logInfo('Ende Job sendAufforderungWiederholer'); + return; + } + $modifier_request_1 = $this->config->item('wiederholung_job_request_1_date_modifier'); $modifier_request_2 = $this->config->item('wiederholung_job_request_2_date_modifier'); $modifier_deadline = $this->config->item('wiederholung_job_deadline_date_modifier'); diff --git a/application/controllers/lehre/Studierendenantrag.php b/application/controllers/lehre/Studierendenantrag.php index 107c9af96..ebc1d2eaa 100644 --- a/application/controllers/lehre/Studierendenantrag.php +++ b/application/controllers/lehre/Studierendenantrag.php @@ -28,7 +28,9 @@ class Studierendenantrag extends FHC_Controller 'studierendenantrag' ]); - if (strtolower($this->router->method) === 'leitung') + $this->load->config('studierendenantrag'); + + if (strtolower($this->router->method) === 'leitung') $this->_isAllowed([ 'leitung' => ['student/studierendenantrag:r', 'student/antragfreigabe:r'] ]); @@ -54,18 +56,27 @@ class Studierendenantrag extends FHC_Controller 'bezeichnungStg' => $antrag->bezeichnung, 'bezeichnungOrgform' => $antrag->orgform ); - - $result = $this->antraglib->getPrestudentWiederholungsBerechtigt($antrag->prestudent_id); - if (getData($result) == 1) - $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Wiederholung'; - $result = $this->antraglib->getPrestudentUnterbrechungsBerechtigt($antrag->prestudent_id); - if (getData($result) == 1) - $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Unterbrechung'; + if ($this->config->item('wiederholung_enabled') === true) + { + $result = $this->antraglib->getPrestudentWiederholungsBerechtigt($antrag->prestudent_id); + if (getData($result) == 1) + $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Wiederholung'; + } - $result = $this->antraglib->getPrestudentAbmeldeBerechtigt($antrag->prestudent_id); - if (getData($result) == 1) - $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Abmeldung'; + if ($this->config->item('unterbrechung_enabled') === true) + { + $result = $this->antraglib->getPrestudentUnterbrechungsBerechtigt($antrag->prestudent_id); + if (getData($result) == 1) + $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Unterbrechung'; + } + + if ($this->config->item('abmeldung_enabled') === true) + { + $result = $this->antraglib->getPrestudentAbmeldeBerechtigt($antrag->prestudent_id); + if (getData($result) == 1) + $prestudentenArr[$antrag->prestudent_id]['allowedNewTypes'][] = 'Abmeldung'; + } } if ($antrag->studierendenantrag_id == null) continue; @@ -88,45 +99,59 @@ class Studierendenantrag extends FHC_Controller $this->load->view('lehre/Antrag/Leitung/List', [ 'stgA' => $stgA, - 'stgL' => $stgL + 'stgL' => $stgL, + 'abmeldung_enabled' => $this->config->item('abmeldung_stg_enabled') ]); } public function abmeldung($prestudent_id, $studierendenantrag_id = null) { - $this->load->view('lehre/Antrag/Create', [ - 'prestudent_id' => $prestudent_id, - 'studierendenantrag_id' => $studierendenantrag_id, - 'antrag_type' => 'Abmeldung' - ]); + if ($this->config->item('abmeldung_enabled') === true) + { + $this->load->view('lehre/Antrag/Create', [ + 'prestudent_id' => $prestudent_id, + 'studierendenantrag_id' => $studierendenantrag_id, + 'antrag_type' => 'Abmeldung' + ]); + } } public function abmeldungstgl($prestudent_id, $studierendenantrag_id = null) { - $this->load->view('lehre/Antrag/Create', [ - 'prestudent_id' => $prestudent_id, - 'studierendenantrag_id' => $studierendenantrag_id, - 'antrag_type' => 'AbmeldungStgl' - ]); + if ($this->config->item('abmeldung_stg_enabled') === true) + { + $this->load->view('lehre/Antrag/Create', [ + 'prestudent_id' => $prestudent_id, + 'studierendenantrag_id' => $studierendenantrag_id, + 'antrag_type' => 'AbmeldungStgl' + ]); + } } public function unterbrechung($prestudent_id, $studierendenantrag_id = null) { - $this->load->view('lehre/Antrag/Create', [ - 'prestudent_id' => $prestudent_id, - 'studierendenantrag_id' => $studierendenantrag_id, - 'antrag_type' => 'Unterbrechung' - ]); + if ($this->config->item('unterbrechung_enabled') === true) + { + $this->load->view('lehre/Antrag/Create', [ + 'prestudent_id' => $prestudent_id, + 'studierendenantrag_id' => $studierendenantrag_id, + 'antrag_type' => 'Unterbrechung' + ]); + } + } public function wiederholung($prestudent_id, $studierendenantrag_id = null) { - $this->load->view('lehre/Antrag/Create', [ - 'prestudent_id' => $prestudent_id, - 'studierendenantrag_id' => $studierendenantrag_id, - 'antrag_type' => 'Wiederholung' - ]); + if ($this->config->item('wiederholung_enabled') === true) + { + $this->load->view('lehre/Antrag/Create', [ + 'prestudent_id' => $prestudent_id, + 'studierendenantrag_id' => $studierendenantrag_id, + 'antrag_type' => 'Wiederholung' + ]); + } } /** diff --git a/application/libraries/AntragLib.php b/application/libraries/AntragLib.php index 885acac90..e476b28d8 100644 --- a/application/libraries/AntragLib.php +++ b/application/libraries/AntragLib.php @@ -1937,6 +1937,7 @@ class AntragLib public function getAntragHistory($antrag_id) { + $this->_ci->StudierendenantragstatusModel->addOrder('insertamum', 'DESC'); $result = $this->_ci->StudierendenantragstatusModel->loadWithTypWhere([ 'studierendenantrag_id' => $antrag_id ]); diff --git a/application/views/lehre/Antrag/Leitung/List.php b/application/views/lehre/Antrag/Leitung/List.php index 1225b16b6..e7d3a47f1 100644 --- a/application/views/lehre/Antrag/Leitung/List.php +++ b/application/views/lehre/Antrag/Leitung/List.php @@ -44,6 +44,7 @@ $this->load->view( diff --git a/public/js/components/Studierendenantrag/Leitung.js b/public/js/components/Studierendenantrag/Leitung.js index 36e3a505d..fd7f5c99a 100644 --- a/public/js/components/Studierendenantrag/Leitung.js +++ b/public/js/components/Studierendenantrag/Leitung.js @@ -15,7 +15,8 @@ export default { }, props: { stgL: Array, - stgA: Array + stgA: Array, + abmeldung_enabled: Boolean }, data() { return { @@ -230,6 +231,7 @@ export default {
- + diff --git a/public/js/components/Studierendenantrag/Leitung/Table.js b/public/js/components/Studierendenantrag/Leitung/Table.js index 8201462b4..a6737ffd7 100644 --- a/public/js/components/Studierendenantrag/Leitung/Table.js +++ b/public/js/components/Studierendenantrag/Leitung/Table.js @@ -53,7 +53,7 @@ export default { return this.$fhcApi.factory .studstatus.leitung.getHistory(this.lastHistoryClickedId) .then(res => { - this.historyData = res.data.sort((a, b) => a.insertamum > b.insertamum); + this.historyData = res.data; }) .catch(this.$fhcApi.handleSystemError); }, From 92a08cebad46d3c0616d336e83f34d44c35dc668 Mon Sep 17 00:00:00 2001 From: ma0048 Date: Mon, 17 Feb 2025 13:47:07 +0100 Subject: [PATCH 002/588] - addOrder ins model verschoben --- application/libraries/AntragLib.php | 1 - application/models/education/Studierendenantragstatus_model.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/application/libraries/AntragLib.php b/application/libraries/AntragLib.php index e476b28d8..885acac90 100644 --- a/application/libraries/AntragLib.php +++ b/application/libraries/AntragLib.php @@ -1937,7 +1937,6 @@ class AntragLib public function getAntragHistory($antrag_id) { - $this->_ci->StudierendenantragstatusModel->addOrder('insertamum', 'DESC'); $result = $this->_ci->StudierendenantragstatusModel->loadWithTypWhere([ 'studierendenantrag_id' => $antrag_id ]); diff --git a/application/models/education/Studierendenantragstatus_model.php b/application/models/education/Studierendenantragstatus_model.php index cf9cce1be..354af7491 100644 --- a/application/models/education/Studierendenantragstatus_model.php +++ b/application/models/education/Studierendenantragstatus_model.php @@ -50,7 +50,7 @@ class Studierendenantragstatus_model extends DB_Model $this->addSelect('bezeichnung[(' . $lang . ')] AS typ'); $this->addJoin('campus.tbl_studierendenantrag_statustyp', 'studierendenantrag_statustyp_kurzbz'); - + $this->addOrder($this->dbTable. '.insertamum', 'DESC'); return $this->loadWhere($where); } From 60e403a28b9e8d9cd5c429b4d06ec027eb8135d2 Mon Sep 17 00:00:00 2001 From: SimonGschnell Date: Mon, 11 Aug 2025 14:18:51 +0200 Subject: [PATCH 003/588] update(Studium Phrases): inserts phrases for the studium component --- .../controllers/api/frontend/v1/Studium.php | 31 +++--- public/js/components/Cis/Mylv/LvUebersicht.js | 2 +- public/js/components/Cis/Studium/Studium.js | 33 +++--- system/phrasesupdate.php | 102 ++++++++++++++++++ 4 files changed, 140 insertions(+), 28 deletions(-) diff --git a/application/controllers/api/frontend/v1/Studium.php b/application/controllers/api/frontend/v1/Studium.php index d17f0c1a1..dc72334a2 100644 --- a/application/controllers/api/frontend/v1/Studium.php +++ b/application/controllers/api/frontend/v1/Studium.php @@ -63,20 +63,21 @@ class Studium extends FHCAPI_Controller if($this->getDataOrTerminateWithError($this->StudentModel->isStudent(getAuthUID()))){ $studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz]); $studentLehrverband = current($this->getDataOrTerminateWithError($studentLehrverband)); - - $student_studiensemester = $studentLehrverband->studiensemester_kurzbz; - $student_studiengang = $studentLehrverband->studiengang_kz; - $student_semester = $studentLehrverband->semester; - $student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id; - - if(!isset($parameter_studiensemester)) - $parameter_studiensemester = $student_studiensemester; - if(!isset($parameter_studiengang)) - $parameter_studiengang = $student_studiengang; - if(!isset($parameter_semester)) - $parameter_semester = $student_semester; - if(!isset($parameter_studienplan)) - $parameter_studienplan = $student_studienplan; + if($studentLehrverband){ + $student_studiensemester = $studentLehrverband->studiensemester_kurzbz; + $student_studiengang = $studentLehrverband->studiengang_kz; + $student_semester = $studentLehrverband->semester; + $student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id; + + if(!isset($parameter_studiensemester)) + $parameter_studiensemester = $student_studiensemester; + if(!isset($parameter_studiengang)) + $parameter_studiengang = $student_studiengang; + if(!isset($parameter_semester)) + $parameter_semester = $student_semester; + if(!isset($parameter_studienplan)) + $parameter_studienplan = $student_studienplan; + } } if(isset($parameter_studiensemester)){ @@ -216,6 +217,8 @@ class Studium extends FHCAPI_Controller $studienplaene = array_map(function($studienplan){ $orgform = current($this->getDataOrTerminateWithError($this->OrgformModel->loadWhere(["orgform_kurzbz" => $studienplan->orgform_kurzbz]))); $studienplan->orgform_bezeichnung = $orgform->bezeichnung; + // bezeichnung_mehrsprachig + $studienplan->orgform_bezeichnung_english = $orgform->bezeichnung_mehrsprachig[1]; return $studienplan; },$studienplaene); return $studienplaene; diff --git a/public/js/components/Cis/Mylv/LvUebersicht.js b/public/js/components/Cis/Mylv/LvUebersicht.js index 1ec93590f..df79275d0 100644 --- a/public/js/components/Cis/Mylv/LvUebersicht.js +++ b/public/js/components/Cis/Mylv/LvUebersicht.js @@ -100,7 +100,7 @@ export default { diff --git a/public/js/components/Cis/Studium/Studium.js b/public/js/components/Cis/Studium/Studium.js index 1b7aff81b..68221af62 100644 --- a/public/js/components/Cis/Studium/Studium.js +++ b/public/js/components/Cis/Studium/Studium.js @@ -195,7 +195,11 @@ export default { }, studiengangTitel(studiengang) { if (!studiengang) return ""; - return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`; + if(this.isGermanLanguage){ + return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`; + }else{ + return `${studiengang?.kurzbzlang} (${studiengang?.english})`; + } }, studiensemesterTitel(studiensemester){ if (!studiensemester) return ""; @@ -213,9 +217,12 @@ export default { }, computed:{ + isGermanLanguage(){ + return this.$p.user_language.value == "German" + }, selectedLehrveranstaltungTitel(){ const studiengang = this.studiengaenge.find((studiengang) => studiengang.studiengang_kz == this.selectedStudiengang); - return `${this.selectedLehrveranstaltung?.bezeichnung} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`; + return `${this.isGermanLanguage ? this.selectedLehrveranstaltung?.bezeichnung : this.selectedLehrveranstaltung?.bezeichnung_english} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`; }, computedStudienOrdnung(){ if(!this.studienOrdnung) return null; @@ -232,14 +239,14 @@ export default { let result = []; Object.entries(this.computedStudienOrdnung).forEach(([key,value])=>{ result.push({ - bezeichnung: `Studienordnung: ${key}`, + bezeichnung: `${this.$p.t('studium', 'studienordnung') }: ${key}`, disabled: true, }); value.forEach((studienplan)=>{ result.push({ studienplan:studienplan, diabled: false, - bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${studienplan?.orgform_bezeichnung}, ${studienplan?.sprache} )` + bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${this.isGermanLanguage ? studienplan?.orgform_bezeichnung : studienplan?.orgform_bezeichnung_english}, ${studienplan?.sprache} )` }); }) @@ -264,14 +271,14 @@ export default { }) }, - template: ` + template: /*html*/`
-

Studium

+

{{$p.t('studium','studium')}}


- -
@@ -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 = { + + + + diff --git a/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js b/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js index fa3ea6618..652b29645 100644 --- a/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js +++ b/public/js/components/Cis/Renderer/Lehreinheit/calendarEvent.js @@ -3,6 +3,11 @@ export default { event: { type: Object, required: true + }, + timeSlotDisplayBehavior: { + type: String, + default: "default", + // options: default, always, never } }, computed:{ @@ -50,7 +55,17 @@ export default { return luxon.Duration .fromISOTime(this.event.ende) .toISOTime({ suppressSeconds: true }); - } + }, + timeSlotDisplayClasses() { + switch (this.$props.timeSlotDisplayBehavior) { + case "always": + return "d-grid"; + case "never": + return "d-none"; + default: + return "d-none d-xl-grid"; + } + }, }, template: /*html*/`
{{ start }} {{ end }} diff --git a/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js b/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js index 2447c4f61..181ef29bd 100644 --- a/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js +++ b/public/js/components/Cis/Renderer/Reservierungen/calendarEvent.js @@ -3,6 +3,11 @@ export default { event: { type: Object, required: true + }, + timeSlotDisplayBehavior: { + type: String, + default: "default", + // options: default, always, never } }, computed: { @@ -50,21 +55,33 @@ export default { return luxon.Duration .fromISOTime(this.event.ende) .toISOTime({ suppressSeconds: true }); - } + }, + timeSlotDisplayClasses() { + switch (this.$props.timeSlotDisplayBehavior) { + case "always": + return "d-grid"; + case "never": + return "d-none"; + default: + return "d-none d-xl-grid"; + } + }, }, template: /* html */`
{{ start }} {{ end }}
{{ event.topic }} + {{ event.ort_kurzbz }} ... +{{ event.lektor.length - 3 }} - {{ event.ort_kurzbz }}
`, From aeebc2a81a07e3fb586d28661d8f0a38baf24fc8 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 14:25:22 +0200 Subject: [PATCH 360/588] reworked localization of collapsed column headings by modifying internal tabulator logic with custom module --- .../Cis/Profil/MitarbeiterProfil.js | 8 ++++ .../Cis/Profil/MitarbeiterViewProfil.js | 15 ++++--- public/js/components/Cis/Profil/Profil.js | 2 +- .../js/components/Cis/Profil/StudentProfil.js | 12 +++-- public/js/components/filter/Filter.js | 45 +++++-------------- .../InternalToExternalEventBroadcastModule.js | 9 ++-- .../customModules/MenuExtensionModule.js | 2 +- .../ResponsiveLayoutExtensionModule.js | 27 +++++++++++ 8 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js diff --git a/public/js/components/Cis/Profil/MitarbeiterProfil.js b/public/js/components/Cis/Profil/MitarbeiterProfil.js index 55a52b5c3..ed63850c7 100644 --- a/public/js/components/Cis/Profil/MitarbeiterProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterProfil.js @@ -59,6 +59,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, @@ -66,6 +67,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, @@ -73,6 +75,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', @@ -84,6 +87,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { + title: "placeholder", titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', @@ -95,6 +99,7 @@ export default { formatterParams: this.datetimeFormatterParams() }, { + title: "placeholder", titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, @@ -129,6 +134,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, @@ -136,6 +142,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, @@ -144,6 +151,7 @@ export default { visible: true }, { + title: "placeholder", titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', diff --git a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js index 78dc53ffc..8670a2808 100644 --- a/public/js/components/Cis/Profil/MitarbeiterViewProfil.js +++ b/public/js/components/Cis/Profil/MitarbeiterViewProfil.js @@ -46,21 +46,24 @@ export default { visible: true }, { - title: "ui/bezeichnung", + title: "placeholder", + titlePhrase: "ui/bezeichnung", field: "Bezeichnung", headerFilter: true, minWidth: 200, visible: true }, { - title: "lehre/organisationseinheit", + title: "placeholder", + titlePhrase: "lehre/organisationseinheit", field: "Organisationseinheit", headerFilter: true, minWidth: 200, visible: true }, { - title: "global/gueltigVon", + title: "placeholder", + titlePhrase: "global/gueltigVon", field: "Gültig_von", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -71,7 +74,8 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "global/gueltigBis", + title: "placeholder", + titlePhrase: "global/gueltigBis", field: "Gültig_bis", headerFilterFunc: 'dates', headerFilter: dateFilter, @@ -82,7 +86,8 @@ export default { formatterParams: this.datetimeFormatterParams() }, { - title: "profil/wochenstunden", + title: "placeholder", + titlePhrase: "profil/wochenstunden", field: "Wochenstunden", headerFilter: true, minWidth: 200, diff --git a/public/js/components/Cis/Profil/Profil.js b/public/js/components/Cis/Profil/Profil.js index 5e656da77..da205816d 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 = "..."; + item.innerHTML = "" + col.title + ""; 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 ee7654b86..f448d01b1 100644 --- a/public/js/components/Cis/Profil/StudentProfil.js +++ b/public/js/components/Cis/Profil/StudentProfil.js @@ -42,7 +42,8 @@ export default { minHeight: 200, layout: "fitColumns", columns: [{ - title: "profil/zutrittsGruppen", + title: "placeholder", + titlePhrase: "profil/zutrittsGruppen", field: "bezeichnung" }], }, @@ -69,14 +70,16 @@ export default { headerClick: this.collapseFunction, }, { - title: "profil/entlehnteBetriebsmittel", + title: "placeholder", + titlePhrase: "profil/entlehnteBetriebsmittel", field: "betriebsmittel", headerFilter: true, minWidth: 200, visible: true }, { - title: "profil/inventarnummer", + title: "placeholder", + titlePhrase: "profil/inventarnummer", field: "Nummer", headerFilter: true, resizable: true, @@ -84,7 +87,8 @@ export default { visible: true }, { - title: "profil/ausgabedatum", + title: "placeholder", + titlePhrase: "profil/ausgabedatum", field: "Ausgegeben_am", headerFilterFunc: 'dates', headerFilter: dateFilter, diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 1b15666a9..77ad648bd 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -24,6 +24,7 @@ import collapseAutoClose from '../../directives/collapseAutoClose.js'; import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js'; import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js" import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js" +import ResponsiveLayoutExtensionModule from "../../tabulator/customModules/ResponsiveLayoutExtensionModule.js" import { debounce } from "../../helpers/DebounceHelper.js"; @@ -128,26 +129,6 @@ export const CoreFilterCmpt = { page: false, }, collapsedHeadingLocalizationTimer: null, - localizeCollapsedColumnHeadings: - debounce( - () => { - const columnHeadings = this.tabulator?.getLang()?.columns; - if (!columnHeadings?.length) return; - - this.$refs.table - .querySelectorAll(".collapsedColumnHeading") - .forEach((collapsedColumnHeadingElement) => { - const field = - collapsedColumnHeadingElement.getAttribute( - "tabulator-column-field", - ); - if (!field?.length) return; - - collapsedColumnHeadingElement.innerHTML = - columnHeadings[field]; - }); - }, 200) - , }; }, computed: { @@ -258,10 +239,6 @@ export const CoreFilterCmpt = { if (!this.$props.tabulatorOptions.locale) return; this.tabulator.setLocale(newSelectedLanguage); - - if (this.$props.tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } }, }, }, @@ -349,7 +326,8 @@ export const CoreFilterCmpt = { // Start the tabulator with the build options this.tabulator = new Tabulator(this.$refs.table, { ...tabulatorOptions, - debugInvalidOptions: false, + // todo: remove comment + // debugInvalidOptions: false, }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) @@ -407,16 +385,8 @@ export const CoreFilterCmpt = { this.filterActive = filters.length > 0; this.$emit("headerFilterOn", this.filterActive); }); - - this.tabulator.on("renderComplete", () => { - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter && this.tableBuilt) { - this.localizeCollapsedColumnHeadings(); - } - }); - this.tabulator.on("layoutRefreshed", () => { - if (tabulatorOptions.locale && tabulatorOptions.responsiveLayoutCollapseFormatter) { - this.localizeCollapsedColumnHeadings(); - } + this.tabulator.on("localized", () => { + this.tabulator.modules.responsiveLayout.generateCollapsedContent(); }); }, async configureTabulatorLocalizations(tabulatorOptions) { @@ -491,6 +461,10 @@ export const CoreFilterCmpt = { } }); + if (!Object.keys(phrasesGroupedByCategoryRequestParam).length) { + return tabulatorOptions; + } + const phrasesResponse = await this.$api.call( ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam), ); @@ -857,6 +831,7 @@ export const CoreFilterCmpt = { Tabulator.registerModule(InternalToExternalEventBroadcastModule); Tabulator.registerModule(MenuExtensionModule); + Tabulator.registerModule(ResponsiveLayoutExtensionModule); }, mounted() { this.initTabulator().then(() => { diff --git a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js index 6bbb58d13..e4d0b2811 100644 --- a/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js +++ b/public/js/tabulator/customModules/InternalToExternalEventBroadcastModule.js @@ -13,10 +13,11 @@ export default class InternalToExternalEventBroadcastModule extends Module { super(table); this.eventsToBroadcast = [ - { - internal: "layout-refreshed", - external: null, - }, + // example event + // { + // internal: "layout-refreshed", + // external: null, + // }, ]; } diff --git a/public/js/tabulator/customModules/MenuExtensionModule.js b/public/js/tabulator/customModules/MenuExtensionModule.js index 18df92e3d..883e82aeb 100644 --- a/public/js/tabulator/customModules/MenuExtensionModule.js +++ b/public/js/tabulator/customModules/MenuExtensionModule.js @@ -9,7 +9,7 @@ export default class MenuExtensionModule extends Menu { } loadMenu(e, component, menu, parentEl, parentPopup){ - const isLocalizationEnabled = !!component.table.options.locale; + const isLocalizationEnabled = component.table.options.locale && component.table.options.locale !== "default"; const menuItemTranslations = component.table.getLang().menuItems; if (isLocalizationEnabled && menuItemTranslations) { menu = menu.map((menuItem) => { diff --git a/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js new file mode 100644 index 000000000..c0e66cff0 --- /dev/null +++ b/public/js/tabulator/customModules/ResponsiveLayoutExtensionModule.js @@ -0,0 +1,27 @@ +import ResponsiveLayout from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/ResponsiveLayout/ResponsiveLayout.js"; + +export default class ResponsiveLayoutExtensionModule extends ResponsiveLayout { + static moduleName = "responsiveLayout"; + static moduleInitNumber = 1; + + constructor(table) { + super(table); + } + + generateCollapsedRowData(row) { + let result = super.generateCollapsedRowData(row) + const isLocalizationEnabled = this.table.options.locale && this.table.options.locale !== "default"; + const columnHeadingTranslations = this.table?.getLang()?.columns; + + if (isLocalizationEnabled && columnHeadingTranslations) { + result = result.map((column) => { + if (columnHeadingTranslations[column.field]) { + column.title = columnHeadingTranslations[column.field]; + } + return column; + }); + } + + return result; + } +} From eb281f3bc3d236807eaab9e601be5fe4e41c7474 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 14:27:22 +0200 Subject: [PATCH 361/588] hiding redundant tabulator warnings --- public/js/components/filter/Filter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index 77ad648bd..a12ec43f8 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -326,8 +326,7 @@ export const CoreFilterCmpt = { // Start the tabulator with the build options this.tabulator = new Tabulator(this.$refs.table, { ...tabulatorOptions, - // todo: remove comment - // debugInvalidOptions: false, + debugInvalidOptions: false, }); // If event handlers have been provided if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0) From 1d5b915717f3a1e379c936e118a2de322d2cb1c9 Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 15:47:11 +0200 Subject: [PATCH 362/588] minor fix --- public/js/components/LVVerwaltung/Setup/Table.js | 2 +- public/js/components/filter/Filter.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/components/LVVerwaltung/Setup/Table.js b/public/js/components/LVVerwaltung/Setup/Table.js index 67c63c3bb..5ebd7f793 100644 --- a/public/js/components/LVVerwaltung/Setup/Table.js +++ b/public/js/components/LVVerwaltung/Setup/Table.js @@ -192,7 +192,7 @@ export default { columns: [ { - title: this.$p.t('lehre', 'kurzbz'), + title: "adis", field: "lv_kurzbz", headerFilterFuncParams: {field: 'lv_kurzbz'}, headerFilter: true, diff --git a/public/js/components/filter/Filter.js b/public/js/components/filter/Filter.js index a12ec43f8..5f0447aea 100644 --- a/public/js/components/filter/Filter.js +++ b/public/js/components/filter/Filter.js @@ -809,7 +809,7 @@ export const CoreFilterCmpt = { getColumnNames() { if (!this.tableBuilt) { return {}; - } else if (this.tabulator.options.locale); { + } else if (this.tabulator.options.locale) { return this.tabulator.getLang().columns; } return this.tabulator.getColumns().reduce((res, col) => { From 23b1e98a7abb89ccbbec4304182465b5337f9a0f Mon Sep 17 00:00:00 2001 From: adisposkofh Date: Tue, 26 May 2026 16:09:21 +0200 Subject: [PATCH 363/588] minor fix --- public/js/components/LVVerwaltung/Setup/Table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/components/LVVerwaltung/Setup/Table.js b/public/js/components/LVVerwaltung/Setup/Table.js index 5ebd7f793..67c63c3bb 100644 --- a/public/js/components/LVVerwaltung/Setup/Table.js +++ b/public/js/components/LVVerwaltung/Setup/Table.js @@ -192,7 +192,7 @@ export default { columns: [ { - title: "adis", + title: this.$p.t('lehre', 'kurzbz'), field: "lv_kurzbz", headerFilterFuncParams: {field: 'lv_kurzbz'}, headerFilter: true, From 2eb2c36d5bc90eea001a5d0a4e36d529822a19b9 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 26 May 2026 17:06:39 +0200 Subject: [PATCH 364/588] replace + in email helpers with spaces; configs for title edit, multi edit table & confetti on endupload; empty student projektarbeiten check; only show dropdown labels on large displays; new phrasen & phrasen fixes; --- application/config/abgabe.php | 8 ++ .../controllers/api/frontend/v1/Abgabe.php | 36 ++++++--- application/views/Cis/Abgabetool.php | 2 +- public/css/components/abgabetool/abgabe.css | 49 ++++++++++++ .../Cis/Abgabetool/AbgabeStudentDetail.js | 76 +++++++++++++++++-- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 10 ++- .../Cis/Abgabetool/AbgabetoolStudent.js | 14 +++- public/js/helpers/EmailHelpers.js | 6 +- system/phrasesupdate.php | 46 ++++++++++- 9 files changed, 218 insertions(+), 29 deletions(-) diff --git a/application/config/abgabe.php b/application/config/abgabe.php index 90aedbd8b..3427046cd 100644 --- a/application/config/abgabe.php +++ b/application/config/abgabe.php @@ -43,3 +43,11 @@ $config['ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'] = true; $config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true; $config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true; + +$config['MULTIEDIT_TABLE'] = true; + +$config['STUDENT_EDIT_PROJEKTARBEIT_TITLE'] = true; + +$config['CONFETTI_ON_ENDUPLOAD'] = true; + + diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index d6f3c29a8..5ba0ef172 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -93,6 +93,7 @@ class Abgabe extends FHCAPI_Controller $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'); $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'); $BETREUER_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('BETREUER_SAMMELMAIL_BUTTON_STUDENT'); + $MULTIEDIT_TABLE = $this->config->item('MULTIEDIT_TABLE'); $ret = array( 'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link, @@ -100,7 +101,7 @@ class Abgabe extends FHCAPI_Controller 'abgabetypenBetreuer' => $abgabetypenBetreuer, 'ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT' => $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT, 'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER, - 'BETREUER_SAMMELMAIL_BUTTON_STUDENT' => $BETREUER_SAMMELMAIL_BUTTON_STUDENT, + 'MULTIEDIT_TABLE' => $MULTIEDIT_TABLE, ); $this->terminateWithSuccess($ret); @@ -110,10 +111,14 @@ class Abgabe extends FHCAPI_Controller * loads config related to abgabetool for students to avoid handing out links reserved for employees */ public function getConfigStudent() { - $moodle_link =$this->config->item('STG_MOODLE_LINK'); + $moodle_link = $this->config->item('STG_MOODLE_LINK'); + $title_edit_allowed = $this->config->item('STUDENT_EDIT_PROJEKTARBEIT_TITLE'); + $confetti_on_endupload = $this->config->item('CONFETTI_ON_ENDUPLOAD'); $ret = array( 'moodle_link' => $moodle_link, + 'title_edit_allowed' => $title_edit_allowed, + 'confetti_on_endupload' => $confetti_on_endupload ); $this->terminateWithSuccess($ret); @@ -190,8 +195,8 @@ class Abgabe extends FHCAPI_Controller } else { $result = $this->ProjektarbeitModel->getStudentProjektarbeitenWithBetreuer(getAuthUID()); } - - $projektarbeiten = getData($result); + + $projektarbeiten = hasData($result) ? getData($result) : array(); if(count($projektarbeiten)) { foreach($projektarbeiten as $pa) { @@ -459,6 +464,10 @@ class Abgabe extends FHCAPI_Controller */ public function postStudentProjektarbeitTitel() { + if(!$this->config->item('STUDENT_EDIT_PROJEKTARBEIT_TITLE')) { + $this->terminateWithError($this->p->t('global', 'c4studentEditNotAllowed'), 'general'); + }; + $projektarbeit_id = $this->input->post('projektarbeit_id'); $titel = $this->input->post('titel'); @@ -467,6 +476,13 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } + // strip all HTML tags to prevent XSS in mail bodies, table views and Projektarbeitsbenotung + $titel = trim(strip_tags($titel)); + if ($titel === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); @@ -484,6 +500,8 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); } + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); $data = getData($result); @@ -492,7 +510,7 @@ class Abgabe extends FHCAPI_Controller $result = $this->ProjektarbeitModel->update( $projektarbeit_id, array( - 'titel' => trim($titel), + 'titel' => $titel, 'updatevon' => getAuthUID(), 'updateamum' => date('Y-m-d H:i:s') ) @@ -504,7 +522,7 @@ class Abgabe extends FHCAPI_Controller 'titelUpdate', array( 'projektarbeit_id' => $projektarbeit_id, - 'titel' => trim($titel), + 'titel' => $titel, 'updatevon' => getAuthUID(), 'updateamum' => date('Y-m-d H:i:s') ), @@ -514,7 +532,7 @@ class Abgabe extends FHCAPI_Controller $this->sendTitelChangedEmail( $projektarbeit_id, - trim($titel), + $titel, $oldTitle, $assignedStudentUid ); @@ -1533,7 +1551,7 @@ class Abgabe extends FHCAPI_Controller }; Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); if(!$projektarbeitIsCurrent) { - $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general'); + $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv2'), 'general'); } // Link to Abgabetool @@ -1739,7 +1757,7 @@ class Abgabe extends FHCAPI_Controller $data = getData($res)[0]; if($data->note !== NULL) { // 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'); + $message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv2'); 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 { diff --git a/application/views/Cis/Abgabetool.php b/application/views/Cis/Abgabetool.php index a0621b1f9..469bc8110 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 12e0e82ee..78a04e114 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -347,3 +347,52 @@ } +/*confetti celebration on endupload - impossible to miss*/ +#confetti-container { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + pointer-events: none; + z-index: 9999; + overflow: hidden; +} + +.confetti-piece { + position: absolute; + opacity: 0; + will-change: top, transform, opacity; +} + +/* Background Rain */ +@keyframes fallAndSpin { + 0% { + top: var(--start-y); + transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg); + opacity: 1; + } + 100% { + top: 105vh; + transform: translate3d(var(--drift), 0, 0) rotateX(720deg) rotateY(360deg); + opacity: 0.3; + } +} + +/* Corner Cannons*/ +@keyframes cannonBlast { + 0% { + transform: translate3d(0, 0, 0) scale(0.3) rotate(0deg); + opacity: 1; + animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1); + } + 30% { + transform: translate3d(var(--blast-x), var(--blast-y), 0) scale(1.2) rotate(270deg); + opacity: 1; + animation-timing-function: linear; + } + 100% { + transform: translate3d(calc(var(--blast-x) * 1.4), 15vh, 0) scale(0.4) rotate(630deg); + opacity: 0; + } +} diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index c9d5280cb..01673539f 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -21,7 +21,7 @@ export const AbgabeStudentDetail = { VueDatePicker, FhcOverlay }, - inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'], + inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link', 'confetti_on_endupload', 'title_edit_allowed'], props: { projektarbeit: { type: Object, @@ -52,6 +52,67 @@ export const AbgabeStudentDetail = { } }, methods: { + confettiCannons() { + const container = document.getElementById('confetti-container'); + if (!container) return; + + const colors = ['#FFC107', '#FF5722', '#E91E63', '#00BCD4', '#4CAF50', '#9C27B0']; + const shapes = ['50%', '0%']; + const fragment = document.createDocumentFragment(); + + // Corner Cannons - Slowed Down) + const cannonCount = 150; + + for (let i = 0; i < cannonCount; i++) { + const leftConfetti = document.createElement('div'); + leftConfetti.classList.add('confetti-piece'); + leftConfetti.style.left = '0px'; + leftConfetti.style.top = '100%'; + + const rightConfetti = document.createElement('div'); + rightConfetti.classList.add('confetti-piece'); + rightConfetti.style.left = '100vw'; + rightConfetti.style.top = '100%'; + + const colorL = colors[Math.floor(Math.random() * colors.length)]; + const colorR = colors[Math.floor(Math.random() * colors.length)]; + const shapeL = shapes[Math.floor(Math.random() * shapes.length)]; + const shapeR = shapes[Math.floor(Math.random() * shapes.length)]; + + // Left Styles + leftConfetti.style.background = colorL; + leftConfetti.style.borderRadius = shapeL; + leftConfetti.style.width = `${Math.random() * 10 + 6}px`; + leftConfetti.style.height = `${Math.random() * 14 + 6}px`; + leftConfetti.style.setProperty('--blast-x', `${Math.random() * 50 + 10}vw`); + leftConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`); + + // Right Styles + rightConfetti.style.background = colorR; + rightConfetti.style.borderRadius = shapeR; + rightConfetti.style.width = `${Math.random() * 10 + 6}px`; + rightConfetti.style.height = `${Math.random() * 14 + 6}px`; + rightConfetti.style.setProperty('--blast-x', `-${Math.random() * 50 + 10}vw`); + rightConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`); + + // Increased durations to 3s - 5s for a floating gravity effect + const durationL = Math.random() * 2 + 3; + const durationR = Math.random() * 2 + 3; + const delayL = Math.random() * 0.2; + const delayR = Math.random() * 0.2; + + leftConfetti.style.animation = `cannonBlast ${durationL}s linear ${delayL}s both`; + rightConfetti.style.animation = `cannonBlast ${durationR}s linear ${delayR}s both`; + + fragment.appendChild(leftConfetti); + fragment.appendChild(rightConfetti); + + setTimeout(() => leftConfetti.remove(), (delayL + durationL) * 1000); + setTimeout(() => rightConfetti.remove(), (delayR + durationR) * 1000); + } + + container.appendChild(fragment); + }, openTitelEdit() { this.editingTitel = this.projektarbeit.titel ?? ''; this.$refs.modalTitelEdit.show(); @@ -155,6 +216,9 @@ export const AbgabeStudentDetail = { this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData)) .then(res => { this.handleUploadRes(res, this.enduploadTermin) + if(this.confetti_on_endupload && res.meta.status == "success") { + this.confettiCannons() + } }).finally(()=> { this.loading = false }) @@ -274,8 +338,7 @@ export const AbgabeStudentDetail = { return qgatefound }, isTitelEditAllowed() { - // blocked once the projektarbeit has a note (finished) - mirrors backend guard - return !this.isViewMode && !this.projektarbeit?.note; + return this.title_edit_allowed && !this.isViewMode && !this.projektarbeit?.note; }, getTooltipVerspaetet() { return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" } @@ -433,7 +496,7 @@ export const AbgabeStudentDetail = {
- +
{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}
@@ -544,6 +607,8 @@ export const AbgabeStudentDetail = {
+
+
{{ editingTitel.length }} / 1024
@@ -674,6 +739,7 @@ export const AbgabeStudentDetail = {
{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}
+ `, }; diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index fee321299..b962fc5ad 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -95,6 +95,7 @@ export const AbgabetoolAssistenz = { old_abgabe_beurteilung_link: null, ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT: null, ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER: null, + MULTIEDIT_TABLE: false, saving: false, loading: false, abgabeTypeOptions: null, @@ -2275,6 +2276,7 @@ export const AbgabetoolAssistenz = { this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link; this.ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = res.data?.ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT; this.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = res.data?.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER; + this.MULTIEDIT_TABLE = res.data?.MULTIEDIT_TABLE; } // 2. Studiengänge @@ -2659,7 +2661,7 @@ export const AbgabetoolAssistenz = {

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

-
+
@@ -2676,10 +2678,10 @@ export const AbgabetoolAssistenz = {
-
+
-
+
- diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index c855b6283..9ffcc1f94 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -18,7 +18,9 @@ export const AbgabetoolStudent = { return { notenOptions: Vue.computed(() => this.notenOptions), isViewMode: Vue.computed(() => this.isViewMode), - moodle_link: Vue.computed(() => this.moodle_link) + moodle_link: Vue.computed(() => this.moodle_link), + title_edit_allowed: Vue.computed(() => this.title_edit_allowed), + confetti_on_endupload: Vue.computed(() => this.confetti_on_endupload) } }, props: { @@ -46,6 +48,8 @@ export const AbgabetoolStudent = { projektarbeiten: null, selectedProjektarbeit: null, moodle_link: null, + title_edit_allowed: null, + confetti_on_endupload: null, editingTitel: '', editingProjektarbeit: null, }; @@ -335,6 +339,8 @@ export const AbgabetoolStudent = { this.$api.call(ApiAbgabe.getConfigStudent()).then(res => { this.moodle_link = res.data?.moodle_link + this.title_edit_allowed = res.data?.title_edit_allowed + this.confetti_on_endupload = res.data?.confetti_on_endupload }).catch(e => { this.loading = false }) @@ -381,7 +387,7 @@ export const AbgabetoolStudent = { rows="10" maxlength="1024" class="form-control w-100" - @keyup.enter="saveTitel" + @keydown.enter.prevent="saveTitel" />
{{ editingTitel.length }} / 1024
@@ -407,7 +413,7 @@ export const AbgabetoolStudent = {

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


-
+
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
@@ -497,7 +503,7 @@ export const AbgabetoolStudent = { style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" >{{ projektarbeit.titel }}