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/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/LvPlan.php b/application/controllers/api/frontend/v1/LvPlan.php index 69cbeca54..2a900e92d 100644 --- a/application/controllers/api/frontend/v1/LvPlan.php +++ b/application/controllers/api/frontend/v1/LvPlan.php @@ -418,6 +418,10 @@ class LvPlan extends FHCAPI_Controller */ 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')); 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/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/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/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index f167ad0d1..c9aaf40ef 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -190,7 +190,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"; @@ -210,7 +213,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"; @@ -243,7 +257,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]); } @@ -297,34 +312,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)); } /** @@ -344,176 +467,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/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/package-lock.json b/package-lock.json deleted file mode 100644 index bbfebad87..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "FHC-Core", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} 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/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/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/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/Cis/Cis.js b/public/js/apps/Cis/Cis.js index 50acd4908..56635157c 100644 --- a/public/js/apps/Cis/Cis.js +++ b/public/js/apps/Cis/Cis.js @@ -20,11 +20,16 @@ const app = Vue.createApp({ provide() { return { // provide injectable & watchable language property language: Vue.computed(() => this.$p.user_language), - isMobile: Vue.computed(() => this.windowWidth < 767), + isMobile: Vue.computed(() => this.isMobile), isStudent: Vue.computed(() => this.isStudent), isMitarbeiter: Vue.computed(() => this.isMitarbeiter) } }, + computed: { + isMobile: function() { + return (this.windowWidth < 767); + } + }, methods: { isInternalRoute(href) { const internalBase = window.location.origin 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 d65f7222b..a2fdb6551 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, @@ -45,9 +48,23 @@ export const AbgabetoolAssistenz = { }, 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, @@ -56,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, @@ -67,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, @@ -74,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' @@ -87,7 +126,9 @@ export const AbgabetoolAssistenz = { }), showAll: false, tabulatorUuid: Vue.ref(0), + tabulatorUuidFlat: Vue.ref(0), selectedData: [], + selectedDataFlat: [], domain: '', student_uid: null, detail: null, @@ -96,6 +137,8 @@ export const AbgabetoolAssistenz = { selectedProjektarbeit: null, tableBuiltResolve: null, tableBuiltPromise: null, + tableBuiltResolveFlat: null, + tableBuiltPromiseFlat: null, abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', @@ -103,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: [ { @@ -158,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" @@ -234,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 [ @@ -268,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; @@ -281,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)); } @@ -313,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(); @@ -343,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()); @@ -420,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 '' @@ -450,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 @@ -468,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 @@ -538,7 +1452,7 @@ export const AbgabetoolAssistenz = { table.on("renderComplete", () => { if(!this.stateRestored) { - + if (saved?.columns && !this.colLayoutRestored) { const layout = saved.columns.map(col => ({ field: col.field, @@ -559,6 +1473,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); } } @@ -579,16 +1495,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 @@ -600,24 +1634,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(); @@ -672,9 +1697,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' @@ -691,7 +1721,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, @@ -716,27 +1746,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 @@ -760,12 +1781,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, @@ -787,8 +1809,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] } @@ -831,20 +1855,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 }) @@ -856,97 +1877,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 '' } @@ -955,6 +1996,9 @@ export const AbgabetoolAssistenz = { tableResolve(resolve) { this.tableBuiltResolve = resolve }, + tableResolveFlat(resolve) { + this.tableBuiltResolveFlat = resolve + }, buildMailToLink(projekt) { return 'mailto:' + projekt.student_uid +'@'+ this.domain }, @@ -972,13 +2016,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 @@ -993,7 +2053,7 @@ export const AbgabetoolAssistenz = { callback() } }).finally(()=>{ - this.loading=false + this.loading = false }) }, loadAbgaben(details) { @@ -1007,23 +2067,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() }, @@ -1032,6 +2101,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 = [] @@ -1082,6 +2212,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) { @@ -1089,6 +2221,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) { @@ -1111,10 +2245,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}) @@ -1139,6 +2281,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 @@ -1157,8 +2300,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 @@ -1196,12 +2349,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 0acb46bb6..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", @@ -33,7 +34,12 @@ export const AbgabetoolMitarbeiter = { }, data() { return { - tableData: null, + filteredRows: null, + count: 0, + filteredcount: 0, + selectedcount: 0, + qgate1FilterSelected: [], + qgate2FilterSelected: [], abgabetypenBetreuer: null, detailIsFullscreen: false, phrasenPromise: null, @@ -48,7 +54,7 @@ export const AbgabetoolMitarbeiter = { allowedNotenOptions: null, notenOptionsNonFinal: null, serienTermin: Vue.reactive({ - datum: new Date(), + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -70,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, @@ -128,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", @@ -190,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) @@ -207,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 [ @@ -247,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)); } @@ -376,6 +803,9 @@ export const AbgabetoolMitarbeiter = { } this.stateRestored = true + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); + } }); @@ -441,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() @@ -450,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 @@ -472,7 +928,7 @@ export const AbgabetoolMitarbeiter = { // seperate check for quality gates this.checkQualityGateStatus(projekt) }, - abgabterminFormatter(cell) { + abgabeterminFormatter(cell) { const val = cell.getValue() if(val) { @@ -504,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)+'

' + '
'+ '
' @@ -535,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 @@ -555,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 @@ -579,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 }, @@ -602,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, @@ -700,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) @@ -717,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 @@ -753,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: { @@ -777,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)) @@ -828,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 = [] @@ -859,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 @@ -900,12 +1381,16 @@ export const AbgabetoolMitarbeiter = { mounted() { this.setupMounted() }, + beforeUnmount() { + document.documentElement.classList.remove('abgabetool'); + }, template: `