diff --git a/application/config/abgabe.php b/application/config/abgabe.php index 90aedbd8b..dd059d047 100644 --- a/application/config/abgabe.php +++ b/application/config/abgabe.php @@ -29,7 +29,7 @@ $config['ALLOWED_NOTEN_ABGABETOOL'] = [10, 14]; // tbl_note pk // benotete projektarbeiten sperren weitere terminanlage & bearbeitung, diese noten sind ausnahmen dieser Regel // wie zB "Nicht beurteilt" & "Noch nicht eingetragen" $config['NONFINAL_NOTEN_ABGABETOOL'] = [9]; -$config['beurteilung_link_fallback'] = 'addons/fhtw/content/projektbeurteilung/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?'; +$config['beurteilung_link_fallback'] = 'cis/private/lehre/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?'; $config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter'; $config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ZWEITBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter'; @@ -38,8 +38,19 @@ $config['SIGNATUR_CHECK_PAABGABETYPEN'] = ['end']; // to be used as "https://moodle.technikum-wien.at/course/view.php?idnumber=dl{$stg_kz}" for stg specific moodle routing $config['STG_MOODLE_LINK'] = 'https://moodle.technikum-wien.at/course/view.php?idnumber=dl'; +// TODO: check if these links change if the file changes and how to better retrieve the link? +$config['SIGNATUR_INFO_LINK_GERMAN'] = 'https://cis.technikum-wien.at/cms/dms.php?id=214779'; +$config['SIGNATUR_INFO_LINK_ENGLISH'] = 'https://cis.technikum-wien.at/cms/dms.php?id=264256'; $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/config/routes.php b/application/config/routes.php index aa4ba9db8..8024f449a 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -64,6 +64,9 @@ $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1'; $route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1'; $route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1'; $route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1'; +$route['Cis/OtherLvPlan/.*'] = 'Cis/OtherLvPlan/index/$1'; +//Route for LV Plan Stg/Semester/Verband/Gruppe +$route['Cis/StgOrgLvPlan/.*'] = 'Cis/StgOrgLvPlan/index/$1'; $route['Abgabetool/Assistenz'] = 'Cis/Abgabetool/Assistenz'; $route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1'; diff --git a/application/config/searchcis.php b/application/config/searchcis.php index 12bad025d..6d513c499 100644 --- a/application/config/searchcis.php +++ b/application/config/searchcis.php @@ -22,8 +22,9 @@ unset($config['student']['searchfields']['email']); unset($config['student']['searchfields']['tel']); $config['student']['resultfields'] = [ "s.student_uid AS uid", - "s.matrikelnr", + "s.matrikelnr AS personenkennzeichen", "p.person_id", + "p.matr_nr AS matrikelnummer", "(p.vorname || ' ' || p.nachname) AS name", "ARRAY[s.student_uid || '@' || '" . DOMAIN . "'] AS email", "CASE 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/Cis/Abgabetool.php b/application/controllers/Cis/Abgabetool.php index 04338b1a9..28414dafa 100644 --- a/application/controllers/Cis/Abgabetool.php +++ b/application/controllers/Cis/Abgabetool.php @@ -31,12 +31,8 @@ class Abgabetool extends Auth_Controller { // TODO: routing from index based on berechtigung? - $viewData = array( - 'uid'=>getAuthUID(), - ); - if(defined('CIS4') && CIS4) { - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Abgabetool']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'Abgabetool']); } else { $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'Abgabetool']); } @@ -44,12 +40,8 @@ class Abgabetool extends Auth_Controller public function Student($student_uid_prop = '') { - $viewData = array( - 'uid'=>getAuthUID(), - ); - if(defined('CIS4') && CIS4) { - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolStudent']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolStudent']); } else { $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolStudent', 'student_uid_prop' => $student_uid_prop]); } @@ -57,12 +49,8 @@ class Abgabetool extends Auth_Controller public function Mitarbeiter() { - $viewData = array( - 'uid'=>getAuthUID(), - ); - if(defined('CIS4') && CIS4) { - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolMitarbeiter']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolMitarbeiter']); } else { $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolMitarbeiter']); } @@ -70,13 +58,8 @@ class Abgabetool extends Auth_Controller public function Assistenz($stg_kz_prop = '') { - - $viewData = array( - 'uid'=>getAuthUID(), - ); - if(defined('CIS4') && CIS4) { - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolAssistenz']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolAssistenz']); } else { $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolAssistenz', 'stg_kz_prop' => $stg_kz_prop]); } @@ -84,12 +67,8 @@ class Abgabetool extends Auth_Controller public function Deadlines() { - $viewData = array( - 'uid'=>getAuthUID(), - ); - if(defined('CIS4') && CIS4) { - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'DeadlinesOverview']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'DeadlinesOverview']); } else { $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'DeadlinesOverview']); } diff --git a/application/controllers/Cis/Auth.php b/application/controllers/Cis/Auth.php index 67267ebf6..87ef5d3da 100644 --- a/application/controllers/Cis/Auth.php +++ b/application/controllers/Cis/Auth.php @@ -40,7 +40,7 @@ class Auth extends FHC_Controller if ($this->form_validation->run()) { - redirect($this->authlib->getLandingPage('/CisVue/Dashboard')); + redirect($this->authlib->getLandingPage('/Cis4')); } else { diff --git a/application/controllers/Cis/LvPlan.php b/application/controllers/Cis/LvPlan.php index 884c8a9a0..32e622f5a 100644 --- a/application/controllers/Cis/LvPlan.php +++ b/application/controllers/Cis/LvPlan.php @@ -28,12 +28,6 @@ class LvPlan extends Auth_Controller */ public function index() { - - $viewData = array( - 'uid'=>getAuthUID(), - 'timezone' => $this->config->item('timezone') - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'LvPlan']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'LvPlan']); } } diff --git a/application/controllers/Cis/MyLv.php b/application/controllers/Cis/MyLv.php index 819d56b05..0f24d3a80 100644 --- a/application/controllers/Cis/MyLv.php +++ b/application/controllers/Cis/MyLv.php @@ -26,11 +26,6 @@ class MyLv extends Auth_Controller */ public function index() { - - $viewData = array( - - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLv']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'MyLv']); } } diff --git a/application/controllers/Cis/MyLvPlan.php b/application/controllers/Cis/MyLvPlan.php index 366ce8e65..a7f21aa20 100644 --- a/application/controllers/Cis/MyLvPlan.php +++ b/application/controllers/Cis/MyLvPlan.php @@ -27,13 +27,7 @@ class MyLvPlan extends Auth_Controller * @return void */ public function index() - { - - $viewData = array( - 'uid'=>getAuthUID(), - 'timezone' => $this->config->item('timezone') - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLvPlan']); + { + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'MyLvPlan']); } } diff --git a/application/controllers/Cis/OtherLvPlan.php b/application/controllers/Cis/OtherLvPlan.php new file mode 100644 index 000000000..e62644dd1 --- /dev/null +++ b/application/controllers/Cis/OtherLvPlan.php @@ -0,0 +1,34 @@ + ['basis/other_lv_plan:r'] + ]); + + // Load Config + $this->load->config('calendar'); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Public methods + + /** + * @return void + */ + public function index() + { + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'OtherLvPlan']); + } +} diff --git a/application/controllers/Cis/Profil.php b/application/controllers/Cis/Profil.php index c287d87d0..dd7963228 100644 --- a/application/controllers/Cis/Profil.php +++ b/application/controllers/Cis/Profil.php @@ -55,15 +55,7 @@ class Profil extends Auth_Controller */ public function index() { - - $this->load->library('ProfilLib'); - $profil_data = $this->profillib->getView(getAuthUID()); - $profil_data = hasData($profil_data) ? getData($profil_data) : null; - $viewData = array( - 'editable'=>true, - 'profil_data' => $profil_data, - ); - $this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilIndex']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'profilIndex']); } /** @@ -73,23 +65,13 @@ class Profil extends Auth_Controller */ public function View($uid) { - $this->load->library('ProfilLib'); - $profil_data = $this->profillib->getView($uid); - $profil_data = hasData($profil_data) ? getData($profil_data) : null; - $viewData = array ( - 'uid' => $uid, - 'profil_data'=>$profil_data, - ); - if($uid == getAuthUID()){ - $viewData['editable'] = true; - } - $this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilViewUid']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'profilViewUid']); } /** - * checks whether a specific userID is a mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php) + * checks whether a specific userID is a student or not (foreword declaration of the function isStudent in Student_model.php) * @access public - * @param $uid the userID used to check if it is a mitarbeiter + * @param $uid the userID used to check if it is a student * @return boolean */ public function isStudent($uid) @@ -119,7 +101,7 @@ class Profil extends Auth_Controller } /** - * gets the adressen that are marked as zustell from the currenlty logged in user + * gets the adressen that are marked as zustell from the currently logged in user * @access public * @return array a list of adresse_id's */ @@ -262,23 +244,23 @@ class Profil extends Auth_Controller $this->GemeindeModel->addDistinct(); $this->GemeindeModel->addSelect(["name"]); if ($nation == "A") { - if (isset($zip) && $zip > 999 && $zip < 32000) { + if (isset($zip) && $zip > 999 && $zip < 32000) { - $gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]); - if (isError($gemeinde_res)) { - show_error("error while trying to query bis.tbl_gemeinde"); - } - $gemeinde_res = hasData($gemeinde_res) ? getData($gemeinde_res) : null; - $gemeinde_res = array_map(function ($obj) { - return $obj->name; - }, $gemeinde_res); - echo json_encode($gemeinde_res); - - } else { - echo json_encode(error("ortschaftskennziffer code was not valid")); + $gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]); + if (isError($gemeinde_res)) { + show_error("error while trying to query bis.tbl_gemeinde"); } + $gemeinde_res = hasData($gemeinde_res) ? getData($gemeinde_res) : null; + $gemeinde_res = array_map(function ($obj) { + return $obj->name; + }, $gemeinde_res); + echo json_encode($gemeinde_res); + + } else { + echo json_encode(error("ortschaftskennziffer code was not valid")); + } } else { - echo json_encode(error("Nation was not 'A' (Austria)")); + echo json_encode(error("Nation was not 'A' (Austria)")); } } @@ -750,6 +732,4 @@ class Profil extends Auth_Controller $zutrittskarte_ausgegebenam = str_replace("-", ".", $zutrittskarte_ausgegebenam); return $zutrittskarte_ausgegebenam; } - - } diff --git a/application/controllers/Cis/Raumsuche.php b/application/controllers/Cis/Raumsuche.php index 055038275..f48beb0f3 100644 --- a/application/controllers/Cis/Raumsuche.php +++ b/application/controllers/Cis/Raumsuche.php @@ -25,11 +25,6 @@ class Raumsuche extends Auth_Controller */ public function index() { - - $viewData = array( - 'uid'=>getAuthUID(), - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Raumsuche']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'Raumsuche']); } } diff --git a/application/controllers/Cis/StgOrgLvPlan.php b/application/controllers/Cis/StgOrgLvPlan.php new file mode 100644 index 000000000..bf329fcb8 --- /dev/null +++ b/application/controllers/Cis/StgOrgLvPlan.php @@ -0,0 +1,33 @@ + ['basis/cis:r'] + ]); + + // Load Config + $this->load->config('calendar'); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Public methods + + /** + * @return void + */ + public function index() + { + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'StgOrgLvPlan']); + } +} diff --git a/application/controllers/Cis/Studium.php b/application/controllers/Cis/Studium.php index a298de648..20f323dff 100644 --- a/application/controllers/Cis/Studium.php +++ b/application/controllers/Cis/Studium.php @@ -29,10 +29,7 @@ class Studium extends Auth_Controller */ public function index() { - $viewData = array( - - ); - $this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'studium']); + $this->load->view('CisRouterView/CisRouterView.php',['route' => 'studium']); } diff --git a/application/controllers/Cis4.php b/application/controllers/Cis4.php index b7ba2029d..48fd6c240 100644 --- a/application/controllers/Cis4.php +++ b/application/controllers/Cis4.php @@ -1,6 +1,7 @@ 'basis/cis:r' - ) + array( + 'index' => 'basis/cis:r' + ) ); // Load Config @@ -30,16 +31,6 @@ class Cis4 extends Auth_Controller */ public function index() { - $this->load->model('person/Person_model', 'PersonModel'); - $personData = getData($this->PersonModel->getByUid(getAuthUID()))[0]; - - $viewData = array( - 'uid' => getAuthUID(), - 'name' => $personData->vorname, - 'person_id' => $personData->person_id, - 'timezone' => $this->config->item('timezone') - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'FhcDashboard']); + $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'FhcDashboard']); } } diff --git a/application/controllers/CisVue/Dashboard.php b/application/controllers/CisVue/Dashboard.php deleted file mode 100644 index ee830cb8b..000000000 --- a/application/controllers/CisVue/Dashboard.php +++ /dev/null @@ -1,43 +0,0 @@ - 'dashboard/benutzer:r' - ) - ); - } - - // ----------------------------------------------------------------------------------------------------------------- - // Public methods - - /** - * @return void - */ - public function index() - { - - $this->load->model('person/Person_model','PersonModel'); - $personData = getData($this->PersonModel->getByUid(getAuthUID()))[0]; - - $viewData = array( - 'uid' => getAuthUID(), - 'name' => $personData->vorname, - 'person_id' => $personData->person_id - ); - - $this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData]); - - } -} \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 43dc18d1c..759c32d58 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -35,9 +35,12 @@ class Abgabe extends FHCAPI_Controller 'getStudentProjektabgaben' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw', 'basis/abgabe_lektor:rw'), 'postStudentProjektarbeitZwischenabgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'postStudentProjektarbeitEndupload' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), + 'postStudentProjektarbeitTitel' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'getMitarbeiterProjektarbeiten' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'postProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'patchProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'deleteProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'deleteProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'postSerientermin' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'fetchDeadlines' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'getPaAbgabetypen' => self::PERM_LOGGED, @@ -46,7 +49,9 @@ class Abgabe extends FHCAPI_Controller 'getStudiengaenge' => array('basis/abgabe_assistenz:rw'), 'getStudentProjektarbeitAbgabeFile' => array('basis/abgabe_student:rw', 'basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), 'postStudentProjektarbeitZusatzdaten' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), - 'getSignaturStatusForProjektarbeitAbgaben' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw') + 'getSignaturStatusForProjektarbeitAbgaben' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), + 'sendZweitbetreuerTokenMail' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'), + 'fetchProjektarbeitenHistory' => array('basis/abgabe_assistenz:rw') ]); $this->load->library('PhrasesLib'); @@ -90,6 +95,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, @@ -98,6 +104,7 @@ class Abgabe extends FHCAPI_Controller '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); @@ -107,10 +114,18 @@ 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'); + $siginfolink_german = $this->config->item('SIGNATUR_INFO_LINK_GERMAN'); + $siginfolink_english = $this->config->item('SIGNATUR_INFO_LINK_ENGLISH'); + $ret = array( 'moodle_link' => $moodle_link, + 'title_edit_allowed' => $title_edit_allowed, + 'confetti_on_endupload' => $confetti_on_endupload, + 'siginfolink_german' => $siginfolink_german, + 'siginfolink_english' => $siginfolink_english ); $this->terminateWithSuccess($ret); @@ -146,23 +161,26 @@ class Abgabe extends FHCAPI_Controller } $data = getData($res)[0]; $student_uid = $data->uid; - - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - $projektarbeitIsCurrent = false; - $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { - $projektarbeitIsCurrent = $result; - }; - Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); - - $ret = $this->ProjektarbeitModel->getProjektarbeitAbgabetermine($projektarbeit_id); - - foreach ($ret->retval as $termin) { - $this->checkAbgabeSignatur($termin, $projektarbeit->student_uid); - } - - $this->terminateWithSuccess(array($ret, $projektarbeitIsCurrent)); + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); } + + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); + + $ret = $this->ProjektarbeitModel->getProjektarbeitAbgabetermine($projektarbeit_id); + + foreach ($ret->retval as $termin) { + $this->checkAbgabeSignatur($termin, $projektarbeit->student_uid); + } + + $this->terminateWithSuccess(array($ret, $projektarbeitIsCurrent)); + } /** @@ -188,7 +206,7 @@ class Abgabe extends FHCAPI_Controller $result = $this->ProjektarbeitModel->getStudentProjektarbeitenWithBetreuer(getAuthUID()); } - $projektarbeiten = getData($result); + $projektarbeiten = hasData($result) ? getData($result) : array(); if(count($projektarbeiten)) { foreach($projektarbeiten as $pa) { @@ -275,6 +293,8 @@ class Abgabe extends FHCAPI_Controller if(count($data) > 0) { $pa->zweitbetreuer = $data[0]; } + + $pa->zweitbetreuer_mail = $this->getProjektbetreuerEmailByPersonID($pa->zweitbetreuer_person_id); } } } @@ -293,55 +313,62 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->post('projektarbeit_id'); $paabgabe_id = $this->input->post('paabgabe_id'); - $student_uid = $this->input->post('student_uid'); - $bperson_id = $this->input->post('bperson_id'); $paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz'); if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' || $paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '' || $paabgabetyp_kurzbz === NULL || trim((string)$paabgabetyp_kurzbz) === '') { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); + } + $path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - $path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - - if ((isset($_FILES) and isset($_FILES['file']) and ! $_FILES['file']['error'])) { - move_uploaded_file($_FILES['file']['tmp_name'], $path); - - if(file_exists($path)) { - - chmod($path, 0640); - - $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); - $res = $this->PaabgabeModel->update($paabgabe_id, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - )); - - $this->logLib->logInfoDB(array('zwischenupload',$res, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - ), getAuthUID(), getAuthPersonId(), $student_uid)); - - $this->terminateWithSuccess($res); - } else { - $this->terminateWithError('Error moving File', 'general'); - } - + if ((isset($_FILES) and isset($_FILES['file']) and ! $_FILES['file']['error'])) { + move_uploaded_file($_FILES['file']['tmp_name'], $path); + + if(file_exists($path)) { + + chmod($path, 0640); + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + $res = $this->PaabgabeModel->update($paabgabe_id, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + )); + + $this->logLib->logInfoDB(array('zwischenupload',$res, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), $student_uid)); + + $this->terminateWithSuccess($res); } else { - $this->terminateWithError('File missing', 'general'); + $this->terminateWithError('Error moving File', 'general'); } + } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError('File missing', 'general'); } } @@ -355,7 +382,6 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->post('projektarbeit_id'); $paabgabe_id = $this->input->post('paabgabe_id'); - $student_uid = $this->input->post('student_uid'); $sprache = $this->input->post('sprache'); $abstract = $this->input->post('abstract'); $abstract_en = $this->input->post('abstract_en'); @@ -367,7 +393,6 @@ class Abgabe extends FHCAPI_Controller if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' || $paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '' || $paabgabetyp_kurzbz === NULL || trim((string)$paabgabetyp_kurzbz) === '' || $abstract === NULL || $abstract_en === NULL || $schlagwoerter === NULL || $schlagwoerter_en === NULL @@ -379,76 +404,317 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - if ((isset($_FILES) and isset($_FILES['file']) and !$_FILES['file']['error'])) { - move_uploaded_file($_FILES['file']['tmp_name'], PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf'); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); + } - if (file_exists(PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf')) { + if ((isset($_FILES) and isset($_FILES['file']) and !$_FILES['file']['error'])) { + move_uploaded_file($_FILES['file']['tmp_name'], PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf'); - $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + if (file_exists(PAABGABE_PATH . $paabgabe_id . '_' . $student_uid . '.pdf')) { - $result = $this->ProjektarbeitModel->load($projektarbeit_id); - $projektarbeitArr = $this->getDataOrTerminateWithError($result, 'general'); + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - if (count($projektarbeitArr) > 0) { - $projektarbeit = $projektarbeitArr[0]; - } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); - } + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $projektarbeitArr = $this->getDataOrTerminateWithError($result, 'general'); - $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); - $result = $this->PaabgabeModel->load($paabgabe_id); - $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); - - if (count($paabgabeArr) > 0) { - $paabgabe = $paabgabeArr[0]; - } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); - } - - $this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid); - $signaturstatus = $paabgabe->signatur; - - // update projektarbeit cols - $this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id, $sprache, $abstract, $abstract_en - , $schlagwoerter, $schlagwoerter_en, $seitenanzahl); - - - // update paabgabe datum - $res = $this->PaabgabeModel->update($paabgabe_id, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - )); - - $res = $this->PaabgabeModel->load($res->retval); - $abgabe = getData($res)[0]; - $abgabe->signatur = $signaturstatus; - - $this->sendUploadEmail($bperson_id, $projektarbeit_id, $paabgabetyp_kurzbz, $student_uid); - - $this->logLib->logInfoDB(array('endupload', $res, array( - 'abgabedatum' => date('Y-m-d'), - 'updatevon' => getAuthUID(), - 'updateamum' => date('Y-m-d H:i:s') - ), getAuthUID(), getAuthPersonId(), array($projektarbeit_id, $sprache, $abstract, $abstract_en - , $schlagwoerter, $schlagwoerter_en, $seitenanzahl))); - - $this->terminateWithSuccess($abgabe); + if (count($projektarbeitArr) > 0) { + $projektarbeit = $projektarbeitArr[0]; } else { - $this->terminateWithError('Error moving File', 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); } + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + $result = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); + + if (count($paabgabeArr) > 0) { + $paabgabe = $paabgabeArr[0]; + } else { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); + } + + $this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid); + $signaturstatus = $paabgabe->signatur; + + // update projektarbeit cols with zusatzdaten AND abgabedatum! + $this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array( + 'sprache' => $sprache, + 'seitenanzahl' => $seitenanzahl, + 'abgabedatum' => date('Y-m-d'), + 'schlagwoerter_en' => $schlagwoerter_en, + 'schlagwoerter' => $schlagwoerter, + 'abstract' => $abstract, + 'abstract_en' => $abstract_en + )); + + + // update paabgabe datum + $res = $this->PaabgabeModel->update($paabgabe_id, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + )); + + $res = $this->PaabgabeModel->load($res->retval); + $abgabe = getData($res)[0]; + $abgabe->signatur = $signaturstatus; + + $this->sendUploadEmail($bperson_id, $projektarbeit_id, $paabgabetyp_kurzbz, $student_uid); + + $this->logLib->logInfoDB(array('endupload', $res, array( + 'abgabedatum' => date('Y-m-d'), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId(), array($projektarbeit_id, $sprache, $abstract, $abstract_en + , $schlagwoerter, $schlagwoerter_en, $seitenanzahl))); + + $this->terminateWithSuccess($abgabe); } else { - $this->terminateWithError('File missing', 'general'); + $this->terminateWithError('Error moving File', 'general'); } + } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError('File missing', 'general'); } } - + + /** + * POST METHOD + * allows a student (or assistenz on their behalf) to update the titel of their own projektarbeit. + * blocked once the projektarbeit has been graded (checkProjektarbeitForFinishedStatus). + */ + public function postStudentProjektarbeitTitel() + { + 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'); + + if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' + || $titel === NULL || trim((string)$titel) === '') { + $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'); + } + + // Reject emojis and pictographs + // allows foreign letters, math symbols, accents, and standard punctuation. + $emojiPattern = '/[\x{1F300}-\x{1F5FF}\x{1F600}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{1F900}-\x{1FAFF}\x{23E9}-\x{23EF}\x{2b50}\x{2700}-\x{27BF}]/u'; + + // i would like this much more but our server does not recognize this utf-8 character range this way, so hexcodes it is +// if (preg_match('/\p{Extended_Pictographic}/u', $titel)) { + if (preg_match($emojiPattern, $titel)) { + + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + + // Verify the projektarbeit actually belongs to the supplied student_uid + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); + } + $assignedStudentUid = getData($res)[0]->uid; + + // A student may only update their own title; assistenz is covered by checkZuordnung + + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); + if (getAuthUID() !== $assignedStudentUid && !$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $data = getData($result); + + $oldTitle = $data[0]->titel ?? ''; + + $result = $this->ProjektarbeitModel->update( + $projektarbeit_id, + array( + 'titel' => $titel, + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + + $this->logLib->logInfoDB(array( + 'titelUpdate', + array( + 'projektarbeit_id' => $projektarbeit_id, + 'titel' => $titel, + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), + getAuthUID(), + getAuthPersonId() + )); + + $this->sendTitelChangedEmail( + $projektarbeit_id, + $titel, + $oldTitle, + $assignedStudentUid + ); + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $titel = hasData($result) ? getData($result)[0]->titel : $titel; + $this->terminateWithSuccess($titel); + } + + /** + * Notifies all betreuer of a projektarbeit and all assistenzen responsible for its studiengang + * when a student updates the titel of their projektarbeit. + * + * Betreuer retrieval mirrors AbgabetoolJob->notifyBetreuerAboutChangedAbgaben. + * Assistenz retrieval mirrors AbgabetoolJob->notifyAssistenzAboutChangedAbgaben. + * + * @param int $projektarbeit_id + * @param string $new_titel The titel as it was saved + * @param string $student_uid + */ + private function sendTitelChangedEmail($projektarbeit_id, $new_titel, $old_titel, $student_uid) + { + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('person/Person_model', 'PersonModel'); + + $this->load->model('person/Person_model', 'PersonModel'); + $studentNameResult = $this->PersonModel->getFullName($student_uid); + $studentFullName = hasData($studentNameResult) ? getData($studentNameResult) : $student_uid; + + $studentInfoResult = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($studentInfoResult) || !hasData($studentInfoResult)) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: student info not found', $projektarbeit_id)); + return; + } + $studentInfo = getData($studentInfoResult)[0]; + $studiengang_kz = $studentInfo->studiengang_kz; + + $stgResult = $this->StudiengangModel->load($studiengang_kz); + $oe_kurzbz = null; + if (!isError($stgResult) && hasData($stgResult)) { + $oe_kurzbz = getData($stgResult)[0]->oe_kurzbz ?? null; + } + + // build shared mail data + $base_mail_data = array( + 'studentFullName' => $studentFullName, + 'new_titel' => $new_titel, + 'old_titel' => $old_titel + ); + + // notify all betreuer + $betreuerResult = $this->ProjektbetreuerModel->getAllBetreuerOfProjektarbeit($projektarbeit_id); + if (!isError($betreuerResult) && hasData($betreuerResult)) { + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_MITARBEITER'); + + foreach (getData($betreuerResult) as $betreuer) { + $email = $betreuer->uid ? $betreuer->uid . '@' . DOMAIN : ($betreuer->private_email ?? null); + if (!$email) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no email for betreuer', $betreuer->person_id)); + continue; + } + + $anredeResult = $this->ProjektarbeitModel->getProjektbetreuerAnrede($betreuer->person_id); + $anrede = (!isError($anredeResult) && hasData($anredeResult)) ? getData($anredeResult)[0] : null; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => ($anrede->anrede ?? '') === 'Herr' ? 'r' : '', + 'anrede' => $anrede->anrede ?? '', + 'fullFormattedNameString' => $anrede->first ?? $email, + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // notify assistenz for the studiengang OE + if (!$oe_kurzbz) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no oe_kurzbz resolved, skipping assistenz', $studiengang_kz)); + return; + } + + $assistenzResult = $this->OrganisationseinheitModel->getAssistenzForOE($oe_kurzbz); + if (isError($assistenzResult) || !hasData($assistenzResult)) { + return; + } + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_ASSISTENZ'); + + // similar pattern as job uses via the assistenzMap + $sentTo = []; + foreach (getData($assistenzResult) as $assistenz) { + if (in_array($assistenz->person_id, $sentTo)) { + continue; + } + $sentTo[] = $assistenz->person_id; + + $email = $assistenz->uid . '@' . DOMAIN; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => $assistenz->anrede === 'Herr' ? 'r' : '', + 'anrede' => $assistenz->anrede ?? '', + 'fullFormattedNameString' => $assistenz->first ?? ($assistenz->uid . '@' . DOMAIN), + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // validate paabgabe deadline against servertime just in case a student spoofs their local clock and thus // unlocks the upload ui private function checkPaabgabeDeadline($paabgabe_id) { @@ -504,7 +770,7 @@ class Abgabe extends FHCAPI_Controller $showAllBool = false; } - $projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool); + $projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthPersonId(), $showAllBool); $mapFunc = function($projektarbeit) { @@ -519,20 +785,18 @@ class Abgabe extends FHCAPI_Controller forEach($projektarbeiten->retval as $pa) { - $result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id); - $anredeArr = $this->getDataOrTerminateWithError($result, 'general'); - $pa->betreuer = $anredeArr[0]; - $oldLink = ''; // show this when paIsCurrent == false -> moodle course template $newLink = ''; // get curated path for betreuer type - $returnFunc = function ( $resultOld, $resultNew) use (&$oldLink, &$newLink) { + $returnFunc = function ($resultOld, $resultNew) use (&$oldLink, &$newLink) { $newLink = $resultNew; $oldLink = $resultOld; }; - Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc); + $own_betreuerart_kurzbz = $pa->betreuer_person_id == getAuthPersonId() ? $pa->betreuer_betreuerart_kurzbz : $pa->zweitbetreuer_betreuerart_kurzbz; + Events::trigger('projektbeurteilung_formular_link', $own_betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc); $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + $pa->betreuerart_kurzbz = $own_betreuerart_kurzbz; // has previously been retrieved via getStudentProjektabgaben but is fetched in advance to avoid having to reload abgaben $projektarbeitIsCurrent = false; @@ -579,9 +843,22 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid])); } $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); @@ -618,12 +895,22 @@ class Abgabe extends FHCAPI_Controller // load existing entry of paabgabe and check if note has changed to negativ, to avoid sending when // only notiz has changed. - // TODO: what if paabgabe is a qualgate1, is benotet negativ and then its type is changed to gate2? - $existingResult = $this->PaabgabeModel->load($paabgabe_id); $existingPaabgabeArr = getData($existingResult); if(count($existingPaabgabeArr) > 0) $existingPaabgabe = $existingPaabgabeArr[0]; + if($existingPaabgabe->note !== null || $existingPaabgabe->abgabedatum !== null) { + // check if a change of paabgabetyp is being attempted -> not allowed at this point + if($paabgabetyp_kurzbz !== $existingPaabgabe->paabgabetyp_kurzbz) { + $this->terminateWithError($this->p->t('abgabetool', 'c4abgabetypAendernNichtErlaubt')); + } + + // check if a change of deadline aka datum is being attempted -> notallowed at this point + if($datum !== $existingPaabgabe->datum) { + $this->terminateWithError($this->p->t('abgabetool', 'c4datumAendernNichtErlaubt')); + } + } + $result = $this->PaabgabeModel->update( $paabgabe_id, array( @@ -687,6 +974,112 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithSuccess([$paabgabe, $existingPaabgabe]); } + /** + * called by abgabetool/assistenz when bulk-editing multiple abgabetermine via the flat termine table view + * only fields present in the payload are updated - absent fields are left untouched + */ + public function patchProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + // collect only fields that were actually sent + $updateFields = []; + + $datum = $this->input->post('datum'); + if ($datum !== NULL && trim((string)$datum) !== '') { + $updateFields['datum'] = $datum; + } + + $paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz'); + if ($paabgabetyp_kurzbz !== NULL && trim((string)$paabgabetyp_kurzbz) !== '') { + $updateFields['paabgabetyp_kurzbz'] = $paabgabetyp_kurzbz; + } + + $kurzbz = $this->input->post('kurzbz'); + if ($kurzbz !== NULL) { + $updateFields['kurzbz'] = $kurzbz; + } + + // booleans: only include if explicitly posted + $upload_allowed = $this->input->post('upload_allowed'); + if ($upload_allowed !== NULL) { + $updateFields['upload_allowed'] = filter_var($upload_allowed, FILTER_VALIDATE_BOOLEAN); + } + + $fixtermin = $this->input->post('fixtermin'); + if ($fixtermin !== NULL) { + $updateFields['fixtermin'] = filter_var($fixtermin, FILTER_VALIDATE_BOOLEAN); + } + + if (empty($updateFields)) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->update( + $paabgabe_id, + array_merge($updateFields, [ + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ]) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + $results[] = getData($this->PaabgabeModel->load($paabgabe_id))[0]; + + $this->logLib->logInfoDB(array( + 'paabgabe bulk updated', + $paabgabe_id, + $updateFields, + getAuthUID(), + getAuthPersonId() + )); + } + + $this->terminateWithSuccess($results); + } + /** * called by abgabetool/mitarbeiter in mitarbeiterdetail.js when deleting an abgabetermin * deletion is only possible if user is assistenz OR betreuer deletes their own custom termin @@ -699,12 +1092,28 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } - $this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id)); + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID()); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); @@ -719,11 +1128,69 @@ class Abgabe extends FHCAPI_Controller $result = $this->PaabgabeModel->delete($paabgabe_id); $result = $this->getDataOrTerminateWithError($result, 'general'); - // TODO: consider this in nightly email job $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($result); } + /** + * called by abgabetool/assistenz when deleting multiple abgabetermine via the flat termine table view + */ + public function deleteProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id)); + + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID(), $student_uid, $studiengang_kz); + + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) == 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->delete($paabgabe_id); + $result = $this->getDataOrTerminateWithError($result, 'general'); + $results[] = $result; + + $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); + } + + $this->terminateWithSuccess($results); + } + /** * endpoint for adding the same paabgabe for multiple projektarbeiten * can be slow for large n since it queries twice per projektarbeit_id @@ -757,16 +1224,27 @@ class Abgabe extends FHCAPI_Controller // and should never unintentionally happen $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - - $res = []; + $abgaben = []; foreach ($projektarbeit_ids as $projektarbeit_id) { $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + // load the $student_uid by $projektarbeit_id so we dont need any post params + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } $result = $this->PaabgabeModel->insert( @@ -847,10 +1325,10 @@ class Abgabe extends FHCAPI_Controller private function getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id) { $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); $result = $this->ProjektarbeitModel->getProjektbetreuerEmail($projektarbeit_id); - if(count($result->retval) > 0) { + if(hasData($result)) { $email = getData($result); return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email; - } else return ''; + } else return null; } @@ -861,9 +1339,12 @@ class Abgabe extends FHCAPI_Controller private function getProjektbetreuerEmailByPersonID($person_id) { $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); $result = $this->ProjektarbeitModel->getProjektbetreuerEmailByPersonID($person_id); - $email = $this->getDataOrTerminateWithError($result, 'general'); - - return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email; + if(hasData($result)) { + $email = getData($result); + return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email; + } else { + return null; + } } //TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API @@ -1034,30 +1515,34 @@ class Abgabe extends FHCAPI_Controller $projektarbeit_id = $this->input->get('projektarbeit_id'); $paabgabe_id = $this->input->get('paabgabe_id'); - $student_uid = $this->input->get('student_uid'); if ($paabgabe_id === NULL || trim((string)$paabgabe_id) === '' - || $projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' - || $student_uid === NULL || trim((string)$student_uid) === '') { + || $projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '') { $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - - // zuordnung function is supposed for mitarbeiter_uids, students should be allowed to download their own files - // without adapting zuordnung logic - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); - if(getAuthUID() == $student_uid || $zugeordnet) { - $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; - - - if(file_exists($file_path)) { - $this->terminateWithFileOutput('application/octet-stream', file_get_contents($file_path), basename($file_path)); - } else { - $this->terminateWithError('File not found', 'general'); - } + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)){ + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); + } + + $file_path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf'; + + if(file_exists($file_path)) { + $this->terminateWithFileOutput('application/octet-stream', file_get_contents($file_path), basename($file_path)); } else { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError('File not found', 'general'); } } @@ -1101,14 +1586,33 @@ class Abgabe extends FHCAPI_Controller $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); - $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + // load the $student_uid by $projektarbeit_id so we dont need any post params + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); if(!$zugeordnet) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]), 'general'); } - // update projektarbeit cols - $this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id,$sprache,$abstract,$abstract_en - ,$schlagwoerter, $schlagwoerter_en, $seitenanzahl); + // update projektarbeit cols with zusatzdaten only + $this->ProjektarbeitModel->update($projektarbeit_id, array( + 'sprache' => $sprache, + 'seitenanzahl' => $seitenanzahl, + 'schlagwoerter_en' => $schlagwoerter_en, + 'schlagwoerter' => $schlagwoerter, + 'abstract' => $abstract, + 'abstract_en' => $abstract_en + )); $this->logLib->logInfoDB(array('zusatzdatenEditMitarbeiter', array( 'updatevon' => getAuthUID(), @@ -1144,6 +1648,69 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithSuccess($data); } + public function sendZweitbetreuerTokenMail() { + $projektarbeit_id = $this->input->post('projektarbeit_id'); + $bperson_id = $this->input->post('bperson_id'); + + + if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' + || $bperson_id === NULL || trim((string)$bperson_id) === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if(isError($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); + } + + if(!hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); + } + $data = getData($res)[0]; + $student_uid = $data->uid; + $studiengang_kz = $data->studiengang_kz; + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz); + if(!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent')); + } + + $this->sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $student_uid); + } + + // basically what getStudentProjektarbeiten intends to do for the student page but + // designed for the assistenz timeline component -> more extensive loading upfront + // with every abgabetermin fetched here aswell + public function fetchProjektarbeitenHistory() { + $student_uid = $this->input->post('student_uid'); + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + + $projektarbeiten = $this->ProjektarbeitModel->getProjektarbeitenForStudent($student_uid); + + $mapFunc = function($projektarbeit) { + return $projektarbeit->projektarbeit_id; + }; + $projektarbeiten_ids = array_map($mapFunc, $projektarbeiten->retval); + + if(count($projektarbeiten_ids) > 0) { + $ret = $this->ProjektarbeitModel->getProjektarbeitenAbgabetermine($projektarbeiten_ids); + $projektabgaben = $this->getDataOrTerminateWithError($ret, 'general'); + } + + forEach($projektarbeiten->retval as $pa) { + + $filterFunc = function($projektabgabe) use ($pa) { + return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; + }; + + $pa->abgabetermine = array_values(array_filter($projektabgaben, $filterFunc)); + } + + $this->terminateWithSuccess($projektarbeiten->retval); + } + /** * helper function to check the signature status of uploaded files for zwischenabgabe & endupload */ @@ -1206,14 +1773,7 @@ class Abgabe extends FHCAPI_Controller }; Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); if(!$projektarbeitIsCurrent) { - $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general'); - } - - // Link to Abgabetool - if (defined('CIS4') && CIS4) { - $ci3BootstrapFilePath = "cis.php"; - } else { - $ci3BootstrapFilePath = "index.ci.php"; + $this->terminateWithError($this->p->t('abgabetool','c4fehlerPAIsNotCurrent'), 'general'); } $path = $this->config->item('URL_MITARBEITER'); @@ -1227,7 +1787,6 @@ class Abgabe extends FHCAPI_Controller // 1. Begutachter mail ohne Token $mail_baselink = APP_ROOT.$this->config->item('PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER'); -// $mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter"; $mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$studentUser->uid; $projekttyp_kurzbz = $projektarbeit->projekttyp_kurzbz; $subject = $projektarbeit->projekttyp_kurzbz == 'Diplom' ? 'Masterarbeitsbetreuung' : 'Bachelorarbeitsbetreuung'; @@ -1247,7 +1806,7 @@ class Abgabe extends FHCAPI_Controller $email = $this->getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id); if(!$email) $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); - + $mailres = sendSanchoMail( 'ParbeitsbeurteilungEndupload', $maildata, @@ -1265,89 +1824,115 @@ class Abgabe extends FHCAPI_Controller // 2. Begutachter mail, wenn Endabgabe, mit Token wenn extern if ($paabgabetyp_kurzbz == 'end') { - // Zweitbegutachter holen - $this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel'); - $zweitbegutachterRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $studentUser->uid)); + $this->sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $studentUser->uid); - if ($zweitbegutachterRetval && count($zweitbegutachterRetval) > 0) - { - - foreach ($zweitbegutachterRetval as $begutachter) - { - // token generieren, wenn noch nicht vorhanden und notwendig (wird in methode überprüft) - $tokenGenRes = $this->ProjektbetreuerModel->generateZweitbegutachterToken($begutachter->person_id, $projektarbeit_id); - - if (!$tokenGenRes) - { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general'); - } - - $begutachterMitTokenRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $studentUser->uid, $begutachter->person_id)); - - if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0) - { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general'); - } - - $begutachterMitToken = $begutachterMitTokenRetval[0]; - - $path = $begutachterMitToken->betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'; - $mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/$path"; - $mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$studentUser->uid; - $intern = isset($begutachterMitToken->uid); - $mail_link = $intern ? $mail_fulllink : $mail_baselink; - - $zweitbetmaildata = array(); - $zweitbetmaildata['geehrt'] = "geehrte" . ($begutachterMitToken->anrede == "Herr" ? "r" : ""); - $zweitbetmaildata['anrede'] = $begutachterMitToken->anrede; - $zweitbetmaildata['betreuer_voller_name'] = $begutachterMitToken->voller_name; - $zweitbetmaildata['student_anrede'] = $maildata['student_anrede']; - $zweitbetmaildata['student_voller_name'] = $maildata['student_voller_name']; - $zweitbetmaildata['abgabetyp'] = $abgabetyp; - $zweitbetmaildata['parbeituebersichtlink'] = $intern ? $maildata['parbeituebersichtlink'] : ""; - $zweitbetmaildata['bewertunglink'] = $projektarbeitIsCurrent ? "

