diff --git a/application/config/abgabe.php b/application/config/abgabe.php index 82782b043..90aedbd8b 100644 --- a/application/config/abgabe.php +++ b/application/config/abgabe.php @@ -41,3 +41,5 @@ $config['STG_MOODLE_LINK'] = 'https://moodle.technikum-wien.at/course/view.php?i $config['ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'] = true; $config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true; + +$config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true; diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index b37c64713..af598a345 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -89,13 +89,15 @@ class Abgabe extends FHCAPI_Controller $abgabetypenBetreuer = $this->config->item('ALLOWED_ABGABETYPEN_BETREUER'); $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'); $ret = array( 'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link, 'turnitin_link' => $turnitin_link, 'abgabetypenBetreuer' => $abgabetypenBetreuer, 'ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT' => $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT, - 'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER + 'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER, + 'BETREUER_SAMMELMAIL_BUTTON_STUDENT' => $BETREUER_SAMMELMAIL_BUTTON_STUDENT, ); $this->terminateWithSuccess($ret); @@ -373,6 +375,8 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } + $this->checkPaabgabeDeadline($paabgabe_id); + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); @@ -444,6 +448,36 @@ class Abgabe extends FHCAPI_Controller } } + + // 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) { + $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'); + } + + // in that case any submission date is fine + if($paabgabe->fixtermin === false) return; + + $tz = new DateTimeZone('Europe/Berlin'); + $now = new DateTimeImmutable('now', $tz); + $deadline = DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $paabgabe->datum . ' 23:59:59', + $tz + ); + + if($now >= $deadline) { + $this->terminateWithError($this->p->t('abgabetool', 'c4deadlineExceeded')); + } + } /** * tabulator tabledata fetch for abgabetool/mitarbeiter @@ -473,6 +507,15 @@ class Abgabe extends FHCAPI_Controller $projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool); + $mapFunc = function($projektarbeit) { + return $projektarbeit->projektarbeit_id; + }; + $projektarbeiten_ids = array_map($mapFunc, $projektarbeiten->retval); + + $ret = $this->ProjektarbeitModel->getProjektarbeitenAbgabetermine($projektarbeiten_ids); + $projektabgaben = $this->getDataOrTerminateWithError($ret, 'general'); + + forEach($projektarbeiten->retval as $pa) { $result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id); @@ -489,6 +532,20 @@ class Abgabe extends FHCAPI_Controller Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc); $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + + // has previously been retrieved via getStudentProjektabgaben but is fetched in advance to avoid having to reload abgaben + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $pa->projektarbeit_id, $returnFunc); + $pa->isCurrent = $projektarbeitIsCurrent; + + $filterFunc = function($projektabgabe) use ($pa) { + return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; + }; + + $pa->abgabetermine = array_values(array_filter($projektabgaben, $filterFunc)); } @@ -544,7 +601,18 @@ class Abgabe extends FHCAPI_Controller 'insertamum' => date('Y-m-d H:i:s') ) ); - $this->logLib->logInfoDB(array('paabgabe created',$result, getAuthUID(), getAuthPersonId())); + $this->logLib->logInfoDB(array('paabgabe created',array( + 'projektarbeit_id' => $projektarbeit_id, + 'paabgabetyp_kurzbz' => $paabgabetyp_kurzbz, + 'fixtermin' => $fixtermin, + 'datum' => $datum, + 'kurzbz' => $kurzbz, + 'note' => $note, + 'beurteilungsnotiz' => $beurteilungsnotiz, + 'upload_allowed' => $upload_allowed, + 'insertvon' => getAuthUID(), + 'insertamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId())); } else { // load existing entry of paabgabe and check if note has changed to negativ, to avoid sending when // only notiz has changed. @@ -718,7 +786,16 @@ class Abgabe extends FHCAPI_Controller $abgaben[]= getData($this->PaabgabeModel->load($dataAbgabe))[0]; } - $this->logLib->logInfoDB(array('serientermin angelegt',$res, getAuthUID(), getAuthPersonId())); + $this->logLib->logInfoDB(array('serientermin angelegt',array( + 'projektarbeit_id' => $projektarbeit_id, + 'paabgabetyp_kurzbz' => $paabgabetyp_kurzbz, + 'fixtermin' => $fixtermin, + 'datum' => $datum, + 'kurzbz' => $kurzbz, + 'upload_allowed' => $upload_allowed, + 'insertvon' => getAuthUID(), + 'insertamum' => date('Y-m-d H:i:s') + ), getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($abgaben); } @@ -1167,7 +1244,7 @@ class Abgabe extends FHCAPI_Controller $email = $this->getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id); - if(!$email) $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachter'), 'general'); + if(!$email) $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); $mailres = sendSanchoMail( 'ParbeitsbeurteilungEndupload', @@ -1180,7 +1257,7 @@ class Abgabe extends FHCAPI_Controller if(!$mailres) { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachter'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); } // 2. Begutachter mail, wenn Endabgabe, mit Token wenn extern @@ -1200,14 +1277,14 @@ class Abgabe extends FHCAPI_Controller if (!$tokenGenRes) { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachter'), 'general'); + $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', 'c4fehlerMailZweitBegutachter'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general'); } $begutachterMitToken = $begutachterMitTokenRetval[0]; @@ -1241,7 +1318,7 @@ class Abgabe extends FHCAPI_Controller if (!$mailres) { - $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachter'), 'general'); + $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general'); } } diff --git a/application/controllers/api/frontend/v1/studstatus/Unterbrechung.php b/application/controllers/api/frontend/v1/studstatus/Unterbrechung.php index abf58cf4f..72d5dbccc 100644 --- a/application/controllers/api/frontend/v1/studstatus/Unterbrechung.php +++ b/application/controllers/api/frontend/v1/studstatus/Unterbrechung.php @@ -127,9 +127,9 @@ class Unterbrechung extends FHCAPI_Controller $this->form_validation->set_rules( 'datum_wiedereinstieg', 'Datum Wiedereinstieg', - 'required|callback_isValidDate|callback_isDateInFuture', + 'required|is_valid_date|callback_isDateInFuture', [ - 'isValidDate' => $this->p->t('ui', 'error_invalid_date'), + 'is_valid_date' => $this->p->t('ui', 'error_invalid_date'), 'isDateInFuture' => $this->p->t('ui', 'error_invalid_date') ] ); @@ -209,18 +209,9 @@ class Unterbrechung extends FHCAPI_Controller $this->terminateWithSuccess(getData($result)); } - public function isValidDate($date) - { - try { - new DateTime($date); - } catch (Exception $e) { - return false; - } - return true; - } - public function isDateInFuture($date) { return new DateTime() < new DateTime($date); } } + diff --git a/application/controllers/api/frontend/v1/stv/Student.php b/application/controllers/api/frontend/v1/stv/Student.php index 8f09b82aa..7694807e7 100644 --- a/application/controllers/api/frontend/v1/stv/Student.php +++ b/application/controllers/api/frontend/v1/stv/Student.php @@ -546,6 +546,7 @@ class Student extends FHCAPI_Controller $this->_validate(); + // TODO(chris): This should be in a library $this->load->model('crm/Student_model', 'StudentModel'); $this->load->model('crm/Prestudent_model', 'PrestudentModel'); $this->load->model('crm/Prestudentstatus_model', 'PrestudentstatusModel'); @@ -797,8 +798,8 @@ class Student extends FHCAPI_Controller $this->form_validation->set_rules('geschlecht', 'Geschlecht', 'callback_requiredIfNotPersonId', [ 'requiredIfNotPersonId' => $this->p->t('ui', 'error_fieldRequired', ['field' => $this->p->t('person', 'geschlecht')]) ]); - $this->form_validation->set_rules('gebdatum', 'Geburtsdatum', ['isValidDate', function($value) { return isValidDate($value); }], [ - 'isValidDate' => $this->p->t('ui', 'error_invalid_date') + $this->form_validation->set_rules('gebdatum', 'Geburtsdatum', 'is_valid_date', [ + 'is_valid_date' => $this->p->t('ui', 'error_invalid_date') ]); //$this->form_validation->set_rules('address[checked]', 'Address', 'required'); $this->form_validation->set_rules('address[plz]', 'PLZ', 'callback_requiredIfAddressFunc', [ diff --git a/application/controllers/jobs/AbgabetoolJob.php b/application/controllers/jobs/AbgabetoolJob.php index 51b2b4920..9b59a72e7 100644 --- a/application/controllers/jobs/AbgabetoolJob.php +++ b/application/controllers/jobs/AbgabetoolJob.php @@ -22,11 +22,272 @@ class AbgabetoolJob extends JOB_Controller $this->_ci->load->model('crm/Student_model', 'StudentModel'); $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->_ci->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); - + + $this->_ci->load->library('SignatureLib'); + $this->_ci->load->config('abgabe'); $this->loadPhrases([ 'abgabetool' ]); + + + } + + // basically the notifyBetreuerMail function but email goes to assistenz + // and new abgaben are further evaluated for missing signature status + public function notifyAssistenzAboutMissingSignatureUploads() { + $this->_ci->logInfo('Start job FHC-Core->notifyAssistenzAboutMissingSignatureUploads'); + + $interval = $this->_ci->config->item('PAABGABE_EMAIL_JOB_INTERVAL'); + $relevantTypes = $this->_ci->config->item('RELEVANT_PAABGABETYPEN_SAMMELMAIL_ASSISTENZ'); + + $result = $this->_ci->PaabgabeModel->findAbgabenNewOrUpdatedSinceByAbgabedatum($interval, $relevantTypes); + $retval = getData($result); + + // retval are paabgaben joined with projektarbeit and betreuer + if(count($retval) == 0) { + $this->logInfo("Keine Emails über neue Paabgaben an Assistenzen versandt"); + return; + } + + // group changed/new abgaben for projektarbeiten + $projektarbeiten = []; + foreach($retval as $abgabeWithNewUpload) { + // Check if the current item has a 'projektarbeit_id' field. + // Replace 'projektarbeit_id' with the actual key name if it's different. + if (isset($abgabeWithNewUpload->projektarbeit_id)) { + $projektarbeitId = $abgabeWithNewUpload->projektarbeit_id; + + // If the 'projektarbeit_id' is not yet a key in $projektarbeiten, + // initialize it as an empty array. + if (!isset($projektarbeiten[$projektarbeitId])) { + $projektarbeiten[$projektarbeitId] = []; + } + + // check signature for that abgabe, main point of this job + $this->checkAbgabeSignatur($abgabeWithNewUpload, $abgabeWithNewUpload->student_uid); + + // Add the current row to the array associated with its 'projektarbeit_id'. + $projektarbeiten[$projektarbeitId][] = $abgabeWithNewUpload; + } + } + + // for each projektarbeit fetch their assistenz and same them in their own dictionary to avoid too many mails + $assistenzMap = []; + // for each projektarbeit fetch their betreuer and save them in their own dictionary to avoid too many mails + $projektarbeitBetreuerMap = []; + forEach($projektarbeiten as $projektarbeit_id => $abgaben) { + + $assistenzResult = $this->_ci->OrganisationseinheitModel->getAssistenzForOE($abgaben[0]->stg_oe_kurzbz); + + forEach($assistenzResult->retval as $assistenzRow) { + if (!isset($assistenzMap[$assistenzRow->person_id])) { + $assistenzMap[$assistenzRow->person_id] = []; + } + + // Add the current $assistenzRow to the $assistenzMap as an array associated with its projektarbeit_id. + $assistenzMap[$assistenzRow->person_id][] = [$projektarbeit_id, $assistenzRow]; + } + + $betreuerResult = $this->_ci->ProjektbetreuerModel->getAllBetreuerOfProjektarbeit($projektarbeit_id); + + forEach($betreuerResult->retval as $betreuerRow) { + if (!isset($projektarbeitBetreuerMap[$projektarbeit_id])) { + $projektarbeitBetreuerMap[$projektarbeit_id] = []; + } + + // Add the current betreuerRow to the betreuerMap as an array associated with its projektarbeit_id. + $projektarbeitBetreuerMap[$projektarbeit_id][] = $betreuerRow; + } + + } + + $count = 0; + foreach($assistenzMap as $assistenz_person_id => $tupelArr) { + + $abgabenString = '
'; + $hasIssues = false; // Track if this assistant actually needs an email + + foreach($tupelArr as $tupel) { + $projektarbeit_id = $tupel[0]; + $assistenzRow = $tupel[1]; + + $betreuerArray = $projektarbeitBetreuerMap[$projektarbeit_id] ?? []; + $allAbgaben = $projektarbeiten[$projektarbeit_id]; + + // only keep abgaben that are not correctly signed + $issueAbgaben = array_filter($allAbgaben, function($abgabe) { + // We only care about cases where it's explicitly NOT true (false, error, or null) + return $abgabe->signatur !== true; + }); + + // if this specific project has no signature issues, skip to the next project + if(empty($issueAbgaben)) { + continue; + } + + // If we reached here, we have at least one issue to report + $hasIssues = true; + + // Format the Student Name (using the first available abgabe object) + $s = reset($issueAbgaben); + $nameParts = array_filter([$s->titelpre, $s->vorname, $s->nachname, $s->titelpost]); + $studentFullName = implode(' ', $nameParts); + + // Format the Supervisors string + $betreuerStrings = []; + foreach($betreuerArray as $b) { + $bNameParts = array_filter([$b->titelpre, $b->vorname, $b->nachname, $b->titelpost]); + $bFullName = implode(' ', $bNameParts); + $betreuerStrings[] = "{$bFullName} ({$b->betreuerart_kurzbz})"; + } + $allBetreuerFormatted = implode(', ', $betreuerStrings); + + $projektarbeit_titel = $s->titel ?? 'Kein Titel vergeben'; + + // Project Header Section + $abgabenString .= " +
+ Projekt: {$projektarbeit_titel}
+
+ Studierende/r: {$studentFullName} +
+
+ Betreuer: {$allBetreuerFormatted} +
+ + ID: {$projektarbeit_id} | Stg: {$s->stgtyp}{$s->stgkz} ({$s->studiensemester_kurzbz}) + +
"; + + // Start Table + $abgabenString .= ' + + + + + + + + + '; + + $printed = []; // lazy hack to avoid duplicate rows + foreach ($issueAbgaben as $abgabe) { + // if we had this paabgabe already (erstbetreuer/zweitbetreuer fetch achieves duplicates + if(in_array($abgabe->paabgabe_id, $printed)) { + continue; // skip this forEach iteration + } + + $printed[] = $abgabe->paabgabe_id; + + $abgabedatumFormatted = (new DateTime($abgabe->abgabedatum))->format('d.m.Y'); + + // label and color + if ($abgabe->signatur === false) { + $sigLabel = "FEHLENDE SIGNATUR"; + $sigBg = "#dc3545"; + } elseif ($abgabe->signatur === 'error') { + $sigLabel = "PRÜFUNG FEHLGESCHLAGEN"; + $sigBg = "#fd7e14"; + } else { + $sigLabel = "DATEI NICHT GEFUNDEN"; + $sigBg = "#6c757d"; + } + + $abgabenString .= " + + + + + "; + } + + $abgabenString .= '
DatumAbgabe/BezeichnungStatus
{$abgabedatumFormatted} + {$abgabe->bezeichnung} + + + {$sigLabel} + +
'; + } + + $abgabenString .= '
'; + + // only send the email if at least one project had an issue + if ($hasIssues) { + $assistenzRow = $tupelArr[0][1]; + $anrede = $assistenzRow->anrede; + $anredeFillString = $assistenzRow->anrede == "Herr" ? "r" : ""; + $fullFormattedNameString = $assistenzRow->first; + + $path = $this->_ci->config->item('URL_ASSISTENZ'); + $url = CIS_ROOT . $path; + + $body_fields = array( + 'anrede' => $anrede, + 'anredeFillString' => $anredeFillString, + 'fullFormattedNameString' => $fullFormattedNameString, + 'abgabenString' => $abgabenString, + 'linkAbgabetool' => $url + ); + + $email = $assistenzRow->uid . "@" . DOMAIN; + + sendSanchoMail( + 'PAANoSigAssSM', + $body_fields, + $email, + $this->p->t('abgabetool', 'c4missingSignatureNotification') + ); + + $count++; + } + } + + $this->_ci->logInfo($count . " Emails bezüglich fehlender Signaturen erfolgreich versandt"); + $this->_ci->logInfo('End job FHC-Core->notifyAssistenzAboutMissingSignatureUploads'); + } + + /** + * helper function to check the signature status of uploaded files for zwischenabgabe & endupload + */ + private function checkAbgabeSignatur($abgabe, $student_uid) { + $paabgabetypenToCheck = $this->config->item('SIGNATUR_CHECK_PAABGABETYPEN'); + + if(!in_array($abgabe->paabgabetyp_kurzbz, $paabgabetypenToCheck)) { + return; + } + + if (!defined('SIGNATUR_URL')) { + $abgabe->signatur = 'error'; + return; + } + + $path = PAABGABE_PATH.$abgabe->paabgabe_id.'_'.$student_uid.'.pdf'; + + $signaturVorhanden = null; // if frontend receives null -> indicates no file found at path + if(file_exists($path)) { + + // Check if the document is signed + $signList = SignatureLib::list($path); + if (is_array($signList) && count($signList) > 0) + { + // The document is signed + $signaturVorhanden = true; + } + elseif ($signList === null) + { + // frontend knows to handle it this way for signatures + $signaturVorhanden = 'error'; + } + else + { + $signaturVorhanden = false; + } + + $abgabe->signatur = $signaturVorhanden; + } } public function notifyAssistenzAboutChangedAbgaben() { @@ -234,11 +495,6 @@ class AbgabetoolJob extends JOB_Controller // get all new or changed termine in interval $result = $this->_ci->PaabgabeModel->findAbgabenNewOrUpdatedSince($interval, $relevantTypes); $retval = getData($result); - - if(count($retval) == 0) { - $this->_ci->logInfo("Keine Emails an Betreuer über neue oder veränderte Termine versandt"); - return; - } // group changed/new abgaben for projektarbeiten $projektarbeiten = []; @@ -248,17 +504,29 @@ class AbgabetoolJob extends JOB_Controller if (isset($newOrChangedAbgabe->projektarbeit_id)) { $projektarbeitId = $newOrChangedAbgabe->projektarbeit_id; + // check if the updatevon field is NOT the same as the student the projektarbeit is assigned to + // since uploading a file to a paabgabe is also putting updateamum & updatevon + // we have our own "student has uploaded a file" emailjob anyways + if($newOrChangedAbgabe->student_uid === $newOrChangedAbgabe->updatevon) { + continue; + } + // If the 'projektarbeit_id' is not yet a key in $projektarbeiten, // initialize it as an empty array. if (!isset($projektarbeiten[$projektarbeitId])) { $projektarbeiten[$projektarbeitId] = []; } - + // Add the current row to the array associated with its 'projektarbeit_id'. $projektarbeiten[$projektarbeitId][] = $newOrChangedAbgabe; } } + if(count($projektarbeiten) == 0) { + $this->_ci->logInfo("Keine Emails an Betreuer über neue oder veränderte Termine versandt"); + return; + } + // for each projektarbeit fetch their betreuer and save them in their own dictionary to avoid too many mails $betreuerMap = []; forEach($projektarbeiten as $projektarbeit_id => $abgaben) { @@ -377,6 +645,11 @@ class AbgabetoolJob extends JOB_Controller ); $email = $betreuerRow->uid ? $betreuerRow->uid."@".DOMAIN : $betreuerRow->private_email; + + if(!$email) { + $this->_ci->logInfo('Could not send Email for Betreuer PersonID: "'.$data->person_id.'".'); + continue; + } // send email with bundled info sendSanchoMail( @@ -500,6 +773,12 @@ class AbgabetoolJob extends JOB_Controller $email = $data->uid ? $data->uid."@".DOMAIN : $data->private_email; + // in rare cases there are betreuer (often zweitbetreuer) without uid and without private email + if(!$email) { + $this->_ci->logInfo('Could not send Email for Betreuer PersonID: "'.$data->person_id.'".'); + continue; + } + // send email with bundled info sendSanchoMail( 'PaabgabeUpdatesBetSM', diff --git a/application/helpers/hlp_common_helper.php b/application/helpers/hlp_common_helper.php index b13d9d44d..06cfa1cfd 100644 --- a/application/helpers/hlp_common_helper.php +++ b/application/helpers/hlp_common_helper.php @@ -91,7 +91,7 @@ function var_dump_to_error_log($parameter) var_dump($parameter); // KEEP IT!!! $ob_get_contents = ob_get_contents(); ob_end_clean(); - error_log(str_replace("\n", '', $ob_get_contents)); // KEEP IT!!! + error_log(str_replace("\n", '', $ob_get_contents) . ', referer: ' . "http".(!empty($_SERVER['HTTPS'])?"s":"")."://".$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']); // KEEP IT!!! } /** @@ -408,22 +408,6 @@ function findResource($path, $resource, $subdir = false, $extraDir = null) return null; } -/** - * check if String can be converted to a date - */ -function isValidDate($dateString) -{ - try - { - return (new DateTime($dateString)) !== false; - } - catch(Exception $e) - { - return false; - } -} - - // ------------------------------------------------------------------------ // PHP functions that don't exist in older versions // ------------------------------------------------------------------------ @@ -446,7 +430,8 @@ if (!function_exists('array_is_list')) { // ------------------------------------------------------------------------ /** - * check if string can be converted to a date + * Check if the provided parameter is a string containing a valid date + * NOTE: the name is in the "snake case" format because othewise the CI form validation _cannot_ use it */ function is_valid_date($dateString) { diff --git a/application/models/education/Paabgabe_model.php b/application/models/education/Paabgabe_model.php index a883043d3..be42b7660 100644 --- a/application/models/education/Paabgabe_model.php +++ b/application/models/education/Paabgabe_model.php @@ -86,27 +86,38 @@ class Paabgabe_model extends DB_Model return $this->execQuery($query, [$interval, $interval, $relevantTypes]); } - public function findAbgabenNewOrUpdatedSinceByAbgabedatum($interval) { - - $query = "SELECT projektarbeit_id, paabgabe_id, paabgabetyp_kurzbz, fixtermin, datum, kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum, - campus.tbl_paabgabe.insertvon, campus.tbl_paabgabe.insertamum, campus.tbl_paabgabe.updatevon, campus.tbl_paabgabe.updateamum, - campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid, tbl_projektarbeit.note, lehre.tbl_projektarbeit.titel, - lehre.tbl_projektbetreuer.betreuerart_kurzbz, lehre.tbl_projektbetreuer.person_id, - public.tbl_person.anrede, public.tbl_person.titelpre, public.tbl_person.vorname, public.tbl_person.nachname, public.tbl_person.titelpost + public function findAbgabenNewOrUpdatedSinceByAbgabedatum($interval, $relevantTypes = null) { + + $queryParams = [$interval]; + $query = "SELECT projektarbeit_id, paabgabe_id, paabgabetyp_kurzbz, fixtermin, datum, campus.tbl_paabgabe.kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum, + campus.tbl_paabgabe.insertvon, campus.tbl_paabgabe.insertamum, campus.tbl_paabgabe.updatevon, campus.tbl_paabgabe.updateamum, + campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid, tbl_projektarbeit.note, lehre.tbl_projektarbeit.titel, + UPPER(tbl_studiengang.typ) as stgtyp, UPPER(tbl_studiengang.kurzbz) as stgkz, public.tbl_studiengang.studiengang_kz, + public.tbl_studiengang.oe_kurzbz as stg_oe_kurzbz, tbl_lehreinheit.studiensemester_kurzbz, + lehre.tbl_projektbetreuer.betreuerart_kurzbz, lehre.tbl_projektbetreuer.person_id, + public.tbl_person.anrede, public.tbl_person.titelpre, public.tbl_person.vorname, public.tbl_person.nachname, public.tbl_person.titelpost - FROM campus.tbl_paabgabe - JOIN campus.tbl_paabgabetyp USING (paabgabetyp_kurzbz) - JOIN lehre.tbl_projektarbeit USING (projektarbeit_id) - JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) - JOIN public.tbl_benutzer ON (public.tbl_benutzer.uid = student_uid) - JOIN public.tbl_person ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) + FROM campus.tbl_paabgabe + JOIN campus.tbl_paabgabetyp USING (paabgabetyp_kurzbz) + JOIN lehre.tbl_projektarbeit USING (projektarbeit_id) + JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id) + JOIN lehre.tbl_lehreinheit using(lehreinheit_id) + JOIN lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id) + JOIN public.tbl_studiengang on(lehre.tbl_lehrveranstaltung.studiengang_kz = public.tbl_studiengang.studiengang_kz) + JOIN public.tbl_benutzer ON (public.tbl_benutzer.uid = student_uid) + JOIN public.tbl_person ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) WHERE campus.tbl_paabgabe.abgabedatum IS NOT NULL - AND campus.tbl_paabgabe.abgabedatum >= NOW() - INTERVAL ? - ORDER BY abgabedatum DESC - "; + AND campus.tbl_paabgabe.abgabedatum >= NOW() - INTERVAL ?"; + + if($relevantTypes !== null) { + $query .= " AND campus.tbl_paabgabe.paabgabetyp_kurzbz IN ?"; + $queryParams[]= $relevantTypes; + } - return $this->execQuery($query, [$interval]); + $query .= " ORDER BY abgabedatum DESC"; + + return $this->execQuery($query, $queryParams); } public function loadByIDs($paabgabe_ids) { diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 5e453056d..3b1ea55e5 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -354,8 +354,10 @@ class Projektarbeit_model extends DB_Model 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, @@ -415,6 +417,50 @@ class Projektarbeit_model extends DB_Model 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, diff --git a/cis/public/coodle.php b/cis/public/coodle.php index 2b8421db0..05eaa41df 100644 --- a/cis/public/coodle.php +++ b/cis/public/coodle.php @@ -1041,7 +1041,7 @@ function sendEmail($coodle_id) ."END:STANDARD\r\n" ."END:VTIMEZONE\r\n" ."BEGIN:VEVENT\r\n" - .$coodle->foldContentLine("ORGANIZER:MAILTO:".$erstellername." <".$coodle->ersteller_uid."@".DOMAIN)."\r\n" + .$coodle->foldContentLine("ORGANIZER:MAILTO:".$erstellername." <".$coodle->ersteller_uid."@".DOMAIN).">\r\n" .rtrim($teilnehmer)."\r\n" ."DTSTART;TZID=Europe/Vienna:".$dtstart."\r\n" ."DTEND;TZID=Europe/Vienna:".$dtend."\r\n" diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 971783746..011e75145 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -1,8 +1,8 @@ import BsModal from '../../Bootstrap/Modal.js'; import VueDatePicker from '../../vueDatepicker.js.php'; import ApiAbgabe from '../../../api/factory/abgabe.js' +import { getDateStyleClass } from "./getDateStyleClass.js"; -const today = new Date() export const AbgabeMitarbeiterDetail = { name: "AbgabeMitarbeiterDetail", components: { @@ -17,6 +17,7 @@ export const AbgabeMitarbeiterDetail = { Message: primevue.message, VueDatePicker }, + emits: ['paUpdated'], inject: [ 'abgabeTypeOptions', 'abgabetypenBetreuer', @@ -81,9 +82,9 @@ export const AbgabeMitarbeiterDetail = { }, methods: { getNoteBezeichnung(termin){ - if(termin.note?.bezeichnung) { - return termin.note?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌' - } else if(termin.bezeichnung?.benotbar === true && !termin.note) { + if(termin.noteBackend?.bezeichnung) { + return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌' + } else if(termin.bezeichnung?.benotbar === true && !termin.noteBackend) { return this.$capitalize(this.$p.t('abgabetool/c4notYetGraded')); } else { return '' @@ -109,7 +110,10 @@ export const AbgabeMitarbeiterDetail = { 'allowedToDelete': true, ...res.data[0] } - if(newTerminRes.note) newTerminRes.note = noteOpt + if(newTerminRes.note) { + newTerminRes.note = noteOpt + newTerminRes.noteBackend = noteOpt // certain UI elements should only reflect persisted state + } newTerminRes.invertedFixtermin = !newTerminRes.fixtermin const existingTerminRes = res.data[1] @@ -121,14 +125,17 @@ export const AbgabeMitarbeiterDetail = { benotbar: abgabeOpt.benotbar } - - // only insert new abgabe if we actually created a new one, not when saving/editing existing if(!existingTerminRes){ + newTerminRes.dateStyle = getDateStyleClass(newTerminRes, this.notenOptions) this.projektarbeit.abgabetermine.push(newTerminRes) } else { const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note) existingTerminRes.note = noteOptExisting + + termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz + termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour" + termin.dateStyle = getDateStyleClass(termin, this.notenOptions) } this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum)) @@ -168,6 +175,8 @@ export const AbgabeMitarbeiterDetail = { } else { this.showAutomagicModalPhrase = false } + + this.$emit("paUpdated", this.projektarbeit) } else if(res?.meta?.status == 'error'){ this.$fhcAlert.alertError() } @@ -251,6 +260,7 @@ export const AbgabeMitarbeiterDetail = { // this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ]) const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id) this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1) + this.$emit("paUpdated", this.projektarbeit) } else if(res?.meta?.status == 'error'){ this.$fhcAlert.alertError() } @@ -270,75 +280,6 @@ export const AbgabeMitarbeiterDetail = { window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url) // this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid)) }, - convertDateToIsoString(date) { - // 1. Check if it is a Date object AND if the date value is valid (not 'Invalid Date') - if (param instanceof Date && !isNaN(param.getTime())) { - const year = param.getFullYear(); - // getMonth() is 0-indexed, so we add 1. - const month = param.getMonth() + 1; - const day = param.getDate(); - - // Helper to pad single-digit numbers with a leading zero - const pad = (num) => String(num).padStart(2, '0'); - - // Return the formatted string: YYYY-MM-DD - return `${year}-${pad(month)}-${pad(day)}`; - } - - // If it's not a valid Date, return the original parameter - return param; - }, - dateDiffInDays(datumParam){ - let datum = datumParam - if(datumParam instanceof Date && !isNaN(datum.getTime())) - { - const year = datumParam.getFullYear(); - const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed - const day = datumParam.getDate(); - const pad = (num) => String(num).padStart(2, '0'); - datum = `${year}-${pad(month)}-${pad(day)}` - } - - const dateToday = luxon.DateTime.now().startOf('day'); - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, - getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; - - // GRADE STATUS - if (termin.note) { - if (termin.note.positiv) return 'bestanden'; - return 'nichtbestanden'; - } - - // ACTION REQUIRED FOR GRADE - if (termin.bezeichnung?.benotbar && datum < today) { - return 'beurteilungerforderlich'; - } - - // SUBMISSION STATUS - if (termin.upload_allowed) { - if (termin.abgabedatum) { - return isLate ? 'verspaetet' : 'abgegeben'; - } - - // no submission yet - if (datum < today) return 'verpasst'; - if (termin.diffindays <= 12) return 'abzugeben'; - return 'standard'; - } - - // GENERIC STATUS - return datum < today ? 'verpasst' : 'standard'; - }, openBeurteilungLink(link) { window.open(link, '_blank') }, @@ -396,6 +337,7 @@ export const AbgabeMitarbeiterDetail = { } }, formatDate(dateParam) { + // unsafe for datepickers, dont use there const date = new Date(dateParam) // handle missing leading 0 const padZero = (num) => String(num).padStart(2, '0'); @@ -476,7 +418,6 @@ export const AbgabeMitarbeiterDetail = { termin.kurzbz = '' } } - }, computed: { getAllowedToCreateNewTermin() { @@ -626,7 +567,6 @@ export const AbgabeMitarbeiterDetail = { return '' }, getProjektarbeitStudent(){ - if(this.projektarbeit?.student) return this.$capitalize(this.$p.t('person/student')) + ': ' + this.projektarbeit.student return '' @@ -671,7 +611,6 @@ export const AbgabeMitarbeiterDetail = { this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.seitenanzahl = newVal.seitenanzahl ?? 1 - }, }, created() { @@ -709,13 +648,14 @@ export const AbgabeMitarbeiterDetail = {
-
{{ $capitalize( $p.t('abgabetool/c4zieldatum') )}}
+
{{ $capitalize( $p.t('abgabetool/c4zieldatumv2') )}}
@@ -746,7 +686,7 @@ export const AbgabeMitarbeiterDetail = {
-
{{ $capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}
+
{{ $capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}
@@ -766,8 +706,8 @@ export const AbgabeMitarbeiterDetail = {

{{getProjektarbeitStudent}}

{{getProjektarbeitTitel}}