From 3d51753419800ee6c049883a09c5d7e893710d40 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 31 Oct 2025 11:14:39 +0100 Subject: [PATCH] new bootstrap offcanvas component; projektarbeit abgabetermine timeline & status legende in offcanvas; fixtermin toggle & allowed to edit all termine for assistenz; studiengang selection + filter for getSTG_isEntitledFor('basis/abgabe_assistenz:rw'); moved filedownload from Cis/Abgabetool Auth Controller to Abgabe.php Api Controller; status symbol in table columns prevTermin/nextTermin; get_betreuer_details pgsql function to avoid rewriting the same subquery for every betreuer anrede; --- application/controllers/Cis/Abgabetool.php | 40 -- .../controllers/api/frontend/v1/Abgabe.php | 64 ++- .../models/education/Projektarbeit_model.php | 185 +++---- .../models/organisation/Studiengang_model.php | 17 + application/views/Cis/Abgabetool.php | 1 + .../views/CisRouterView/CisRouterView.php | 1 + public/js/api/factory/abgabe.js | 9 +- public/js/components/Bootstrap/Offcanvas.js | 151 ++++++ .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 79 ++- .../Cis/Abgabetool/AbgabeStudentDetail.js | 2 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 502 ++++++++++++++++-- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 3 + .../Cis/Abgabetool/StatusLegende.js | 57 ++ .../61164_abgabetool_quality_gates.php | 34 ++ system/phrasesupdate.php | 166 +++++- 15 files changed, 1081 insertions(+), 230 deletions(-) create mode 100644 public/js/components/Bootstrap/Offcanvas.js create mode 100644 public/js/components/Cis/Abgabetool/StatusLegende.js diff --git a/application/controllers/Cis/Abgabetool.php b/application/controllers/Cis/Abgabetool.php index 223af86f5..6bf7d951c 100644 --- a/application/controllers/Cis/Abgabetool.php +++ b/application/controllers/Cis/Abgabetool.php @@ -14,7 +14,6 @@ class Abgabetool extends Auth_Controller { parent::__construct([ 'index' => self::PERM_LOGGED, - 'getStudentProjektarbeitAbgabeFile' => array('basis/abgabe_student:rw', 'basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), 'Mitarbeiter' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), 'Assistenz' => array('basis/abgabe_assistenz:rw'), 'Student' => array('basis/abgabe_student:rw', 'basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), @@ -100,43 +99,4 @@ class Abgabetool extends Auth_Controller $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'DeadlinesOverview']); } } - - - public function getStudentProjektarbeitAbgabeFile() - { - $this->_ci =& get_instance(); - $this->_ci->load->helper('download'); - - $paabgabe_id = $this->_ci->input->get('paabgabe_id'); - $student_uid = $this->_ci->input->get('student_uid'); - - if (!isset($paabgabe_id) || isEmptyString($paabgabe_id) || !isset($student_uid) || isEmptyString($student_uid)) - $this->terminateWithJsonError($this->p->t('global', 'wrongParameters'), 'general'); - - $this->_ci->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - - $isZugeteilterBetreuer = count($this->_ci->ProjektarbeitModel->checkZuordnung($student_uid, getAuthUID())->retval) > 0; - - if(getAuthUID() == $student_uid || $isZugeteilterBetreuer) { - $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - if(file_exists($file_path)) { - - header('Content-Description: File Transfer'); - header('Content-Type: application/octet-stream'); - header('Expires: 0'); - header('Cache-Control: must-revalidate'); - header('Pragma: public'); - header('Content-Disposition: attachment; filename="'.basename($file_path).'"'); - header('Content-Length: ' . filesize($file_path)); - - flush(); // send headers first just in case - readfile($file_path); // read file content to output buffer - - } else { - $this->terminateWithJsonError('File not found'); - } - } else { - $this->terminateWithJsonError('Keine Zuordnung!'); - } - } } diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 4a9706789..3a05bd6f6 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -39,7 +39,9 @@ class Abgabe extends FHCAPI_Controller 'fetchDeadlines' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'getPaAbgabetypen' => self::PERM_LOGGED, 'getNoten' => self::PERM_LOGGED, - 'getProjektarbeitenForStudiengang' =>array('basis/abgabe_assistenz:rw') + 'getProjektarbeitenForStudiengang' =>array('basis/abgabe_assistenz:rw'), + 'getStudiengaenge' => array('basis/abgabe_assistenz:rw'), + 'getStudentProjektarbeitAbgabeFile' => array('basis/abgabe_student:rw', 'basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), ]); $this->load->library('PhrasesLib'); @@ -869,6 +871,10 @@ class Abgabe extends FHCAPI_Controller $result = $this->ProjektarbeitModel->getProjektarbeitenForStudiengang($studiengang_kz); $projektarbeiten = $this->getDataOrTerminateWithError($result); + if(count($projektarbeiten) == 0) { // avoid further abgabetermin queries if the are no projektarbeiten + $this->terminateWithSuccess(array($projektarbeiten, DOMAIN)); + } + $mapFunc = function($projektarbeit) { return $projektarbeit->projektarbeit_id; }; @@ -891,4 +897,60 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithSuccess(array($projektarbeiten, DOMAIN)); } + + public function getStudiengaenge() { + $this->load->library('PermissionLib'); + + $stg_allowed = $this->permissionlib->getSTG_isEntitledFor('basis/abgabe_assistenz:rw'); + + if($stg_allowed == false) { + $this->terminateWithError($this->p->t('global', 'keineBerechtigung'), 'general'); + } + + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $result = $this->StudiengangModel->getStudiengaengeFiltered($stg_allowed); + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess($data); + } + + public function getStudentProjektarbeitAbgabeFile() + { + $this->load->helper('download'); + + $paabgabe_id = $this->input->get('paabgabe_id'); + $student_uid = $this->input->get('student_uid'); + + if (!isset($paabgabe_id) || isEmptyString($paabgabe_id) || !isset($student_uid) || isEmptyString($student_uid)) + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + + $isZugeteilterBetreuer = count($this->ProjektarbeitModel->checkZuordnung($student_uid, getAuthUID())->retval) > 0; + $isAssistenz = $this->permissionlib->isBerechtigt('extension/abgabe_assistenz'); + + if(getAuthUID() == $student_uid || $isZugeteilterBetreuer || $isAssistenz) { + $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; + if(file_exists($file_path)) { + + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + header('Content-Disposition: attachment; filename="'.basename($file_path).'"'); + header('Content-Length: ' . filesize($file_path)); + + flush(); // send headers first just in case + readfile($file_path); // read file content to output buffer + + } else { + $this->terminateWithError('File not found'); + } + } else { + $this->terminateWithError('Keine Zuordnung!'); + } + } + } \ No newline at end of file diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 821a8d007..9cd28cdf8 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -320,107 +320,85 @@ class Projektarbeit_model extends DB_Model } public function getProjektarbeitenForStudiengang($studiengang_kz) { - - // TODO: select less fields, a lot of useless data over here - - $old_qry = "SELECT *, - (SELECT orgform_kurzbz - FROM tbl_prestudentstatus - WHERE prestudent_id=(Select prestudent_id from tbl_student where student_uid=xy.uid limit 1) - ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1 - ) as organisationsform - FROM (SELECT DISTINCT ON(tbl_projektarbeit.projektarbeit_id) public.tbl_studiengang.bezeichnung as stgbez,tbl_projekttyp.bezeichnung AS prjbez,* FROM lehre.tbl_projektarbeit - LEFT JOIN public.tbl_benutzer on(uid=student_uid) - LEFT JOIN public.tbl_person on(tbl_benutzer.person_id=tbl_person.person_id) - LEFT JOIN lehre.tbl_lehreinheit using(lehreinheit_id) - LEFT JOIN lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id) - LEFT JOIN public.tbl_studiengang using(studiengang_kz) - LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) - WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom') - AND public.tbl_benutzer.aktiv - AND lehre.tbl_projektarbeit.note IS NULL - AND public.tbl_studiengang.studiengang_kz = ? - ORDER BY tbl_projektarbeit.projektarbeit_id desc) as xy - ORDER BY nachname"; - - - $new_qry = " - 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.vorname as betreuer_vorname, - betreuer_person.nachname as betreuer_nachname, - 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, - ( - 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') - 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') - 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') - LIMIT 1 - ) - as zweitbetreuer_full_name + $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.vorname as betreuer_vorname, + betreuer_person.nachname as betreuer_nachname, + 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') + 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') + 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') + LIMIT 1 + ) + as zweitbetreuer_full_name 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 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) @@ -429,14 +407,11 @@ class Projektarbeit_model extends DB_Model 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') - -- AND lehre.tbl_projektarbeit.note IS NULL - -- AND public.tbl_studiengang.studiengang_kz= 257 + -- AND lehre.tbl_projektarbeit.note IS NULL + -- AND public.tbl_studiengang.studiengang_kz= 257 AND public.tbl_studiengang.studiengang_kz = ? - ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC - "; -// $oldres = $this->execReadOnlyQuery($old_qry, array($studiengang_kz)); -// $newres = $this->execReadOnlyQuery($new_qry, array($studiengang_kz)); -// return array($newres, $oldres); + 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/models/organisation/Studiengang_model.php b/application/models/organisation/Studiengang_model.php index 02f972690..660657f28 100644 --- a/application/models/organisation/Studiengang_model.php +++ b/application/models/organisation/Studiengang_model.php @@ -801,4 +801,21 @@ class Studiengang_model extends DB_Model return $this->execReadOnlyQuery($qry, array($studiengang_kz, $orgform_kurzbz, $studiensemester_kurzbz)); } + + public function getStudiengaengeFiltered($allowed_stg) { + $query ="SELECT DISTINCT + public.tbl_studiengang.studiengang_kz, + public.tbl_studiengang.bezeichnung, + public.tbl_studiengang.kurzbzlang, + public.tbl_studiengang.orgform_kurzbz + FROM public.tbl_studiengang JOIN lehre.tbl_studienordnung USING(studiengang_kz) + JOIN lehre.tbl_studienplan USING(studienordnung_id) + JOIN lehre.tbl_studienplan_semester USING(studienplan_id) + WHERE public.tbl_studiengang.aktiv = true + + AND public.tbl_studiengang.studiengang_kz IN ? + ORDER BY public.tbl_studiengang.kurzbzlang"; + + return $this->execReadOnlyQuery($query, [$allowed_stg]); + } } diff --git a/application/views/Cis/Abgabetool.php b/application/views/Cis/Abgabetool.php index af259ca48..d47f33283 100644 --- a/application/views/Cis/Abgabetool.php +++ b/application/views/Cis/Abgabetool.php @@ -24,6 +24,7 @@ $includesArray = array( 'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js', 'vendor/npm-asset/primevue/speeddial/speeddial.min.js', 'vendor/npm-asset/primevue/textarea/textarea.min.js', + 'vendor/npm-asset/primevue/timeline/timeline.min.js', 'vendor/moment/luxonjs/luxon.min.js' ), 'customJSModules' => array( diff --git a/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index 5b16ceb87..4fa215f3d 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -32,6 +32,7 @@ $includesArray = array( 'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js', 'vendor/npm-asset/primevue/speeddial/speeddial.min.js', 'vendor/npm-asset/primevue/textarea/textarea.min.js', + 'vendor/npm-asset/primevue/timeline/timeline.min.js', 'vendor/moment/luxonjs/luxon.min.js' ), 'customJSModules' => array( diff --git a/public/js/api/factory/abgabe.js b/public/js/api/factory/abgabe.js index 8f4ba61de..1087ec2a0 100644 --- a/public/js/api/factory/abgabe.js +++ b/public/js/api/factory/abgabe.js @@ -36,7 +36,7 @@ export default { }; }, getStudentProjektarbeitAbgabeFile(paabgabe_id, student_uid) { - const url = `/Cis/Abgabetool/getStudentProjektarbeitAbgabeFile?paabgabe_id=${paabgabe_id}&student_uid=${student_uid}`; + const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${paabgabe_id}&student_uid=${student_uid}`; window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url) }, @@ -106,5 +106,12 @@ export default { url: '/api/frontend/v1/Abgabe/getProjektarbeitenForStudiengang', params: { studiengang_kz } }; + }, + // TODO: this could also very well be generic info api :^) + getStudiengaenge() { + return { + method: 'get', + url: '/api/frontend/v1/Abgabe/getStudiengaenge' + }; } }; \ No newline at end of file diff --git a/public/js/components/Bootstrap/Offcanvas.js b/public/js/components/Bootstrap/Offcanvas.js new file mode 100644 index 000000000..aab2c55f5 --- /dev/null +++ b/public/js/components/Bootstrap/Offcanvas.js @@ -0,0 +1,151 @@ +export default { + name: 'BootstrapOffcanvas', + data: () => ({ + offcanvas: null + }), + props: { + backdrop: { + type: [Boolean, String], + default: true, + validator(value) { + return ['static', true, false].includes(value); + } + }, + keyboard: { + type: Boolean, + default: true + }, + scroll: { + type: Boolean, + default: false + }, + placement: { + type: String, + default: 'start', // start | end | top | bottom + validator(value) { + return ['start', 'end', 'top', 'bottom'].includes(value); + } + }, + noCloseBtn: Boolean, + headerClass: { + type: [String, Array, Object], + default: '' + }, + bodyClass: { + type: [String, Array, Object], + default: 'p-4' + }, + footerClass: { + type: [String, Array, Object], + default: '' + }, + dialogClass: [String, Array, Object] + }, + emits: [ + "hideBsOffcanvas", + "hiddenBsOffcanvas", + "hidePreventedBsOffcanvas", + "showBsOffcanvas", + "shownBsOffcanvas" + ], + methods: { + dispose() { + return this.offcanvas?.dispose(); + }, + hide() { + return this.offcanvas?.hide(); + }, + show(relatedTarget) { + return this.offcanvas?.show(relatedTarget); + }, + toggle() { + return this.offcanvas?.toggle(); + }, + popup(body, options, title, footer) { + const BsOffcanvas = this, + slots = {}; + + if (body !== undefined) + slots.default = () => body; + if (title !== undefined) + slots.title = () => title; + if (footer !== undefined) + slots.footer = () => footer; + + let includedPrimevue = false; + if (typeof primevue !== 'undefined') + includedPrimevue = true; + + return new Promise((resolve, reject) => { + const instance = Vue.createApp({ + name: 'OffcanvasTmpApp', + setup() { + return () => + Vue.h(BsOffcanvas, { + class: 'offcanvas-wrapper', + ref: 'offcanvas', + ...options + }, slots); + }, + mounted() { + this.$refs.offcanvas.show(); + }, + beforeUnmount() { + if (this.$refs.offcanvas) + this.$refs.offcanvas.result !== false ? resolve(this.$refs.offcanvas.result) : reject(); + }, + unmounted() { + wrapper.parentElement.removeChild(wrapper); + } + }); + const wrapper = document.createElement('div'); + + if (includedPrimevue) { + instance.use(primevue.config.default, { zIndex: { overlay: 9999 } }); + } + + import('../../plugins/Phrasen.js').then((Phrasen) => { + instance.use(Phrasen.default); + instance.mount(wrapper); + document.body.appendChild(wrapper); + }); + }); + } + }, + mounted() { + if (this.$refs.offcanvas) { + this.offcanvas = new bootstrap.Offcanvas(this.$refs.offcanvas, { + backdrop: this.backdrop, + keyboard: this.keyboard, + scroll: this.scroll + }); + } + }, + template: ` +
+
+
+ +
+ +
+ +
+ +
+ + +
+ ` +} diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index fa202a020..aa8b2b858 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -31,6 +31,10 @@ export const AbgabeMitarbeiterDetail = { isFullscreen: { type: Boolean, default: false + }, + assistenzMode: { + type: Boolean, + default: false } }, data() { @@ -173,7 +177,7 @@ export const AbgabeMitarbeiterDetail = { const oneDayMs = 1000 * 60 * 60 * 24 return Math.round((new Date(datum) - new Date(today)) / oneDayMs) }, - getDateStyleClass(termin, mode) { + getDateStyleClass(termin) { const datum = new Date(termin.datum) const abgabedatum = new Date(termin.abgabedatum) @@ -305,9 +309,9 @@ export const AbgabeMitarbeiterDetail = { }, computed: { getActiveIndexTabArray() { - // here we try to do mind reading logic by assuming which abgabetermine are the most relevant to the current user + // here we try to assume which abgabetermine are the most relevant to the current user - // lets try to take the termin with nearest date and watch who complains and why + // lets try to take the termin with nearest date let closestIndex = -1; let minDiff = Infinity; const today = new Date(); @@ -401,6 +405,17 @@ export const AbgabeMitarbeiterDetail = { value: this.$p.t('abgabetool/c4notAllowedToDeleteAbgabeTermin'), class: "custom-tooltip" } + }, + getProjektarbeitTitel() { + if(this.projektarbeit?.titel) return this.projektarbeit.titel + + return '' + }, + getProjektarbeitStudent(){ + + if(this.projektarbeit?.student) return this.projektarbeit.student + + return '' } }, watch: { @@ -500,7 +515,6 @@ export const AbgabeMitarbeiterDetail = {
-
-

{{projektarbeit?.student}}

-

{{projektarbeit?.titel}}

+

{{getProjektarbeitStudent}}

+

{{getProjektarbeitTitel}}

{{projektarbeit?.zweitbegutachter}}

@@ -533,7 +547,6 @@ export const AbgabeMitarbeiterDetail = { />
- - - - - - - - - - - - - +
+
{{$p.t('abgabetool/c4fixterminv2')}}
+
+ + +
+
{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}
@@ -584,7 +596,7 @@ export const AbgabeMitarbeiterDetail = {
-
{{$capitalize( $p.t('abgabetool/c4abgabetypv2') )}}
+
{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}
{{$capitalize( $p.t('abgabetool/c4abgabedatum') )}}
+ +
+

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

+
+
`, }; diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index 9c1682537..884023420 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -377,7 +377,7 @@ export const AbgabeStudentDetail = { -
+
{{ termin?.bezeichnung }}
diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 067ef3030..40baf97bc 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -2,18 +2,27 @@ import {CoreFilterCmpt} from "../../../components/filter/Filter.js"; import AbgabeDetail from "./AbgabeMitarbeiterDetail.js"; import VerticalSplit from "../../verticalsplit/verticalsplit.js" import BsModal from '../../Bootstrap/Modal.js'; +import BsOffcanvas from '../../Bootstrap/Offcanvas.js'; import VueDatePicker from '../../vueDatepicker.js.php'; import ApiAbgabe from '../../../api/factory/abgabe.js' +import AbgabeterminStatusLegende from "./StatusLegende.js"; + +const todayISO = '2025-08-08' +const today = new Date(todayISO) export const AbgabetoolAssistenz = { name: "AbgabetoolAssistenz", components: { + AbgabeterminStatusLegende, BsModal, + BsOffcanvas, CoreFilterCmpt, AbgabeDetail, VerticalSplit, + Checkbox: primevue.checkbox, Dropdown: primevue.dropdown, Textarea: primevue.textarea, + Timeline: primevue.timeline, VueDatePicker }, provide() { @@ -36,6 +45,14 @@ export const AbgabetoolAssistenz = { }, data() { return { + headerFiltersRestored: false, + filtersRestored: false, + colLayoutRestored: false, + sortRestored: false, + stateRestored: false, + timelineProjekt: null, + selectedStudiengangOption: null, + studiengaengeOptions: null, detailIsFullscreen: false, showZweitbetreuerCol: false, phrasenPromise: null, @@ -69,13 +86,15 @@ export const AbgabetoolAssistenz = { abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', - layout: 'fitDataStretch', - placeholder: this.$p.t('global/noDataAvailable'), + layout: 'fitData', + placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), selectable: true, selectableCheck: this.selectionCheck, - rowHeight: 80, + rowHeight: 40, + responsiveLayout: true, columns: [ { + field: 'rowSelection', formatter: 'rowSelection', titleFormatter: 'rowSelection', titleFormatterParams: { @@ -84,23 +103,26 @@ export const AbgabetoolAssistenz = { hozAlign:"center", headerSort: false, frozen: true, - width: 70 + width: 40 }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false}, - {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/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, tooltip: false} - // {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', formatter: this.abgabterminFormatter, widthGrow: 1, tooltip: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, 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/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/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, maxWidth: 500, widthGrow: 8}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuer'))), field: 'erstbetreuer', formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuer'))), field: 'zweitbetreuer', formatter: this.centeredTextFormatter, widthGrow: 1, visible: this.showZweitbetreuerCol} + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, tooltip:false, minWidth: 150,}, + // {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1,responsive:0, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter,responsive:0, widthGrow: 1, tooltip: false}, + // {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4termineTimeLine'))), headerFilter: true, field: 'abgabetermine',responsive:2, formatter: this.timelineFormatter, widthGrow: 1, tooltip: false}, + // {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, visible: false, widthGrow: 1, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true,responsive:2, formatter: this.centeredTextFormatter,widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true,responsive:2, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', responsive:3, visible: false, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, responsive:3, visible: false, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, visible: false, responsive:3,formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, responsive:3, visible: false, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuer'))), field: 'erstbetreuer', headerFilter: true, responsive:3,formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuer'))), field: 'zweitbetreuer', headerFilter: true, responsive:3,formatter: this.centeredTextFormatter, widthGrow: 1, visible: Vue.computed(()=>{return this.showZweitbetreuerCol})}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', responsive:4, formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', responsive:4, formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, ], persistence: false, + persistenceID: "abgabetableV27" }, abgabeTableEventHandlers: [{ event: "tableBuilt", @@ -108,17 +130,20 @@ export const AbgabetoolAssistenz = { this.tableBuiltResolve() } }, - { - event: "cellClick", - handler: async (e, cell) => { - if(cell.getColumn().getField() === "details") { - this.setDetailComponent(cell.getValue()) - this.undoSelection(cell) - } else if (cell.getColumn().getField() === "mail") { - this.undoSelection(cell) - } - } - }, + // { + // event: "cellClick", + // handler: async (e, cell) => { + // if(cell.getColumn().getField() === "details") { + // this.setDetailComponent(cell.getValue()) + // this.undoSelection(cell) + // } else if (cell.getColumn().getField() === "mail") { + // this.undoSelection(cell) + // } else if (cell.getColumn().getField() === "abgabetermine") { + // this.openTimeline(cell.getValue()) + // this.undoSelection(cell) + // } + // } + // }, { event: "rowSelectionChanged", handler: async(data) => { @@ -128,13 +153,128 @@ export const AbgabetoolAssistenz = { ]}; }, methods: { + loadState() { + return JSON.parse(localStorage.getItem(this.abgabeTableOptions.persistenceID) || "null"); + }, + saveState(table) { + // avoid storing state after first restore part happened + if(!this.stateRestored) 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.abgabeTableOptions.persistenceID, JSON.stringify(state)); + }, + handleTableBuilt() { + const table = this.$refs.abgabeTable.tabulator + 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) { + debugger + 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 + })); + + console.log(layout) + + 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 + + } + + }); + }, handleToggleFullscreenDetail() { this.detailIsFullscreen = !this.detailIsFullscreen }, getOptionLabelAbgabetyp(option){ return option.bezeichnung }, + getOptionLabelStg(option){ + return option.kurzbzlang + ' ' + option.bezeichnung + }, + sgChanged(e) { + debugger + }, formatDate(dateParam) { + if(dateParam === null) return '' const date = new Date(dateParam) // handle missing leading 0 const padZero = (num) => String(num).padStart(2, '0'); @@ -145,6 +285,43 @@ export const AbgabetoolAssistenz = { return `${day}.${month}.${year}`; }, + formAction(cell) { + const actionButtons = document.createElement('div'); + actionButtons.className = "d-flex gap-3"; // you can keep Bootstrap gap if loaded + actionButtons.style.display = "flex"; + actionButtons.style.alignItems = "stretch"; // buttons stretch to full height + actionButtons.style.justifyContent = "center"; + actionButtons.style.height = "100%"; // full grid cell height + + const val = cell.getValue(); + + const createButton = (iconClass, titleKey, clickHandler) => { + const btn = document.createElement('button'); + btn.className = 'btn btn-outline-secondary'; + btn.style.display = "flex"; + btn.style.alignItems = "center"; // center icon vertically + btn.style.justifyContent = "center"; // center icon horizontally + btn.style.height = "100%"; // fill parent container height + btn.style.aspectRatio = "1 / 1"; // keep square shape (optional) + btn.style.padding = "0"; // remove extra padding for compactness + btn.innerHTML = ``; + btn.title = this.$capitalize(this.$p.t(titleKey)); + btn.addEventListener('click', (e) => { + e.stopPropagation(); + e.stopImmediatePropagation(); + clickHandler(); + }); + return btn; + }; + + actionButtons.append( + createButton('fa fa-folder-open', 'abgabetool/c4details', () => this.setDetailComponent(val)), + createButton('fa fa-timeline', 'abgabetool/c4termineTimeLine', () => this.openTimeline(val)) + ); + + return actionButtons; + }, + undoSelection(cell) { // checks if cells row is selected and unselects -> imitates columns which dont trigger row selection // but actually just revert it after the fact @@ -221,27 +398,72 @@ export const AbgabetoolAssistenz = { termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) termin.file = [] - termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter' - termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum + // assistenz should be able to edit every abgabe + termin.allowedToSave = true + + // assistenz are not allowed to delete deadlines with existing submissions + termin.allowedToDelete = !termin.abgabedatum termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) }) - pa.student_uid = details.student_uid - pa.student = `${pa.vorname} ${pa.nachname}` + + // TODO: do same thing for sidebar + + const vorname = pa.vorname ?? pa.student_vorname + const nachname = pa.nachname ?? pa.student_nachname + pa.student = `${vorname} ${nachname}` this.selectedProjektarbeit = pa this.$refs.modalContainerAbgabeDetail.show() + }, + dateDiffInDays(datum, today){ + const oneDayMs = 1000 * 60 * 60 * 24 + return Math.round((new Date(datum) - new Date(today)) / oneDayMs) + }, + getDateStyleClass(termin) { + const datum = new Date(termin.datum) + const abgabedatum = new Date(termin.abgabedatum) - + // https://wiki.fhcomplete.info/doku.php?id=cis:abgabetool_fuer_studierende + if (termin.abgabedatum === null) { + if(datum < today) { + return 'verpasst' + } else if (datum > today && this.dateDiffInDays(datum, today) <= 12) { + return 'abzugeben' + } else { + return 'standard' + } + } else if(abgabedatum > datum) { + return 'verspaetet' + } else { + return 'abgegeben' + } + }, + openTimeline(val) { + const projekt = this.projektarbeiten.find(p => p.projektarbeit_id == val.projektarbeit_id) + if(!projekt) { + + this.$fhcAlert.alertInfo('keine projektarbeit gefunden') + + return + } + projekt.abgabetermine.forEach(termin => { + // show note only on termine with abgabetypen which are benotbar + const terminTypOpt = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz == termin.paabgabetyp_kurzbz) + termin.benotbar = terminTypOpt.benotbar + }) + this.timelineProjekt = projekt + // this.timelineAbgabetermine = projekt.abgabetermine + this.$refs.drawer.show() }, centeredTextFormatter(cell) { const val = cell.getValue() if(!val) return return '
' + - '

'+val+'

' + '

'+val+'

' }, detailFormatter(cell) { return '
' + @@ -252,6 +474,10 @@ export const AbgabetoolAssistenz = { return '
' + '
' }, + timelineFormatter() { + return '
' + + '
' + }, beurteilungFormatter(cell) { const val = cell.getValue() if(val) { @@ -265,7 +491,41 @@ export const AbgabetoolAssistenz = { return '
' + '

'+val+'

' }, - abgabterminFormatter(termin) { + abgabterminFormatter(cell) { + const val = cell.getValue() + + if(val) { + let icon = '' + switch(val.dateStyle) { + case 'verspaetet': + icon = '' + break + case 'verpasst': + icon = '' + break + case 'verpasst': + icon = '' + break + case 'standard': + icon = '' + break + case 'abgegeben': + icon = '' + break + } + + return '
' + + '
' + + icon + + '
' + + '
' + + '

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

' + + '
'+ + '
' + + } else { + return '' + } }, tableResolve(resolve) { @@ -281,37 +541,56 @@ export const AbgabetoolAssistenz = { return (projekt.typ + projekt.kurzbz)?.toUpperCase() }, buildErstbetreuer(projekt) { - return projekt.betreuer_vorname + ' ' + projekt.betreuer_nachname + ' - ' + projekt.betreuerart + '(' + projekt.betreuer_benutzer_uid + ')' + if(projekt.erstbetreuer_full_name) return projekt.erstbetreuer_full_name + return projekt.betreuer_vorname + ' ' + projekt.betreuer_nachname }, buildZweitbetreuer(projekt) { return projekt.zweitbetreuer_full_name ?? '' }, setupData(data){ this.projektarbeiten = data[0] - this.domain = data[1] - + this.domain = data[1] + + const now = luxon.DateTime.fromISO(todayISO) + // const now = luxon.DateTime.now(); const d = data[0].map(projekt => { let mode = 'detailTermine' + projekt.prevTermin = undefined; + projekt.nextTermin = undefined; + // only show 2tbetreuer col if any projektarbeit has one if(projekt.zweitbetreuer_full_name) this.showZweitbetreuerCol = true - // first Abgabetermin in the past + // calculate Abgabetermin time diff to now and assign last and next to projekt projekt.abgabetermine.forEach(termin => { + + // while already looping through each termin, calculate datestyle beforehand + termin.dateStyle = this.getDateStyleClass(termin) + const date = luxon.DateTime.fromISO(termin.datum) - termin.diff = date.diffNow('milliseconds') - - // console.log(termin.diff) + termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past + + if (termin.diffMs < 0) { + if (!projekt.prevTermin || + termin.diffMs > projekt.prevTermin.diffMs // larger (less negative) = closer to now + ) { + projekt.prevTermin = termin; + } + } else if (termin.diffMs > 0) { + if (!projekt.nextTermin || + termin.diffMs < projekt.nextTermin.diffMs // smaller positive = closer to now + ) { + projekt.nextTermin = termin; + } + } }) - - // console.log('\n\n') - return { ...projekt, abgabetermine: projekt.abgabetermine, details: { - student_uid: projekt.uid, + student_uid: projekt.student_uid, projektarbeit_id: projekt.projektarbeit_id, }, pkz: this.buildPKZ(projekt), @@ -326,6 +605,7 @@ export const AbgabetoolAssistenz = { } }) + this.$refs.abgabeTable.tabulator.clearData() this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns) this.$refs.abgabeTable.tabulator.setData(d); }, @@ -378,19 +658,21 @@ export const AbgabetoolAssistenz = { sendEmailStudierende() { // TODO: implement } - }, watch: { - + selectedStudiengangOption(newVal, oldVal) { + this.loadProjektarbeiten() + } }, computed: { getCurrentStudiengang() { // TODO: sophisticated logic pulling from default value by viewData or dropdown select - return 257 + return this.selectedStudiengangOption?.studiengang_kz ?? 257 } }, created() { + this.loading = true this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global']) this.phrasenPromise.then(()=> {this.phrasenResolved = true}) // fetch config to avoid hard coded links @@ -402,6 +684,14 @@ export const AbgabetoolAssistenz = { this.loading = false }) + // fetch studiengänge options + this.$api.call(ApiAbgabe.getStudiengaenge()).then(res => { + this.studiengaengeOptions = res.data + console.log(this.studiengaengeOptions) + }).catch(e => { + this.loading = false + }) + // fetch noten options //TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API this.$api.call(ApiAbgabe.getNoten()).then(res => { @@ -440,17 +730,28 @@ export const AbgabetoolAssistenz = { + + + + + + + + + + + + + + + +
-

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