Zur Beurteilung der Arbeit

" : ""; - $zweitbetmaildata['token'] = $projektarbeitIsCurrent && isset($begutachterMitToken->zugangstoken) && !$intern ? "

Zugangstoken: " . $begutachterMitToken->zugangstoken . "

" : ""; - - $mailres = sendSanchoMail( - 'ParbeitsbeurteilungEndupload', - $zweitbetmaildata, - $begutachterMitToken->email, - $subject, - 'sancho_header_min_bw.jpg', - 'sancho_footer_min_bw.jpg', - get_uid()."@".DOMAIN - ); - - if (!$mailres) - { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); - } - - } - } } } } - private function checkZuordnung($projektarbeit_id, $betreuer_uid) { + private function sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $student_uid) { + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $projektarbeitArr = $this->getDataOrTerminateWithError($this->ProjektarbeitModel->load($projektarbeit_id)); + if(count($projektarbeitArr) > 0) { + $projektarbeit = $projektarbeitArr[0]; + } else { + $this->terminateWithError($this->p->t('abgabetool','c4projektarbeitNichtGefunden'), 'general'); + } + + // Zweitbegutachter holen + $this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel'); + $zweitbegutachterRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $student_uid)); + + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc); + if(!$projektarbeitIsCurrent) { + $this->terminateWithError($this->p->t('abgabetool','c4fehlerPAIsNotCurrent'), 'general'); + } + + if ($zweitbegutachterRetval && count($zweitbegutachterRetval) > 0) + { + + foreach ($zweitbegutachterRetval as $begutachter) + { + // token generieren, wenn noch nicht vorhanden und notwendig (wird in methode überprüft) + $tokenGenRes = $this->ProjektbetreuerModel->generateZweitbegutachterToken($begutachter->person_id, $projektarbeit_id); + + if (!$tokenGenRes) + { + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general'); + } + + $begutachterMitTokenRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $student_uid, $begutachter->person_id)); + + if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0) + { + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general'); + } + + $begutachterMitToken = $begutachterMitTokenRetval[0]; + + $studentUser = $this->ProjektarbeitModel->getProjektarbeitBenutzer($student_uid)->retval[0]; + + $path = $begutachterMitToken->betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter'; + $mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/$path"; + $mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$student_uid; + $intern = isset($begutachterMitToken->uid); + $mail_link = $intern ? $mail_fulllink : $mail_baselink; + + // automatic email ensures that, client only exposes this method if that happened already + $paabgabetyp_kurzbz = 'end'; + $abgabetyp = $paabgabetyp_kurzbz == 'end' ? 'Endabgabe' : 'Zwischenabgabe'; + + $path = $this->config->item('URL_MITARBEITER'); + $url = APP_ROOT.$path; + + $subject = $projektarbeit->projekttyp_kurzbz == 'Diplom' ? 'Masterarbeitsbetreuung' : 'Bachelorarbeitsbetreuung'; + + $zweitbetmaildata = array(); + $zweitbetmaildata['geehrt'] = "geehrte" . ($begutachterMitToken->anrede == "Herr" ? "r" : ""); + $zweitbetmaildata['anrede'] = $begutachterMitToken->anrede; + $zweitbetmaildata['betreuer_voller_name'] = $begutachterMitToken->voller_name; + $zweitbetmaildata['student_anrede'] = $studentUser->anrede; + $zweitbetmaildata['student_voller_name'] = trim($studentUser->titelpre." ".$studentUser->vorname." ".$studentUser->nachname." ".$studentUser->titelpost); + $zweitbetmaildata['abgabetyp'] = $abgabetyp; + $zweitbetmaildata['parbeituebersichtlink'] = $intern ? "

Zur Projektarbeitsübersicht

" : ""; + $zweitbetmaildata['bewertunglink'] = $projektarbeitIsCurrent ? "

Zur Beurteilung der Arbeit

" : ""; + $zweitbetmaildata['token'] = $projektarbeitIsCurrent && isset($begutachterMitToken->zugangstoken) && !$intern ? "

Zugangstoken: " . $begutachterMitToken->zugangstoken . "