+
+
+

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

+
+
+ + + +
+

diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index a50d9cf52..2e4cb64a0 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -234,7 +234,10 @@ export const AbgabetoolMitarbeiter = { termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) termin.file = [] + // lektoren are only allowed to edit their own deadline entries termin.allowedToSave = termin.insertvon == this.viewData?.uid && pa.betreuerart_kurzbz != 'Zweitbegutachter' + + // lektoren are not allowed to delete deadlines with existing submissions termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) diff --git a/public/js/components/Cis/Abgabetool/StatusLegende.js b/public/js/components/Cis/Abgabetool/StatusLegende.js new file mode 100644 index 000000000..557e27a89 --- /dev/null +++ b/public/js/components/Cis/Abgabetool/StatusLegende.js @@ -0,0 +1,57 @@ +export const AbgabeterminStatusLegende = { + name: 'AbgabeterminStatusLegende', + template: ` +
+
+ +
+
+ +
+
+
{{ $capitalize($p.t('abgabetool/c4tooltipVerspaetet')) }}
+
+
+ +
+
+ +
+
+
{{ $capitalize($p.t('abgabetool/c4tooltipVerpasst')) }}
+
+
+ +
+
+ +
+
+
{{ $capitalize($p.t('abgabetool/c4tooltipAbzugeben')) }}
+
+
+ +
+
+ +
+
+
{{ $capitalize($p.t('abgabetool/c4tooltipStandard')) }}
+
+
+ +
+
+ +
+
+
{{ $capitalize($p.t('abgabetool/c4tooltipAbgegeben')) }}
+
+ +
+ +
+
+ ` +}; +export default AbgabeterminStatusLegende; \ No newline at end of file diff --git a/system/dbupdate_3.4/61164_abgabetool_quality_gates.php b/system/dbupdate_3.4/61164_abgabetool_quality_gates.php index 83dd91c2d..9d743c91d 100644 --- a/system/dbupdate_3.4/61164_abgabetool_quality_gates.php +++ b/system/dbupdate_3.4/61164_abgabetool_quality_gates.php @@ -254,3 +254,37 @@ if($result = $db->db_query("SELECT 1 FROM system.tbl_berechtigung WHERE berechti echo "
system.tbl_berechtigung insert basis/abgabe_assistenz hinzugefuegt"; } } + +if($result = $db->db_query("SELECT 1 FROM information_schema.routines WHERE routine_schema = 'campus' AND routine_name = 'get_betreuer_details'")) +{ + if($db->db_num_rows($result) === 0) + { + $qry = "CREATE OR REPLACE FUNCTION campus.get_betreuer_details(b_person_id INT) + RETURNS TABLE ( + full_name TEXT + ) + LANGUAGE sql + AS $$ + SELECT DISTINCT + trim( + COALESCE(titelpre,'') || ' ' || + COALESCE(vorname,'') || ' ' || + COALESCE(nachname,'') || ' ' || + COALESCE(titelpost,'') + ) AS full_name + 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 public.tbl_person.person_id = b_person_id; + $$;"; + + if(!$db->db_query($qry)) + echo 'campus.get_betreuer_details: '.$db->db_last_error().'
'; + else + echo "
campus.get_betreuer_details function hinzugefuegt"; + } +} \ No newline at end of file diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index f48347e02..22a3a6e94 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -41936,6 +41936,7 @@ array( ) ) ), + // ABGABETOOL PHRASEN BEGIN array( 'app' => 'core', 'category' => 'abgabetool', @@ -42670,7 +42671,7 @@ array( ), array( 'sprache' => 'English', - 'text' => 'Supervisor type', + 'text' => 'Assessor type', 'description' => '', 'insertvon' => 'system' ) @@ -43286,7 +43287,7 @@ array( ), array( 'sprache' => 'English', - 'text' => 'Emailcontact Assesor', + 'text' => 'Emailcontact Assessor', 'description' => '', 'insertvon' => 'system' ) @@ -43607,6 +43608,167 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4prevAbgabetermin', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Vorheriger Termin', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Previous Deadline', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4nextAbgabetermin', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nächster Termin', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Next Deadline', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4erstbetreuer', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'ErstbetreuerIn', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'First Assessor', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4zweitbetreuer', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'ZweitbetreuerIn', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Second Assessor', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4projektarbeitTimelineTitle', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Projektarbeit Abgabetermine Zeitstrahl', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Project Deadline Timeline', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4sendEmailStudierende', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Email an Studierende schicken', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Send Email to students', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4sendEmailBetreuer', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Email an Betreuende schicken', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Send Email to assessors', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4keineAbgabetermineGefunden', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Die Projektarbeit hat keine zugeordneten Abgabetermine!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Project has no assigned Deadlines!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), + // ABGABETOOL PHRASEN END array( 'app' => 'core', 'category' => 'lehre',