" : ""; + + $this->addMeta('$zweitbetmaildata', $zweitbetmaildata); + + $mailres = sendSanchoMail( + 'ParbeitsbeurteilungEndupload', + $zweitbetmaildata, + $begutachterMitToken->email, + $subject, + 'sancho_header_min_bw.jpg', + 'sancho_footer_min_bw.jpg', + get_uid()."@".DOMAIN + ); + + if (!$mailres) + { + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); + } + + } + } + } + + private function checkZuordnung($projektarbeit_id, $auth_uid, $student_uid, $studiengang_kz) { // check if authenticated user is zugewiesen as betreuer to projektarbeit or has admin/assistenz berechtigung // over the studiengang of the student working on that projektarbeit_id - $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); - $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); - if(isError($res)) { - $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingStudentForProjektarbeitID'), 'general'); - } + // student itself is always considered zugeordnet towards their own projektarbeit + if($auth_uid == $student_uid) return true; - if(!hasData($res)) { - $this->terminateWithError($this->p->t('abgabetool', 'c4noAssignedStudentForProjektarbeitID'), 'general'); - } - $data = getData($res)[0]; - $student_uid = $data->uid; - $studiengang_kz = $data->studiengang_kz; - - $res = $this->ProjektarbeitModel->checkZuordnung($student_uid, $betreuer_uid); + // if auth_uid is of a betreuer, look for actual betreuungszuordnung between those ids + $res = $this->ProjektarbeitModel->checkZuordnung($student_uid, $auth_uid); if(isError($res)) { $this->terminateWithError($this->p->t('abgabetool', 'c4errorLoadingBetreuerStudentZuordnung'), 'general'); } @@ -1394,9 +1979,9 @@ class Abgabe extends FHCAPI_Controller return $data->projektarbeit_id; } - private function checkZuordnungByPaabgabe($paabgabe_id, $betreuer_uid) { + private function checkZuordnungByPaabgabe($paabgabe_id, $auth_uid, $student_uid, $studiengang_kz) { $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); - return $this->checkZuordnung($projektarbeit_id, $betreuer_uid); + return $this->checkZuordnung($projektarbeit_id, $auth_uid, $student_uid, $studiengang_kz); } // loads a projektarbeit table row by id and looks if a note has been set. A non null note field @@ -1411,7 +1996,13 @@ class Abgabe extends FHCAPI_Controller $data = getData($res)[0]; if($data->note !== NULL) { - $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general'); + // hardcode this error msg cause phrasen arent reliable and people keep bugging why the cant edit old entries they definitely shouldnt update + $message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv3'); + if(strpos($message, "<<") === 0) { // phrase could not be loaded + $this->terminateWithError('Die Projektarbeit wurde bereits benotet, Sie dürfen deshalb keine weiteren Termine anlegen oder bearbeiten.', 'general'); + } else { + $this->terminateWithError($message); + } } } diff --git a/application/controllers/api/frontend/v1/Bookmark.php b/application/controllers/api/frontend/v1/Bookmark.php index 3e646bb51..aa45709d4 100644 --- a/application/controllers/api/frontend/v1/Bookmark.php +++ b/application/controllers/api/frontend/v1/Bookmark.php @@ -18,6 +18,8 @@ if (! defined('BASEPATH')) exit('No direct script access allowed'); +use \DateTime as DateTime; + class Bookmark extends FHCAPI_Controller { @@ -28,111 +30,162 @@ class Bookmark extends FHCAPI_Controller { parent::__construct([ 'getBookmarks' => self::PERM_LOGGED, - 'delete' => self::PERM_LOGGED, - 'insert' => self::PERM_LOGGED, + 'delete' => self::PERM_LOGGED, + 'insert' => self::PERM_LOGGED, 'update' => self::PERM_LOGGED, - ]); + 'changeOrder' => self::PERM_LOGGED + ]); $this->load->model('dashboard/Bookmark_model', 'BookmarkModel'); $this->uid = getAuthUID(); $this->pid = getAuthPersonID(); - } //------------------------------------------------------------------------------------------------------------------ // Public methods - /** - * gets the bookmarks associated to a user + /** + * gets the bookmarks associated to a user * @access public * @return void */ public function getBookmarks() { - $this->BookmarkModel->addOrder("bookmark_id"); + $this->BookmarkModel->addOrder("sort"); $bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]); - $bookmarks = $this->getDataOrTerminateWithError($bookmarks); + $bookmarks = $this->getDataOrTerminateWithError($bookmarks); - $this->terminateWithSuccess($bookmarks); - } - - /** - * deletes bookmark from associated user - * @access public - * @return void - */ - public function delete($bookmark_id) - { - $bookmark = $this->BookmarkModel->load($bookmark_id); - - $bookmark = current($this->getDataOrTerminateWithError($bookmark)); - - // only delete bookmark if the user is the owner of the bookmark - if($bookmark->uid == $this->uid || $this->permissionlib->isBerechtigt('admin')){ - - $delete_result = $this->BookmarkModel->delete($bookmark_id); - - $delete_result = $this->getDataOrTerminateWithError($delete_result); - - $this->terminateWithSuccess($delete_result); - }else{ - $this->_outputAuthError(['delete' => ['admin:rw']]); - } - } - - /** - * inserts new bookmark into the bookmark table - * @access public - * @return void - */ - public function insert() - { - // form validation - $this->load->library('form_validation'); - $this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]'); - $this->form_validation->set_rules('title', 'Title', 'required|max_length[255]'); - if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); - - $url = $this->input->post('url',true); - $title = $this->input->post('title',true); - $tag = $this->input->post('tag', true); - - $insert_into_result = $this->BookmarkModel->insert(['uid'=>$this->uid, 'url'=>$url, 'title'=>$title,'tag'=>$tag, 'insertvon'=>$this->uid, 'updateamum'=>NULL, 'updatevon'=>NULL]); - - $insert_into_result = $this->getDataOrTerminateWithError($insert_into_result); - - $this->terminateWithSuccess($insert_into_result); - - } + $this->terminateWithSuccess($bookmarks); + } /** - * updates bookmark in the bookmark table + * deletes bookmark from associated user * @access public * @return void */ - public function update($bookmark_id) + public function delete($bookmark_id) { - // form validation - $this->load->library('form_validation'); - $this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]'); - $this->form_validation->set_rules('title', 'Title', 'required|max_length[255]'); - if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array()); + $bookmark = $this->BookmarkModel->load($bookmark_id); - $url = $this->input->post('url',true); - $title = $this->input->post('title',true); + $bookmark = current($this->getDataOrTerminateWithError($bookmark)); + + // only delete bookmark if the user is the owner of the bookmark + if ($bookmark->uid == $this->uid || $this->permissionlib->isBerechtigt('admin')) { + $delete_result = $this->BookmarkModel->delete($bookmark_id); + + $delete_result = $this->getDataOrTerminateWithError($delete_result); + + $this->terminateWithSuccess($delete_result); + } else { + $this->_outputAuthError(['delete' => ['admin:rw']]); + } + } + + /** + * inserts new bookmark into the bookmark table + * @access public + * @return void + */ + public function insert() + { + // form validation + $this->load->library('form_validation'); + $this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]'); + $this->form_validation->set_rules('title', 'Title', 'required|max_length[255]'); + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + $url = $this->input->post('url', true); + $title = $this->input->post('title', true); + $tag = $this->input->post('tag', true); + if (is_array($tag)) { + $tag = json_encode($tag); // convert PHP array to JSON string + } + $sort = $this->input->post('sort', true); + + $insert_into_result = $this->BookmarkModel->insert([ + 'uid' => $this->uid, + 'url' => $url, + 'title' => $title, + 'tag' => $tag, + 'insertvon' => $this->uid, + 'updateamum' => null, + 'updatevon' => null, + 'sort' => $sort + ]); + + $insert_into_result = $this->getDataOrTerminateWithError($insert_into_result); + + $this->terminateWithSuccess($insert_into_result); + } + + /** + * updates bookmark in the bookmark table + * @access public + * @return void + */ + public function update($bookmark_id) + { + // form validation + $this->load->library('form_validation'); + $this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]'); + $this->form_validation->set_rules('title', 'Title', 'required|max_length[255]'); + if (!$this->form_validation->run()) + $this->terminateWithValidationErrors($this->form_validation->error_array()); + + $url = $this->input->post('url', true); + $title = $this->input->post('title', true); + $tag = $this->input->post('tag', true); + if (is_array($tag)) { + $tag = json_encode($tag); + } $now = new DateTime(); $now = $now->format('Y-m-d H:i:s'); - $update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title,'updateamum'=>$now]); + $update_result = $this->BookmarkModel->update($bookmark_id, [ + 'url' => $url, + 'title' => $title, + 'tag' => $tag, + 'updateamum' => $now + ]); - $update_result = $this->getDataOrTerminateWithError($update_result); + $update_result = $this->getDataOrTerminateWithError($update_result); - $this->terminateWithSuccess($update_result); + $this->terminateWithSuccess($update_result); + } - } + /** + * changes sort of two bookmarks in the bookmark table + * @access public + * @return void + */ + public function changeOrder($bookmark_id1, $bookmark_id2) + { + $update_result = []; + + $result1 = $this->BookmarkModel->load($bookmark_id1); + $data1 = $this->getDataOrTerminateWithError($result1); + $sort1 = current($data1)->sort; + + $result2 = $this->BookmarkModel->load(["bookmark_id"=>$bookmark_id2]); + $data2 = $this->getDataOrTerminateWithError($result2); + $sort2 = current($data2)->sort; + + $update_result1 = $this->BookmarkModel->update($bookmark_id1, [ + 'sort' => $sort2 + ]); + $update_result[] = $this->getDataOrTerminateWithError($update_result1); + + $update_result2 = $this->BookmarkModel->update($bookmark_id2, [ + 'sort' => $sort1 + ]); + $update_result[] = $this->getDataOrTerminateWithError($update_result2); + + $this->terminateWithSuccess($update_result); + } } diff --git a/application/controllers/api/frontend/v1/Cis4FhcApi.php b/application/controllers/api/frontend/v1/Cis4FhcApi.php index 372e4bfaa..4d0f906f0 100644 --- a/application/controllers/api/frontend/v1/Cis4FhcApi.php +++ b/application/controllers/api/frontend/v1/Cis4FhcApi.php @@ -27,7 +27,7 @@ class Cis4FhcApi extends FHCAPI_Controller public function __construct() { parent::__construct([ - 'getViewData' => self::PERM_LOGGED, + 'dashboardViewData' => self::PERM_LOGGED, ]); } @@ -36,17 +36,22 @@ class Cis4FhcApi extends FHCAPI_Controller // Public methods /** - * fetches ViewData - */ - public function getViewData() + * retrieves view data for dashboard view + * @access public + * @param $uid the userID for which profile is being viewed, null or missing value implies one's own profile + */ + public function dashboardViewData() { $this->load->model('person/Person_model','PersonModel'); $personData = getData($this->PersonModel->getByUid(getAuthUID()))[0]; + $this->load->config('calendar'); + $viewData = array( 'uid' => getAuthUID(), 'name' => $personData->vorname, - 'person_id' => $personData->person_id + 'person_id' => $personData->person_id, + 'timezone' => $this->config->item('timezone'), ); $this->terminateWithSuccess($viewData); diff --git a/application/controllers/api/frontend/v1/CisMenu.php b/application/controllers/api/frontend/v1/CisMenu.php index 4f4f2573e..163c337ae 100644 --- a/application/controllers/api/frontend/v1/CisMenu.php +++ b/application/controllers/api/frontend/v1/CisMenu.php @@ -16,12 +16,13 @@ * along with this program. If not, see . */ -if (! defined('BASEPATH')) exit('No direct script access allowed'); +if (!defined('BASEPATH')) + exit('No direct script access allowed'); class CisMenu extends FHCAPI_Controller { - + /** * Object initialization */ @@ -31,28 +32,95 @@ class CisMenu extends FHCAPI_Controller 'getMenu' => self::PERM_LOGGED, ]); - + } //------------------------------------------------------------------------------------------------------------------ // Public methods - /** + /** * fetches the menu for CIS from the database based on the userLanguage */ - public function getMenu() + public function getMenu() { $this->load->model('content/Content_model', 'ContentModel'); $this->load->config('cis'); - $cis4_content_id =$this->config->item('cis_menu_root_content_id'); - $result = $this->ContentModel->getMenu($cis4_content_id, getAuthUID(),getUserLanguage()); + $cis4_content_id = $this->config->item('cis_menu_root_content_id'); + $result = $this->ContentModel->getMenu($cis4_content_id, getAuthUID(), getUserLanguage()); $result = $this->getDataOrTerminateWithError($result); $menu = $result->childs ?? []; - $this->terminateWithSuccess($menu); - } - - + $menu = $this->generateUrlsForMenuItems($menu); + $this->terminateWithSuccess($menu); + } + + private function generateUrlsForMenuItems($menuItems) + { + return array_map( + function ($menuItem) { + return $this->generateUrlForMenuItem($menuItem); + }, + $menuItems + ); + } + + private function generateUrlForMenuItem($menuItem) + { + $menuItem->url = $this->menuItemUrlHelper($menuItem); + unset($menuItem->content); + + if ($menuItem->childs && count($menuItem->childs)) { + $menuItem->childs = $this->generateUrlsForMenuItems($menuItem->childs); + } + + return $menuItem; + } + + private function menuItemUrlHelper($menuItem) + { + if ($menuItem->template_kurzbz !== 'redirect') { + return site_url("/CisVue/Cms/content/" . $menuItem->content_id); + } + + if (!$menuItem->content || !mb_strlen($menuItem->content)) { + return ''; + } + + $doc = new DOMDocument(); + $doc->loadXML($menuItem->content); + $urlElem = $doc->getElementsByTagName('url')->item(0); + + if (!$urlElem) { + return ''; + } + + $url = $urlElem->textContent; + + if (strpos($url, '../cms/news.php') !== false) { + $newsRegex = '/^\.\.\/cms\/news\.php/'; + $url = preg_replace($newsRegex, site_url("/CisVue/Cms/news"), $url); + } + + if (strpos($url, '../cms/content.php?') !== false) { + $contentRegex = '/^\.\.\/cms\/content\.php\?content_id=([0-9]+)/'; + $matches = []; + preg_match($contentRegex, $url, $matches); + $url = site_url('/CisVue/Cms/content/' . $matches[1]); + } + + if (strpos($url, '../index.ci.php') !== false) { + $indexRegex = '/^\.\.\/index\.ci\.php/'; + $url = preg_replace($indexRegex, site_url(), $url); + } + + if (strpos($url, '../') !== false) { + $relativeRegex = '/^\.\.\//'; + $url = preg_replace($relativeRegex, base_url(), $url); + } + + return $url; + } + } diff --git a/application/controllers/api/frontend/v1/Lehre.php b/application/controllers/api/frontend/v1/Lehre.php index 10d945a3e..c2ebeb10b 100644 --- a/application/controllers/api/frontend/v1/Lehre.php +++ b/application/controllers/api/frontend/v1/Lehre.php @@ -38,7 +38,8 @@ class Lehre extends FHCAPI_Controller parent::__construct([ 'lvStudentenMail' => self::PERM_LOGGED, 'LV' => self::PERM_LOGGED, - 'Pruefungen' => self::PERM_LOGGED + 'Pruefungen' => self::PERM_LOGGED, + 'semesterAverageGrade' => self::PERM_LOGGED, ]); } @@ -100,5 +101,49 @@ class Lehre extends FHCAPI_Controller $this->terminateWithSuccess($result); } + + /** + * calculates and returns the grade average and weighted average for a specific semester + * @param string $studiensemester_kurzbz + * @return void + */ + + public function semesterAverageGrade($studiensemester_kurzbz) + { + $this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel'); + $semesterLvs = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage()); + + if (isError($semesterLvs)) + return $this->outputJsonError(getError($semesterLvs)); + + $semesterLvsData = getData($semesterLvs); + + $doGradesExist = false; + $sum = 0; + $count = 0; + $sumWeighted = 0; + $sumEcts = 0; + + foreach ($semesterLvsData as $lv) { + if (!$lv->znote || $lv->znote < 1 || $lv->znote > 5) + continue; + + $doGradesExist = true; + + $sum += $lv->znote; + $count++; + $sumWeighted += $lv->znote * floatval($lv->ects); + $sumEcts += floatval($lv->ects); + } + + $averageGrade = null; + $weightedAverageGrade = null; + if ($doGradesExist) { + $averageGrade = $sum/$count; + $weightedAverageGrade = $sumWeighted/$sumEcts; + } + + $this->terminateWithSuccess(['average_grade' => $averageGrade, 'weighted_average_grade' => $weightedAverageGrade]); + } } diff --git a/application/controllers/api/frontend/v1/LvPlan.php b/application/controllers/api/frontend/v1/LvPlan.php index 28b48e3f1..2a900e92d 100644 --- a/application/controllers/api/frontend/v1/LvPlan.php +++ b/application/controllers/api/frontend/v1/LvPlan.php @@ -16,7 +16,8 @@ * along with this program. If not, see . */ -if (! defined('BASEPATH')) exit('No direct script access allowed'); +if (!defined('BASEPATH')) + exit('No direct script access allowed'); use CI3_Events as Events; use \DateTime as DateTime; @@ -33,19 +34,25 @@ class LvPlan extends FHCAPI_Controller parent::__construct([ 'getRoomplan' => self::PERM_LOGGED, - 'Stunden' => self::PERM_LOGGED, - 'getReservierungen' => self::PERM_LOGGED, + 'Stunden' => self::PERM_LOGGED, + 'getReservierungen' => self::PERM_LOGGED, 'LvPlanEvents' => self::PERM_LOGGED, 'eventsPersonal' => self::PERM_LOGGED, 'eventsLv' => self::PERM_LOGGED, 'getLehreinheitStudiensemester' => self::PERM_LOGGED, 'studiensemesterDateInterval' => self::PERM_LOGGED, 'getLvPlanForStudiensemester' => self::PERM_LOGGED, - 'getLv' => self::PERM_LOGGED + 'getLv' => self::PERM_LOGGED, + 'eventsStgOrg' => self::PERM_LOGGED, + 'fetchFerienEvents' => self::PERM_LOGGED, + 'getStudiengaenge' => self::PERM_LOGGED, + 'getLehrverband' => self::PERM_LOGGED, + 'permissionOtherLvPlan' => self::PERM_LOGGED, + 'compactibleEventTypes' => self::PERM_LOGGED, ]); - $this->load->library('LogLib'); - $this->loglib->setConfigs(array( + $this->load->library('LogLib'); + $this->loglib->setConfigs(array( 'classIndex' => 5, 'functionIndex' => 5, 'lineIndex' => 4, @@ -53,17 +60,17 @@ class LvPlan extends FHCAPI_Controller 'dbExecuteUser' => 'RESTful API' )); - $this->load->library('form_validation'); + $this->load->library('form_validation'); } //------------------------------------------------------------------------------------------------------------------ // Public methods /** - * fetches LvPlan and Moodle events together - * @access public - * - */ + * fetches LvPlan and Moodle events together + * @access public + * + */ public function LvPlanEvents() { $hasLv = $this->input->post('lv_id'); @@ -83,24 +90,30 @@ class LvPlan extends FHCAPI_Controller // form validation $this->form_validation->set_rules('start_date', "start_date", "required"); $this->form_validation->set_rules('end_date', "end_date", "required"); - + if (!$this->form_validation->run()) $this->terminateWithValidationErrors($this->form_validation->error_array()); // storing the post parameter in local variables $start_date = $this->input->post('start_date', true); $end_date = $this->input->post('end_date', true); + $uid = $this->input->post('uid', true); + + // disallow accessing other user's events if missing permission + if ($uid && $uid !== getAuthUID() && !$this->permissionlib->isBerechtigt('basis/other_lv_plan')) { + $this->terminateWithError("Missing permission to view other users' timetables!"); + } // fetching lvplan events - $result = $this->stundenplanlib->getEventsUser($start_date, $end_date); + $result = $this->stundenplanlib->getEventsUser($start_date, $end_date, $uid); $lvplanEvents = $this->getDataOrTerminateWithError($result); // fetching moodle events - $moodleEvents = $this->fetchMoodleEvents($start_date, $end_date); + $moodleEvents = $this->fetchMoodleEvents($start_date, $end_date, $uid); // fetching ferien events - $ferienEvents = $this->fetchFerienEvents($start_date, $end_date); - + $ferienEvents = $this->fetchFerienEvents($start_date, $end_date, $uid); + $this->terminateWithSuccess(array_merge( $lvplanEvents, @@ -109,6 +122,45 @@ class LvPlan extends FHCAPI_Controller )); } + /** + * fetches LvPlan for studiengang / semester / verband / gruppe + * + * @access public + */ + public function eventsStgOrg() + { + $this->load->library('StundenplanLib'); + + // form validation + $this->form_validation->set_rules('start_date', "start_date", "required"); + $this->form_validation->set_rules('end_date', "end_date", "required"); + //$this->form_validation->set_rules('stg_kz', "stg_kz", "required"); //no validation show empty calendar + + if (!$this->form_validation->run()) { + $this->terminateWithValidationErrors($this->form_validation->error_array()); + $stgOrgEvents = []; + $ferienEvents = []; + } else { + $start_date = $this->input->post('start_date', true); + $end_date = $this->input->post('end_date', true); + $stg_kz = $this->input->post('stg_kz', true); + $sem = $this->input->post('sem', true); + $verband = $this->input->post('verband', true); + $gruppe = $this->input->post('gruppe', true); + + $result = $this->stundenplanlib->getEventsStgOrg($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe); + $stgOrgEvents = $this->getDataOrTerminateWithError($result); + + $result = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $stg_kz); + $ferienEvents = $this->getDataOrTerminateWithError($result); + } + + $this->terminateWithSuccess(array_merge( + $stgOrgEvents, + $ferienEvents + )); + } + /** * fetches LvPlan and Ferien events together for the lv * @@ -122,7 +174,7 @@ class LvPlan extends FHCAPI_Controller $this->form_validation->set_rules('start_date', "start_date", "required"); $this->form_validation->set_rules('end_date', "end_date", "required"); $this->form_validation->set_rules('lv_id', "lv_id", "required|integer"); - + if (!$this->form_validation->run()) $this->terminateWithValidationErrors($this->form_validation->error_array()); @@ -137,7 +189,6 @@ class LvPlan extends FHCAPI_Controller // fetching ferien events $ferienEvents = $this->fetchFerienEvents($start_date, $end_date); - $this->terminateWithSuccess(array_merge( $lvplanEvents, @@ -146,40 +197,42 @@ class LvPlan extends FHCAPI_Controller } //TODO: delete this function if we don't use the old calendar export endpoints anymore - public function studiensemesterDateInterval($date){ - $this->load->model('organisation/Studiensemester_model','StudiensemesterModel'); - $studiensemester =$this->StudiensemesterModel->getByDate(date_format(date_create($date),'Y-m-d')); - $studiensemester =current($this->getDataOrTerminateWithError($studiensemester)); + public function studiensemesterDateInterval($date) + { + $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + $studiensemester = $this->StudiensemesterModel->getByDate(date_format(date_create($date), 'Y-m-d')); + $studiensemester = current($this->getDataOrTerminateWithError($studiensemester)); $this->terminateWithSuccess($studiensemester); } - public function getLvPlanForStudiensemester($studiensemester,$lvid){ + public function getLvPlanForStudiensemester($studiensemester, $lvid) + { $this->load->library('StundenplanLib'); - $this->load->model('organisation/Studiensemester_model','StudiensemesterModel'); - - $studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz"=>$studiensemester]); + $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); + + $studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz" => $studiensemester]); $studiensemester_result = current($this->getDataOrTerminateWithError($studiensemester_result)); $timespan_start = new DateTime($studiensemester_result->start); $timespan_ende = new DateTime($studiensemester_result->ende); - $lvplan = $this->stundenplanlib->getStundenplan(date_format($timespan_start, 'Y-m-d'),date_format($timespan_ende, 'Y-m-d'), $lvid); + $lvplan = $this->stundenplanlib->getStundenplan(date_format($timespan_start, 'Y-m-d'), date_format($timespan_ende, 'Y-m-d'), $lvid); $this->terminateWithSuccess($lvplan); - - } - - /** - * fetches Stunden layout from database - * @access public - * - */ - public function Stunden() + } + + + /** + * fetches Stunden layout from database + * @access public + * + */ + public function Stunden() { $this->load->model('ressource/Stunde_model', 'StundeModel'); $this->StundeModel->addOrder('stunde', 'ASC'); $stunden = $this->StundeModel->load(); - $stunden = $this->getDataOrTerminateWithError($stunden); + $stunden = $this->getDataOrTerminateWithError($stunden); $this->terminateWithSuccess($stunden); } @@ -210,10 +263,10 @@ class LvPlan extends FHCAPI_Controller $roomplan_data = $this->stundenplanlib->getRoomplan($ort_kurzbz, $start_date, $end_date); $roomplan_data = $this->getDataOrTerminateWithError($roomplan_data); - + $this->terminateWithSuccess($roomplan_data); } - + /** * gets the reservierungen of a room if the ort_kurzbz parameter is * supplied otherwise gets the reservierungen of the lvplan of a student @@ -226,25 +279,32 @@ class LvPlan extends FHCAPI_Controller { $this->form_validation->set_rules('start_date', "StartDate", "required"); $this->form_validation->set_rules('end_date', "EndDate", "required"); - + if (!$this->form_validation->run()) $this->terminateWithValidationErrors($this->form_validation->error_array()); // storing the post parameter in local variables $start_date = $this->input->post('start_date', true); $end_date = $this->input->post('end_date', true); + $uid = $this->input->post('uid', true); + + // disallow accessing other user's reservierungen if missing permission + if ($uid && $uid !== getAuthUID() && !$this->permissionlib->isBerechtigt('basis/other_lv_plan')) { + $this->terminateWithError("Missing permission to view other users' timetables!"); + } // get data $this->load->library('StundenplanLib'); - $result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz); + $result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid); $result = $this->getDataOrTerminateWithError($result); $this->terminateWithSuccess($result); } - public function getLehreinheitStudiensemester($lehreinheit_id){ + public function getLehreinheitStudiensemester($lehreinheit_id) + { $this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); $this->LehreinheitModel->addSelect(["studiensemester_kurzbz"]); $result = $this->LehreinheitModel->load($lehreinheit_id); @@ -287,6 +347,68 @@ class LvPlan extends FHCAPI_Controller return $this->terminateWithSuccess(current($result)); } + public function getStudiengaenge() + { + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + + $this->StudiengangModel->addOrder('typ'); + $this->StudiengangModel->addOrder('kurzbz'); + $result = $this->StudiengangModel->loadWhere([ + 'aktiv' => true + ]); + + $data = $this->getDataOrTerminateWithError($result); + + return $this->terminateWithSuccess($data); + } + + public function getLehrverband($studiengang_kz, $semester = null, $verband = null) + { + $this->load->model('organisation/Lehrverband_model', 'LehrverbandModel'); + + $where = [ + 'aktiv' => true, + 'studiengang_kz' => $studiengang_kz, + ]; + + if ($semester !== null && $semester !== 'null' && $semester !== 'undefined') { + $where['semester'] = $semester; + } + if ($verband !== null && $verband !== 'null' && $verband !== 'undefined') { + $where['verband'] = $verband; + } + + $this->LehrverbandModel->addOrder('studiengang_kz'); + $this->LehrverbandModel->addOrder('semester'); + $this->LehrverbandModel->addOrder('verband'); + $this->LehrverbandModel->addOrder('gruppe'); + $result = $this->LehrverbandModel->loadWhere($where); + + $data = $this->getDataOrTerminateWithError($result); + + return $this->terminateWithSuccess($data); + } + + /** + * Checks if the current user has permission to view other users' timetables + * + * @return void + */ + public function permissionOtherLvPlan() + { + $this->terminateWithSuccess($this->permissionlib->isBerechtigt('basis/other_lv_plan')); + } + + /** + * get event types which can be compacted in lv plan display + * + * @return void + */ + public function compactibleEventTypes() + { + $this->terminateWithSuccess(["lehreinheit", "reservierung", "ferien", "moodle"]); + } + /** * fetch moodle events * @@ -294,30 +416,34 @@ class LvPlan extends FHCAPI_Controller * @param string $end_date * @return array */ - private function fetchMoodleEvents($start_date, $end_date) + private function fetchMoodleEvents($start_date, $end_date, $uid = null) { + if ($uid && $uid !== getAuthUID()) { + return []; + } + $this->load->config('calendar'); $tz = new DateTimeZone($this->config->item('timezone')); - + $start = new DateTime($start_date); $start->setTimezone($tz); - + $end = new DateTime($end_date); $end->setTimezone($tz); $end->modify('+1 day -1 second'); - + $moodle_events = []; - + Events::trigger( 'moodleCalendarEvents', - function & () use (&$moodle_events) { + function &() use (&$moodle_events) { return $moodle_events; }, [ 'start_date' => $start->format('c'), 'end_date' => $end->format('c'), - 'username' => getAuthUID() + 'username' => $uid ?? getAuthUID() ] ); @@ -331,23 +457,23 @@ class LvPlan extends FHCAPI_Controller * @param string $end_date * @return array */ - private function fetchFerienEvents($start_date, $end_date) + private function fetchFerienEvents($start_date, $end_date, $uid = null) { $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); $this->load->model('education/Studentlehrverband_model', 'StudentLehrverbandModel'); $currentStudiensemester = $this->StudiensemesterModel->getByDate($start_date); $currentStudiensemester = $this->getDataOrTerminateWithError($currentStudiensemester); - + if ($currentStudiensemester) { $studentsemester_kurzbz = current($currentStudiensemester)->studiensemester_kurzbz; $studiengang = $this->StudentLehrverbandModel->loadWhere([ - "student_uid" => getAuthUID(), + "student_uid" => $uid ?? getAuthUID(), "studiensemester_kurzbz" => $studentsemester_kurzbz ]); $studiengang = $this->getDataOrTerminateWithError($studiengang); - + if ($studiengang) $studiengang_kz = current($studiengang)->studiengang_kz; else @@ -357,7 +483,7 @@ class LvPlan extends FHCAPI_Controller } $ferienEvents = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $studiengang_kz); - + return $this->getDataOrTerminateWithError($ferienEvents); } } diff --git a/application/controllers/api/frontend/v1/OtherLvPlan.php b/application/controllers/api/frontend/v1/OtherLvPlan.php new file mode 100644 index 000000000..cad654f12 --- /dev/null +++ b/application/controllers/api/frontend/v1/OtherLvPlan.php @@ -0,0 +1,76 @@ +. + */ + +if (!defined('BASEPATH')) + exit('No direct script access allowed'); + +class OtherLvPlan extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'otherLvPlanViewData' => ['basis/other_lv_plan:r'], + ]); + + $this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel'); + $this->load->model('person/Benutzer_model', 'BenutzerModel'); + + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * retrieves viewData for other lv plan view + * @access public + * @param $uid the userID for which the other lv plan is being viewed + */ + public function otherLvPlanViewData($uid) + { + $isMitarbeiterResult = $this->MitarbeiterModel->isMitarbeiter($uid); + $isMitarbeiter = getData($isMitarbeiterResult); + $isStudent = !$isMitarbeiter; + + $this->BenutzerModel->addSelect(["foto", "vorname", "nachname"]); + $this->BenutzerModel->addJoin("tbl_person", "person_id"); + $personResult = $this->BenutzerModel->load([$uid]); + $person = hasData($personResult) ? getData($personResult) : null; + + $viewData = [ + "user_data" => [ + "username" => $uid, + "is_student" => $isStudent, + "is_mitarbeiter" => $isMitarbeiter, + "foto" => $person[0]->foto, + "vorname" => $person[0]->vorname, + "nachname" => $person[0]->nachname, + ], + ]; + + $this->terminateWithSuccess($viewData); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Private methods + +} + diff --git a/application/controllers/api/frontend/v1/Phrasen.php b/application/controllers/api/frontend/v1/Phrasen.php index 7cc652c71..3e13bb61e 100644 --- a/application/controllers/api/frontend/v1/Phrasen.php +++ b/application/controllers/api/frontend/v1/Phrasen.php @@ -32,6 +32,8 @@ class Phrasen extends FHCAPI_Controller 'setLanguage' => self::PERM_ANONYMOUS, 'getLanguage' => self::PERM_ANONYMOUS, 'getAllLanguages' => self::PERM_ANONYMOUS, + 'getPhrases' => self::PERM_ANONYMOUS, + 'getTabulatorPhrases' => self::PERM_ANONYMOUS, ]); $this->load->helper('hlp_language'); @@ -92,4 +94,59 @@ class Phrasen extends FHCAPI_Controller $this->terminateWithSuccess($langs); } + public function getPhrases() + { + $postParams = $this->getPostJSON(); + + $languages = $postParams->languages; + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $phrasesGroupedByCategory = $postParams->phrasesGroupedByCategory; + $result = []; + foreach ($languages as $language) { + $phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language); + $result[$language] = $this->getDataOrTerminateWithError($phrases); + } + + $this->terminateWithSuccess($result); + } + + public function getTabulatorPhrases() + { + $languages = json_decode($this->input->get('languages')); + if (!$languages || !count($languages)) { + $this->load->model('system/Sprache_model', 'sprachenModel'); + $activeLanguages = $this->sprachenModel->loadWhere(array('content' => true)); + $activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages); + $languages = array_map( + function ($languageData) { + return $languageData->sprache; + }, + $activeLanguagesData + ); + } + + $this->load->model('system/Phrase_model', 'phraseModel'); + $result = []; + foreach ($languages as $language) { + $tabulatorPhrases = $this->phraseModel->getPhrasesByCategoryAndLanguage(['tabulator'], $language); + $result[$language] = $this->getDataOrTerminateWithError($tabulatorPhrases); + } + + header('Pragma: private'); + header('Cache-Control: private, max-age=' . (60 * 60 * 24 * 30)); + $this->terminateWithSuccess($result); + } + } \ No newline at end of file diff --git a/application/controllers/api/frontend/v1/Profil.php b/application/controllers/api/frontend/v1/Profil.php index 3133b107a..96cdea855 100644 --- a/application/controllers/api/frontend/v1/Profil.php +++ b/application/controllers/api/frontend/v1/Profil.php @@ -16,7 +16,8 @@ * along with this program. If not, see . */ -if (! defined('BASEPATH')) exit('No direct script access allowed'); +if (!defined('BASEPATH')) + exit('No direct script access allowed'); class Profil extends FHCAPI_Controller { @@ -27,13 +28,13 @@ class Profil extends FHCAPI_Controller public function __construct() { parent::__construct([ - 'fotoSperre' => self::PERM_LOGGED, + 'fotoSperre' => self::PERM_LOGGED, 'getGemeinden' => self::PERM_LOGGED, 'getAllNationen' => self::PERM_LOGGED, 'isMitarbeiter' => self::PERM_LOGGED, 'profilViewData' => self::PERM_LOGGED, ]); - + $this->load->library('PermissionLib'); $this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel'); @@ -48,28 +49,37 @@ class Profil extends FHCAPI_Controller //------------------------------------------------------------------------------------------------------------------ // Public methods - public function profilViewData($uid=null){ + + /** + * retrieves view data for profile view + * @access public + * @param $uid the userID for which profile is being viewed, null or missing value implies one's own profile + */ + public function profilViewData($uid = null) + { + $authUid = getAuthUID(); + $isProfilOfAuthUser = !$uid || $uid === $authUid; + $this->load->library('ProfilLib'); - $editable = false; - if(isset($uid) && $uid != null){ - $profil_data = $this->profillib->getView($uid); - if($uid == getAuthUID()){ - $editable = true; - } - }else{ - $editable = true; - $profil_data = $this->profillib->getView(getAuthUID()); + $profileData = $this->profillib->getView($uid ?? $authUid); + $profileData = hasData($profileData) ? getData($profileData) : null; + + $viewData = [ + 'editable' => $isProfilOfAuthUser, + 'profil_data' => $profileData, + 'permissions' => [ + 'basis/other_lv_plan' => $this->permissionlib->isBerechtigt(('basis/other_lv_plan')) + ] + ]; + + if ($isProfilOfAuthUser) { + $viewData['calendar_sync_urls'] = $this->getCalendarSyncUrlData(); } - - $profil_data = hasData($profil_data) ? getData($profil_data) : null; - $viewData = array( - 'editable'=>$editable, - 'profil_data' => $profil_data, - ); + $this->terminateWithSuccess($viewData); } - /** + /** * update column foto_sperre in public.tbl_person * @access public * @param boolean $value new value for the column @@ -77,9 +87,9 @@ class Profil extends FHCAPI_Controller */ public function fotoSperre($value) { - if(!isset($value)){ - $this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL); - } + if (!isset($value)) { + $this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL); + } $res = $this->PersonModel->update($this->pid, ["foto_sperre" => $value]); if (isError($res)) { @@ -87,10 +97,10 @@ class Profil extends FHCAPI_Controller } $this->PersonModel->addSelect("foto_sperre"); $res = $this->PersonModel->load($this->pid); - - $res = $this->getDataOrTerminateWithError($res); - - $this->terminateWithSuccess(current($res)); + + $res = $this->getDataOrTerminateWithError($res); + + $this->terminateWithSuccess(current($res)); } /** @@ -109,7 +119,7 @@ class Profil extends FHCAPI_Controller if (isError($nation_res)) { $this->terminateWithError("error while trying to query table codex.tbl_nation", self::ERROR_TYPE_GENERAL); } - + $nation_res = $this->getDataOrTerminateWithError($nation_res); $this->terminateWithSuccess($nation_res); @@ -117,30 +127,30 @@ class Profil extends FHCAPI_Controller public function getGemeinden($nation, $zip) { - if(!isset($nation) || !isset($zip)){ + if (!isset($nation) || !isset($zip)) { echo json_encode(error("Missing parameters")); return; } - + $this->load->model('codex/Gemeinde_model', "GemeindeModel"); - + $gemeinde_res = $this->GemeindeModel->getGemeindeByPlz($zip); - + if (isError($gemeinde_res)) { - $this->terminateWithError(getError($gemeinde_res),self::ERROR_TYPE_GENERAL); + $this->terminateWithError(getError($gemeinde_res), self::ERROR_TYPE_GENERAL); } $gemeinde_res = $this->getDataOrTerminateWithError($gemeinde_res); - + /* $gemeinde_res = array_map(function ($obj) { return $obj->ortschaftsname; }, $gemeinde_res); */ - $this->terminateWithSuccess($gemeinde_res); - + $this->terminateWithSuccess($gemeinde_res); + } - + /** * checks whether a specific userID is a mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php) * @access public @@ -150,23 +160,48 @@ class Profil extends FHCAPI_Controller public function isMitarbeiter($uid) { - if(!$uid) $this->terminateWithError("No uid provided", self::ERROR_TYPE_GENERAL); - - + if (!$uid) + $this->terminateWithError("No uid provided", self::ERROR_TYPE_GENERAL); + + $result = $this->MitarbeiterModel->isMitarbeiter($uid); - + if (isError($result)) { $this->terminateWithError("error when calling Mitarbeiter_model function isMitarbeiter with uid " . $uid, self::ERROR_TYPE_GENERAL); } $result = $this->getDataOrTerminateWithError($result); - + $this->terminateWithSuccess($result); } // ----------------------------------------------------------------------------------------------------------------- // Private methods - + /** + * gets the identifier, phrase, and url for each calendar sync option + * @access private + * @return array array of arrays, where each child array is a sync option + */ + private function getCalendarSyncUrlData() + { + return [ + [ + "identifier" => "cal_dav", + "labelPhrase" => "profil/calendar_sync_cal_dav", + "url" => APP_ROOT . "webdav/lvplan.php/calendars/" . $this->uid . "/LVPlan-" . $this->uid, + ], + [ + "identifier" => "cal_dav_principal", + "labelPhrase" => "profil/calendar_sync_cal_dav_principal", + "url" => APP_ROOT . "webdav/lvplan.php/principals/" . $this->uid, + ], + [ + "identifier" => "i_cal", + "labelPhrase" => "profil/calendar_sync_i_cal", + "url" => APP_ROOT . "webdav/google.php?cal=" . encryptData($this->uid, LVPLAN_CYPHER_KEY) . "&" . microtime(true), + ], + ]; + } } diff --git a/application/controllers/api/frontend/v1/StgOrgLvPlan.php b/application/controllers/api/frontend/v1/StgOrgLvPlan.php new file mode 100644 index 000000000..ffbf85e57 --- /dev/null +++ b/application/controllers/api/frontend/v1/StgOrgLvPlan.php @@ -0,0 +1,64 @@ +. + */ + +if (!defined('BASEPATH')) + exit('No direct script access allowed'); + +class StgOrgLvPlan extends FHCAPI_Controller +{ + + /** + * Object initialization + */ + public function __construct() + { + parent::__construct([ + 'stgOrgLvPlanViewData' => self::PERM_LOGGED, + ]); + + } + + //------------------------------------------------------------------------------------------------------------------ + // Public methods + + /** + * fetches view data for stg org lv plan + * @access public + */ + public function stgOrgLvPlanViewData() + { + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->StudiengangModel->addOrder('typ'); + $this->StudiengangModel->addOrder('kurzbz'); + $result = $this->StudiengangModel->loadWhere([ + 'aktiv' => true + ]); + $studiengaenge = $this->getDataOrTerminateWithError($result); + + $viewData = array( + 'studiengaenge' => $studiengaenge, + ); + + $this->terminateWithSuccess($viewData); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Private methods + +} + diff --git a/application/controllers/api/frontend/v1/Studium.php b/application/controllers/api/frontend/v1/Studium.php index d17f0c1a1..f53a4105c 100644 --- a/application/controllers/api/frontend/v1/Studium.php +++ b/application/controllers/api/frontend/v1/Studium.php @@ -62,21 +62,36 @@ 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; + + //TODO(Manu) check if use Fallback or just comment out all paramschecks? + //add Fallback: if no LehrverbandData of actual semester, get Data of previous one + if(!hasData($studentLehrverband)) + { + $result= $this->StudiensemesterModel->getPreviousFrom($aktuelles_studiensemester->studiensemester_kurzbz); + + $data = $this->getDataOrTerminateWithError($result); + $vorheriges_studiensemester = current($data)->studiensemester_kurzbz; + $studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $vorheriges_studiensemester]); + } + $studentLehrverband = current(getData($studentLehrverband)); + $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_studiensemester)) { + $student_studiensemester = $studentLehrverband->studiensemester_kurzbz; + $parameter_studiensemester = $student_studiensemester; + } + if(!isset($parameter_studiengang)) { + $student_studiengang = $studentLehrverband->studiengang_kz; + $parameter_studiengang = $student_studiengang; + } + if(!isset($parameter_semester)) { + $student_semester = $studentLehrverband->semester; + $parameter_semester = $student_semester; + } if(!isset($parameter_studienplan)) - $parameter_studienplan = $student_studienplan; + $parameter_studienplan = $student_studienplan; + } if(isset($parameter_studiensemester)){ @@ -96,8 +111,7 @@ class Studium extends FHCAPI_Controller // fetch studiensemester $allStudienSemester = $this->getDataOrTerminateWithError($this->StudiensemesterModel->load()); - - + if(isset($parameter_studiensemester) && !empty(array_filter($allStudienSemester, function($studiensemester) use($parameter_studiensemester){ return $studiensemester->studiensemester_kurzbz == $parameter_studiensemester->studiensemester_kurzbz; }))){ @@ -216,6 +230,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/application/controllers/api/frontend/v1/dashboard/Board.php b/application/controllers/api/frontend/v1/dashboard/Board.php index c50fec128..fdded61e3 100644 --- a/application/controllers/api/frontend/v1/dashboard/Board.php +++ b/application/controllers/api/frontend/v1/dashboard/Board.php @@ -40,11 +40,32 @@ class Board extends FHCAPI_Controller public function list() { + $this->DashboardModel->addSelect('dashboard_id'); + $this->DashboardModel->addSelect('dashboard_kurzbz'); + $this->DashboardModel->addSelect('tbl_dashboard.beschreibung'); + $this->DashboardModel->addSelect("( + SELECT json_agg(w.*) + FROM dashboard.tbl_widget w + JOIN dashboard.tbl_dashboard_widget dw + USING(widget_id) + WHERE dw.dashboard_id=tbl_dashboard.dashboard_id + ) AS \"widgetSetup\""); + $result = $this->DashboardModel->load(); $data = $this->getDataOrTerminateWithError($result); - $this->terminateWithSuccess($result); + $data = array_map(function ($dashboard) { + $tmpSetups = json_decode($dashboard->widgetSetup); + $tmpSetups = array_map(function ($widget) { + $widget->setup->file = absoluteJsImportUrl($widget->setup->file); + return $widget; + }, $tmpSetups); + $dashboard->widgetSetup = $tmpSetups; + return $dashboard; + }, $data); + + $this->terminateWithSuccess($data); } public function create() @@ -82,7 +103,7 @@ class Board extends FHCAPI_Controller $data = $this->getDataOrTerminateWithError($result); - $this->terminateWithSuccess($result); + $this->terminateWithSuccess($data); } public function delete() @@ -116,6 +137,6 @@ class Board extends FHCAPI_Controller $data = $this->getDataOrTerminateWithError($result); - $this->terminateWithSuccess($result); + $this->terminateWithSuccess($data); } } diff --git a/application/controllers/api/frontend/v1/dashboard/Preset.php b/application/controllers/api/frontend/v1/dashboard/Preset.php index 5983d9660..d9be307cf 100644 --- a/application/controllers/api/frontend/v1/dashboard/Preset.php +++ b/application/controllers/api/frontend/v1/dashboard/Preset.php @@ -120,10 +120,7 @@ class Preset extends FHCAPI_Controller $conf = $this->dashboardlib->getPreset($db, $funktion); if ($conf) { $preset = json_decode($conf->preset, true); - if (!isset($preset[$funktion]) || !isset($preset[$funktion]['widgets'])) - $result[$funktion] = []; - else - $result[$funktion] = $preset[$funktion]['widgets']; + $result[$funktion] = $preset; } else { $result[$funktion] = []; } @@ -154,7 +151,7 @@ class Preset extends FHCAPI_Controller $preset_decoded = json_decode($preset->preset, true); - $this->dashboardlib->addWidgetsToWidgets($preset_decoded, $dashboard_kurzbz, $funktion_kurzbz, [$widget]); + $preset_decoded[$widget['widgetid']] = $widget; $preset->preset = json_encode($preset_decoded); @@ -186,8 +183,10 @@ class Preset extends FHCAPI_Controller $preset_decoded = json_decode($preset->preset, true); - if (!$this->dashboardlib->removeWidgetFromWidgets($preset_decoded, $funktion_kurzbz, $widgetid)) + if (!isset($preset_decoded[$widgetid])) show_404(); + + unset($preset_decoded[$widgetid]); $preset->preset = json_encode($preset_decoded); diff --git a/application/controllers/api/frontend/v1/dashboard/User.php b/application/controllers/api/frontend/v1/dashboard/User.php index 9d020649e..e603573ed 100644 --- a/application/controllers/api/frontend/v1/dashboard/User.php +++ b/application/controllers/api/frontend/v1/dashboard/User.php @@ -48,25 +48,9 @@ class User extends FHCAPI_Controller $uid = $this->authlib->getAuthObj()->username; - /*$mergedconfig = $this->dashboardlib->getMergedConfig($dashboard->dashboard_id, $uid); + $mergedconfig = $this->dashboardlib->getMergedUserConfig($dashboard->dashboard_id, $uid); - $this->terminateWithSuccess([ - 'general' => call_user_func_array( - 'array_merge_recursive', - $mergedconfig - ) - ]);*/ - $defaultconfig = $this->dashboardlib->getDefaultConfig($dashboard->dashboard_id); - $userconfig = $this->dashboardlib->getUserConfig($dashboard->dashboard_id, $uid); - - $defaultconfig_squashed = $defaultconfig ? call_user_func_array('array_replace_recursive', $defaultconfig) : []; - $userconfig_squashed = $userconfig ? call_user_func_array('array_replace_recursive', $userconfig) : []; - - $mergedconfig = array_replace_recursive($defaultconfig_squashed, $userconfig_squashed); - - $this->terminateWithSuccess([ - DashboardLib::SECTION_IF_FUNKTION_KURZBZ_IS_NULL => $mergedconfig - ]); + $this->terminateWithSuccess($mergedconfig); } public function addWidget() @@ -86,26 +70,15 @@ class User extends FHCAPI_Controller if (!isset($widget['widgetid'])) $widget['widgetid'] = $this->dashboardlib->generateWidgetId($dashboard_kurzbz); + if (isset($widget['source'])) + unset($widget['source']); + $override = $this->dashboardlib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid); - + $override_decoded = json_decode($override->override, true); - if (!isset($override_decoded['general']) || !is_array($override_decoded['general'])) - $override_decoded['general'] = []; + $override_decoded[$widget['widgetid']] = $widget; - if (!isset($override_decoded['general']['widgets'])) - $override_decoded['general']['widgets'] = []; - - $override_decoded['general']['widgets'][$widget['widgetid']] = $widget; - - // NOTE(chris): remove doubles in other funktionen - foreach ($override_decoded as $funktion => $array) { - if ($funktion == 'general') - continue; - if (isset($array['widgets']) && isset($array['widgets'][$widget['widgetid']])) - unset($override_decoded[$funktion]['widgets'][$widget['widgetid']]); - } - $override->override = json_encode($override_decoded); $result = $this->dashboardlib->insertOrUpdateOverride($override); @@ -135,18 +108,10 @@ class User extends FHCAPI_Controller $override_decoded = json_decode($override->override, true); - foreach (array_keys($override_decoded) as $k) { - if (!isset($override_decoded[$k]["widgets"])) { - unset($override_decoded[$k]); - continue; - } - if (isset($override_decoded[$k]["widgets"][$widget_id])) { - unset($override_decoded[$k]["widgets"][$widget_id]); - } - if (!$override_decoded[$k]["widgets"]) { - unset($override_decoded[$k]); - } - } + if (!isset($override_decoded[$widget_id])) + show_404(); + + unset($override_decoded[$widget_id]); $override->override = json_encode($override_decoded); diff --git a/application/controllers/api/frontend/v1/stv/Projektarbeit.php b/application/controllers/api/frontend/v1/stv/Projektarbeit.php index 8740ef3d6..d26d6a10c 100644 --- a/application/controllers/api/frontend/v1/stv/Projektarbeit.php +++ b/application/controllers/api/frontend/v1/stv/Projektarbeit.php @@ -90,6 +90,15 @@ class Projektarbeit extends FHCAPI_Controller if (!isset($projektarbeit_id) || !is_numeric($projektarbeit_id)) return $this->terminateWithError('Projektarbeit Id missing', self::ERROR_TYPE_GENERAL); + $result = $this->fetchProjektarbeitByID($projektarbeit_id); + + $data = $this->getDataOrTerminateWithError($result); + + $this->terminateWithSuccess(current($data)); + } + + private function fetchProjektarbeitById($projektarbeit_id) { + $this->ProjektarbeitModel->resetQuery(); $this->ProjektarbeitModel->addSelect( 'lehre.tbl_projektarbeit.projektarbeit_id, titel, titel_english, themenbereich, projekttyp_kurzbz, lehrveranstaltung_id, lehreinheit_id, firma_id, beginn, ende, gesperrtbis, note, final, freigegeben, tbl_projektarbeit.anmerkung, fa.name AS firma_name' @@ -97,13 +106,10 @@ class Projektarbeit extends FHCAPI_Controller $this->ProjektarbeitModel->addJoin('lehre.tbl_lehreinheit le', 'lehreinheit_id'); $this->ProjektarbeitModel->addJoin('lehre.tbl_lehrveranstaltung lv', 'lehrveranstaltung_id'); $this->ProjektarbeitModel->addJoin('public.tbl_firma fa', 'firma_id', 'LEFT'); - $result = $this->ProjektarbeitModel->loadWhere( + return $this->ProjektarbeitModel->loadWhere( array('projektarbeit_id' => $projektarbeit_id) ); - $data = $this->getDataOrTerminateWithError($result); - - $this->terminateWithSuccess(current($data)); } /** @@ -132,7 +138,8 @@ class Projektarbeit extends FHCAPI_Controller ); $data = $this->getDataOrTerminateWithError($result); - + $data = $this->getDataOrTerminateWithError($this->fetchProjektarbeitById($data)); + $this->terminateWithSuccess($data); } @@ -280,10 +287,7 @@ class Projektarbeit extends FHCAPI_Controller */ public function getNoten() { - $this->NoteModel->addOrder('notenwert', 'ASC'); - $this->NoteModel->addOrder('bezeichnung', 'ASC'); - - $result = $this->NoteModel->load(); + $result = $this->NoteModel->getAllActive(); if (isError($result)) return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL); diff --git a/application/controllers/jobs/AbgabetoolJob.php b/application/controllers/jobs/AbgabetoolJob.php index b81053032..2e5c4d7b9 100644 --- a/application/controllers/jobs/AbgabetoolJob.php +++ b/application/controllers/jobs/AbgabetoolJob.php @@ -358,7 +358,8 @@ class AbgabetoolJob extends JOB_Controller foreach($assistenzMap as $assistenz_person_id => $tupelArr) { $abgabenString = '
'; - + $hasContent = false; + foreach($tupelArr as $tupel) { $projektarbeit_id = $tupel[0]; $assistenzRow = $tupel[1]; @@ -377,6 +378,7 @@ class AbgabetoolJob extends JOB_Controller if(count($relevantAbgaben) == 0) { continue; } + $hasContent = true; // Format the Student Name $s = $relevantAbgaben[0]; @@ -447,7 +449,12 @@ class AbgabetoolJob extends JOB_Controller } $abgabenString .= '
'; - + + // skip send entirely + if (!$hasContent) { + continue; + } + // done with building the change list, now send it $assistenzRow = $tupelArr[0][1]; $anrede = $assistenzRow->anrede; diff --git a/application/controllers/jobs/AntragJob.php b/application/controllers/jobs/AntragJob.php index 8dc4870ea..5b72518d2 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)) @@ -206,6 +226,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) @@ -363,6 +387,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'); @@ -481,6 +512,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'); @@ -614,6 +652,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 3d8a2ea26..35edc826c 100644 --- a/application/libraries/AntragLib.php +++ b/application/libraries/AntragLib.php @@ -1657,7 +1657,7 @@ class AntragLib $result = $this->_ci->StudierendenantragModel->loadWithStatusWhere($where); if (isError($result)) - return $result; + return $result; if (!hasData($result)) return error($this->_ci->p->t('studierendenantrag', "error_no_antrag_found", ['id' => $studierendenantrag_id])); @@ -1709,6 +1709,7 @@ class AntragLib $result->statustyp = $antrag->statustyp; $result->status_insertvon = $antrag->status_insertvon; $result->grund = $antrag->grund; + $result->status_grund = $antrag->status_grund; $result->studierendenantrag_id = $antrag->studierendenantrag_id; $result->typ = $antrag->typ; $result->datum = $antrag->datum; diff --git a/application/libraries/ProfilLib.php b/application/libraries/ProfilLib.php index 97cb8ce5d..43a110b85 100644 --- a/application/libraries/ProfilLib.php +++ b/application/libraries/ProfilLib.php @@ -263,6 +263,59 @@ class ProfilLib{ $element->mailto = "mailto:" . $element->gruppe_kurzbz . "@" . DOMAIN; return $element; }, $mailverteiler_res); + + $this->ci->load->model("crm/Student_model", "StudentModel"); + $this->ci->StudentModel->addJoin("tbl_benutzer", "tbl_benutzer.uid = tbl_student.student_uid"); + $this->ci->StudentModel->addJoin("tbl_person", "tbl_benutzer.person_id = tbl_person.person_id"); + $this->ci->StudentModel->addJoin("tbl_studiengang", "tbl_student.studiengang_kz = tbl_studiengang.studiengang_kz"); + $this->ci->StudentModel->addSelect("matr_nr, semester, verband, gruppe, kurzbzlang"); + + $studentResult = $this->ci->StudentModel->loadWhere(["student_uid" => $uid]); + if (isError($studentResult)) { + return error(getData($studentResult)); + } + + $studentResultData = getData($studentResult); + $studentData = null; + if (is_array($studentResultData) && count($studentResultData)) { + $studentData = $studentResultData[0]; + } + + if ($studentData && $studentData->matr_nr) { + $this->ci->load->library("phrasesLib", ["profil"], "phrases"); + $standardCourseVerteiler = trim($studentData->kurzbzlang) . "_STD"; + $mailverteiler_res[] = [ + "beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $standardCourseVerteiler, + "gruppe_kurzbz" => $standardCourseVerteiler, + "mailto" => "mailto:" . strtolower($standardCourseVerteiler) . "@" . DOMAIN, + ]; + + $semesterVerteiler = trim($studentData->kurzbzlang) . trim($studentData->semester); + $mailverteiler_res[] = [ + "beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $semesterVerteiler, + "gruppe_kurzbz" => $semesterVerteiler, + "mailto" => "mailto:" . strtolower($semesterVerteiler) . "@" . DOMAIN, + ]; + + if ($studentData->verband && strlen(trim($studentData->verband))) { + $verbandVerteiler = $semesterVerteiler . trim($studentData->verband); + $mailverteiler_res[] = [ + "beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $verbandVerteiler, + "gruppe_kurzbz" => $verbandVerteiler, + "mailto" => "mailto:" . strtolower($verbandVerteiler) . "@" . DOMAIN, + ]; + + if ($studentData->gruppe && strlen(trim($studentData->gruppe))) { + $gruppeVerteiler = $verbandVerteiler . trim($studentData->gruppe); + $mailverteiler_res[] = [ + "beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $gruppeVerteiler, + "gruppe_kurzbz" => $gruppeVerteiler, + "mailto" => "mailto:" . strtolower($gruppeVerteiler) . "@" . DOMAIN, + ]; + } + } + } + return $mailverteiler_res; } diff --git a/application/libraries/StundenplanLib.php b/application/libraries/StundenplanLib.php index 7ed64da2c..a7bec61ec 100644 --- a/application/libraries/StundenplanLib.php +++ b/application/libraries/StundenplanLib.php @@ -40,13 +40,16 @@ class StundenplanLib * @return stdClass * @access public */ - public function getEventsUser($start, $end) + public function getEventsUser($start, $end, $uid = null) { $this->_ci =& get_instance(); $this->_ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); - $uid = getAuthUID(); + if (!$uid) { + $uid = getAuthUID(); + } + if (is_null($uid)) return error("No UID"); @@ -217,7 +220,7 @@ class StundenplanLib * @param string $ort_kurzbz * @return stdClass */ - public function getReservierungen($start_date, $end_date, $ort_kurzbz = '') + public function getReservierungen($start_date, $end_date, $ort_kurzbz = '', $uid = null) { $this->_ci =& get_instance(); @@ -228,14 +231,14 @@ class StundenplanLib $this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel'); $this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel'); - $is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter(getAuthUID())); + $is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter($uid ?? getAuthUID())); if ($is_mitarbeiter && empty($ort_kurzbz)) { // request for personal lvplan show only reservations of logged in user - $reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date); + $reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date, $uid); } else { // querying the reservierungen - $reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz); + $reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid); } if (isError($reservierungen)) @@ -357,7 +360,10 @@ class StundenplanLib if (isError($ort_content_object)) { return error(getData($ort_content_object)); } - $ort_content_object = getData($ort_content_object)[0]; + $ort_content_object_data = getData($ort_content_object); + $ort_content_object = (is_array($ort_content_object_data) && count($ort_content_object_data) > 0) + ? $ort_content_object_data[0] + : null; if($ort_content_object) { $item->ort_content_id = $ort_content_object->content_id; } @@ -445,6 +451,24 @@ class StundenplanLib return success($ferienEventsFlattened); } + public function getEventsStgOrg( $start, $end, $stg_kz, $sem, $verband, $gruppe) + { + $this->_ci =& get_instance(); + + $this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel'); + + $stundenplan_data = $this->_ci->StundenplanModel->getStundenplanStudiengang($start, $end, $stg_kz, $sem, $verband, $gruppe); + if (isError($stundenplan_data)) + return $stundenplan_data; + $stundenplan_data = getData($stundenplan_data) ?? []; + + $function_error = $this->expandObjectInformation($stundenplan_data); + if ($function_error) + return $function_error; + + return success($stundenplan_data); + } + // start of the private functions ######################################################################################################## // function used to sort an array of studiensemester strings diff --git a/application/libraries/dashboard/DashboardLib.php b/application/libraries/dashboard/DashboardLib.php index 1c3983108..c9838f0e7 100644 --- a/application/libraries/dashboard/DashboardLib.php +++ b/application/libraries/dashboard/DashboardLib.php @@ -37,7 +37,9 @@ class DashboardLib public function getDashboardByKurzbz($dashboard_kurzbz) { - $result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz); + $result = $this->_ci->DashboardModel->loadWhere([ + 'dashboard_kurzbz' => $dashboard_kurzbz + ]); if (hasData($result)) { @@ -47,17 +49,21 @@ class DashboardLib return null; } - public function getMergedConfig($dashboard_id, $uid) + public function getMergedUserConfig($dashboard_id, $uid) { - $defaultconfig = $this->getDefaultConfig($dashboard_id); - $userconfig = $this->getUserConfig($dashboard_id, $uid); + $defaultconfig = $this->getUserBaseConfig($dashboard_id); + $userconfig = $this->getUserOverrideConfig($dashboard_id, $uid); - $mergedconfig = array_replace_recursive($defaultconfig, $userconfig); + $sourceconfig = array_map(function ($value) { + return ['source' => $value['source']]; + }, $defaultconfig); + + $mergedconfig = array_replace_recursive($defaultconfig, $userconfig, $sourceconfig); return $mergedconfig; } - public function getDefaultConfig($dashboard_id) + protected function getUserBaseConfig($dashboard_id) { $funktion_kurzbzs = []; $rights = $this->_ci->permissionlib->getAccessRights(); @@ -87,7 +93,11 @@ class DashboardLib $preset = json_decode($presetobj->preset, true); if (null !== $preset) { - $defaultconfig = array_replace_recursive($defaultconfig, $preset); + $preset = array_map(function ($value) use ($presetobj) { + $value['source'] = $presetobj->funktion_kurzbz ?: self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL; + return $value; + }, $preset); + $defaultconfig = array_merge_recursive($defaultconfig, $preset); } } } @@ -95,7 +105,7 @@ class DashboardLib return $defaultconfig; } - public function getUserConfig($dashboard_id, $uid) + protected function getUserOverrideConfig($dashboard_id, $uid) { $res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid); @@ -124,7 +134,7 @@ class DashboardLib $emptyoverride = new stdClass(); $emptyoverride->dashboard_id = $dashboard->dashboard_id; $emptyoverride->uid = $uid; - $emptyoverride->override = '{"' . self::USEROVERRIDE_SECTION . '": {"widgets":{}}, "custom": { "widgets" : {}}}'; + $emptyoverride->override = '[]'; return $emptyoverride; } @@ -143,8 +153,7 @@ class DashboardLib $emptypreset = new stdClass(); $emptypreset->dashboard_id = $dashboard->dashboard_id; $emptypreset->funktion_kurzbz = $funktion_kurzbz; - $section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL; - $emptypreset->preset = '{"' . $section . '": { "widgets" : {}},"custom": { "widgets" : {}}}'; + $emptypreset->preset = '[]'; return $emptypreset; } @@ -209,44 +218,4 @@ class DashboardLib return $result; } - - public function addWidgetsToWidgets(&$widgets, $dashboard_kurzbz, $section, $addwigets) - { - foreach ($addwigets as $widget) - { - if(!isset($widget['widgetid'])) - { - $widget['widgetid'] = $this->generateWidgetId($dashboard_kurzbz); - } - $this->addWidgetToWidgets($widgets, $section, $widget, $widget['widgetid']); - } - } - - public function addWidgetToWidgets(&$widgets, $section, $widget, $widgetid) - { - $section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL; - if (!isset($widgets[$section]) || !isset($widgets[$section]["widgets"]) || !is_array($widgets[$section])) - { - $widgets[$section] = array(); - $widgets[$section]["widgets"] = array(); - } - - $widgets[$section]["widgets"][$widgetid] = $widget; - } - - public function removeWidgetFromWidgets(&$widgets, $section, $widgetid) - { - $section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL; - if (isset($widgets[$section]) && isset($widgets[$section]["widgets"][$widgetid])) - { - unset($widgets[$section]["widgets"][$widgetid]); - if(empty($widgets[$section]["widgets"]) && $section !== self::USEROVERRIDE_SECTION) { - unset($widgets[$section]); - } - return true; - } - else { - return false; - } - } } diff --git a/application/models/content/Content_model.php b/application/models/content/Content_model.php index 278022b59..48a155cb2 100644 --- a/application/models/content/Content_model.php +++ b/application/models/content/Content_model.php @@ -234,9 +234,9 @@ class Content_model extends DB_Model FROM campus.tbl_content c1 LEFT JOIN - campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=? + campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=? AND sichtbar=true WHERE - sichtbar=true + c1.aktiv = true ) s2 LEFT JOIN campus.tbl_contentsprache s3 USING(content_id, sprache) @@ -277,7 +277,7 @@ class Content_model extends DB_Model JOIN campus.tbl_contentsprache s USING(contentsprache_id) LEFT JOIN - campus.tbl_contentchild k ON(m.content_id=k.content_id) + campus.tbl_contentchild k ON(m.content_id=k.content_id) and c.aktiv = true WHERE EXISTS ( SELECT 1 FROM campus.tbl_contentgruppe diff --git a/application/models/dashboard/Bookmark_model.php b/application/models/dashboard/Bookmark_model.php index 5efacc26b..d9756294e 100644 --- a/application/models/dashboard/Bookmark_model.php +++ b/application/models/dashboard/Bookmark_model.php @@ -11,8 +11,4 @@ class Bookmark_model extends DB_Model $this->dbTable = 'dashboard.tbl_bookmark'; $this->pk = 'bookmark_id'; } - - - - } diff --git a/application/models/dashboard/Dashboard_model.php b/application/models/dashboard/Dashboard_model.php index 88946ed83..78f6b1100 100644 --- a/application/models/dashboard/Dashboard_model.php +++ b/application/models/dashboard/Dashboard_model.php @@ -11,15 +11,4 @@ class Dashboard_model extends DB_Model $this->dbTable = 'dashboard.tbl_dashboard'; $this->pk = 'dashboard_id'; } - - - /** - * Get Dashboard by kurzbz. - * @param string dashboard_kurzbz - * @return array - */ - public function getDashboardByKurzbz($dashboard_kurzbz) - { - return $this->loadWhere(array('dashboard_kurzbz' => $dashboard_kurzbz)); - } } diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 3b1ea55e5..0fc3710f0 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -187,7 +187,10 @@ class Projektarbeit_model extends DB_Model campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabetyp.benotbar, campus.tbl_paabgabe.abgabedatum, - campus.tbl_paabgabe.insertvon + campus.tbl_paabgabe.insertvon, + campus.tbl_paabgabe.insertamum, + campus.tbl_paabgabe.updatevon, + campus.tbl_paabgabe.updateamum FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz) WHERE campus.tbl_paabgabe.projektarbeit_id = ? ORDER BY campus.tbl_paabgabe.datum"; @@ -207,7 +210,18 @@ class Projektarbeit_model extends DB_Model campus.tbl_paabgabetyp.paabgabetyp_kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum, - campus.tbl_paabgabe.insertvon + campus.tbl_paabgabe.insertvon, + campus.tbl_paabgabe.updatevon, + campus.tbl_paabgabe.insertamum, + campus.tbl_paabgabe.updateamum, + (SELECT p.vorname || ' ' || p.nachname + FROM public.tbl_benutzer b + JOIN public.tbl_person p USING(person_id) + WHERE b.uid = campus.tbl_paabgabe.insertvon) AS insertvon_fullname, + (SELECT p.vorname || ' ' || p.nachname + FROM public.tbl_benutzer b + JOIN public.tbl_person p USING(person_id) + WHERE b.uid = campus.tbl_paabgabe.updatevon) AS updatevon_fullname FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz) WHERE campus.tbl_paabgabe.projektarbeit_id IN ? ORDER BY campus.tbl_paabgabe.datum"; @@ -240,7 +254,8 @@ class Projektarbeit_model extends DB_Model LEFT JOIN public.tbl_benutzer ben USING (person_id) LEFT JOIN public.tbl_mitarbeiter ma ON ben.uid = ma.mitarbeiter_uid WHERE (ben.aktiv OR ben.aktiv IS NULL) - AND projektarbeit_id = ?"; + AND projektarbeit_id = ? + AND lehre.tbl_projektbetreuer.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}')"; return $this->execReadOnlyQuery($qry, [$projektarbeit_id]); } @@ -294,34 +309,142 @@ class Projektarbeit_model extends DB_Model * Get a List of Projektarbeiten of a mitarbeiter with zuordnung * used by the mitarbeiter cis4 abgabetool. */ - public function getMitarbeiterProjektarbeiten($uid, $showAll){ - $qry = "SELECT - * - FROM - (SELECT tbl_person.vorname, tbl_person.nachname, tbl_studiengang.typ, tbl_studiengang.kurzbz, - tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung, tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note, - tbl_projektbetreuer.person_id as betreuer_person_id, tbl_projektbetreuer.betreuerart_kurzbz, tbl_betreuerart.beschreibung AS betreuerart_beschreibung, - tbl_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid - FROM lehre.tbl_projektarbeit - LEFT JOIN lehre.tbl_projektbetreuer using(projektarbeit_id) - LEFT JOIN lehre.tbl_betreuerart using(betreuerart_kurzbz) - LEFT JOIN public.tbl_benutzer on(uid=student_uid) - LEFT JOIN public.tbl_student on(public.tbl_benutzer.uid=public.tbl_student.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 on(lehre.tbl_lehrveranstaltung.studiengang_kz=public.tbl_studiengang.studiengang_kz) - LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) - WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom') - AND tbl_projektbetreuer.person_id IN (SELECT person_id FROM public.tbl_benutzer - WHERE public.tbl_benutzer.person_id=lehre.tbl_projektbetreuer.person_id - AND public.tbl_benutzer.uid= ? ) - ".($showAll?'':' AND public.tbl_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')." + public function getMitarbeiterProjektarbeiten($person_id, $showAll){ + $qry = "WITH secondary_betreuer AS ( + SELECT DISTINCT ON (pb.projektarbeit_id) + pb.projektarbeit_id, + pb.person_id AS zweitbetreuer_person_id, + b.uid AS zweitbetreuer_benutzer_uid, + pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz, + ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung, + p.titelpre AS zweitbetreuer_titelpre, + p.vorname AS zweitbetreuer_vorname, + p.nachname AS zweitbetreuer_nachname, + p.titelpost AS zweitbetreuer_titelpost, + trim( + COALESCE(p.titelpre, '') || ' ' || + COALESCE(p.vorname, '') || ' ' || + COALESCE(p.nachname, '') || ' ' || + COALESCE(p.titelpost, '') + ) AS zweitbetreuer_full_name + FROM lehre.tbl_projektbetreuer pb + JOIN public.tbl_person p ON p.person_id = pb.person_id + LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id + LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz + WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}') + ORDER BY pb.projektarbeit_id + ), + primary_betreuer AS ( + SELECT DISTINCT ON (pb.projektarbeit_id) + pb.projektarbeit_id, + pb.person_id AS betreuer_person_id, + pb.betreuerart_kurzbz AS betreuer_betreuerart_kurzbz, + ba.beschreibung AS betreuer_betreuerart_beschreibung, + p.titelpre AS betreuer_titelpre, + p.vorname AS betreuer_vorname, + p.nachname AS betreuer_nachname, + p.titelpost AS betreuer_titelpost, + trim( + COALESCE(p.titelpre, '') || ' ' || + COALESCE(p.vorname, '') || ' ' || + COALESCE(p.nachname, '') || ' ' || + COALESCE(p.titelpost, '') + ) AS betreuer_full_name + FROM lehre.tbl_projektbetreuer pb + JOIN public.tbl_person p ON p.person_id = pb.person_id + LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id + LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz + WHERE pb.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}') + ORDER BY pb.projektarbeit_id + ) + + SELECT + * + FROM + (SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id) + student_person.vorname, student_person.nachname, + tbl_studiengang.typ, tbl_studiengang.kurzbz, + tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung, + tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note, + student_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid, + + lehre.tbl_projektarbeit.sprache, + lehre.tbl_projektarbeit.seitenanzahl, + lehre.tbl_projektarbeit.kontrollschlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter_en, + lehre.tbl_projektarbeit.abstract, + lehre.tbl_projektarbeit.abstract_en, + lehre.tbl_projektarbeit.insertamum, + + ( + SELECT orgform_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = ( + SELECT prestudent_id FROM public.tbl_student + WHERE student_uid = student_benutzer.uid LIMIT 1 + ) + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS organisationsform, + ( + SELECT COALESCE(tbl_studienplan.orgform_kurzbz, + tbl_prestudentstatus.orgform_kurzbz, + tbl_studiengang.orgform_kurzbz) + FROM public.tbl_prestudent + JOIN public.tbl_prestudentstatus USING (prestudent_id) + JOIN public.tbl_studiensemester USING (studiensemester_kurzbz) + JOIN public.tbl_studiengang USING (studiengang_kz) + LEFT JOIN lehre.tbl_studienplan USING (studienplan_id) + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY tbl_prestudentstatus.datum DESC + LIMIT 1 + ) AS orgform, + ( + SELECT status_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS studienstatus, + + pb.betreuer_person_id, + pb.betreuer_betreuerart_kurzbz, + pb.betreuer_betreuerart_beschreibung, + pb.betreuer_titelpre, + pb.betreuer_vorname, + pb.betreuer_nachname, + pb.betreuer_titelpost, + pb.betreuer_full_name, + + sb.zweitbetreuer_person_id, + sb.zweitbetreuer_benutzer_uid, + sb.zweitbetreuer_betreuerart_kurzbz, + sb.zweitbetreuer_betreuerart_beschreibung, + sb.zweitbetreuer_titelpre, + sb.zweitbetreuer_vorname, + sb.zweitbetreuer_nachname, + sb.zweitbetreuer_titelpost, + sb.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 lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) + LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz + LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) + LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) + LEFT JOIN primary_betreuer pb ON pb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id + LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id + WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom') + AND (pb.betreuer_person_id = ? OR sb.zweitbetreuer_person_id = ?) + ".($showAll?'':' AND student_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')." AND betreuerart_kurzbz IN ('Betreuer', 'Begutachter', 'Erstbegutachter', 'Zweitbegutachter', 'Erstbetreuer', 'Senatsvorsitz', 'Senatsmitglied') - ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy + ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy ORDER BY nachname;"; - return $this->execReadOnlyQuery($qry, array($uid)); + return $this->execReadOnlyQuery($qry, array($person_id, $person_id)); } /** @@ -341,176 +464,257 @@ class Projektarbeit_model extends DB_Model public function getProjektarbeitenForStudiengang($studiengang_kz, $benotet) { - $new_qry = "SELECT DISTINCT ON(tmp.projektarbeit_id) *, campus.get_betreuer_details(tmp.zweitbetreuer_person_id) as zweitbetreuer_full_name, campus.get_betreuer_details(tmp.betreuer_person_id) as erstbetreuer_full_name - FROM( - SELECT - DISTINCT ON(tbl_projektarbeit.projektarbeit_id) - tbl_projektarbeit.projekttyp_kurzbz, - tbl_projektarbeit.titel, - tbl_projektarbeit.projektarbeit_id, - tbl_studiengang.typ, tbl_studiengang.kurzbz, - student_benutzer.uid as student_uid, - student_person.vorname as student_vorname, - student_person.nachname as student_nachname, - tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, - betreuer_benutzer.uid as betreuer_benutzer_uid, - betreuer_person.titelpre as betreuer_titelpre, - betreuer_person.vorname as betreuer_vorname, - betreuer_person.nachname as betreuer_nachname, - betreuer_person.titelpost as betreuer_titelpost, - lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart, - lehre.tbl_projektbetreuer.person_id as betreuer_person_id, - lehre.tbl_projektarbeit.sprache as sprache, - lehre.tbl_projektarbeit.seitenanzahl as seitenanzahl, - lehre.tbl_projektarbeit.kontrollschlagwoerter as kontrollschlagwoerter, - lehre.tbl_projektarbeit.schlagwoerter as schlagwoerter, - lehre.tbl_projektarbeit.schlagwoerter_en as schlagwoerter_en, - lehre.tbl_projektarbeit.abstract as abstract, - lehre.tbl_projektarbeit.abstract_en as abstract_en, - lehre.tbl_projektarbeit.insertamum as insertamum, - lehre.tbl_projektarbeit.note as note, - ( - SELECT orgform_kurzbz - FROM tbl_prestudentstatus - WHERE prestudent_id = (SELECT prestudent_id - FROM tbl_student - WHERE student_uid = student_benutzer.uid - LIMIT 1) - ORDER BY datum DESC, insertamum DESC, ext_id DESC - LIMIT 1 - ) - as organisationsform, - ( - SELECT person_id - FROM lehre.tbl_projektbetreuer - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_person_id, - ( - SELECT betreuerart_kurzbz - FROM lehre.tbl_projektbetreuer - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_betreuerart_kurzbz, - ( - SELECT tbl_betreuerart.beschreibung - FROM lehre.tbl_projektbetreuer - JOIN lehre.tbl_betreuerart USING (betreuerart_kurzbz) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - AS zweitbetreuer_betreuerart_beschreibung, - ( - SELECT trim(COALESCE(titelpre, '') || ' ' || COALESCE(vorname, '') || ' ' || COALESCE(nachname, '') || ' ' || - COALESCE(titelpost, '')) - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_full_name, - ( - SELECT titelpre - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_titelpre, - ( - SELECT vorname - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_vorname, - ( - SELECT nachname - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_nachname, - ( - SELECT titelpost - FROM public.tbl_person - JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) - LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid) - WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id - AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied') - LIMIT 1 - ) - as zweitbetreuer_titelpost, - ( - SELECT - COALESCE(tbl_studienplan.orgform_kurzbz, - tbl_prestudentstatus.orgform_kurzbz, tbl_studiengang.orgform_kurzbz) as - orgform - FROM - public.tbl_prestudent - JOIN public.tbl_prestudentstatus USING(prestudent_id) - JOIN public.tbl_studiensemester USING(studiensemester_kurzbz) - JOIN public.tbl_studiengang USING(studiengang_kz) - LEFT JOIN lehre.tbl_studienplan USING(studienplan_id) - WHERE - prestudent_id=tbl_student.prestudent_id - ORDER BY tbl_prestudentstatus.datum DESC LIMIT 1 - ) as orgform, - (SELECT status_kurzbz FROM public.tbl_prestudentstatus - WHERE prestudent_id=tbl_student.prestudent_id - ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus - FROM lehre.tbl_projektarbeit - LEFT JOIN public.tbl_benutzer student_benutzer ON (student_benutzer.uid = lehre.tbl_projektarbeit.student_uid) - LEFT JOIN public.tbl_person student_person ON (student_benutzer.person_id = student_person.person_id) - LEFT JOIN public.tbl_student on(student_benutzer.uid = public.tbl_student.student_uid) - LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id) - LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) - LEFT JOIN public.tbl_studiengang ON (public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz) - LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) - LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) - LEFT JOIN public.tbl_person betreuer_person ON (betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id) - LEFT JOIN public.tbl_benutzer betreuer_benutzer ON (betreuer_person.person_id = betreuer_benutzer.person_id) - WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom') - AND student_benutzer.aktiv AND ( - lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbegutachter' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Begutachter' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Betreuer' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbetreuer' - OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Senatsvorsitz' - ) - AND public.tbl_studiengang.studiengang_kz = ?"; + $new_qry = "WITH secondary_betreuer AS ( + SELECT DISTINCT ON (pb.projektarbeit_id) + pb.projektarbeit_id, + pb.person_id AS zweitbetreuer_person_id, + pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz, + ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung, + p.titelpre AS zweitbetreuer_titelpre, + p.vorname AS zweitbetreuer_vorname, + p.nachname AS zweitbetreuer_nachname, + p.titelpost AS zweitbetreuer_titelpost, + trim( + COALESCE(p.titelpre, '') || ' ' || + COALESCE(p.vorname, '') || ' ' || + COALESCE(p.nachname, '') || ' ' || + COALESCE(p.titelpost, '') + ) AS zweitbetreuer_full_name + FROM lehre.tbl_projektbetreuer pb + JOIN public.tbl_person p ON p.person_id = pb.person_id + LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id + LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz + WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}') + ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic + ) - if($benotet == 0) { - $new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL "; - } else if ($benotet == 1) { - $new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL "; - } - - $new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC + SELECT DISTINCT ON (tmp.projektarbeit_id) + *, + campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name, + campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name + FROM ( + SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id) + tbl_projektarbeit.projekttyp_kurzbz, + tbl_projektarbeit.titel, + tbl_projektarbeit.projektarbeit_id, + tbl_studiengang.typ, + tbl_studiengang.kurzbz, + student_benutzer.uid AS student_uid, + student_person.vorname AS student_vorname, + student_person.nachname AS student_nachname, + public.tbl_student.matrikelnr, + tbl_lehreinheit.studiensemester_kurzbz, + betreuer_benutzer.uid AS betreuer_benutzer_uid, + betreuer_person.titelpre AS betreuer_titelpre, + betreuer_person.vorname AS betreuer_vorname, + betreuer_person.nachname AS betreuer_nachname, + betreuer_person.titelpost AS betreuer_titelpost, + lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart, + lehre.tbl_projektbetreuer.person_id AS betreuer_person_id, + lehre.tbl_projektarbeit.sprache, + lehre.tbl_projektarbeit.seitenanzahl, + lehre.tbl_projektarbeit.kontrollschlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter_en, + lehre.tbl_projektarbeit.abstract, + lehre.tbl_projektarbeit.abstract_en, + lehre.tbl_projektarbeit.insertamum, + lehre.tbl_projektarbeit.note, + + sb.zweitbetreuer_person_id, + sb.zweitbetreuer_betreuerart_kurzbz, + sb.zweitbetreuer_betreuerart_beschreibung, + sb.zweitbetreuer_full_name, + sb.zweitbetreuer_titelpre, + sb.zweitbetreuer_vorname, + sb.zweitbetreuer_nachname, + sb.zweitbetreuer_titelpost, + + ( + SELECT orgform_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = ( + SELECT prestudent_id FROM public.tbl_student + WHERE student_uid = student_benutzer.uid LIMIT 1 + ) + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS organisationsform, + ( + SELECT COALESCE(tbl_studienplan.orgform_kurzbz, + tbl_prestudentstatus.orgform_kurzbz, + tbl_studiengang.orgform_kurzbz) + FROM public.tbl_prestudent + JOIN public.tbl_prestudentstatus USING (prestudent_id) + JOIN public.tbl_studiensemester USING (studiensemester_kurzbz) + JOIN public.tbl_studiengang USING (studiengang_kz) + LEFT JOIN lehre.tbl_studienplan USING (studienplan_id) + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY tbl_prestudentstatus.datum DESC + LIMIT 1 + ) AS orgform, + ( + SELECT status_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS studienstatus + + FROM lehre.tbl_projektarbeit + LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid + LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id + LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid + LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id) + LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) + LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz + LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) + LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) + LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id + LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id + LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id + + WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom') + AND student_benutzer.aktiv + AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN ( + 'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz' + ) + AND public.tbl_studiengang.studiengang_kz = ?"; + + if($benotet == 0) { + $new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL "; + } else if ($benotet == 1) { + $new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL "; + } + + $new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC ) as tmp"; return $this->execReadOnlyQuery($new_qry, array($studiengang_kz)); } + + public function getProjektarbeitenForStudent($student_uid) { + $new_qry = "WITH secondary_betreuer AS ( + SELECT DISTINCT ON (pb.projektarbeit_id) + pb.projektarbeit_id, + pb.person_id AS zweitbetreuer_person_id, + pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz, + ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung, + p.titelpre AS zweitbetreuer_titelpre, + p.vorname AS zweitbetreuer_vorname, + p.nachname AS zweitbetreuer_nachname, + p.titelpost AS zweitbetreuer_titelpost, + trim( + COALESCE(p.titelpre, '') || ' ' || + COALESCE(p.vorname, '') || ' ' || + COALESCE(p.nachname, '') || ' ' || + COALESCE(p.titelpost, '') + ) AS zweitbetreuer_full_name + FROM lehre.tbl_projektbetreuer pb + JOIN public.tbl_person p ON p.person_id = pb.person_id + LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id + LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz + WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}') + ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic + ) + + SELECT DISTINCT ON (tmp.projektarbeit_id) + *, + campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name, + campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name + FROM ( + SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id) + tbl_projektarbeit.projekttyp_kurzbz, + tbl_projektarbeit.titel, + tbl_projektarbeit.projektarbeit_id, + tbl_studiengang.typ, + tbl_studiengang.kurzbz, + student_benutzer.uid AS student_uid, + student_person.vorname AS student_vorname, + student_person.nachname AS student_nachname, + public.tbl_student.matrikelnr, + tbl_lehreinheit.studiensemester_kurzbz, + betreuer_benutzer.uid AS betreuer_benutzer_uid, + betreuer_person.titelpre AS betreuer_titelpre, + betreuer_person.vorname AS betreuer_vorname, + betreuer_person.nachname AS betreuer_nachname, + betreuer_person.titelpost AS betreuer_titelpost, + lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart, + lehre.tbl_projektbetreuer.person_id AS betreuer_person_id, + lehre.tbl_projektarbeit.sprache, + lehre.tbl_projektarbeit.seitenanzahl, + lehre.tbl_projektarbeit.kontrollschlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter, + lehre.tbl_projektarbeit.schlagwoerter_en, + lehre.tbl_projektarbeit.abstract, + lehre.tbl_projektarbeit.abstract_en, + lehre.tbl_projektarbeit.insertamum, + lehre.tbl_projektarbeit.note, + + sb.zweitbetreuer_person_id, + sb.zweitbetreuer_betreuerart_kurzbz, + sb.zweitbetreuer_betreuerart_beschreibung, + sb.zweitbetreuer_full_name, + sb.zweitbetreuer_titelpre, + sb.zweitbetreuer_vorname, + sb.zweitbetreuer_nachname, + sb.zweitbetreuer_titelpost, + + ( + SELECT orgform_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = ( + SELECT prestudent_id FROM public.tbl_student + WHERE student_uid = student_benutzer.uid LIMIT 1 + ) + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS organisationsform, + ( + SELECT COALESCE(tbl_studienplan.orgform_kurzbz, + tbl_prestudentstatus.orgform_kurzbz, + tbl_studiengang.orgform_kurzbz) + FROM public.tbl_prestudent + JOIN public.tbl_prestudentstatus USING (prestudent_id) + JOIN public.tbl_studiensemester USING (studiensemester_kurzbz) + JOIN public.tbl_studiengang USING (studiengang_kz) + LEFT JOIN lehre.tbl_studienplan USING (studienplan_id) + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY tbl_prestudentstatus.datum DESC + LIMIT 1 + ) AS orgform, + ( + SELECT status_kurzbz + FROM public.tbl_prestudentstatus + WHERE prestudent_id = public.tbl_student.prestudent_id + ORDER BY datum DESC, insertamum DESC, ext_id DESC + LIMIT 1 + ) AS studienstatus + + FROM lehre.tbl_projektarbeit + LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid + LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id + LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid + LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id) + LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id) + LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz + LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz) + LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) + LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id + LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id + LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id + + WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom') + AND student_benutzer.aktiv + AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN ( + 'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz' + ) + AND public.tbl_student.student_uid = ? + ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC + ) as tmp"; + + return $this->execReadOnlyQuery($new_qry, array($student_uid)); + } /** * diff --git a/application/models/education/Studierendenantrag_model.php b/application/models/education/Studierendenantrag_model.php index 677d01f04..40224ffc9 100644 --- a/application/models/education/Studierendenantrag_model.php +++ b/application/models/education/Studierendenantrag_model.php @@ -149,6 +149,7 @@ class Studierendenantrag_model extends DB_Model $this->addSelect($this->dbTable . '.grund AS grund'); $this->addSelect('s.studierendenantrag_statustyp_kurzbz status'); $this->addSelect('s.insertvon status_insertvon'); + $this->addSelect('s.grund AS status_grund'); $this->addSelect('t.bezeichnung[(' . $lang . ')] statustyp'); $this->addSelect('p.unruly AS unruly'); $this->addSelect($this->dbTable . '.insertamum AS insertamum'); @@ -269,6 +270,7 @@ class Studierendenantrag_model extends DB_Model $this->addSelect($this->dbTable . '.grund'); $this->addSelect($this->dbTable . '.dms_id'); $this->addSelect('s.insertvon AS status_insertvon'); + $this->addSelect('s.grund AS status_grund'); $this->addSelect( "(SELECT count(1) FROM campus.tbl_studierendenantrag_status WHERE studierendenantrag_id = " . $this->dbTable . 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); } diff --git a/application/models/ressource/Reservierung_model.php b/application/models/ressource/Reservierung_model.php index 0c391ea20..cf5f1e4f9 100644 --- a/application/models/ressource/Reservierung_model.php +++ b/application/models/ressource/Reservierung_model.php @@ -18,10 +18,10 @@ class Reservierung_model extends DB_Model * * @return stdClass */ - public function getReservierungen($start_date, $end_date, $ort_kurzbz = null) + public function getReservierungen($start_date, $end_date, $ort_kurzbz = null, $uid = null) { - - $lvplan_reservierungen_query="SELECT r.* , stund.beginn, stund.ende, + + $lvplan_reservierungen_query = "SELECT r.* , stund.beginn, stund.ende, CASE WHEN r.gruppe_kurzbz IS NOT NULL THEN r.gruppe_kurzbz ELSE CONCAT(UPPER(studg.typ),UPPER(studg.kurzbz),'-',COALESCE(CAST(r.semester AS varchar),'/'),COALESCE(CAST(r.verband AS varchar),'/')) @@ -35,7 +35,7 @@ class Reservierung_model extends DB_Model LEFT JOIN public.tbl_studiensemester ss2 ON slv.studiensemester_kurzbz = ss2.studiensemester_kurzbz AND ss2.start <=r.datum AND ss2.ende >= r.datum WHERE datum >= ? AND datum <= ? AND (ss1.studiensemester_kurzbz IS NOT NULL OR ss2.studiensemester_kurzbz IS NOT NULL)"; - + $raum_reservierungen_query = "SELECT res.*, beginn, ende, CASE WHEN res.gruppe_kurzbz IS NOT NULL THEN res.gruppe_kurzbz @@ -46,9 +46,9 @@ class Reservierung_model extends DB_Model JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = res.stunde WHERE res.ort_kurzbz = ? AND datum >= ? AND datum <= ?"; - $subquery = is_null($ort_kurzbz)? $lvplan_reservierungen_query:$raum_reservierungen_query; - - $query_result= $this->execReadOnlyQuery(" + $subquery = is_null($ort_kurzbz) ? $lvplan_reservierungen_query : $raum_reservierungen_query; + + $query_result = $this->execReadOnlyQuery(" SELECT 'reservierung' as type, beginn, ende, datum, COALESCE(titel, beschreibung) as topic, @@ -59,15 +59,15 @@ class Reservierung_model extends DB_Model FROM ( - ". $subquery ." + " . $subquery . " ) AS subquery GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung ORDER BY datum, beginn - ", is_null($ort_kurzbz) ?[getAuthUID(), getAuthUID(),$start_date,$end_date]: [$ort_kurzbz, $start_date, $end_date]); + ", is_null($ort_kurzbz) ? [$uid ?? getAuthUID(), $uid ?? getAuthUID(), $start_date, $end_date] : [$ort_kurzbz, $start_date, $end_date]); + - return $query_result; } @@ -76,7 +76,7 @@ class Reservierung_model extends DB_Model * * @return stdClass */ - public function getReservierungenMitarbeiter($start_date, $end_date) + public function getReservierungenMitarbeiter($start_date, $end_date, $uid = null) { $raum_reservierungen_query = "SELECT res.*, beginn, ende, @@ -91,8 +91,8 @@ class Reservierung_model extends DB_Model $subquery = $raum_reservierungen_query; - - $query_result= $this->execReadOnlyQuery(" + + $query_result = $this->execReadOnlyQuery(" SELECT 'reservierung' as type, beginn, ende, datum, COALESCE(titel, beschreibung) as topic, @@ -103,13 +103,13 @@ class Reservierung_model extends DB_Model FROM ( - ". $subquery ." + " . $subquery . " ) AS subquery GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung ORDER BY datum, beginn - ", [getAuthUID(), $start_date, $end_date]); + ", [$uid ?? getAuthUID(), $start_date, $end_date]); return $query_result; @@ -129,9 +129,9 @@ class Reservierung_model extends DB_Model $this->addJoin('public.tbl_studiensemester ss2', 'slv.studiensemester_kurzbz=ss2.studiensemester_kurzbz AND ss2.start<=r.datum AND ss2.ende>=r.datum', 'LEFT'); $this->db->or_where('ss1.studiensemester_kurzbz IS NOT NULL', null, false); $this->db->or_where('ss2.studiensemester_kurzbz IS NOT NULL', null, false); - + $query = $this->db->get_compiled_select('campus.vw_reservierung r'); - + return $this->execQuery($query, [$uid, $uid]); } diff --git a/application/models/ressource/Stundenplan_model.php b/application/models/ressource/Stundenplan_model.php index d0a97ed9d..997451243 100644 --- a/application/models/ressource/Stundenplan_model.php +++ b/application/models/ressource/Stundenplan_model.php @@ -388,6 +388,84 @@ class Stundenplan_model extends DB_Model ORDER BY datum, beginn", [$start_date, $end_date, $ma_uid]); } + + /** + * queries Stundenplan and filters by studiengang, semester, verband gruppe + * + * @return void + */ + public function getStundenplanStudiengang($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe) { + + $qry_params = [$start_date, $end_date, $stg_kz]; + + $qry = " + SELECT + 'lehreinheit' as type, beginn, ende, datum, + CONCAT(lehrfach,'-',lehrform) as topic, + array_agg(DISTINCT lektor) as lektor, + array_agg(DISTINCT (gruppe,verband,semester,studiengang_kz,gruppen_kuerzel)) as gruppe, + string_agg(DISTINCT ort_kurzbz, '/') as ort_kurzbz, + array_agg(DISTINCT lehreinheit_id) as lehreinheit_id, + titel, lehrfach, lehrform, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id + FROM + ( + SELECT unr,datum,beginn, ende, + CASE + WHEN sp.mitarbeiter_kurzbz IS NOT NULL THEN sp.mitarbeiter_kurzbz + ELSE sp.lektor + END as lektor, + CASE + WHEN gruppe_kurzbz IS NOT NULL THEN gruppe_kurzbz + ELSE CONCAT(UPPER(sp.stg_typ),UPPER(sp.stg_kurzbz),'-',COALESCE(CAST(sp.semester AS varchar),'/'),COALESCE(CAST(sp.verband AS varchar),'/')) + END as gruppen_kuerzel, + (SELECT bezeichnung + FROM public.tbl_organisationseinheit + WHERE oe_kurzbz IN( + SELECT oe_kurzbz + FROM lehre.tbl_lehrveranstaltung + WHERE lehrveranstaltung_id = sp.lehrveranstaltung_id + )) as organisationseinheit, + sp.ort_kurzbz, sp.studiengang_kz, sp.titel,sp.lehreinheit_id,sp.lehrfach_id,sp.anmerkung,fix,lehrveranstaltung_id,stg_kurzbzlang,stg_bezeichnung,stg_typ,fachbereich_kurzbz,lehrfach,lehrfach_bez,farbe,lehrform,anmerkung_lehreinheit,gruppe, verband, semester,stg_kurzbz + FROM ( + SELECT sp.* + FROM lehre.vw_stundenplan sp + WHERE + sp.datum >= ? + AND sp.datum <= ? + ) sp + JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = sp.stunde + WHERE studiengang_kz = ? "; + + if($sem != NULL) + { + $qry_params[] = $sem; + $qry .= " AND (semester = ? OR semester IS NULL)"; + } + if($verband != NULL) + { + $qry_params[] = $verband; + $qry .= " AND (verband = ? OR verband IS NULL OR verband = '0' OR verband = '')"; + } + if($gruppe != NULL) + { + $qry_params[] = $gruppe; + $qry .= " AND (gruppe = ? OR gruppe IS NULL OR gruppe = '0' OR gruppe = '') "; + } + $qry.= " AND ( + gruppe_kurzbz is null OR EXISTS( + SELECT 1 + FROM + public.tbl_gruppe WHERE gruppe_kurzbz = sp.gruppe_kurzbz AND direktinskription = false + ) + )"; + + $qry.= " ) as subquery + + GROUP BY unr, datum, beginn, ende, titel, lehrform, lehrfach, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id + ORDER BY datum, beginn; "; + + return $this->execReadOnlyQuery($qry, $qry_params); + } /** * NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester. diff --git a/application/views/Cis/Abgabetool.php b/application/views/Cis/Abgabetool.php index 86e8721f2..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/application/views/CisRouterView/CisRouterView.php b/application/views/CisRouterView/CisRouterView.php index 6ff428362..c51ca40c3 100644 --- a/application/views/CisRouterView/CisRouterView.php +++ b/application/views/CisRouterView/CisRouterView.php @@ -23,12 +23,14 @@ $includesArray = array( 'public/css/components/FormUnderline.css', 'public/css/components/abgabetool/abgabe.css', 'public/css/Cis4/Cms.css', - 'public/css/Cis4/Studium.css', + 'public/css/Cis4/Studium.css' ), 'customJSs' => array( 'vendor/npm-asset/primevue/accordion/accordion.min.js', 'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js', 'vendor/npm-asset/primevue/checkbox/checkbox.min.js', + 'vendor/npm-asset/primevue/chips/chips.min.js', + 'vendor/npm-asset/primevue/multiselect/multiselect.min.js', 'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js', 'vendor/npm-asset/primevue/speeddial/speeddial.min.js', 'vendor/npm-asset/primevue/textarea/textarea.min.js', @@ -39,7 +41,7 @@ $includesArray = array( 'vendor/moment/luxonjs/luxon.min.js' ), 'customJSModules' => array( - 'public/js/apps/Dashboard/Fhc.js', + 'public/js/apps/Cis/Cis.js', ), ); @@ -47,8 +49,6 @@ $includesArray = array( $this->load->view('templates/CISVUE-Header', $includesArray); ?>
> - +
load->view('templates/CISVUE-Footer', $includesArray); ?> 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/application/views/templates/CISVUE-Footer.php b/application/views/templates/CISVUE-Footer.php index d7c1de24c..eae2a94ff 100644 --- a/application/views/templates/CISVUE-Footer.php +++ b/application/views/templates/CISVUE-Footer.php @@ -6,7 +6,7 @@ $includesArray = array( 'fontawesome6' => true, 'axios027' => true, 'customJSModules' => array_merge([ - 'public/js/apps/Cis.js' + 'public/js/apps/Cis/Menu.js' ], $customJSModules ?? []), 'customCSSs' => array_merge([ 'public/css/Cis4/Cis.css' diff --git a/application/views/templates/CISVUE-Header.php b/application/views/templates/CISVUE-Header.php index 804a43821..d98cbc9cd 100644 --- a/application/views/templates/CISVUE-Header.php +++ b/application/views/templates/CISVUE-Header.php @@ -8,7 +8,7 @@ $includesArray = array( 'axios027' => true, 'primevue3' => true, 'customJSModules' => array_merge([ - 'public/js/apps/Cis.js' + 'public/js/apps/Cis/Menu.js' ], $customJSModules ?? []), 'customCSSs' => array_merge([ 'public/css/Cis4/Cis.css', diff --git a/cis/private/lehre/notenliste.xls.php b/cis/private/lehre/notenliste.xls.php index 25f353c12..52b18b056 100644 --- a/cis/private/lehre/notenliste.xls.php +++ b/cis/private/lehre/notenliste.xls.php @@ -36,6 +36,7 @@ require_once('../../../include/notenschluessel.class.php'); require_once('../../../include/Excel/excel.php'); require_once('../../../include/phrasen.class.php'); require_once('../../../include/pruefung.class.php'); +require_once('../../../include/benutzerberechtigung.class.php'); $uid = get_uid(); @@ -44,7 +45,7 @@ $uid = get_uid(); $sprache = getSprache(); $p = new phrasen($sprache); -if(!check_lektor($uid)) +if (!check_lektor($uid)) die('Sie haben keine Berechtigung fuer diese Seite'); if (!$db = new basis_db()) @@ -90,6 +91,21 @@ if(isset($_GET['lehreinheit_id'])) else $lehreinheit_id = ''; +// Permissions +$berechtigung = new benutzerberechtigung(); +$berechtigung->getBerechtigungen($uid); + +// LV load +$lvobj = new lehrveranstaltung($lvid); + +// Check permissions +if (!$berechtigung->isBerechtigt('admin') + && !$berechtigung->isBerechtigt('assistenz') + && !$berechtigung->isBerechtigt('lehre', $lvobj->oe_kurzbz, 's') + && !check_lektor_lehrveranstaltung($uid, $lvid, $stsem) +) + die('Sie haben keine Berechtigung fuer diese Seite'); + /* * Create Excel File */ @@ -143,8 +159,6 @@ else // let's merge $format_title->setAlign('merge'); - $lvobj = new lehrveranstaltung($lvid); - $worksheet->write(0,0,$p->t('anwesenheitsliste/notenliste')." ".($sprache=='English'?$lvobj->bezeichnung_english:$lvobj->bezeichnung),$format_bold); $stg_obj = new studiengang($stg); diff --git a/composer.json b/composer.json index c1f4506c6..4d37abdbb 100644 --- a/composer.json +++ b/composer.json @@ -70,6 +70,18 @@ } } }, + { + "type": "package", + "package": { + "name": "drag-drop-touch-js/dragdroptouch", + "version": "2.0.3", + "source": { + "url": "https://github.com/drag-drop-touch-js/dragdroptouch.git", + "type": "git", + "reference": "master" + } + } + }, { "type": "package", "package": { @@ -452,6 +464,8 @@ "easyrdf/easyrdf": "0.9.*", + "drag-drop-touch-js/dragdroptouch": "*", + "fgelinas/timepicker": "0.3.3", "fortawesome/font-awesome4": "4.7.*", "fortawesome/font-awesome6": "6.1.*", @@ -519,5 +533,9 @@ "phpmetrics/phpmetrics": "2.*", "sebastian/phpcpd": "3.*", "phpunit/phpunit": "^6" + }, + "scripts": { + "post-install-cmd": "@symlink_vendor_to_public", + "symlink_vendor_to_public": "ln -sfn ../vendor ./public/" } } diff --git a/composer.lock b/composer.lock index 7525b7f5b..212eea3be 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4f0af4586f46f97d8b6092c1ac0fb3a", + "content-hash": "869cbc35bd1ba90ab90934fcb41b0f51", "packages": [ { "name": "afarkas/html5shiv", @@ -804,6 +804,16 @@ "abandoned": true, "time": "2018-03-09T06:07:41+00:00" }, + { + "name": "drag-drop-touch-js/dragdroptouch", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/drag-drop-touch-js/dragdroptouch.git", + "reference": "master" + }, + "type": "library" + }, { "name": "easyrdf/easyrdf", "version": "0.9.1", diff --git a/config/global.config-default.inc.php b/config/global.config-default.inc.php index 943363f6d..7351b865b 100644 --- a/config/global.config-default.inc.php +++ b/config/global.config-default.inc.php @@ -373,4 +373,8 @@ define('TESTTOOL_EXTERNE_UEBERWACHUNG_ALLOWED', false); //enable tags in StudVW define('STV_TAGS_ENABLED', false); + +//student accounts grace period +define('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD', '0 days'); +define('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES', serialize(array('NO_DEFINED_ROLE'))); ?> diff --git a/include/benutzerberechtigung.class.php b/include/benutzerberechtigung.class.php index 69d4947bf..e015ce210 100644 --- a/include/benutzerberechtigung.class.php +++ b/include/benutzerberechtigung.class.php @@ -340,6 +340,40 @@ class benutzerberechtigung extends basis_db return true; } + protected function hasPreStudentStatusInGracePeriod($uid) + { + $period = defined('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD') + ? STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_PERIOD + : '0 days'; + $mapfunc = function($val) { + return $this->db_add_param($val); + }; + $roles = defined('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES') + ? implode(', ', array_map($mapfunc, unserialize(STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES))) + : "'NO_DEFINED_ROLE'"; + + $sql = <<db_add_param($uid)} + AND + vs.aktiv = false + AND + public.get_rolle_prestudent(vs.prestudent_id, null) IN ({$roles}) + AND + (vs.updateaktivam + INTERVAL {$this->db_add_param($period)})::date >= CURRENT_DATE +EOSQL; + $result = $this->db_query($sql); + if($result && $this->db_num_rows($result) > 0) + { + return true; + } + return false; + } + /** * Laedt die Berechtigungen eines Users * @param $uid @@ -355,7 +389,8 @@ class benutzerberechtigung extends basis_db if($row = $this->db_fetch_object($result)) { // Wenn die Person nicht aktiv ist dann hat diese auch keine Rechte - if($this->db_parse_bool($row->aktiv) == false) + if($this->db_parse_bool($row->aktiv) == false + && $this->hasPreStudentStatusInGracePeriod($uid) === false) return false; } else diff --git a/public/css/Cis4/Cis.css b/public/css/Cis4/Cis.css index 3056d36b6..fd417ed22 100644 --- a/public/css/Cis4/Cis.css +++ b/public/css/Cis4/Cis.css @@ -147,6 +147,8 @@ html { --fhc-cis-menu-lvl-5-color-hover: var(--fhc-text); --fhc-cis-grade-positive: var(--fhc-success); --fhc-cis-grade-negative: var(--fhc-danger); + + --fhc-offcanvas-zindex: 1045; } #themeSwitch i{ @@ -375,7 +377,7 @@ html { /* searchbar */ #nav-search { background-color: var(--fhc-primary); - z-index: 1; + z-index: calc(var(--fhc-offcanvas-zindex) + 1) !important; } #nav-search.me-3 { margin: 0 !important; @@ -386,12 +388,6 @@ html { #nav-search > .input-group > * { border-radius: 0 !important; } -#nav-search .searchbar_results { - top: 100% !important; - left: 0; - right: 0 !important; - width: 100% !important; -} /* frame */ .in-frame { @@ -413,10 +409,18 @@ html { color: var(--fhc-link) !important; } +#nav-main { + z-index: var(--fhc-offcanvas-zindex); +} + #nav-main-sticky { max-height: calc(100vh - var(--fhc-cis-header-height)); } +#nav-user-menu { + z-index: calc(var(--fhc-offcanvas-zindex) + 1); +} + #nav-user-menu img { object-fit: cover; height: calc( 3 * var(--fhc-cis-header-py)); @@ -457,7 +461,14 @@ html { /* overflow: visible !important; */ } #cis-header { - z-index: 10; + z-index: 10; + } + #cis-header-bar { + position: fixed; + top: 0; + height: var(--fhc-cis-header-height); + width: 100%; + background-color: var(--fhc-primary); } #cis-header nav { position: initial; @@ -473,12 +484,7 @@ html { display: none; } #nav-logo { - position: fixed; - top: 0; - left: 0; - height: var(--fhc-cis-header-height); width: var(--fhc-cis-menu-width); - background-color: var(--fhc-primary); padding: var(--fhc-cis-header-py) var(--fhc-cis-header-px); z-index: 2; } @@ -493,37 +499,27 @@ html { top: var(--fhc-cis-header-height); display: flex; flex-direction: column; - } - #nav-main-sticky > :not(#nav-main-toggle) { - overflow: auto; + height: 100%; } #nav-main-toggle { - width: 0; z-index: 1; } - #nav-main-toggle .btn, - #nav-main-toggle .fa-arrow-circle-left { + #nav-main-toggle:hover { + background-color: var(--fhc-secondary-highlight); + } + #nav-main-toggle .div, + #nav-main-toggle .fa-chevron-left { transition: all 0.5s ease-in-out; } #nav-main-toggle .collapsed.btn { background-color: transparent !important; } - #nav-main-toggle .collapsed .fa-arrow-circle-left { + #nav-main-toggle .collapsed .fa-chevron-left { transform: scaleX(-1); color: var(--fhc-black-40); } #nav-search { - position: fixed; - top: 0; - left: var(--fhc-cis-menu-width); - height: var(--fhc-cis-header-height); - right: calc(var(--fhc-cis-header-height) + 2 * var(--fhc-cis-header-px) - 2 * var(--fhc-cis-header-py)); - width: auto !important; - } - #nav-user { - position: fixed; - top: 0; - right: 0; + position: relative; } #nav-user-btn { border-width: 0; @@ -558,7 +554,7 @@ html { #nav-main-menu { height: 100%; - background-color: var(--fhc-cis-menu-bg); + background-color: var(--fhc-primary); } #nav-main-menu > div { width: var(--fhc-cis-menu-width); @@ -600,9 +596,6 @@ html { } #nav-user{ position: relative; - } - #nav-user-btn { - } #nav-user-btn img { object-fit: cover; diff --git a/public/css/Studentenverwaltung.css b/public/css/Studentenverwaltung.css index fb56ec75c..86e3d6be3 100644 --- a/public/css/Studentenverwaltung.css +++ b/public/css/Studentenverwaltung.css @@ -277,3 +277,7 @@ html.fs_huge { } */ /* slim ende */ + +.fhc-xxl-modal { + min-width: 80vw; +} \ No newline at end of file diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index d70481805..af9578124 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -305,4 +305,104 @@ /* If you use hover rows, you need to ensure the sticky cell matches the hover color */ #abgabetable .tabulator-row:hover .tabulator-cell.sticky-col { background-color: #ccc; /* Match your existing hover color */ -} \ No newline at end of file +} + +.tabulator-cell { + container-type: inline-size; +} + +.tabulator-col-title { + container-type: inline-size; +} + +@container (max-width: 100px) { + .full-text { + display: none !important; + } + + .short-text { + display: inline-block !important; + width: 100%; + } +} + +/*conditional tooltips fix*/ +.p-tooltip.custom-tooltip { + z-index: 8001 !important; +} + + /* Shrinks font and table rows for desktop users who have zoomed in their browser (150%+). + Does not affect mobile/touchscreen devices, which use touch input instead of a mouse. */ +@media (pointer: fine) and (min-resolution: 1.5dppx) { + + html.abgabetool { + font-size: 0.5rem; + } + + .abgabetool .tabulator-cell, + .abgabetool .tabulator-row { + height: 20px; + max-height: 20px; + } + +} + +/*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; + } +} + +.p-timeline-event-opposite { + flex: 0 0 90px; + max-width: 90px; +} + +.p-timeline-event-content { + flex: 1 1 auto; + min-width: 0; +} diff --git a/public/css/components/dashboard.css b/public/css/components/dashboard.css index 88d136b55..eb9f0a4b1 100644 --- a/public/css/components/dashboard.css +++ b/public/css/components/dashboard.css @@ -2,7 +2,7 @@ @import './dashboard/news.css'; @import './dashboard/LvPlan.css'; -:root{ +:root { --fhc-dashboard-danger: var(--fhc-danger, #842029); --fhc-dashboard-grid-size: 4; --fhc-dashboard-link: var(--fhc-link, #0a57ca); @@ -17,22 +17,16 @@ --fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585); } -@media(max-width: 577px) { - :root { - --fhc-dashboard-grid-size: 1; - } -} - -.core-dashboard a{ +.core-dashboard a { color: var(--fhc-dashboard-link); } -@media (max-width: 576px){ +@media (max-width: 576px) { .widget-icon { max-height: 250px; object-fit: cover; } - .widget-icon-container{ + .widget-icon-container { max-width: 250px; margin-left: auto; margin-right: auto; @@ -46,27 +40,36 @@ background-repeat: no-repeat; background-position: center; background-size: cover; - cursor:pointer; + cursor: pointer; } -.dashboard-section > .newGridRow{ - position:absolute; - width:20px; - height:20px; - padding:0; - bottom:0; - left:50%; - transform:translate(-50%, 50%); +.dashboard-section.edit-active { + /** + * replaces margin for extra row + * 10% equals 0.1 of 100% + * 1rem equals the padding of pb-3 that is overwritten here + */ + padding-bottom: calc(10% / var(--fhc-dashboard-grid-size) + 1rem) !important; +} + +.dashboard-section > .newGridRow { + position: absolute; + width: 20px; + height: 20px; + padding: 0; + bottom: 0; + left: 50%; + transform: translate(-50%, 50%); background-color: var(--fhc-dashboard-gridrow-background); } .newGridRow:hover { - color:white; + color: white; background-color: var(--fhc-dashboard-gridrow-background-highlight); } .empty-tile-hover:hover { - background-image: url('data:image/svg+xml;utf8,'); + background-image: url('data:image/svg+xml;utf8,'); } .alert-danger .form-check-input:checked { @@ -74,16 +77,6 @@ background-color: var(--fhc-dashboard-danger); } -:root { - --fhc-dashboard-grid-size: 4; -} - -@media(max-width: 1400px) { - :root { - --fhc-dashboard-grid-size: 4; - } -} - @media(max-width: 1200px) { :root { --fhc-dashboard-grid-size: 3; @@ -105,6 +98,7 @@ @media(max-width: 577px) { :root { --fhc-dashboard-grid-size: 1; + --fhc-dg-item-py: .75rem; } } @@ -132,50 +126,64 @@ cursor: move !important; } -.draggedItem { +.drop-grid-item-resize > .dashboard-item, +.drop-grid-item-move > .dashboard-item { height: 100%; width: 100%; background-color: var(--fhc-dashboard-draggeditem-background); position: relative; } -.dashboard-item-overlay{ +.drop-grid-item-resize > .dashboard-item > *, +.drop-grid-item-move > .dashboard-item > * { + display: none; +} + +.drop-grid-item-sizechanged > .dashboard-item, +.drop-grid-item-move > .dashboard-item { background-color: var(--fhc-dashboard-item-overlay-background); } -.dashboard-item-overlay::before{ - position:absolute; - content:""; - top:0.25rem; - left:0.25rem; - right:0.25rem; - bottom:0.25rem; - border:4px dashed var(--fhc-dashboard-item-overly-border-color); - opacity: 0.5; +.drop-grid-item-sizechanged > .dashboard-item::before, +.drop-grid-item-move > .dashboard-item::before { + position: absolute; + content: ""; + top: .25rem; + left: .25rem; + right: .25rem; + bottom: .25rem; + border: 4px dashed var(--fhc-dashboard-item-overly-border-color); + opacity: .5; } -#deleteBookmark i{ +.drop-grid-item-oversized > .dashboard-item { + /* Bootstrap: border-danger */ + --bs-border-opacity: 1; + border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; +} + +#deleteBookmark i { color: var(--fhc-dashboard-danger); } -.pin:hover{ +.pin:hover { cursor: pointer; } -.pin[pinned]:hover{ +.pin[pinned]:hover { color: var(--fhc-dashboard-pin-pinned-hover-color); } -.section-info{ +.section-info { color: var(--fhc-dashboard-section-info-color); - cursor:pointer; + cursor: pointer; } .section-info:hover { color: var(--fhc-dashboard-section-info-color-hover); } -.denied-dragging-animation { +.drop-grid-item-blocker [pinned='true'] { animation: wiggle 0.5s linear; color: var(--fhc-dashboard-denied-dragging-animation-color) !important; } @@ -204,13 +212,13 @@ } -.hiddenWidget{ +.hidden-widget { background: var(--fhc-disabled-background); opacity: 40%; } -.hiddenWidget .card, -.hiddenWidget .card-body, -.hiddenWidget .card-body *{ +.hidden-widget .card, +.hidden-widget .card-body, +.hidden-widget .card-body * { background: inherit !important; } diff --git a/public/js/api/factory/abgabe.js b/public/js/api/factory/abgabe.js index c6f229973..2e582f742 100644 --- a/public/js/api/factory/abgabe.js +++ b/public/js/api/factory/abgabe.js @@ -77,6 +77,13 @@ export default { } }; }, + patchProjektarbeitAbgabeMultiple(payload) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/patchProjektarbeitAbgabeMultiple', + params: payload + }; + }, deleteProjektarbeitAbgabe(paabgabe_id) { return { method: 'post', @@ -84,6 +91,13 @@ export default { params: { paabgabe_id } }; }, + deleteProjektarbeitAbgabeMultiple(paabgabe_ids) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabeMultiple', + params: { paabgabe_ids } + }; + }, postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) { return { method: 'post', @@ -139,6 +153,30 @@ export default { url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben', params: {paabgabe_ids, student_uid}, + }; + }, + postStudentProjektarbeitTitel(projektarbeit_id, titel) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitTitel', + params: {projektarbeit_id, titel}, + + }; + }, + sendZweitbetreuerTokenMail(projektarbeit_id, betreuer_person_id, student_uid) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/sendZweitbetreuerTokenMail', + params: {projektarbeit_id, bperson_id: betreuer_person_id, student_uid}, + + }; + }, + fetchProjektarbeitenHistory(student_uid) { + return { + method: 'post', + url: '/api/frontend/v1/Abgabe/fetchProjektarbeitenHistory', + params: {student_uid}, + }; } }; \ No newline at end of file diff --git a/public/js/api/factory/cis/dashboard.js b/public/js/api/factory/cis/dashboard.js index cb1ec16bb..694340b97 100644 --- a/public/js/api/factory/cis/dashboard.js +++ b/public/js/api/factory/cis/dashboard.js @@ -19,7 +19,7 @@ export default { getViewData() { return { method: 'get', - url: '/api/frontend/v1/Cis4FhcApi/getViewData' + url: '/api/frontend/v1/Cis4FhcApi/dashboardViewData' }; } }; \ No newline at end of file diff --git a/public/js/api/factory/cis/studium.js b/public/js/api/factory/cis/studium.js new file mode 100644 index 000000000..eed78a9ba --- /dev/null +++ b/public/js/api/factory/cis/studium.js @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2026 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getAllStudienSemester(studiensemester, studiengang, semester, studienplan) { + return { + method: 'get', + url: 'api/frontend/v1/Studium/getStudienAllSemester/', + params: {studiensemester, studiengang, semester, studienplan} + }; + }, +} \ No newline at end of file diff --git a/public/js/api/factory/lehre.js b/public/js/api/factory/lehre.js index 84c8e8662..4f32a1e55 100644 --- a/public/js/api/factory/lehre.js +++ b/public/js/api/factory/lehre.js @@ -35,5 +35,11 @@ export default { method: 'get', url: `/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}` }; + }, + getSemesterAverageGrade(semester) { + return { + method: 'get', + url: `/api/frontend/v1/Lehre/semesterAverageGrade/${semester}` + } } }; \ No newline at end of file diff --git a/public/js/api/factory/lvPlan.js b/public/js/api/factory/lvPlan.js index 0b179c9ba..ac369c0e8 100644 --- a/public/js/api/factory/lvPlan.js +++ b/public/js/api/factory/lvPlan.js @@ -16,6 +16,12 @@ */ export default { + getMyLvPlanViewData() { + return { + method: 'get', + url: `/api/frontend/v1/LvPlan/myLvPlanViewData`, + }; + }, getRoomInfo(ort_kurzbz, start_date, end_date) { return { method: 'post', @@ -30,11 +36,11 @@ export default { params: { start_date, end_date, lv_id } }; }, - eventsPersonal(start_date, end_date) { + eventsPersonal(start_date, end_date, uid = null) { return { method: 'post', url: '/api/frontend/v1/lvPlan/eventsPersonal', - params: { start_date, end_date } + params: { start_date, end_date, uid } }; }, eventsLv(lv_id, start_date, end_date) { @@ -57,11 +63,11 @@ export default { params: { start_date, end_date } }; }, - getLvPlanReservierungen(start_date, end_date) { + getLvPlanReservierungen(start_date, end_date, uid = null) { return { method: 'post', url: '/api/frontend/v1/LvPlan/getReservierungen', - params: { start_date, end_date } + params: { start_date, end_date, uid } }; }, getLehreinheitStudiensemester(lehreinheit_id) { @@ -92,5 +98,42 @@ export default { method: 'get', url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id }; - } + }, + eventsStgOrg(start_date, end_date, stg_kz, sem, verband, gruppe) { + return { + method: 'post', + url: '/api/frontend/v1/lvPlan/eventsStgOrg', + params: { start_date, end_date, stg_kz, sem, verband, gruppe } + }; + }, + getStudiengaenge(){ + return { + method: 'get', + url: '/api/frontend/v1/lvPlan/getStudiengaenge' + } + }, + getLehrverband(stg_kz, sem){ + return { + method: 'get', + url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}` + } + }, + getGruppe(stg_kz, sem, verband){ + return { + method: 'get', + url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}/${verband}` + } + }, + checkPermissionOtherLvPlan(){ + return { + method: 'get', + url: '/api/frontend/v1/lvPlan/permissionOtherLvPlan', + } + }, + getCompactibleEventTypes(){ + return { + method: 'get', + url: '/api/frontend/v1/lvPlan/compactibleEventTypes', + } + }, }; \ No newline at end of file diff --git a/public/js/api/factory/otherLvPlan.js b/public/js/api/factory/otherLvPlan.js new file mode 100644 index 000000000..21a115496 --- /dev/null +++ b/public/js/api/factory/otherLvPlan.js @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getOtherLvPlanViewData(uid) { + return { + method: 'get', + url: `/api/frontend/v1/OtherLvPlan/otherLvPlanViewData/${uid}`, + }; + }, +}; \ No newline at end of file diff --git a/public/js/api/factory/phrasen.js b/public/js/api/factory/phrasen.js index a87e4f4be..1f8e772ba 100644 --- a/public/js/api/factory/phrasen.js +++ b/public/js/api/factory/phrasen.js @@ -41,5 +41,24 @@ export default { method: 'get', url: '/api/frontend/v1/phrasen/getAllLanguages' }; - } + }, + getPhrases(phrasesGroupedByCategory, languages = []) { + return { + method: 'post', + url: '/api/frontend/v1/phrasen/getPhrases', + params: { + phrasesGroupedByCategory, + languages, + }, + }; + }, + getTabulatorPhrases(languages = []) { + return { + method: 'get', + url: '/api/frontend/v1/phrasen/getTabulatorPhrases', + params: { + languages: JSON.stringify(languages), + }, + }; + }, }; \ No newline at end of file diff --git a/public/js/api/factory/profil.js b/public/js/api/factory/profil.js index 1d884c714..6959b4345 100644 --- a/public/js/api/factory/profil.js +++ b/public/js/api/factory/profil.js @@ -17,7 +17,7 @@ export default { - profilViewData(uid) { + getProfilViewData(uid = null) { let url = "/api/frontend/v1/Profil/profilViewData"; if(uid){ url += `/${uid}`; diff --git a/public/js/api/factory/stgOrgLvPlan.js b/public/js/api/factory/stgOrgLvPlan.js new file mode 100644 index 000000000..bac753f6b --- /dev/null +++ b/public/js/api/factory/stgOrgLvPlan.js @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2025 fhcomplete.org + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export default { + getStgOrgLvPlanViewData(uid) { + return { + method: 'get', + url: `/api/frontend/v1/StgOrgLvPlan/stgOrgLvPlanViewData`, + }; + }, +}; \ No newline at end of file diff --git a/public/js/api/factory/widget/bookmark.js b/public/js/api/factory/widget/bookmark.js index 9768e25ac..2035a9f8f 100644 --- a/public/js/api/factory/widget/bookmark.js +++ b/public/js/api/factory/widget/bookmark.js @@ -17,6 +17,7 @@ export default { getBookmarks() { + return { method: 'get', url: '/api/frontend/v1/Bookmark/getBookmarks' @@ -28,18 +29,24 @@ export default { url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}` }; }, - update({ bookmark_id, url, title, tag=null }) { + update({ bookmark_id, url, title, tag }) { return { method: 'post', url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`, - params: { url, title } + params: { url, title, tag } }; }, - insert({ url, title, tag }) { + insert({ url, title, tag, sort }) { return { method: 'post', url: `/api/frontend/v1/Bookmark/insert`, - params: { url, title, tag } + params: { url, title, tag, sort } }; + }, + changeOrder(bookmark_id1, bookmark_id2) { + return { + method: 'post', + url: `/api/frontend/v1/Bookmark/changeOrder/${bookmark_id1}/${bookmark_id2}`, + }; } }; \ No newline at end of file diff --git a/public/js/apps/Abgabetool/Abgabetool.js b/public/js/apps/Abgabetool/Abgabetool.js index 83233ba22..748f10159 100644 --- a/public/js/apps/Abgabetool/Abgabetool.js +++ b/public/js/apps/Abgabetool/Abgabetool.js @@ -25,9 +25,6 @@ const app = Vue.createApp({ }, computed: { - viewData() { - return { uid: this.uid} - }, student_uid_computed() { return this.student_uid ?? this.uid }, @@ -55,10 +52,10 @@ const app = Vue.createApp({ }, template: ` ` }); diff --git a/public/js/apps/Dashboard/Fhc.js b/public/js/apps/Cis/Cis.js similarity index 79% rename from public/js/apps/Dashboard/Fhc.js rename to public/js/apps/Cis/Cis.js index 140c76402..d4f846f25 100644 --- a/public/js/apps/Dashboard/Fhc.js +++ b/public/js/apps/Cis/Cis.js @@ -4,25 +4,27 @@ import Theme from '../../plugins/Theme.js'; import contrast from '../../directives/contrast.js'; import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js"; -import MyLvPlan from "../../components/Cis/LvPlan/Personal.js"; +import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js"; import MylvStudent from "../../components/Cis/Mylv/Student.js"; import Profil from "../../components/Cis/Profil/Profil.js"; import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js"; import CmsNews from "../../components/Cis/Cms/News.js"; import CmsContent from "../../components/Cis/Cms/Content.js"; import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; -import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js"; +import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js"; import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js"; import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js"; import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js"; import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js"; import Studium from "../../components/Cis/Studium/Studium.js"; +import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js"; +import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js"; -import ApiRenderers from '../../api/factory/renderers.js'; import ApiRouteInfo from '../../api/factory/routeinfo.js'; import {capitalize} from "../../helpers/StringHelpers.js"; const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router; +const isMobile = window.matchMedia("(max-width: 767px)").matches; const router = VueRouter.createRouter({ history: VueRouter.createWebHistory(`/${ciPath}`), @@ -85,7 +87,7 @@ const router = VueRouter.createRouter({ name: "RoomInformation", params: { // in this case always populate other params since they are not optional ort_kurzbz: to.params.ort_kurzbz, - mode: DEFAULT_MODE_RAUMINFO, + mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP, focus_date: new Date().toISOString().split("T")[0] }, }; @@ -102,7 +104,7 @@ const router = VueRouter.createRouter({ const mode = route.params.mode && validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()) ? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase() - : DEFAULT_MODE_RAUMINFO; + : (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP); // default to today date if not provided const d = new Date(route.params.focus_date) @@ -197,6 +199,26 @@ const router = VueRouter.createRouter({ }; } }, + { + path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`, + name: 'StgOrgLvPlan', + component: StgOrgLvPlan, + props(route) { + return { + propsViewData: route.params + }; + } + }, + { + path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`, + name: "OtherLvPlan", + component: OtherLvPlan, + props(route) { + return { + propsViewData: route.params + }; + } + }, { path: `/Cis4`, name: 'Cis4', @@ -227,26 +249,22 @@ const router = VueRouter.createRouter({ }) const app = Vue.createApp({ - name: 'FhcApp', + name: 'CisApp', data: () => ({ appSideMenuEntries: {}, - renderers: null, + windowWidth: 0, }), - components: {}, - computed: { - isMobile() { - const smallScreen = window.matchMedia("(max-width: 767px)").matches; - const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0; - return smallScreen;// && touchCapable; - } - }, provide() { return { // provide injectable & watchable language property language: Vue.computed(() => this.$p.user_language), - renderers: Vue.computed(() => this.renderers), - isMobile: this.isMobile + isMobile: Vue.computed(() => this.isMobile), } }, + computed: { + isMobile: function() { + return (this.windowWidth < 767); + } + }, methods: { isInternalRoute(href) { const internalBase = window.location.origin @@ -280,54 +298,21 @@ const app = Vue.createApp({ this.$router.push(route); } - } + }, + handleWindowResize() { + this.windowWidth = window.innerWidth; + }, }, - async created(){ - - await this.$api - .call(ApiRenderers.loadRenderers()) - .then(res => res.data) - .then(data => { - for (let rendertype of Object.keys(data)) { - let modalTitle = null; - let modalContent = null; - let calendarEvent = null; - if (data[rendertype].modalTitle) - modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle))); - if (data[rendertype].modalContent) - modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent))); - if (data[rendertype].calendarEvent) - calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent))); - - if (data[rendertype].calendarEventStyles){ - var head = document.head; - if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){ - var link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; - link.href = data[rendertype].calendarEventStyles; - head.appendChild(link); - } - } - - if(this.renderers === null) { - this.renderers = {}; - } - if (!this.renderers[rendertype]) { - this.renderers[rendertype] = {} - } - this.renderers[rendertype].modalTitle = modalTitle; - this.renderers[rendertype].modalContent = modalContent; - this.renderers[rendertype].calendarEvent = calendarEvent; - } - }); + created() { + this.windowWidth = window.innerWidth; }, - mounted() { + async mounted() { document.addEventListener('click', this.handleClick); - + window.addEventListener("resize", this.handleWindowResize); }, beforeUnmount() { document.removeEventListener('click', this.handleClick); + window.removeEventListener("resize", this.handleWindowResize); }, }); @@ -353,4 +338,4 @@ app.mount('#fhccontent'); router.afterEach((to, from, failure) => { app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath)); -}); \ No newline at end of file +}); diff --git a/public/js/apps/Cis.js b/public/js/apps/Cis/Menu.js similarity index 73% rename from public/js/apps/Cis.js rename to public/js/apps/Cis/Menu.js index c88a47a35..3463dda4a 100644 --- a/public/js/apps/Cis.js +++ b/public/js/apps/Cis/Menu.js @@ -1,11 +1,12 @@ -import FhcSearchbar from "../components/searchbar/searchbar.js"; -import CisMenu from "../components/Cis/Menu.js"; -import PluginsPhrasen from '../plugins/Phrasen.js'; -import ApiSearchbar from '../api/factory/searchbar.js'; -import Theme from "../plugins/Theme.js"; +import FhcSearchbar from "../../components/searchbar/searchbar.js"; +import CisMenu from "../../components/Cis/Menu.js"; +import PluginsPhrasen from '../../plugins/Phrasen.js'; +import Theme from "../../plugins/Theme.js"; +import ApiSearchbar from '../../api/factory/searchbar.js'; +import ApiLvPlan from "../../api/factory/lvPlan.js"; const app = Vue.createApp({ - name: 'CisApp', + name: 'CisMenuApp', components: { FhcSearchbar, CisMenu @@ -133,14 +134,61 @@ const app = Vue.createApp({ childactions: [] } } - } + }, + windowWidth: 0, }; }, + provide() { + return { + isNarrow: Vue.computed(() => this.windowWidth < 992), + isMobile: Vue.computed(() => this.windowWidth < 767), + } + }, methods: { searchfunction: function(searchsettings) { return this.$api.call(ApiSearchbar.searchCis(searchsettings)); - } - } + }, + handleWindowResize() { + this.windowWidth = window.innerWidth; + }, + }, + created() { + this.windowWidth = window.innerWidth; + }, + async mounted() { + const openOtherLvPlanAction = { + label: Vue.computed(() => this.$p.t("lehre/stundenplan")), + icon: "fas fa-calendar-days", + type: "link", + action: function (data) { + const uid = JSON.parse(data.data).uid; + const link = + FHC_JS_DATA_STORAGE_OBJECT.app_root + + FHC_JS_DATA_STORAGE_OBJECT.ci_router + + "/Cis/OtherLvPlan/" + + uid; + return link; + }, + }; + let checkPermissionOtherLvPlanResult = await this.$api.call( + ApiLvPlan.checkPermissionOtherLvPlan(), + ); + if ( + checkPermissionOtherLvPlanResult.meta.status === "success" && + checkPermissionOtherLvPlanResult.data + ) { + this.searchbaroptions.actions.employee.childactions.push( + openOtherLvPlanAction, + ); + this.searchbaroptions.actions.student.childactions.push( + openOtherLvPlanAction, + ); + } + window.addEventListener("resize", this.handleWindowResize); + }, + beforeUnmount() { + window.removeEventListener("resize", this.handleWindowResize); + }, }); FhcApps.makeExtendable(app); diff --git a/public/js/apps/Dashboard/Admin.js b/public/js/apps/Dashboard/Admin.js index 32909a50a..c361c2fb4 100644 --- a/public/js/apps/Dashboard/Admin.js +++ b/public/js/apps/Dashboard/Admin.js @@ -3,13 +3,10 @@ import DashboardAdmin from '../../components/Dashboard/Admin.js'; import PluginsPhrasen from '../../plugins/Phrasen.js'; -import ApiRenderers from '../../api/factory/renderers.js'; - const app = Vue.createApp({ name: 'DashboardAdminApp', data: () => ({ - appSideMenuEntries: {}, - renderers: null + appSideMenuEntries: {} }), components: { CoreNavigationCmpt, @@ -17,51 +14,16 @@ const app = Vue.createApp({ }, provide() { return { - // TODO(chris): move those two into the components that need it - renderers: Vue.computed(() => this.renderers), timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone }; - }, - created() { - this.$api - .call(ApiRenderers.loadRenderers()) - .then(res => { - for (let rendertype of Object.keys(res.data)) { - let modalTitle = null; - let modalContent = null; - let calendarEvent = null; - if (res.data[rendertype].modalTitle) - modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalTitle))); - if (res.data[rendertype].modalContent) - modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalContent))); - if (res.data[rendertype].calendarEvent) - calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].calendarEvent))); - - if (res.data[rendertype].calendarEventStyles) { - var head = document.head; - if (!head.querySelector(`link[href="${res.data[rendertype].calendarEventStyles}"]`)) { - var link = document.createElement("link"); - link.type = "text/css"; - link.rel = "stylesheet"; - link.href = res.data[rendertype].calendarEventStyles; - head.appendChild(link); - } - } - - if (this.renderers === null) { - this.renderers = {}; - } - if (!this.renderers[rendertype]) { - this.renderers[rendertype] = {} - } - this.renderers[rendertype].modalTitle = modalTitle; - this.renderers[rendertype].modalContent = modalContent; - this.renderers[rendertype].calendarEvent = calendarEvent; - } - }) - .catch(this.$fhcAlert.handleSystemErrors); } }); +app.use(primevue.config.default, { + zIndex: { + overlay: 9000, + tooltip: 8000 + } +}) app.use(PluginsPhrasen); app.directive('tooltip', primevue.tooltip); app.mount('#main'); \ No newline at end of file diff --git a/public/js/apps/DashboardAdmin_DEPR.js b/public/js/apps/DashboardAdmin_DEPR.js deleted file mode 100644 index 05b438798..000000000 --- a/public/js/apps/DashboardAdmin_DEPR.js +++ /dev/null @@ -1,16 +0,0 @@ -import {CoreNavigationCmpt} from '../components/navigation/Navigation.js'; -import DashboardAdmin from '../components/Dashboard/Admin.js'; -import Phrases from "../plugin/Phrasen.js" - -Vue.createApp({ - name: 'DashboardAdminApp', - data: () => ({ - appSideMenuEntries: {} - }), - components: { - CoreNavigationCmpt, - DashboardAdmin - }, - mounted() { - } -}).use(Phrases).mount('#main'); \ No newline at end of file diff --git a/public/js/components/Bootstrap/Modal.js b/public/js/components/Bootstrap/Modal.js index e320d4429..0f2504da6 100644 --- a/public/js/components/Bootstrap/Modal.js +++ b/public/js/components/Bootstrap/Modal.js @@ -4,7 +4,9 @@ export default { name: 'BootstrapModal', data: () => ({ modal: null, - fullscreen: false + fullscreen: false, + expandBtnHovered: false, + expandBtnFocused: false, }), props: { backdrop: { @@ -70,6 +72,29 @@ export default { this.$emit('toggleFullscreen') } }, + computed: { + getExpandButtonStyles() { + const hovered = this.expandBtnHovered; + const focused = this.expandBtnFocused; + return `display: flex; + align-items: center; + justify-content: center; + width: 1em; + height: 1em; + padding: 0; + border: 0; + background: transparent; + font-size: 1em; + opacity: 0.5; + color: inherit; + cursor: pointer; + line-height: 1; + transition: opacity 0.15s ease; + opacity: ${focused ? '1' : hovered ? '0.75' : '0.5'}; + outline: ${focused ? '1px solid currentColor' : 'none'}; + outline-offset: 2px;` + } + }, mounted() { if (this.$refs.modal) this.modal = new bootstrap.Modal(this.$refs.modal, { @@ -140,9 +165,13 @@ export default {
@@ -932,6 +947,9 @@ export const AbgabeMitarbeiterDetail = {
+
+ {{getSavedTerminInfoString(termin)}} +
@@ -944,7 +962,8 @@ export const AbgabeMitarbeiterDetail = { + dialogClass="bordered-modal modal-lg" + bodyClass="px-4 py-4"> - -
@@ -430,7 +529,7 @@ export const AbgabeStudentDetail = {
- +
{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}
@@ -466,7 +565,7 @@ export const AbgabeStudentDetail = {
@@ -542,25 +638,66 @@ export const AbgabeStudentDetail = {
{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}
-
- + + +
+ + + + + - `, }; diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentTimeline.js b/public/js/components/Cis/Abgabetool/AbgabeStudentTimeline.js new file mode 100644 index 000000000..7b2986637 --- /dev/null +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentTimeline.js @@ -0,0 +1,251 @@ +import AbgabeterminStatusLegende from "./StatusLegende.js"; +import {formatDateTime} from "./dateUtils.js"; + +export const AbgabeStudentTimeline = { + name: "AbgabeStudentTimeline", + components: { + AbgabeterminStatusLegende, + Timeline: primevue.timeline, + }, + props: { + projekte: { type: Array, default: () => [] }, + notenOptions: { type: Array, default: () => [] }, + formatDateFn: { type: Function, required: true } + }, + data() { + return { + legendExpanded: false, + expandedProjects: {} + } + }, + computed: { + student() { + return this.projekte?.[0] ?? null + } + }, + watch: { + projekte: { + immediate: true, + handler(val) { + // open all projects by default whenever the student changes + const state = {} + val?.forEach(p => { state[p.projektarbeit_id] = true }) + this.expandedProjects = state + this.legendExpanded = false + } + } + }, + methods: { + getNoteBezeichnung(projektarbeit) { + if(projektarbeit.note && this.notenOptions) { + const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note) + return noteOpt?.bezeichnung + } else { + return '' + } + }, + getSavedTerminInfoString(termin) { + const isUpdate = termin.updateamum != null; + + const fullname = isUpdate + ? termin.updatevon_fullname + : termin.insertvon_fullname; + + const datetime = isUpdate + ? termin.updateamum + : termin.insertamum; + + return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname]) + }, + getItemBezeichnung(item) { + if (!item?.bezeichnung) return '' + return item.bezeichnung?.bezeichnung ?? item.bezeichnung + }, + getItemNote(item) { + if (!item?.note) return '' + if (item.note?.bezeichnung) return item.note.bezeichnung + return this.notenOptions?.find(n => n.note == item.note)?.bezeichnung ?? String(item.note) + }, + getIconClass(dateStyle) { + return ({ + verspaetet: 'fa-solid fa-triangle-exclamation', + verpasst: 'fa-solid fa-calendar-xmark', + abzugeben: 'fa-solid fa-hourglass-half', + standard: 'fa-solid fa-clock', + abgegeben: 'fa-solid fa-paperclip', + beurteilungerforderlich: 'fa-solid fa-list-check', + bestanden: 'fa-solid fa-check', + nichtbestanden: 'fa-solid fa-circle-exclamation', + })[dateStyle] ?? '' + }, + getBetreuerLabel(projekt) { + return projekt.erstbetreuer_full_name + || (projekt.betreuer_vorname ? `${projekt.betreuer_vorname} ${projekt.betreuer_nachname}`.trim() : null) + }, + toggleProject(id) { + this.expandedProjects[id] = !this.expandedProjects[id] + } + }, + template: ` +
+ +
+ {{ student.student_vorname }} {{ student.student_nachname }} + {{ student.student_uid }} + {{ student.matrikelnr }} +
+ +
+ + + +
+ +
+ {{ projekt.betreuerart || $capitalize($p.t('abgabetool/c4erstbetreuerv2')) }}: + {{ getBetreuerLabel(projekt) }} +
+ + + + + + + + + + +
+
+ +
+ +
+ +
+
+ +
+ ` +} + +export default AbgabeStudentTimeline; \ No newline at end of file diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index bb5c6a710..3d8fc1d79 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -7,14 +7,17 @@ import ApiAbgabe from '../../../api/factory/abgabe.js' import ApiStudiensemester from '../../../api/factory/studiensemester.js'; import AbgabeterminStatusLegende from "./StatusLegende.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js"; +import AbgabeStudentTimeline from "./AbgabeStudentTimeline.js"; import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" import { getDateStyleClass} from "./getDateStyleClass.js"; -import { dateFilter } from '../../../tabulator/filters/Dates.js'; +import { dateFilter } from '../../../tabulator/filters/DatesManual.js'; +import { compareISODateValues, formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js"; export const AbgabetoolAssistenz = { name: "AbgabetoolAssistenz", components: { AbgabeterminStatusLegende, + AbgabeStudentTimeline, BsModal, BsOffcanvas, CoreFilterCmpt, @@ -42,20 +45,26 @@ export const AbgabetoolAssistenz = { stg_kz_prop: { default: null }, - viewData: { - type: Object, - required: true, - default: () => ({name: '', uid: ''}), - validator(value) { - return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards - } - } }, data() { return { - tableData: null, + flatDataDirty: true, + mode: 'perProjectView', + qgate1FilterSelected: [], + qgate2FilterSelected: [], + pa_noteFilterSelected: [], + noteFilterSelected: [], + count: 0, + filteredcount: 0, + selectedcount: 0, + countFlat: 0, + filteredcountFlat: 0, + selectedcountFlat: 0, + filteredRows: null, + filteredRowsFlat: null, studiensemesterOptions: null, allSem: null, + allSemOption: null, curSem: null, notenOptionFilter: null, inplaceToggle: false, @@ -64,7 +73,12 @@ export const AbgabetoolAssistenz = { colLayoutRestored: false, sortRestored: false, stateRestored: false, - timelineProjekt: null, + headerFiltersRestoredFlat: false, + filtersRestoredFlat: false, + colLayoutRestoredFlat: false, + sortRestoredFlat: false, + stateRestoredFlat: false, + timelineProjekte: [], selectedStudiengangOption: null, studiengaengeOptions: null, detailIsFullscreen: false, @@ -75,6 +89,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, @@ -82,8 +97,24 @@ 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(), + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -95,7 +126,9 @@ export const AbgabetoolAssistenz = { }), showAll: false, tabulatorUuid: Vue.ref(0), + tabulatorUuidFlat: Vue.ref(0), selectedData: [], + selectedDataFlat: [], domain: '', student_uid: null, detail: null, @@ -104,6 +137,8 @@ export const AbgabetoolAssistenz = { selectedProjektarbeit: null, tableBuiltResolve: null, tableBuiltPromise: null, + tableBuiltResolveFlat: null, + tableBuiltPromiseFlat: null, abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', @@ -111,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: [ { @@ -166,64 +200,78 @@ export const AbgabetoolAssistenz = { handleClick: this.selectAllHandler }, width: 50, - cssClass: 'sticky-col' + cssClass: 'sticky-col', + visible: true }, - // { - // field: 'rowSelection', - // formatter: 'rowSelection', - // titleFormatter: 'rowSelection', - // titleFormatterParams: { - // rowRange: "active" // Only toggle the values of the active filtered rows - // }, - // hozAlign:"center", - // headerSort: false, - // frozen: true, - // width: 40 - // }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 100, cssClass: 'sticky-col'}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgform'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, - formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 130, visible: true, cssClass: 'sticky-col'}, + {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/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, sorter: this.notenSorter, visible: false, minWidth: 200, formatter: this.centeredTextFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, visible: false, formatter: this.centeredTextFormatter, minWidth: 100}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + field: 'prevTermin', formatter: this.abgabeterminFormatter, width: 250, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabeterminFormatter, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate1Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate2Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + }, ], persistence: false, persistenceID: "abgabetool_2026_03_16" @@ -242,14 +290,776 @@ export const AbgabetoolAssistenz = { }) this.selectedData = data + this.selectedcount = data.length; } - } - ]}; + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredRows = rows; + this.filteredcount = rows.length; + + if (!this.selectedData.length) return; + + const visibleData = new Set(rows.map(r => r.getData())); + const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd)); + + if (!filteredOut.length) return; + + const filteredOutSet = new Set(filteredOut); + this.$refs.abgabeTable.tabulator.getSelectedRows() + .filter(r => filteredOutSet.has(r.getData())) + .forEach(r => r.deselect()); + } + }], + abgabeTableOptionsFlat: { + minHeight: 250, + height: 700, + index: 'paabgabe_id', + layout: 'fitColumns', + placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), + selectable: true, + selectableCheck: this.selectionCheckFlat, + renderVerticalBuffer: 400, + columns: [ + { + formatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + if(!cell.getRow().getData().selectable) return + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle select manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + cell.getRow().getData().checkbox = checkbox + + let wrapper = document.createElement("div"); + wrapper.style.cssText = "display: flex; justify-content: center; align-items: center; height: 100%; width: 100%;"; + + wrapper.appendChild(checkbox); + + return wrapper; + }, + titleFormatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle "select all" manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + return checkbox; + }, + hozAlign: "center", + headerSort: false, + formatterParams: { + handleClick: this.selectHandler + }, + titleFormatterParams: { + handleClick: this.selectAllHandlerFlat + }, + width: 50, + cssClass: 'sticky-col' + }, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, 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.paabgabetypFormatter, + + minWidth: 120, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), + field: 'kurzbz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 120 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), + field: 'datum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminColISO, + sorter: compareISODateValues, + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 100 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabedatum'))), + field: 'abgabedatum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminColISO, + sorter: compareISODateValues, + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 100 + }, + { + title: 'Status', + field: 'dateStyle', + headerSort: false, + headerFilter: this.statusHeaderFilterEditor, + headerFilterFunc: this.statusHeaderFilterFunc, + headerFilterParams: {}, + formatter: this.abgabeterminFormatter, + formatterParams: { iconOnly: true }, + width: 70, + tooltip: (e, cell) => this.mapDateStyleToTabulatorTooltip(cell.getValue()) + }, + { + 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; + }, + sorter: this.notenSorterFlat, + minWidth: 100, + tooltip: false, + headerFilter: this.notenHeaderFilterEditor, + headerFilterFunc: this.notenHeaderFilterFunc, + headerFilterParams: {}, + }, + { + 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, + headerFilter: this.notenHeaderFilterEditor, + headerFilterFunc: this.notenHeaderFilterFunc, + headerFilterParams: {}, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notizQualGatev2'))), + field: 'beurteilungsnotiz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 150, visible: false + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), + field: 'fixtermin', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4upload_allowed'))), + field: 'upload_allowed', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + ], + persistence: false, + persistenceID: "abgabetoolflat_2026_05_05" + }, + abgabeTableEventHandlersFlat: [ + { + event: "rowSelectionChanged", + handler: async(data) => + { + this.selectedDataFlat.filter(sd => !data.includes(sd)).forEach(fsd => { + if(fsd.checkbox) fsd.checkbox.checked = false + }) + + data.forEach(d => { + if(d.checkbox) d.checkbox.checked = true + }) + + this.selectedDataFlat = data + this.selectedcountFlat = data.length; + } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredRowsFlat = rows; + this.filteredcountFlat = rows.length; + + if (!this.selectedDataFlat.length) return; + + const visibleData = new Set(rows.map(r => r.getData())); + const filteredOut = this.selectedDataFlat.filter(sd => !visibleData.has(sd)); + + if (!filteredOut.length) return; + + const filteredOutSet = new Set(filteredOut); + this.$refs.abgabeTableFlat.tabulator.getSelectedRows() + .filter(r => filteredOutSet.has(r.getData())) + .forEach(r => r.deselect()); + } + } + ] + }; }, methods: { + notenSorterFlat(a, b, aRow, bRow, column, dir, sorterParams) { + // flat table has their own sort since the field is called sligthly different in that context + // since note would be bestanden/nicht bestanden and that hardly needs sorting + const aData = aRow.getData() + const bData = bRow.getData() + return aData.pa_note - bData.pa_note + }, + notenSorter(a, b, aRow, bRow, column, dir, sorterParams) { + const aData = aRow.getData() + const bData = bRow.getData() + return aData.note - bData.note + }, + notenHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { + if (!this.notenOptions) return; + + const field = cell.getField(); + const stateKey = field + 'FilterSelected'; + 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: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + + // mapping evaluated at render time, not at column definition time + const fieldOptionsMap = { + 'pa_note': this.notenOptions, + 'note': this.allowedNotenOptions, + }; + const options = fieldOptionsMap[cell.getField()] ?? this.notenOptions; + if (!options) return; + + const updateDisplay = () => { + display.value = options + .filter(o => selected.includes(o.note)) + .map(o => o.bezeichnung) + .join(', '); + }; + options.forEach(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.note; + cb.checked = selected.includes(opt.note); + cb.style.cssText = 'margin: 0 6px;'; + cb.addEventListener('change', () => { + selected = cb.checked + ? [...selected, opt.note] + : selected.filter(v => v !== opt.note); + this[stateKey] = [...selected]; + updateDisplay(); + success([...selected]); + }); + + const labelText = document.createElement('span'); + labelText.textContent = opt.bezeichnung; + + row.appendChild(cb); + 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; + }, + + notenHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + // rowVal is the raw integer note id or a note object + const noteId = typeof rowVal === 'object' ? rowVal?.note : rowVal; + return filterVal.some(val => val == noteId); // loose equality: filter vals are numbers, noteId might be string + }, + handleFilterActiveChanged(active) { + if(!active && this.allSemOption && this.stateRestored) { + this.curSem = this.allSemOption + } + }, + reloadData() { + this.loadProjektarbeiten() + }, + openEditModal() { + // reset + this.serienEditFields = { + datum: false, + bezeichnung: false, + kurzbz: false, + upload_allowed: false, + fixtermin: false, + } + this.serienEdit.datum = getViennaTodayISO() + 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; + + 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 = [ + { label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' }, + { label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' }, + { label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' }, + { label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' }, + { label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' }, + { label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' }, + ]; + + const field = cell.getField(); + const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected'; + let selected = [...(this[stateKey] || [])]; // restore persistence state + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; width: 100%;'; + + const display = document.createElement('input'); + display.readOnly = true; + display.placeholder = ''; + display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;'; + + const dropdown = document.createElement('div'); + dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + options.forEach(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.value; + cb.checked = selected.includes(opt.value); // sync with persistence + cb.addEventListener('change', () => { + if (cb.checked) { + selected.push(opt.value); + } else { + selected = selected.filter(v => v !== opt.value); + } + this[stateKey] = [...selected]; // sync with persistence + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + success([...selected]); + }); + + row.appendChild(cb); + row.appendChild(document.createTextNode(opt.label)); + dropdown.appendChild(row); + }); + + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + + display.addEventListener('click', () => { + if (dropdown.style.display === 'none') { + const rect = display.getBoundingClientRect(); + dropdown.style.top = rect.bottom + 'px'; + dropdown.style.left = rect.left + 'px'; + dropdown.style.display = 'block'; + } else { + dropdown.style.display = 'none'; + } + }); + + display.addEventListener('blur', () => { + setTimeout(() => { dropdown.style.display = 'none'; }, 150); + }); + + document.body.appendChild(dropdown); + wrapper.appendChild(display); + + cell.getElement().addEventListener('remove', () => dropdown.remove()); + + onRendered(() => display.focus()); + + return wrapper; + }, + qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + + const matches = (val) => { + switch (val) { + case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet'); + case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet'); + case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded'); + case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted'); + case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet'); + case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden'); + default: return true; + } + }; + + // OR logic — row passes if it matches any selected filter + return filterVal.some(val => matches(val)); + }, + redrawTableScrollSave() { + const table = this.$refs.abgabeTable.tabulator; + const scrollX = table.rowManager.scrollLeft; + const scrollY = table.rowManager.scrollTop; + this.$refs.abgabeTable.tabulator.redraw(true) + + Vue.nextTick(()=> { + const tableholder = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(tableholder) { + tableholder.scrollLeft = scrollX; + tableholder.scrollTop = scrollY; + } + }) + }, + shortLongTitleFormatter(cell, formatterParams, onRendered) { + const longForm = cell.getValue() + const shortForm = formatterParams?.shortForm + + if(longForm && shortForm) { + return ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + toolTipFuncPrevTermin(e, cell, onRendered) { + const data = cell.getData(); + 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) { + switch(dateStyleString) { + case 'bestanden': + return this.$p.t('abgabetool/c4tooltipBestanden') + break; + case 'nichtbestanden': + return this.$p.t('abgabetool/c4tooltipNichtBestanden') + break; + case 'beurteilungerforderlich': + return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich') + break; + case 'verspaetet': + return this.$p.t('abgabetool/c4tooltipVerspaetet') + break; + case 'abgegeben': + return this.$p.t('abgabetool/c4tooltipAbgegeben') + break; + case 'verpasst': + return this.$p.t('abgabetool/c4tooltipVerpasst') + break; + case 'abzugeben': + return this.$p.t('abgabetool/c4tooltipAbzugeben') + break; + case 'standard': + return this.$p.t('abgabetool/c4tooltipStandardv2') + break; + default: return '' + } + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) - this.$refs.abgabeTable.tabulator.redraw(true) + this.redrawTableScrollSave() }, getQGateStatusList() { return [ @@ -276,6 +1086,41 @@ export const AbgabetoolAssistenz = { // just in case someone reuses this return Math.abs(b.diffMs) - Math.abs(a.diffMs) }, + headerFilterTerminColISO(filterVal, rowVal) { + if (!rowVal) { + return false; + } + + const toLuxon = (val) => { + if (!val) return null; + let dt; + if (val instanceof Date) { + dt = luxon.DateTime.fromJSDate(val); + } else if (typeof val === "string") { + dt = toViennaDate(val); + } else { // fallback + dt = luxon.DateTime.fromMillis(Number(val)); + } + + return dt.isValid ? dt : null; + }; + + const rowDate = toLuxon(rowVal); + const von = toLuxon(filterVal[0]); + const bis = toLuxon(filterVal[1]); + + // specific day + if (von && !bis) { + return rowDate.hasSame(von, "day"); + } + + // range case + if (von && bis) { + return rowDate >= von.startOf("day") && rowDate <= bis.endOf("day"); + } + + return false + }, headerFilterTerminCol(filterVal, rowVal) { if (!rowVal || !rowVal.luxonDate || !rowVal.luxonDate.isValid) { return false; @@ -289,7 +1134,7 @@ export const AbgabetoolAssistenz = { if (val instanceof Date) { dt = luxon.DateTime.fromJSDate(val); } else if (typeof val === "string") { - dt = luxon.DateTime.fromISO(val); + dt = toViennaDate(val); } else { // fallback dt = luxon.DateTime.fromMillis(Number(val)); } @@ -321,20 +1166,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(); @@ -351,11 +1207,31 @@ export const AbgabetoolAssistenz = { }, selectAllHandler(e, cell) { const table = cell.getTable(); - const rows = table.getRows(); + const rows = this.filteredRows ?? table.getRows(); // custom select all logic const allowed = rows.filter(r => r.getData().selectable); - const selected = allowed.every(r => r.isSelected()); + const selected = rows.every(r => r.isSelected()); + + if(selected){ + allowed.forEach(r => r.deselect()); + e.target.checked = false; + } else { + allowed.forEach(r => r.select()); + e.target.checked = true; + } + + // stop built-in handler + 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 = allowed.length > 0 && allowed.every(r => r.isSelected()); if(selected){ allowed.forEach(r => r.deselect()); @@ -428,6 +1304,32 @@ export const AbgabetoolAssistenz = { projekt.qgate2StatusRank = 1 } }) + + // set shorthand statuscode once real status has been determined + projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank) + projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank) + }, + mapRankToShortStatus(rank) { + switch(rank){ + case 0: // kein termin vorhanden + return '--' + break; + case 1: // noch nicht stattgefunden + return 'o' + break; + case 2: // noch nicht abgegeben + return '?' + break; + case 3: // noch nicht benotet + return '~' + break; + case 4: // negativ benotet + return '-' + break; + case 5: // positiv benotet + return '+' + break; + } }, getItemBezeichnung(item){ if(!item.bezeichnung) return '' @@ -458,7 +1360,6 @@ export const AbgabetoolAssistenz = { if(this.$refs.abgabeTable.tabulator) { const table = this.$refs.abgabeTable.tabulator - // TODO: maybe check if existing synergy really works with many filters const existing = table.getFilters().filter(f => f.field != 'studiensemester_kurzbz'); const compVal = e.value.studiensemester_kurzbz == this.$p.t('abgabetool/c4all') ? '' : e.value.studiensemester_kurzbz @@ -476,12 +1377,17 @@ export const AbgabetoolAssistenz = { // calculate Abgabetermin time diff to now and assign last and next to projekt projekt.abgabetermine.forEach(termin => { + // only set this if it has not been set yet and abgabetermin has a note (qgate) + if(!termin.noteBackend && termin.note) { + termin.noteBackend = this.notenOptions.find(opt => opt.note == termin.note) + } + termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) // while already looping through each termin, calculate datestyle beforehand termin.dateStyle = getDateStyleClass(termin, this.notenOptions) - const date = luxon.DateTime.fromISO(termin.datum).endOf('day') + const date = toViennaDate(termin.datum).endOf('day') termin.luxonDate = date termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past @@ -567,7 +1473,7 @@ export const AbgabetoolAssistenz = { table.on("renderComplete", () => { if(!this.stateRestored) { - + if (saved?.columns && !this.colLayoutRestored) { const layout = saved.columns.map(col => ({ field: col.field, @@ -588,6 +1494,8 @@ export const AbgabetoolAssistenz = { if (saved?.headerFilters && !this.headerFiltersRestored) { this.headerFiltersRestored = true // instantly avoid retriggers for (let hf of saved.headerFilters) { + if (hf.field === 'qgate1Status') this.qgate1FilterSelected = hf.value || []; + if (hf.field === 'qgate2Status') this.qgate2FilterSelected = hf.value || []; table.setHeaderFilterValue(hf.field, hf.value); } } @@ -608,16 +1516,134 @@ export const AbgabetoolAssistenz = { }, 100); } this.stateRestored = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); } }); }, + handleTableBuiltFlat() { + const table = this.$refs.abgabeTableFlat.tabulator + + this.tableBuiltResolveFlat() + + table.on("columnMoved", () => { + this.saveStateFlat(table); + }); + + table.on("columnResized", () => { + this.saveStateFlat(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveStateFlat(table); + }); + + table.on("filterChanged", () => { + this.saveStateFlat(table); + }); + + table.on("headerFilterChanged", () => { + this.saveStateFlat(table); + }); + + table.on("dataSorted", () => { + this.saveStateFlat(table); + }); + + table.on("columnSorted", () => { + this.saveStateFlat(table); + }); + + table.on("sortersChanged", () => { + this.saveStateFlat(table); + }); + + const saved = this.loadStateFlat(); + + table.on("renderComplete", () => { + if(!this.stateRestoredFlat) { + + if (saved?.columns && !this.colLayoutRestoredFlat) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestoredFlat = true; + } + + if (saved?.filters && !this.filtersRestoredFlat) { + this.filtersRestoredFlat = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestoredFlat) { + this.headerFiltersRestoredFlat = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + if (hf.field === 'note') this.noteFilterSelected = hf.value || []; + if (hf.field === 'pa_note') this.pa_noteFilterSelected = hf.value || []; + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestoredFlat) { + this.sortRestoredFlat = 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.stateRestoredFlat = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTableFlat.setSelectedFields(); + + } + + }); + }, + 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 }, getOptionLabelAbgabetyp(option){ - return option.bezeichnung + return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz) }, getOptionLabelStg(option){ return option.kurzbzlang + ' ' + option.bezeichnung @@ -629,24 +1655,15 @@ export const AbgabetoolAssistenz = { return option.bezeichnung }, formatDate(dateParam) { - if(dateParam === null) return '' - const date = new Date(dateParam) - // handle missing leading 0 - const padZero = (num) => String(num).padStart(2, '0'); - - const month = padZero(date.getMonth() + 1); // Months are zero-based - const day = padZero(date.getDate()); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; + return formatISODate(dateParam); }, formAction(cell) { const actionButtons = document.createElement('div'); - actionButtons.className = "d-flex gap-3"; // you can keep Bootstrap gap if loaded + actionButtons.className = "d-flex gap-3"; actionButtons.style.display = "flex"; - actionButtons.style.alignItems = "stretch"; // buttons stretch to full height + actionButtons.style.alignItems = "stretch"; actionButtons.style.justifyContent = "start"; - actionButtons.style.height = "100%"; // full grid cell height + actionButtons.style.height = "100%"; const val = cell.getValue(); @@ -701,9 +1718,14 @@ export const AbgabetoolAssistenz = { }, selectionCheck(row) { const data = row.getData() - if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false + + // currently assistenz is allowed to select everything in projektarbeit table + return true }, + selectionCheckFlat(row) { + return row.getData().selectable + }, showDeadlines(){ const link = FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/Cis/Abgabetool/Deadlines' @@ -720,7 +1742,7 @@ export const AbgabetoolAssistenz = { this.saving = true this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.$api.call(ApiAbgabe.postSerientermin( - this.serienTermin.datum.toISOString(), + this.serienTermin.datum, this.serienTermin.bezeichnung.paabgabetyp_kurzbz, this.serienTermin.bezeichnung.bezeichnung, this.serienTermin.kurzbz, @@ -745,27 +1767,18 @@ export const AbgabetoolAssistenz = { abgabe.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz == abgabe.paabgabetyp_kurzbz) pa.abgabetermine.push(abgabe) - pa.abgabetermine.sort((a, b) => new Date(a.datum) - new Date(b.datum)) + pa.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum)) }) - // reset selection to empty - // this.$refs.abgabeTable.tabulator.deselectRow() - const table = this.$refs.abgabeTable.tabulator; - const scrollX = table.rowManager.scrollLeft; - const scrollY = table.rowManager.scrollTop; - - const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + this.projektarbeiten = this.mapProjekteToTableData(this.projektarbeiten) - table.setData(mappedData) - table.redraw(true) + this.redrawTableScrollSave() - Vue.nextTick(()=> { - const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') - if(table) { - table.scrollLeft = scrollX; - table.scrollTop = scrollY; - } - }) + // 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 @@ -789,12 +1802,13 @@ export const AbgabetoolAssistenz = { if(this.notenOptions && projekt.note) { const opt = this.notenOptions.find(n => n.note == projekt.note) - // TODO: mehrsprachig englisch + // TODO: mehrsprachig englisch -> nevermind the english field in + // notenoption->bezeichnung_mehrsprachig is ALWAYS german projekt.note_bez = opt.bezeichnung } const latestTerminWithUpload = this.findLatestTerminWithUpload(projekt) - + return { ...projekt, abgabetermine: projekt.abgabetermine, @@ -816,8 +1830,10 @@ export const AbgabetoolAssistenz = { }) }, findLatestTerminWithUpload(projekt) { - const withAbgabedatumSorted = projekt?.abgabetermine?.filter(t => t.abgabedatum != null)?.sort((a,b) => a < b) - + const withAbgabedatumSorted = projekt?.abgabetermine + ?.filter(t => t.abgabedatum != null) + ?.sort((a, b) => compareISODateValues(b.abgabedatum, a.abgabedatum)); + if(withAbgabedatumSorted.length) { return withAbgabedatumSorted[0] } @@ -860,20 +1876,17 @@ export const AbgabetoolAssistenz = { if(typeof termin.note !== 'object') { termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) } - - // only set this if it has not been set yet and abgabetermin has a note (qgate) - if(!termin.noteBackend && termin.note) { - termin.noteBackend = termin.note - } termin.file = [] // assistenz should be able to edit every abgabe // update 21-01-2026: actually blocking operations on finished projektarbeiten seems like a decent idea + const terminHasAbgabe = termin.abgabedatum != null + const terminHasNote = termin.noteBackend termin.allowedToSave = paIsBenotet ? false : true // assistenz are not allowed to delete deadlines with existing submissions - termin.allowedToDelete = paIsBenotet ? false : !termin.abgabedatum + termin.allowedToDelete = paIsBenotet || terminHasNote || terminHasAbgabe ? false : true }) @@ -885,97 +1898,117 @@ export const AbgabetoolAssistenz = { this.$refs.modalContainerAbgabeDetail.show() }, openTimeline(val) { - const projekt = this.projektarbeiten.find(p => p.projektarbeit_id == val.projektarbeit_id) - if(!projekt) { + + this.$api.call(ApiAbgabe.fetchProjektarbeitenHistory(val.student_uid)).then(res => { - 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 + res.data.forEach(projekt => { + projekt.abgabetermine?.forEach(termin => { + // only set this if it has not been set yet and abgabetermin has a note (qgate) + if(!termin.noteBackend && termin.note) { + termin.noteBackend = this.notenOptions.find(opt => termin.note == opt.note) + } + + termin.dateStyle = getDateStyleClass(termin, this.notenOptions) + + const terminTypOpt = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz == termin.paabgabetyp_kurzbz) + if (terminTypOpt) termin.benotbar = terminTypOpt.benotbar + }) + }) + + // keep the history API stub for future use + + + this.timelineProjekte = res.data + this.$refs.drawer.show() }) - this.timelineProjekt = projekt - this.$refs.drawer.show() + }, + paabgabetypFormatter(cell) { + const key = cell.getValue() + return this.$p.t('abgabetool/c4paatyp' + key) }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return + const longForm = cell.getValue() + if(!longForm) return + const data = cell.getData() + const entry = Object.entries(data).find(entry => entry[1] == longForm) - return '
' + - '

'+val+'

' - }, - detailFormatter(cell) { - return '
' + - '
' - }, - mailFormatter(cell) { - const val = cell.getValue() - return '
' + - '
' - }, - timelineFormatter() { - return '
' + - '
' - }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' + // shortFormKey must have same keyname as longForm but with 'Short' appended + const shortForm = data[entry[0]+'Short'] + + if(shortForm && longForm) { + return `
+ + ${longForm} + + +
`; + } else { + return '
' + + '

'+longForm+'

' + } }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, - abgabterminFormatter(cell) { + abgabeterminFormatter(cell, formatterParams, onRendered,) { const val = cell.getValue() - + const dateStyle = val?.dateStyle ?? val + + if(val) { let icon = '' - switch(val.dateStyle) { + switch(dateStyle) { case 'verspaetet': - icon = '' + icon = '' break case 'verpasst': - icon = '' + icon = '' break case 'abzugeben': - icon = '' + icon = '' break case 'standard': - icon = '' + icon = '' break case 'abgegeben': - icon = '' + icon = '' break case 'beurteilungerforderlich': - icon = '' + icon = '' break case 'bestanden': - icon = '' + icon = '' break case 'nichtbestanden': - icon = '' + icon = '' break } + + const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz + const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz) - const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung + if(formatterParams?.iconOnly) { + return '
' + + '
' + + icon + + '
' + + '
' + } - return '
' + - '
' + + return '
' + + '
' + icon + '
' + '
' + - '

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

' + + '

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

' + '
'+ '
' - + } else { return '' } @@ -984,6 +2017,9 @@ export const AbgabetoolAssistenz = { tableResolve(resolve) { this.tableBuiltResolve = resolve }, + tableResolveFlat(resolve) { + this.tableBuiltResolveFlat = resolve + }, buildMailToLink(projekt) { return 'mailto:' + projekt.student_uid +'@'+ this.domain }, @@ -1001,14 +2037,29 @@ export const AbgabetoolAssistenz = { return projekt.zweitbetreuer_full_name ?? '' }, async setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - - this.tableData = this.mapProjekteToTableData(this.projektarbeiten) + this.projektarbeiten = this.mapProjekteToTableData(data[0]) + this.count = this.projektarbeiten.length + await this.tableBuiltPromise - this.$refs.abgabeTable.tabulator.setData(this.tableData); + 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 @@ -1023,7 +2074,7 @@ export const AbgabetoolAssistenz = { callback() } }).finally(()=>{ - this.loading=false + this.loading = false }) }, loadAbgaben(details) { @@ -1037,23 +2088,32 @@ export const AbgabetoolAssistenz = { handleUuidDefined(uuid) { this.tabulatorUuid = uuid }, + handleUuidDefinedFlat(uuid) { + this.tabulatorUuidFlat = uuid + }, calcMaxTableHeight() { const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' const tableDataSet = document.getElementById('filterTableDataset' + tableID); 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) + this.tableBuiltPromiseFlat = new Promise(this.tableResolveFlat); await this.tableBuiltPromise await this.allConfigPromise // called through notenOptionFilter/selectedStudiengangOption watcher on startup - // this.loadProjektarbeiten() + this.loadProjektarbeiten() this.calcMaxTableHeight() }, @@ -1062,6 +2122,67 @@ export const AbgabetoolAssistenz = { }, }, computed: { + getDisableDeleteForSelectedFlat() { + return this.selectedDataFlat.some(s => s.allowedToDelete === false) + }, + getDisableSaveForSelectedFlat(){ + return this.selectedDataFlat.some(s => s.allowedToSave === false) + }, + getAllTermine() { + if (!this.projektarbeiten) return []; + return this.projektarbeiten.flatMap(pa => + pa.abgabetermine.map(termin => { + const terminHasAbgabe = termin.abgabedatum != null + const terminHasNote = termin.noteBackend + + // IN multiedit changing anything for a termin with an existing note is not allowed anymore, to avoid + // confusing UX behaviour why some fields could be edited and others not -> just edit in detail view + const allowedToSave = pa.note !== null || terminHasNote || terminHasAbgabe ? false : true + const allowedToDelete = pa.note !== null || terminHasNote || terminHasAbgabe ? false : true + + 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, + pkz: pa.pkz, + studienstatus: pa.studienstatus, + orgform: pa.orgform + } + } + + ) + ); + }, + countsToHTMLFlat() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.countFlat || 0) + ''; + }, + countsToHTML() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, emailItems() { const menu = [] @@ -1112,6 +2233,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) { @@ -1119,6 +2242,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) { @@ -1141,10 +2266,18 @@ 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() { + // make sure zoom media query doesnt spill ever to other CIS4 sites + document.documentElement.classList.add('abgabetool'); + this.loading = true this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global']) this.phrasenPromise.then(()=> {this.phrasenResolved = true}) @@ -1169,6 +2302,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 @@ -1187,8 +2321,18 @@ 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.allSemOption = 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 @@ -1226,12 +2370,121 @@ export const AbgabetoolAssistenz = { mounted() { this.setupMounted() }, + beforeUnmount() { + document.documentElement.classList.remove('abgabetool'); + }, template: ` `, diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 6791d31aa..b13010cad 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -5,8 +5,9 @@ import VueDatePicker from '../../vueDatepicker.js.php'; import ApiAbgabe from '../../../api/factory/abgabe.js' import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { getDateStyleClass } from "./getDateStyleClass.js"; -import { dateFilter } from '../../../tabulator/filters/Dates.js'; +import { dateFilter } from '../../../tabulator/filters/DatesManual.js'; import {splitMailsHelper} from "../../../helpers/EmailHelpers.js"; +import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js"; export const AbgabetoolMitarbeiter = { name: "AbgabetoolMitarbeiter", @@ -31,19 +32,14 @@ export const AbgabetoolMitarbeiter = { old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link) } }, - props: { - viewData: { - type: Object, - required: true, - default: () => ({name: '', uid: ''}), - validator(value) { - return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards - } - } - }, data() { return { - tableData: null, + filteredRows: null, + count: 0, + filteredcount: 0, + selectedcount: 0, + qgate1FilterSelected: [], + qgate2FilterSelected: [], abgabetypenBetreuer: null, detailIsFullscreen: false, phrasenPromise: null, @@ -58,7 +54,7 @@ export const AbgabetoolMitarbeiter = { allowedNotenOptions: null, notenOptionsNonFinal: null, serienTermin: Vue.reactive({ - datum: new Date(), + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -80,7 +76,7 @@ export const AbgabetoolMitarbeiter = { abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', - layout: 'fitDataStretch', + layout: 'fitData', placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')), selectable: true, selectableCheck: this.selectionCheck, @@ -138,38 +134,82 @@ export const AbgabetoolMitarbeiter = { handleClick: this.selectAllHandler }, width: 50, - cssClass: 'sticky-col' + cssClass: 'sticky-col', + visible: true }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: '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/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), field: 'prevTermin', + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, headerFilter: false, headerSort: false, width: 140, minWidth: 50, visible: true, tooltip: false, cssClass: 'sticky-col'}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, visible: false,tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: '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/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + field: 'prevTermin', formatter: this.abgabeterminFormatter, width: 250, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabeterminFormatter, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate1Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate2Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + } ], persistence: false, - persistenceID: 'abgabeTableBetreuer2026-02-26' + persistenceID: 'abgabeTableBetreuer2026-05-26' }, abgabeTableEventHandlers: [{ event: "tableBuilt", @@ -200,11 +240,388 @@ export const AbgabetoolMitarbeiter = { }) this.selectedData = data + this.selectedcount = data.length; + } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredRows = rows; + this.filteredcount = rows.length; + + if (!this.selectedData.length) return; + + const visibleData = new Set(rows.map(r => r.getData())); + const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd)); + + if (!filteredOut.length) return; + + const filteredOutSet = new Set(filteredOut); + this.$refs.abgabeTable.tabulator.getSelectedRows() + .filter(r => filteredOutSet.has(r.getData())) + .forEach(r => r.deselect()); } } ]}; }, methods: { + notenSorter(a, b, aRow, bRow, column, dir, sorterParams) { + const aData = aRow.getData() + const bData = bRow.getData() + return aData.note - bData.note + }, + async openBenotung(type, link) { + if(type === 'new') { + window.open(link, '_blank') + } else if(type === 'old') { + if(await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4aeltereParbeitBenotenv2'), + acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')), + acceptClass: 'btn btn-danger', + rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')), + rejectClass: 'btn btn-outline-secondary' + }) === false) { + return false + } + + window.open(link, '_blank') + } else { + // show info text that no endupload with abgabe has been found + if(await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4keinEnduploadErfolgt'), + acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')), + acceptClass: 'btn btn-danger', + rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')), + rejectClass: 'btn btn-outline-secondary' + }) === false) { + return false + } + } + }, + formAction(cell) { + + const actionButtons = document.createElement('div'); + actionButtons.className = "d-flex gap-3"; + actionButtons.style.display = "flex"; + actionButtons.style.alignItems = "stretch"; + actionButtons.style.justifyContent = "start"; + actionButtons.style.height = "100%"; + + const val = cell.getValue(); + const data = cell.getRow().getData() + + 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 + if(iconClass == 'fa fa-timeline') btn.style.transform = "rotate(90deg)"; + 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)), + ); + + if(data.isCurrent && data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkNew) { + actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('new', data.beurteilungLinkNew))) + } else if(data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkOld) { + actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('old', data.beurteilungLinkOld))) + } + + if(this.checkForZweitbetreuerTokenMailAvailability(data)) { + actionButtons.append(createButton('fa fa-envelope-open-text', 'abgabetool/c4zweitBegutachterTokenMailSenden', () => this.sendZweitbetreuerToken(data))) + + } + + return actionButtons; + }, + checkForZweitbetreuerTokenMailAvailability(data) { + const hasEndabgabeWithUpload = !!data.abgabetermine.find(termin => termin.abgabedatum !== null && termin.paabgabetyp_kurzbz == 'end') + const hasZweitbetreuerWithoutBenutzerUid = data.zweitbetreuer_person_id !== null && data.zweitbetreuer_benutzer_uid === null + + return hasEndabgabeWithUpload && hasZweitbetreuerWithoutBenutzerUid + }, + sendZweitbetreuerToken(data) { + + this.$api.call(ApiAbgabe.sendZweitbetreuerTokenMail(data.projektarbeit_id, data.betreuer_person_id, data.student_uid)) + .then(res => { + if(res.meta.status == 'success') this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4zweitBegutachterTokenMailSuccess')) + }) + }, + 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 = [ + { label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' }, + { label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' }, + { label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' }, + { label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' }, + { label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' }, + { label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' }, + ]; + + const field = cell.getField(); + const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected'; + let selected = [...(this[stateKey] || [])]; // restore persistence state + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; width: 100%;'; + + const display = document.createElement('input'); + display.readOnly = true; + display.placeholder = ''; + display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;'; + + const dropdown = document.createElement('div'); + dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + options.forEach(opt => { + const row = document.createElement('label'); + row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;'; + row.addEventListener('mousedown', e => e.preventDefault()); + + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.value = opt.value; + cb.checked = selected.includes(opt.value); // sync with persistence + cb.addEventListener('change', () => { + if (cb.checked) { + selected.push(opt.value); + } else { + selected = selected.filter(v => v !== opt.value); + } + this[stateKey] = [...selected]; // sync with persistence + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + success([...selected]); + }); + + row.appendChild(cb); + row.appendChild(document.createTextNode(opt.label)); + dropdown.appendChild(row); + }); + + display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', '); + + display.addEventListener('click', () => { + if (dropdown.style.display === 'none') { + const rect = display.getBoundingClientRect(); + dropdown.style.top = rect.bottom + 'px'; + dropdown.style.left = rect.left + 'px'; + dropdown.style.display = 'block'; + } else { + dropdown.style.display = 'none'; + } + }); + + display.addEventListener('blur', () => { + setTimeout(() => { dropdown.style.display = 'none'; }, 150); + }); + + document.body.appendChild(dropdown); + wrapper.appendChild(display); + + cell.getElement().addEventListener('remove', () => dropdown.remove()); + + onRendered(() => display.focus()); + + return wrapper; + }, + qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + + const matches = (val) => { + switch (val) { + case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet'); + case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet'); + case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded'); + case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted'); + case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet'); + case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden'); + default: return true; + } + }; + + // OR logic — row passes if it matches any selected filter + return filterVal.some(val => matches(val)); + }, + shortLongTitleFormatter(cell, formatterParams, onRendered) { + const longForm = cell.getValue() + const shortForm = formatterParams?.shortForm + + if(longForm && shortForm) { + return ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + toolTipFuncPrevTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle); + }, + toolTipFuncNextTermin(e, cell, onRendered) { + const data = cell.getData(); + return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle); + }, + mapDateStyleToTabulatorTooltip(dateStyleString) { + switch(dateStyleString) { + case 'bestanden': + return this.$p.t('abgabetool/c4tooltipBestanden') + break; + case 'nichtbestanden': + return this.$p.t('abgabetool/c4tooltipNichtBestanden') + break; + case 'beurteilungerforderlich': + return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich') + break; + case 'verspaetet': + return this.$p.t('abgabetool/c4tooltipVerspaetet') + break; + case 'abgegeben': + return this.$p.t('abgabetool/c4tooltipAbgegeben') + break; + case 'verpasst': + return this.$p.t('abgabetool/c4tooltipVerpasst') + break; + case 'abzugeben': + return this.$p.t('abgabetool/c4tooltipAbzugeben') + break; + case 'standard': + return this.$p.t('abgabetool/c4tooltipStandardv2') + break; + default: return '' + } + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) this.$refs.abgabeTable.tabulator.redraw(true) @@ -217,7 +634,7 @@ export const AbgabetoolMitarbeiter = { }) const uniqueRecipients = [...new Set(recipientList)]; const subject = ""; // empty subject line - splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p) }, getQGateStatusList() { return [ @@ -257,7 +674,7 @@ export const AbgabetoolMitarbeiter = { if (val instanceof Date) { dt = luxon.DateTime.fromJSDate(val); } else if (typeof val === "string") { - dt = luxon.DateTime.fromISO(val); + dt = toViennaDate(val); } else { // fallback dt = luxon.DateTime.fromMillis(Number(val)); } @@ -386,6 +803,9 @@ export const AbgabetoolMitarbeiter = { } this.stateRestored = true + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); + } }); @@ -451,6 +871,32 @@ export const AbgabetoolMitarbeiter = { projekt.qgate2StatusRank = 1 } }) + + // set shorthand statuscode once real status has been determined + projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank) + projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank) + }, + mapRankToShortStatus(rank) { + switch(rank){ + case 0: // kein termin vorhanden + return '--' + break; + case 1: // noch nicht stattgefunden + return 'o' + break; + case 2: // noch nicht abgegeben + return '?' + break; + case 3: // noch nicht benotet + return '~' + break; + case 4: // negativ benotet + return '-' + break; + case 5: // positiv benotet + return '+' + break; + } }, checkAbgabetermineProjektarbeit(projekt) { const now = luxon.DateTime.now() @@ -460,7 +906,7 @@ export const AbgabetoolMitarbeiter = { // while already looping through each termin, calculate datestyle beforehand termin.dateStyle = getDateStyleClass(termin, this.notenOptions) - const date = luxon.DateTime.fromISO(termin.datum).endOf('day') + const date = toViennaDate(termin.datum).endOf('day') termin.luxonDate = date termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past @@ -482,7 +928,7 @@ export const AbgabetoolMitarbeiter = { // seperate check for quality gates this.checkQualityGateStatus(projekt) }, - abgabterminFormatter(cell) { + abgabeterminFormatter(cell) { const val = cell.getValue() if(val) { @@ -514,14 +960,15 @@ export const AbgabetoolMitarbeiter = { break } - const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung + const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz + const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz) return '
' + - '
' + - icon + + '
' + + icon + '
' + '
' + - '

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

' + + '

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

' + '
'+ '
' @@ -545,16 +992,19 @@ export const AbgabetoolMitarbeiter = { }, selectAllHandler(e, cell) { const table = cell.getTable(); - const rows = table.getRows(); + const rows = this.filteredRows ?? table.getRows(); // custom select all logic const allowed = rows.filter(r => r.getData().selectable); + // since betreuerpage acctually has logic behind selectable flag, it is important to go over allowed only here const selected = allowed.every(r => r.isSelected()); - if(selected) { + if(selected){ allowed.forEach(r => r.deselect()); + e.target.checked = false; } else { allowed.forEach(r => r.select()); + e.target.checked = true; } // stop built-in handler @@ -565,18 +1015,10 @@ export const AbgabetoolMitarbeiter = { this.detailIsFullscreen = !this.detailIsFullscreen }, getOptionLabelAbgabetyp(option){ - return option.bezeichnung + return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz) }, formatDate(dateParam) { - const date = new Date(dateParam) - // handle missing leading 0 - const padZero = (num) => String(num).padStart(2, '0'); - - const month = padZero(date.getMonth() + 1); // Months are zero-based - const day = padZero(date.getDate()); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; + return formatISODate(dateParam); }, undoSelection(cell) { // checks if cells row is selected and unselects -> imitates columns which dont trigger row selection @@ -589,6 +1031,8 @@ export const AbgabetoolMitarbeiter = { }, selectionCheck(row) { const data = row.getData() + + // zweitbetreuer cant select projektarbeiten for serientermine if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false return true }, @@ -612,7 +1056,7 @@ export const AbgabetoolMitarbeiter = { addSeries() { this.saving = true this.$api.call(ApiAbgabe.postSerientermin( - this.serienTermin.datum.toISOString(), + this.serienTermin.datum, this.serienTermin.bezeichnung.paabgabetyp_kurzbz, this.serienTermin.bezeichnung.bezeichnung, this.serienTermin.kurzbz, @@ -710,7 +1154,7 @@ export const AbgabetoolMitarbeiter = { termin.allowedToSave = paIsBenotet ? false : true // lektoren are not allowed to delete deadlines with existing submissions - termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum + termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum && !termin.note termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) @@ -727,28 +1171,37 @@ export const AbgabetoolMitarbeiter = { }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return - - return '
' + - '

'+val+'

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

'+longForm+'

' + } }, detailFormatter(cell) { - return '
' + + return '
' + '
' }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' - }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, tableResolve(resolve) { this.tableBuiltResolve = resolve @@ -763,15 +1216,19 @@ export const AbgabetoolMitarbeiter = { return (projekt.typ + projekt.kurzbz)?.toUpperCase() }, setupData(data){ - - - this.projektarbeiten = data[0] this.domain = data[1] - this.tableData = data[0]?.retval?.map(projekt => { + this.projektarbeiten = data[0]?.retval?.map(projekt => { this.checkAbgabetermineProjektarbeit(projekt) projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' + if(this.notenOptions && projekt.note) { + const opt = this.notenOptions.find(n => n.note == projekt.note) + // TODO: mehrsprachig englisch -> nevermind the english field in + // notenoption->bezeichnung_mehrsprachig is ALWAYS german + projekt.note_bez = opt?.bezeichnung + } + return { ...projekt, details: { @@ -787,9 +1244,10 @@ export const AbgabetoolMitarbeiter = { titel: projekt.titel } }) + this.count = this.projektarbeiten.length this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns) - this.$refs.abgabeTable.tabulator.setData(this.tableData); + this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); }, loadProjektarbeiten(all = false, callback) { this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all)) @@ -838,9 +1296,20 @@ export const AbgabetoolMitarbeiter = { } this.serienTermin.upload_allowed = newVal.upload_allowed_default - }, + } }, computed: { + countsToHTML() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, emailItems() { const menu = [] @@ -869,6 +1338,8 @@ export const AbgabetoolMitarbeiter = { } }, created() { + document.documentElement.classList.add('abgabetool'); + this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global']) this.phrasenPromise.then(()=> {this.phrasenResolved = true}) // fetch config to avoid hard coded links @@ -910,12 +1381,16 @@ export const AbgabetoolMitarbeiter = { mounted() { this.setupMounted() }, + beforeUnmount() { + document.documentElement.classList.remove('abgabetool'); + }, template: `