Merge branch 'master' into bug-56209/is_valid_date_vs_isValidDate

This commit is contained in:
Andreas Österreicher
2026-03-03 11:07:18 +01:00
15 changed files with 1463 additions and 390 deletions
+2
View File
@@ -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_STUDENT'] = true;
$config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true; $config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true;
$config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true;
@@ -89,13 +89,15 @@ class Abgabe extends FHCAPI_Controller
$abgabetypenBetreuer = $this->config->item('ALLOWED_ABGABETYPEN_BETREUER'); $abgabetypenBetreuer = $this->config->item('ALLOWED_ABGABETYPEN_BETREUER');
$ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'); $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT');
$ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'); $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER');
$BETREUER_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('BETREUER_SAMMELMAIL_BUTTON_STUDENT');
$ret = array( $ret = array(
'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link, 'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link,
'turnitin_link' => $turnitin_link, 'turnitin_link' => $turnitin_link,
'abgabetypenBetreuer' => $abgabetypenBetreuer, 'abgabetypenBetreuer' => $abgabetypenBetreuer,
'ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT' => $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT, '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); $this->terminateWithSuccess($ret);
@@ -373,6 +375,8 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
} }
$this->checkPaabgabeDeadline($paabgabe_id);
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id); $this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
$zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID());
@@ -445,6 +449,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 * tabulator tabledata fetch for abgabetool/mitarbeiter
* initially fetches all currently active projektarbeiten with assigned mentorship * initially fetches all currently active projektarbeiten with assigned mentorship
@@ -473,6 +507,15 @@ class Abgabe extends FHCAPI_Controller
$projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool); $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) { forEach($projektarbeiten->retval as $pa) {
$result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id); $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); Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc);
$pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkNew = $newLink;
$pa->beurteilungLinkOld = $oldLink; $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') '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 { } else {
// load existing entry of paabgabe and check if note has changed to negativ, to avoid sending when // load existing entry of paabgabe and check if note has changed to negativ, to avoid sending when
// only notiz has changed. // only notiz has changed.
@@ -718,7 +786,16 @@ class Abgabe extends FHCAPI_Controller
$abgaben[]= getData($this->PaabgabeModel->load($dataAbgabe))[0]; $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); $this->terminateWithSuccess($abgaben);
} }
@@ -1167,7 +1244,7 @@ class Abgabe extends FHCAPI_Controller
$email = $this->getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id); $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( $mailres = sendSanchoMail(
'ParbeitsbeurteilungEndupload', 'ParbeitsbeurteilungEndupload',
@@ -1180,7 +1257,7 @@ class Abgabe extends FHCAPI_Controller
if(!$mailres) 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 // 2. Begutachter mail, wenn Endabgabe, mit Token wenn extern
@@ -1200,14 +1277,14 @@ class Abgabe extends FHCAPI_Controller
if (!$tokenGenRes) 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)); $begutachterMitTokenRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $studentUser->uid, $begutachter->person_id));
if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0) if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0)
{ {
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachter'), 'general'); $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
} }
$begutachterMitToken = $begutachterMitTokenRetval[0]; $begutachterMitToken = $begutachterMitTokenRetval[0];
@@ -1241,7 +1318,7 @@ class Abgabe extends FHCAPI_Controller
if (!$mailres) if (!$mailres)
{ {
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachter'), 'general'); $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general');
} }
} }
+284 -5
View File
@@ -23,10 +23,271 @@ class AbgabetoolJob extends JOB_Controller
$this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel'); $this->_ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->_ci->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); $this->_ci->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$this->_ci->load->library('SignatureLib');
$this->_ci->load->config('abgabe'); $this->_ci->load->config('abgabe');
$this->loadPhrases([ $this->loadPhrases([
'abgabetool' '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 = '<div style="font-family: Arial, sans-serif; color: #333;">';
$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 .= "
<div style='margin-top: 25px; padding: 12px; background-color: #fff5f5; border-left: 4px solid #dc3545; border-bottom: 1px solid #fee;'>
<strong style='font-size: 16px; color: #b02a37;'>Projekt: {$projektarbeit_titel}</strong><br/>
<div style='margin-top: 5px; font-size: 14px;'>
<strong>Studierende/r:</strong> {$studentFullName}
</div>
<div style='margin-top: 3px; font-size: 14px;'>
<strong>Betreuer:</strong> {$allBetreuerFormatted}
</div>
<span style='color: #666; font-size: 12px;'>
ID: {$projektarbeit_id} | Stg: {$s->stgtyp}{$s->stgkz} ({$s->studiensemester_kurzbz})
</span>
</div>";
// Start Table
$abgabenString .= '
<table style="width: 100%; border-collapse: collapse; margin-bottom: 25px;">
<thead>
<tr style="background-color: #f8f9fa; text-align: left;">
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 20%;">Datum</th>
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 45%;">Abgabe/Bezeichnung</th>
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 35%;">Status</th>
</tr>
</thead>
<tbody>';
$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 .= "
<tr>
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$abgabedatumFormatted}</td>
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px;'>
<strong>{$abgabe->bezeichnung}</strong>
</td>
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; text-align: center;'>
<span style='color: #fff; background-color: {$sigBg}; padding: 3px 8px; border-radius: 3px; font-weight: bold; font-size: 11px;'>
{$sigLabel}
</span>
</td>
</tr>";
}
$abgabenString .= '</tbody></table>';
}
$abgabenString .= '</div>';
// 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() { public function notifyAssistenzAboutChangedAbgaben() {
@@ -235,11 +496,6 @@ class AbgabetoolJob extends JOB_Controller
$result = $this->_ci->PaabgabeModel->findAbgabenNewOrUpdatedSince($interval, $relevantTypes); $result = $this->_ci->PaabgabeModel->findAbgabenNewOrUpdatedSince($interval, $relevantTypes);
$retval = getData($result); $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 // group changed/new abgaben for projektarbeiten
$projektarbeiten = []; $projektarbeiten = [];
foreach($retval as $newOrChangedAbgabe) { foreach($retval as $newOrChangedAbgabe) {
@@ -248,6 +504,13 @@ class AbgabetoolJob extends JOB_Controller
if (isset($newOrChangedAbgabe->projektarbeit_id)) { if (isset($newOrChangedAbgabe->projektarbeit_id)) {
$projektarbeitId = $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, // If the 'projektarbeit_id' is not yet a key in $projektarbeiten,
// initialize it as an empty array. // initialize it as an empty array.
if (!isset($projektarbeiten[$projektarbeitId])) { if (!isset($projektarbeiten[$projektarbeitId])) {
@@ -259,6 +522,11 @@ class AbgabetoolJob extends JOB_Controller
} }
} }
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 // for each projektarbeit fetch their betreuer and save them in their own dictionary to avoid too many mails
$betreuerMap = []; $betreuerMap = [];
forEach($projektarbeiten as $projektarbeit_id => $abgaben) { forEach($projektarbeiten as $projektarbeit_id => $abgaben) {
@@ -378,6 +646,11 @@ class AbgabetoolJob extends JOB_Controller
$email = $betreuerRow->uid ? $betreuerRow->uid."@".DOMAIN : $betreuerRow->private_email; $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 // send email with bundled info
sendSanchoMail( sendSanchoMail(
'PAAChangesBetSM', 'PAAChangesBetSM',
@@ -500,6 +773,12 @@ class AbgabetoolJob extends JOB_Controller
$email = $data->uid ? $data->uid."@".DOMAIN : $data->private_email; $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 // send email with bundled info
sendSanchoMail( sendSanchoMail(
'PaabgabeUpdatesBetSM', 'PaabgabeUpdatesBetSM',
@@ -86,11 +86,14 @@ class Paabgabe_model extends DB_Model
return $this->execQuery($query, [$interval, $interval, $relevantTypes]); return $this->execQuery($query, [$interval, $interval, $relevantTypes]);
} }
public function findAbgabenNewOrUpdatedSinceByAbgabedatum($interval) { public function findAbgabenNewOrUpdatedSinceByAbgabedatum($interval, $relevantTypes = null) {
$query = "SELECT projektarbeit_id, paabgabe_id, paabgabetyp_kurzbz, fixtermin, datum, kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum, $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.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, 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, 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.tbl_person.anrede, public.tbl_person.titelpre, public.tbl_person.vorname, public.tbl_person.nachname, public.tbl_person.titelpost
@@ -98,15 +101,23 @@ class Paabgabe_model extends DB_Model
JOIN campus.tbl_paabgabetyp USING (paabgabetyp_kurzbz) JOIN campus.tbl_paabgabetyp USING (paabgabetyp_kurzbz)
JOIN lehre.tbl_projektarbeit USING (projektarbeit_id) JOIN lehre.tbl_projektarbeit USING (projektarbeit_id)
JOIN lehre.tbl_projektbetreuer 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_benutzer ON (public.tbl_benutzer.uid = student_uid)
JOIN public.tbl_person ON (public.tbl_benutzer.person_id = public.tbl_person.person_id) JOIN public.tbl_person ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
WHERE campus.tbl_paabgabe.abgabedatum IS NOT NULL WHERE campus.tbl_paabgabe.abgabedatum IS NOT NULL
AND campus.tbl_paabgabe.abgabedatum >= NOW() - INTERVAL ? AND campus.tbl_paabgabe.abgabedatum >= NOW() - INTERVAL ?";
ORDER BY abgabedatum DESC
";
return $this->execQuery($query, [$interval]); if($relevantTypes !== null) {
$query .= " AND campus.tbl_paabgabe.paabgabetyp_kurzbz IN ?";
$queryParams[]= $relevantTypes;
}
$query .= " ORDER BY abgabedatum DESC";
return $this->execQuery($query, $queryParams);
} }
public function loadByIDs($paabgabe_ids) { public function loadByIDs($paabgabe_ids) {
@@ -354,8 +354,10 @@ class Projektarbeit_model extends DB_Model
student_person.nachname as student_nachname, student_person.nachname as student_nachname,
tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid as betreuer_benutzer_uid, betreuer_benutzer.uid as betreuer_benutzer_uid,
betreuer_person.titelpre as betreuer_titelpre,
betreuer_person.vorname as betreuer_vorname, betreuer_person.vorname as betreuer_vorname,
betreuer_person.nachname as betreuer_nachname, betreuer_person.nachname as betreuer_nachname,
betreuer_person.titelpost as betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart, lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart,
lehre.tbl_projektbetreuer.person_id as betreuer_person_id, lehre.tbl_projektbetreuer.person_id as betreuer_person_id,
lehre.tbl_projektarbeit.sprache as sprache, lehre.tbl_projektarbeit.sprache as sprache,
@@ -415,6 +417,50 @@ class Projektarbeit_model extends DB_Model
LIMIT 1 LIMIT 1
) )
as zweitbetreuer_full_name, 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 SELECT
COALESCE(tbl_studienplan.orgform_kurzbz, COALESCE(tbl_studienplan.orgform_kurzbz,
+1 -1
View File
@@ -1041,7 +1041,7 @@ function sendEmail($coodle_id)
."END:STANDARD\r\n" ."END:STANDARD\r\n"
."END:VTIMEZONE\r\n" ."END:VTIMEZONE\r\n"
."BEGIN:VEVENT\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" .rtrim($teilnehmer)."\r\n"
."DTSTART;TZID=Europe/Vienna:".$dtstart."\r\n" ."DTSTART;TZID=Europe/Vienna:".$dtstart."\r\n"
."DTEND;TZID=Europe/Vienna:".$dtend."\r\n" ."DTEND;TZID=Europe/Vienna:".$dtend."\r\n"
@@ -1,8 +1,8 @@
import BsModal from '../../Bootstrap/Modal.js'; import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php'; import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js' import ApiAbgabe from '../../../api/factory/abgabe.js'
import { getDateStyleClass } from "./getDateStyleClass.js";
const today = new Date()
export const AbgabeMitarbeiterDetail = { export const AbgabeMitarbeiterDetail = {
name: "AbgabeMitarbeiterDetail", name: "AbgabeMitarbeiterDetail",
components: { components: {
@@ -17,6 +17,7 @@ export const AbgabeMitarbeiterDetail = {
Message: primevue.message, Message: primevue.message,
VueDatePicker VueDatePicker
}, },
emits: ['paUpdated'],
inject: [ inject: [
'abgabeTypeOptions', 'abgabeTypeOptions',
'abgabetypenBetreuer', 'abgabetypenBetreuer',
@@ -81,9 +82,9 @@ export const AbgabeMitarbeiterDetail = {
}, },
methods: { methods: {
getNoteBezeichnung(termin){ getNoteBezeichnung(termin){
if(termin.note?.bezeichnung) { if(termin.noteBackend?.bezeichnung) {
return termin.note?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌' 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.note) { } else if(termin.bezeichnung?.benotbar === true && !termin.noteBackend) {
return this.$capitalize(this.$p.t('abgabetool/c4notYetGraded')); return this.$capitalize(this.$p.t('abgabetool/c4notYetGraded'));
} else { } else {
return '' return ''
@@ -109,7 +110,10 @@ export const AbgabeMitarbeiterDetail = {
'allowedToDelete': true, 'allowedToDelete': true,
...res.data[0] ...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 newTerminRes.invertedFixtermin = !newTerminRes.fixtermin
const existingTerminRes = res.data[1] const existingTerminRes = res.data[1]
@@ -121,14 +125,17 @@ export const AbgabeMitarbeiterDetail = {
benotbar: abgabeOpt.benotbar benotbar: abgabeOpt.benotbar
} }
// only insert new abgabe if we actually created a new one, not when saving/editing existing // only insert new abgabe if we actually created a new one, not when saving/editing existing
if(!existingTerminRes){ if(!existingTerminRes){
newTerminRes.dateStyle = getDateStyleClass(newTerminRes, this.notenOptions)
this.projektarbeit.abgabetermine.push(newTerminRes) this.projektarbeit.abgabetermine.push(newTerminRes)
} else { } else {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note) const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting 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)) this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
@@ -168,6 +175,8 @@ export const AbgabeMitarbeiterDetail = {
} else { } else {
this.showAutomagicModalPhrase = false this.showAutomagicModalPhrase = false
} }
this.$emit("paUpdated", this.projektarbeit)
} else if(res?.meta?.status == 'error'){ } else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError() this.$fhcAlert.alertError()
} }
@@ -251,6 +260,7 @@ export const AbgabeMitarbeiterDetail = {
// this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ]) // this.$p.t('global/tooltipLektorDeleteKontrolle', [this.$entryParams.permissions.kontrolleDeleteMaxReach ])
const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id) const deletedTerminIndex = this.projektarbeit.abgabetermine.findIndex(t => t.paabgabe_id === termin.paabgabe_id)
this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1) this.projektarbeit.abgabetermine.splice(deletedTerminIndex, 1)
this.$emit("paUpdated", this.projektarbeit)
} else if(res?.meta?.status == 'error'){ } else if(res?.meta?.status == 'error'){
this.$fhcAlert.alertError() 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) 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)) // 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) { openBeurteilungLink(link) {
window.open(link, '_blank') window.open(link, '_blank')
}, },
@@ -396,6 +337,7 @@ export const AbgabeMitarbeiterDetail = {
} }
}, },
formatDate(dateParam) { formatDate(dateParam) {
// unsafe for datepickers, dont use there
const date = new Date(dateParam) const date = new Date(dateParam)
// handle missing leading 0 // handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0'); const padZero = (num) => String(num).padStart(2, '0');
@@ -476,7 +418,6 @@ export const AbgabeMitarbeiterDetail = {
termin.kurzbz = '' termin.kurzbz = ''
} }
} }
}, },
computed: { computed: {
getAllowedToCreateNewTermin() { getAllowedToCreateNewTermin() {
@@ -626,7 +567,6 @@ export const AbgabeMitarbeiterDetail = {
return '' return ''
}, },
getProjektarbeitStudent(){ getProjektarbeitStudent(){
if(this.projektarbeit?.student) return this.$capitalize(this.$p.t('person/student')) + ': ' + this.projektarbeit.student if(this.projektarbeit?.student) return this.$capitalize(this.$p.t('person/student')) + ': ' + this.projektarbeit.student
return '' return ''
@@ -671,7 +611,6 @@ export const AbgabeMitarbeiterDetail = {
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
this.form.seitenanzahl = newVal.seitenanzahl ?? 1 this.form.seitenanzahl = newVal.seitenanzahl ?? 1
}, },
}, },
created() { created() {
@@ -709,13 +648,14 @@ export const AbgabeMitarbeiterDetail = {
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-4 col-md-3 fw-bold align-content-center">{{ $capitalize( $p.t('abgabetool/c4zieldatum') )}}</div> <div class="col-4 col-md-3 fw-bold align-content-center">{{ $capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
<div class="col-8 col-md-9"> <div class="col-8 col-md-9">
<VueDatePicker <VueDatePicker
v-model="newTermin.datum" v-model="newTermin.datum"
:clearable="false" :clearable="false"
:enable-time-picker="false" :enable-time-picker="false"
:format="formatDate" locale="de"
format="dd.MM.yyyy"
:text-input="true" :text-input="true"
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
@@ -746,7 +686,7 @@ export const AbgabeMitarbeiterDetail = {
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-4 col-md-3 fw-bold align-content-center">{{ $capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div> <div class="col-4 col-md-3 fw-bold align-content-center">{{ $capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-8 col-md-9"> <div class="col-8 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="newTermin.kurzbz" rows="1" class="w-100"></Textarea> <Textarea style="margin-bottom: 4px;" v-model="newTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div> </div>
@@ -766,8 +706,8 @@ export const AbgabeMitarbeiterDetail = {
<p> {{getProjektarbeitStudent}}</p> <p> {{getProjektarbeitStudent}}</p>
<p> {{getProjektarbeitTitel}}</p> <p> {{getProjektarbeitTitel}}</p>
<template v-if="assistenzMode"> <template v-if="assistenzMode">
<p v-if="projektarbeit?.erstbetreuer_full_name"> {{ projektarbeit.betreuerart ? $capitalize($p.t('abgabetool/c4betrart' + projektarbeit.betreuerart)) : $capitalize( $p.t('abgabetool/c4betreuer') )}}: {{projektarbeit?.erstbetreuer_full_name}}</p> <p v-if="projektarbeit?.erstbetreuer_full_name"> {{ projektarbeit.betreuerart ? $capitalize($p.t('abgabetool/c4betrart' + projektarbeit.betreuerart)) : $capitalize( $p.t('abgabetool/c4betreuerv2') )}}: {{projektarbeit?.erstbetreuer_full_name}}</p>
<p v-if="projektarbeit?.zweitbetreuer_full_name"> {{ projektarbeit?.zweitbetreuer_betreuerart_kurzbz ? $capitalize($p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz)) : $capitalize( $p.t('abgabetool/c4zweitbetreuer') )}}: {{projektarbeit?.zweitbetreuer_full_name}}</p> <p v-if="projektarbeit?.zweitbetreuer_full_name"> {{ projektarbeit?.zweitbetreuer_betreuerart_kurzbz ? $capitalize($p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz)) : $capitalize( $p.t('abgabetool/c4zweitbetreuerv2') )}}: {{projektarbeit?.zweitbetreuer_full_name}}</p>
</template> </template>
<template v-else> <template v-else>
<p v-if="projektarbeit?.betreuer"> {{$capitalize($p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz))}}: {{projektarbeit?.betreuer?.first}}</p> <p v-if="projektarbeit?.betreuer"> {{$capitalize($p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz))}}: {{projektarbeit?.betreuer?.first}}</p>
@@ -805,20 +745,20 @@ export const AbgabeMitarbeiterDetail = {
</div> </div>
</div> </div>
<Accordion :multiple="true"> <Accordion :multiple="true">
<template v-for="termin in this.projektarbeit?.abgabetermine"> <template v-for="termin in this.projektarbeit?.abgabetermine" :key="termin.paabgabe_id">
<AccordionTab :headerClass="getDateStyleClass(termin) + '-header'"> <AccordionTab :headerClass="termin.dateStyle + '-header'">
<template #header> <template #header>
<div class="d-flex flex-nowrap align-items-center w-100"> <div class="d-flex flex-nowrap align-items-center w-100">
<div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -66px;"> <div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -68px;">
<i v-if="getDateStyleClass(termin) == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i> <i v-if="termin.dateStyle == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="getDateStyleClass(termin) == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i> <i v-else-if="termin.dateStyle == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="getDateStyleClass(termin) == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i> <i v-else-if="termin.dateStyle == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
<i v-else-if="getDateStyleClass(termin) == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i> <i v-else-if="termin.dateStyle == 'standard'" v-tooltip.right="getTooltipStandard" class="fa-solid fa-clock"></i>
<i v-else-if="getDateStyleClass(termin) == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-paperclip"></i> <i v-else-if="termin.dateStyle == 'abgegeben'" v-tooltip.right="getTooltipAbgegeben" class="fa-solid fa-paperclip"></i>
<i v-else-if="getDateStyleClass(termin) == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i> <i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
<i v-else-if="getDateStyleClass(termin) == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i> <i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
<i v-else-if="getDateStyleClass(termin) == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i> <i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
</div> </div>
@@ -855,7 +795,7 @@ export const AbgabeMitarbeiterDetail = {
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 align-content-center"> <div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div> <div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div> <div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
@@ -864,7 +804,8 @@ export const AbgabeMitarbeiterDetail = {
:clearable="false" :clearable="false"
:disabled="!termin.allowedToSave" :disabled="!termin.allowedToSave"
:enable-time-picker="false" :enable-time-picker="false"
:format="formatDate" locale="de"
format="dd.MM.yyyy"
:text-input="true" :text-input="true"
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
@@ -916,7 +857,7 @@ export const AbgabeMitarbeiterDetail = {
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div> <div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" class="w-100" rows="1" :disabled="!termin.allowedToSave"></Textarea> <Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" class="w-100" rows="1" :disabled="!termin.allowedToSave"></Textarea>
</div> </div>
@@ -931,7 +872,9 @@ export const AbgabeMitarbeiterDetail = {
v-model="termin.abgabedatum" v-model="termin.abgabedatum"
:clearable="false" :clearable="false"
:disabled="true" :disabled="true"
:format="formatDate"> locale="de"
format="dd.MM.yyyy"
>
</VueDatePicker> </VueDatePicker>
</div> </div>
@@ -334,7 +334,7 @@ export const AbgabeStudentDetail = {
<div class="col-8"> <div class="col-8">
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p> <p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p> <p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4betreuer') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p> <p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
</div> </div>
<div class="col-4"> <div class="col-4">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }} <p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
@@ -344,11 +344,11 @@ export const AbgabeStudentDetail = {
</div> </div>
<Accordion :multiple="true"> <Accordion :multiple="true">
<template v-for="termin in this.projektarbeit?.abgabetermine"> <template v-for="termin in this.projektarbeit?.abgabetermine" :key="termin.paabgabe_id">
<AccordionTab :headerClass="termin.dateStyle + '-header'"> <AccordionTab :headerClass="termin.dateStyle + '-header'">
<template #header> <template #header>
<div class="d-flex flex-nowrap align-items-center w-100"> <div class="d-flex flex-nowrap align-items-center w-100">
<div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -66px;"> <div class="flex-shrink-0 d-flex align-items-center justify-content-center" style="width: 36px; height: 36px; margin-left: -68px;">
<i v-if="termin.dateStyle == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i> <i v-if="termin.dateStyle == 'verspaetet'" v-tooltip.right="getTooltipVerspaetet" class="fa-solid fa-triangle-exclamation"></i>
<i v-else-if="termin.dateStyle == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i> <i v-else-if="termin.dateStyle == 'verpasst'" v-tooltip.right="getTooltipVerpasst" class="fa-solid fa-calendar-xmark"></i>
<i v-else-if="termin.dateStyle == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i> <i v-else-if="termin.dateStyle == 'abzugeben'" v-tooltip.right="getTooltipAbzugeben" class="fa-solid fa-hourglass-half"></i>
@@ -414,7 +414,7 @@ export const AbgabeStudentDetail = {
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 align-content-center"> <div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div> <div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
<div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div> <div class="row fw-light" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4abgabeuntil2359') )}}</div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
@@ -423,7 +423,8 @@ export const AbgabeStudentDetail = {
:clearable="false" :clearable="false"
:disabled="true" :disabled="true"
:enable-time-picker="false" :enable-time-picker="false"
:format="formatDate" locale="de"
format="dd.MM.yyyy"
:text-input="true" :text-input="true"
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
@@ -454,7 +455,7 @@ export const AbgabeStudentDetail = {
</div> </div>
<div v-if="termin.kurzbz && termin.kurzbz.length > 0" class="row mt-2"> <div v-if="termin.kurzbz && termin.kurzbz.length > 0" class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div> <div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" class="w-100" :disabled="true"></Textarea> <Textarea style="margin-bottom: 4px;" v-model="termin.kurzbz" rows="1" class="w-100" :disabled="true"></Textarea>
</div> </div>
@@ -8,15 +8,8 @@ import ApiStudiensemester from '../../../api/factory/studiensemester.js';
import AbgabeterminStatusLegende from "./StatusLegende.js"; import AbgabeterminStatusLegende from "./StatusLegende.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" import { splitMailsHelper } from "../../../helpers/EmailHelpers.js"
import { getDateStyleClass} from "./getDateStyleClass.js";
// spoofed date testing import { dateFilter } from '../../../tabulator/filters/Dates.js';
// const todayISO = '2025-08-08'
// const today = new Date(todayISO)
// const now = luxon.DateTime.fromISO(todayISO)
// prod code
const today = new Date()
const now = luxon.DateTime.now()
export const AbgabetoolAssistenz = { export const AbgabetoolAssistenz = {
name: "AbgabetoolAssistenz", name: "AbgabetoolAssistenz",
@@ -187,7 +180,7 @@ export const AbgabetoolAssistenz = {
// frozen: true, // frozen: true,
// width: 40 // width: 40
// }, // },
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, tooltip:false, minWidth: 150, cssClass: 'sticky-col'}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 150, 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/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/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/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
@@ -199,20 +192,47 @@ export const AbgabetoolAssistenz = {
formatter: this.centeredTextFormatter, widthGrow: 1}, 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/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/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuer'))), field: 'erstbetreuer', 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/c4zweitbetreuer'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: 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/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: 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/c4qgate1Status'))), headerFilter: true, field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: 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/c4qgate2Status'))), headerFilter: true, field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: 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/c4zweitbetreuerv2'))), field: 'zweitbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{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/c4prevAbgabetermin'))),
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: 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},
{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},
{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},
], ],
persistence: false, persistence: false,
persistenceID: "abgabetool_2025_12" persistenceID: "abgabetool_2026_02_26"
}, },
abgabeTableEventHandlers: [ abgabeTableEventHandlers: [
{ {
event: "rowSelectionChanged", event: "rowSelectionChanged",
handler: async(data) => { handler: async(data) =>
{
this.selectedData.filter(sd => !data.includes(sd)).forEach(fsd => { this.selectedData.filter(sd => !data.includes(sd)).forEach(fsd => {
if(fsd.checkbox) fsd.checkbox.checked = false if(fsd.checkbox) fsd.checkbox.checked = false
}) })
@@ -227,12 +247,79 @@ export const AbgabetoolAssistenz = {
]}; ]};
}, },
methods: { methods: {
handlePaUpdated(projektarbeit) {
this.checkAbgabetermineProjektarbeit(projektarbeit)
this.$refs.abgabeTable.tabulator.redraw(true)
},
getQGateStatusList() {
return [
this.$p.t('abgabetool/c4keinTerminVorhanden'),
this.$p.t('abgabetool/c4positivBenotet'),
this.$p.t('abgabetool/c4negativBenotet'),
this.$p.t('abgabetool/c4notYetGraded'),
this.$p.t('abgabetool/c4notSubmitted'),
this.$p.t('abgabetool/c4notHappenedYet')
]
},
sortFuncTerminCol(a, b, aRow, bRow, column, dir, params) {
if (a === null || typeof a === "undefined") return 1;
if (b === null || typeof b === "undefined") return -1;
// try to handle the prev/next interpretation consistently
// can only make this wrong UX whise so whatever
if(column._column.field == 'prevTermin') {
return Math.abs(b.diffMs) - Math.abs(a.diffMs)
} else if (column._column.field == 'nextTermin') {
return Math.abs(a.diffMs) - Math.abs(b.diffMs)
}
// just in case someone reuses this
return Math.abs(b.diffMs) - Math.abs(a.diffMs)
},
headerFilterTerminCol(filterVal, rowVal) {
if (!rowVal || !rowVal.luxonDate || !rowVal.luxonDate.isValid) {
return false;
}
const rowDate = rowVal.luxonDate;
const toLuxon = (val) => {
if (!val) return null;
let dt;
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = luxon.DateTime.fromISO(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
return dt.isValid ? dt : null;
};
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
},
sammelMailStudent(param) { sammelMailStudent(param) {
const emails = this.selectedData const recipientList = [];
.map(row => `${row.student_uid}@${this.domain}`) this.selectedData.forEach(d => {
.join(','); recipientList.push(`${d.student_uid}@${this.domain}`)
const uniqueRecipients = [...new Set(emails)]; })
const uniqueRecipients = [...new Set(recipientList)];
const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]); const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]);
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
}, },
@@ -281,7 +368,6 @@ export const AbgabetoolAssistenz = {
return false; return false;
}, },
checkQualityGateStatus(projekt) { checkQualityGateStatus(projekt) {
// TODO: might refine the representation of these states and maybe refactor code a little
const qgate1Termine = [] const qgate1Termine = []
const qgate2Termine = [] const qgate2Termine = []
@@ -301,7 +387,7 @@ export const AbgabetoolAssistenz = {
// reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check // reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check
qgate1Termine.forEach(qgate => { qgate1Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) { if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) { if(noteOpt.positiv) {
projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet') projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate1StatusRank = 5 projekt.qgate1StatusRank = 5
@@ -323,7 +409,7 @@ export const AbgabetoolAssistenz = {
qgate2Termine.forEach(qgate => { qgate2Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) { if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) { if(noteOpt.positiv) {
projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet') projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate2StatusRank = 5 projekt.qgate2StatusRank = 5
@@ -385,14 +471,18 @@ export const AbgabetoolAssistenz = {
}, },
checkAbgabetermineProjektarbeit(projekt) { checkAbgabetermineProjektarbeit(projekt) {
const now = luxon.DateTime.now()
// calculate Abgabetermin time diff to now and assign last and next to projekt // calculate Abgabetermin time diff to now and assign last and next to projekt
projekt.abgabetermine.forEach(termin => { projekt.abgabetermine.forEach(termin => {
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
// while already looping through each termin, calculate datestyle beforehand // while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = this.getDateStyleClass(termin) termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = luxon.DateTime.fromISO(termin.datum) const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
if (termin.diffMs < 0) { if (termin.diffMs < 0) {
@@ -612,6 +702,9 @@ export const AbgabetoolAssistenz = {
}, },
addSeries() { addSeries() {
const pids = this.selectedData?.map(projekt => projekt.projektarbeit_id) const pids = this.selectedData?.map(projekt => projekt.projektarbeit_id)
const preserveSelected = [...this.selectedData]
this.saving = true this.saving = true
this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin
this.$api.call(ApiAbgabe.postSerientermin( this.$api.call(ApiAbgabe.postSerientermin(
@@ -644,14 +737,27 @@ export const AbgabetoolAssistenz = {
}) })
// reset selection to empty // reset selection to empty
this.$refs.abgabeTable.tabulator.deselectRow() // 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) const mappedData = this.mapProjekteToTableData(this.projektarbeiten)
this.$refs.abgabeTable.tabulator.setData(mappedData) table.setData(mappedData)
this.$refs.abgabeTable.tabulator.redraw(true) table.redraw(true)
Vue.nextTick(()=> {
const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder')
if(table) {
table.scrollLeft = scrollX;
table.scrollTop = scrollY;
}
})
}).finally(()=>{ }).finally(()=>{
this.saving = false this.saving = false
this.selectedData = preserveSelected
}) })
this.$refs.modalContainerAddSeries.hide() this.$refs.modalContainerAddSeries.hide()
@@ -705,10 +811,11 @@ export const AbgabetoolAssistenz = {
return str return str
}, },
isPastDate(date) { isPastDate(date) {
return new Date(date) < new Date(Date.now()) const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
}, },
setDetailComponent(details){ setDetailComponent(details){
const pa = this.projektarbeiten.find(projektarbeit => projektarbeit.projektarbeit_id == details.projektarbeit_id) const pa = this.projektarbeiten.find(projektarbeit => projektarbeit.projektarbeit_id == details.projektarbeit_id)
if(pa?.abgabetermine?.length) { if(pa?.abgabetermine?.length) {
@@ -730,6 +837,11 @@ export const AbgabetoolAssistenz = {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) 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 = [] termin.file = []
// assistenz should be able to edit every abgabe // assistenz should be able to edit every abgabe
@@ -739,8 +851,6 @@ export const AbgabetoolAssistenz = {
// assistenz are not allowed to delete deadlines with existing submissions // assistenz are not allowed to delete deadlines with existing submissions
termin.allowedToDelete = paIsBenotet ? false : !termin.abgabedatum termin.allowedToDelete = paIsBenotet ? false : !termin.abgabedatum
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
}) })
const vorname = pa.vorname ?? pa.student_vorname const vorname = pa.vorname ?? pa.student_vorname
@@ -748,52 +858,8 @@ export const AbgabetoolAssistenz = {
pa.student = `${vorname} ${nachname}` pa.student = `${vorname} ${nachname}`
this.selectedProjektarbeit = pa this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show() this.$refs.modalContainerAbgabeDetail.show()
}, },
dateDiffInDays(datum){
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';
},
openTimeline(val) { openTimeline(val) {
const projekt = this.projektarbeiten.find(p => p.projektarbeit_id == val.projektarbeit_id) const projekt = this.projektarbeiten.find(p => p.projektarbeit_id == val.projektarbeit_id)
if(!projekt) { if(!projekt) {
@@ -864,7 +930,7 @@ export const AbgabetoolAssistenz = {
case 'abgegeben': case 'abgegeben':
icon = '<i class="fa-solid fa-paperclip"></i>' icon = '<i class="fa-solid fa-paperclip"></i>'
break break
case 'beurteilungerfolderlich': case 'beurteilungerforderlich':
icon = '<i class="fa-solid fa-list-check"></i>' icon = '<i class="fa-solid fa-list-check"></i>'
break break
case 'bestanden': case 'bestanden':
@@ -984,7 +1050,7 @@ export const AbgabetoolAssistenz = {
if(this.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER) { if(this.ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER) {
menu.push({ menu.push({
label: this.$p.t('abgabetool/c4sendEmailBetreuerv2', [this.uniqueBetreuerEmailCount]), label: this.$p.t('abgabetool/c4sendEmailBetreuerv3', [this.uniqueBetreuerEmailCount]),
command: this.sammelMailBetreuer command: this.sammelMailBetreuer
}) })
} }
@@ -1034,6 +1100,24 @@ export const AbgabetoolAssistenz = {
if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) {
this.loadProjektarbeiten() this.loadProjektarbeiten()
} }
},
selectedData(newVal) {
const table = this.$refs.abgabeTable?.tabulator
if(!table) return
const allRows = table.getRows();
newVal.forEach(selected => {
const row = allRows.find(r => {
const data = r.getData()
if (data.projektarbeit_id == selected.projektarbeit_id) return r
})
row.select()
const cb = row.getElement().children[0]?.children[0]?.children[0]
if(cb) cb.checked = true
})
} }
}, },
created() { created() {
@@ -1145,15 +1229,16 @@ export const AbgabetoolAssistenz = {
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 align-content-center"> <div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div> <div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<VueDatePicker <VueDatePicker
style="width: 95%;" style="width: 95%;"
v-model="serienTermin.datum" v-model="serienTermin.datum"
:clearable="false" :clearable="false"
locale="de"
format="dd.MM.yyyy"
:enable-time-picker="false" :enable-time-picker="false"
:format="formatDate"
:text-input="true" :text-input="true"
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
@@ -1186,7 +1271,7 @@ export const AbgabetoolAssistenz = {
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div> <div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea> <Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div> </div>
@@ -1207,7 +1292,13 @@ export const AbgabetoolAssistenz = {
</div> </div>
</template> </template>
<template v-slot:default> <template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen" :assistenzMode="true"></AbgabeDetail> <AbgabeDetail
:projektarbeit="selectedProjektarbeit"
:isFullscreen="detailIsFullscreen"
:assistenzMode="true"
@paUpdated="handlePaUpdated">
</AbgabeDetail>
</template> </template>
</bs-modal> </bs-modal>
@@ -1279,7 +1370,7 @@ export const AbgabetoolAssistenz = {
<template #opposite="slotProps"> <template #opposite="slotProps">
<div class="row g-1"> <div class="row g-1">
<div class="col-5 fw-semibold text-end"> <div class="col-5 fw-semibold text-end">
{{ $capitalize($p.t('abgabetool/c4zieldatum')) }}: {{ $capitalize($p.t('abgabetool/c4zieldatumv2')) }}:
</div> </div>
<div class="col-7"> <div class="col-7">
{{ formatDate(slotProps.item.datum) }} {{ formatDate(slotProps.item.datum) }}
@@ -4,6 +4,9 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php'; import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js' import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass } from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/Dates.js';
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
export const AbgabetoolMitarbeiter = { export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter", name: "AbgabetoolMitarbeiter",
@@ -14,6 +17,7 @@ export const AbgabetoolMitarbeiter = {
Checkbox: primevue.checkbox, Checkbox: primevue.checkbox,
Dropdown: primevue.dropdown, Dropdown: primevue.dropdown,
Textarea: primevue.textarea, Textarea: primevue.textarea,
TieredMenu: primevue.tieredmenu,
VueDatePicker, VueDatePicker,
FhcOverlay FhcOverlay
}, },
@@ -46,6 +50,7 @@ export const AbgabetoolMitarbeiter = {
phrasenResolved: false, phrasenResolved: false,
turnitin_link: null, turnitin_link: null,
old_abgabe_beurteilung_link: null, old_abgabe_beurteilung_link: null,
BETREUER_SAMMELMAIL_BUTTON_STUDENT: null,
saving: false, saving: false,
loading: false, loading: false,
abgabeTypeOptions: null, abgabeTypeOptions: null,
@@ -79,7 +84,7 @@ export const AbgabetoolMitarbeiter = {
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')), placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
selectable: true, selectable: true,
selectableCheck: this.selectionCheck, selectableCheck: this.selectionCheck,
rowHeight: 80, rowHeight: 40,
columns: [ columns: [
{ {
formatter: function (cell, formatterParams, onRendered) { formatter: function (cell, formatterParams, onRendered) {
@@ -135,18 +140,36 @@ export const AbgabetoolMitarbeiter = {
width: 50, width: 50,
cssClass: 'sticky-col' cssClass: 'sticky-col'
}, },
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'}, {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/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4kontakt'))), field: 'mail', formatter: this.mailFormatter, widthGrow: 1, tooltip: false, visible: 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/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/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/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/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/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/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1} {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',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: 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},
{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},
{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}
], ],
persistence: false, persistence: false,
persistenceID: 'abgabeTableBetreuer2026-02-26'
}, },
abgabeTableEventHandlers: [{ abgabeTableEventHandlers: [{
event: "tableBuilt", event: "tableBuilt",
@@ -182,6 +205,331 @@ export const AbgabetoolMitarbeiter = {
]}; ]};
}, },
methods: { methods: {
handlePaUpdated(projektarbeit) {
this.checkAbgabetermineProjektarbeit(projektarbeit)
this.$refs.abgabeTable.tabulator.redraw(true)
},
sammelMailStudent(param) {
const recipientList = [];
this.selectedData.forEach(d => {
recipientList.push(`${d.student_uid}@${this.domain}`)
})
const uniqueRecipients = [...new Set(recipientList)];
const subject = ""; // empty subject line
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
},
getQGateStatusList() {
return [
this.$p.t('abgabetool/c4keinTerminVorhanden'),
this.$p.t('abgabetool/c4positivBenotet'),
this.$p.t('abgabetool/c4negativBenotet'),
this.$p.t('abgabetool/c4notYetGraded'),
this.$p.t('abgabetool/c4notSubmitted'),
this.$p.t('abgabetool/c4notHappenedYet')
]
},
sortFuncTerminCol(a, b, aRow, bRow, column, dir, params) {
if (a === null || typeof a === "undefined") return 1;
if (b === null || typeof b === "undefined") return -1;
// try to handle the prev/next interpretation consistently
// can only make this wrong UX whise so whatever
if(column._column.field == 'prevTermin') {
return Math.abs(b.diffMs) - Math.abs(a.diffMs)
} else if (column._column.field == 'nextTermin') {
return Math.abs(a.diffMs) - Math.abs(b.diffMs)
}
// just in case someone reuses this
return Math.abs(b.diffMs) - Math.abs(a.diffMs)
},
headerFilterTerminCol(filterVal, rowVal) {
if (!rowVal || !rowVal.luxonDate || !rowVal.luxonDate.isValid) {
return false;
}
const rowDate = rowVal.luxonDate;
const toLuxon = (val) => {
if (!val) return null;
let dt;
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = luxon.DateTime.fromISO(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
return dt.isValid ? dt : null;
};
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
},
loadState() {
return JSON.parse(localStorage.getItem(this.abgabeTableOptions.persistenceID) || "null");
},
saveState(table) {
// avoid storing state after first restore part happened
if(!this.stateRestored) return
const rawLayout = table.getColumnLayout();
const state = {
columns: rawLayout.map(col => ({
field: col.field,
visible: col.visible,
width: col.width,
})),
sort: table.getSorters().map(s => ({
field: s.field,
dir: s.dir,
})),
filters: table.getFilters(),
headerFilters: table.getHeaderFilters()
};
localStorage.setItem(this.abgabeTableOptions.persistenceID, JSON.stringify(state));
},
handleTableBuilt() {
const table = this.$refs.abgabeTable.tabulator
this.tableBuiltResolve()
table.on("columnMoved", () => {
this.saveState(table);
});
table.on("columnResized", () => {
this.saveState(table);
});
table.on("columnVisibilityChanged", () => {
this.saveState(table);
});
table.on("filterChanged", () => {
this.saveState(table);
});
table.on("headerFilterChanged", () => {
this.saveState(table);
});
table.on("dataSorted", () => {
this.saveState(table);
});
table.on("columnSorted", () => {
this.saveState(table);
});
table.on("sortersChanged", () => {
this.saveState(table);
});
const saved = this.loadState();
table.on("renderComplete", () => {
if(!this.stateRestored) {
if (saved?.columns && !this.colLayoutRestored) {
const layout = saved.columns.map(col => ({
field: col.field,
width: col.width,
visible: col.visible,
// add more if needed, but keep it simple
}));
table.setColumnLayout(layout);
this.colLayoutRestored = true;
}
if (saved?.filters && !this.filtersRestored) {
this.filtersRestored = true // instantly avoid retriggers
table.setFilter(saved.filters);
}
if (saved?.headerFilters && !this.headerFiltersRestored) {
this.headerFiltersRestored = true // instantly avoid retriggers
for (let hf of saved.headerFilters) {
table.setHeaderFilterValue(hf.field, hf.value);
}
}
if (saved?.sort?.length && !this.sortRestored) {
this.sortRestored = true;
setTimeout(() => {
const sortList = saved.sort.map(s => {
const col = table.columnManager.findColumn(s.field);
if (!col) {
return null;
}
return { column: col, dir: s.dir };
}).filter(Boolean);
table.setSort(sortList);
}, 100);
}
this.stateRestored = true
}
});
},
checkQualityGateStatus(projekt) {
const qgate1Termine = []
const qgate2Termine = []
projekt.qgate1Status = this.$p.t('abgabetool/c4keinTerminVorhanden')// 'Kein Termin vorhanden'
projekt.qgate1StatusRank = 0
projekt.qgate2Status = this.$p.t('abgabetool/c4keinTerminVorhanden')
projekt.qgate2StatusRank = 0
projekt.abgabetermine.forEach(termin => {
if(termin.paabgabetyp_kurzbz == 'qualgate1') qgate1Termine.push(termin)
if(termin.paabgabetyp_kurzbz == 'qualgate2') qgate2Termine.push(termin)
})
// calculate qgateStatusRank and display the highest order status rank of all quality gate termine until one
// counts as passed, which is just a positive note no matter if anything has been uploaded
// reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check
qgate1Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate1StatusRank = 5
} else {
projekt.qgate1Status = this.$p.t('abgabetool/c4negativBenotet')
projekt.qgate1StatusRank = 4
}
} else if (qgate.note == null && projekt.qgate1StatusRank <= 3) {
projekt.qgate1Status = this.$p.t('abgabetool/c4notYetGraded')
projekt.qgate1StatusRank = 3
} else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate1StatusRank <= 2) {
projekt.qgate1Status = this.$p.t('abgabetool/c4notSubmitted')
projekt.qgate1StatusRank = 2
} else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate1StatusRank <= 1) {
projekt.qgate1Status = this.$p.t('abgabetool/c4notHappenedYet')
projekt.qgate1StatusRank = 1
}
})
qgate2Termine.forEach(qgate => {
if(qgate.note != null && projekt.qgate1StatusRank <= 5) {
const noteOpt = typeof qgate.note !== 'object' ? this.notenOptions.find(opt => opt.note == qgate.note) : qgate.note
if(noteOpt.positiv) {
projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet')
projekt.qgate2StatusRank = 5
} else {
projekt.qgate2Status = this.$p.t('abgabetool/c4negativBenotet')
projekt.qgate2StatusRank = 4
}
} else if (qgate.note == null && projekt.qgate2StatusRank <= 3) {
projekt.qgate2Status = this.$p.t('abgabetool/c4notYetGraded')
projekt.qgate2StatusRank = 3
} else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate2StatusRank <= 2) {
projekt.qgate2Status = this.$p.t('abgabetool/c4notSubmitted')
projekt.qgate2StatusRank = 2
} else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate2StatusRank <= 1) {
projekt.qgate2Status = this.$p.t('abgabetool/c4notHappenedYet')
projekt.qgate2StatusRank = 1
}
})
},
checkAbgabetermineProjektarbeit(projekt) {
const now = luxon.DateTime.now()
// calculate Abgabetermin time diff to now and assign last and next to projekt
projekt.abgabetermine.forEach(termin => {
// while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
if (termin.diffMs < 0) {
if (!projekt.prevTermin ||
termin.diffMs > projekt.prevTermin.diffMs // larger (less negative) = closer to now
) {
projekt.prevTermin = termin;
}
} else if (termin.diffMs > 0) {
if (!projekt.nextTermin ||
termin.diffMs < projekt.nextTermin.diffMs // smaller positive = closer to now
) {
projekt.nextTermin = termin;
}
}
})
// seperate check for quality gates
this.checkQualityGateStatus(projekt)
},
abgabterminFormatter(cell) {
const val = cell.getValue()
if(val) {
let icon = ''
switch(val.dateStyle) {
case 'verspaetet':
icon = '<i class="fa-solid fa-triangle-exclamation"></i>'
break
case 'verpasst':
icon = '<i class="fa-solid fa-calendar-xmark"></i>'
break
case 'abzugeben':
icon = '<i class="fa-solid fa-hourglass-half"></i>'
break
case 'standard':
icon = '<i class="fa-solid fa-clock"></i>'
break
case 'abgegeben':
icon = '<i class="fa-solid fa-paperclip"></i>'
break
case 'beurteilungerforderlich':
icon = '<i class="fa-solid fa-list-check"></i>'
break
case 'bestanden':
icon = '<i class="fa-solid fa-check"></i>'
break
case 'nichtbestanden':
icon = '<i class="fa-solid fa-circle-exclamation"></i>'
break
}
const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung
return '<div style="display: flex; height: 100%">' +
'<div class=' + val.dateStyle + "-header" + ' style="width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
icon +
'</div>' +
'<div style="margin-left: 4px;">' +
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
'</div>'+
'</div>'
} else {
return ''
}
},
selectHandler(e, cell) { selectHandler(e, cell) {
const row = cell.getRow(); const row = cell.getRow();
@@ -274,6 +622,24 @@ export const AbgabetoolMitarbeiter = {
)).then(res => { )).then(res => {
if (res.meta.status === "success" && res.data) { if (res.meta.status === "success" && res.data) {
this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert')) this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert'))
const oldScrollLeft = this.$refs.abgabeTable?.tabulator.rowManager.scrollLeft
const oldScrollTop = this.$refs.abgabeTable?.tabulator.rowManager.scrollTop
this.loading = true
this.loadProjektarbeiten(this.showAll, () => {
this.$refs.abgabeTable?.tabulator.redraw(true)
this.$refs.abgabeTable?.tabulator.setSort([]);
this.loading = false
Vue.nextTick(()=> {
const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder')
if(table) {
table.scrollLeft = oldScrollLeft;
table.scrollTop = oldScrollTop;
}
})
})
} else { } else {
this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern')) this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern'))
} }
@@ -294,14 +660,14 @@ export const AbgabetoolMitarbeiter = {
return str return str
}, },
isPastDate(date) { isPastDate(date) {
return new Date(date) < new Date(Date.now()) const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
}, },
setDetailComponent(details){ setDetailComponent(details){
this.loading=true this.loading=true
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id) const pa = this.projektarbeiten?.retval?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
pa.isCurrent = res.data[1]
let paIsBenotet = false let paIsBenotet = false
if(pa.note !== undefined && pa.note !== null) { if(pa.note !== undefined && pa.note !== null) {
@@ -313,10 +679,28 @@ export const AbgabetoolMitarbeiter = {
paIsBenotet = true paIsBenotet = true
} }
if(pa?.abgabetermine?.length) {
this.$api.call(ApiAbgabe.getSignaturStatusForProjektarbeitAbgaben(pa.abgabetermine.map(termin => termin.paabgabe_id), pa.student_uid))
.then(res => {
if(res.meta.status === 'success') {
res.data.forEach(paabgabe => {
const termin = pa.abgabetermine.find(abgabe => abgabe.paabgabe_id == paabgabe.paabgabe_id)
if(termin && paabgabe.signatur !== undefined) termin.signatur = paabgabe.signatur
})
}
})
}
pa.abgabetermine.forEach(termin => { pa.abgabetermine.forEach(termin => {
termin.note = this.allowedNotenOptions.find(opt => opt.note == termin.note) const noteOpt = this.allowedNotenOptions.find(opt => opt.note == termin.note)
if(noteOpt) termin.note = noteOpt
termin.file = [] termin.file = []
// only set this if it has not been set yet and abgabetermin has a note (qgate)
if(!termin.noteBackend && noteOpt) {
termin.noteBackend = noteOpt
}
// update 08-01-2026: everybody is allowed to do everything in client, critical checks happen at backend level // update 08-01-2026: everybody is allowed to do everything in client, critical checks happen at backend level
// termin.allowedToSave = true // termin.allowedToSave = true
@@ -329,13 +713,16 @@ export const AbgabetoolMitarbeiter = {
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
}) })
pa.student_uid = details.student_uid pa.student_uid = details.student_uid
pa.student = `${pa.vorname} ${pa.nachname}` pa.student = `${pa.vorname} ${pa.nachname}`
this.selectedProjektarbeit = pa this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show() this.$refs.modalContainerAbgabeDetail.show()
}).finally(()=>{this.loading = false})
this.loading = false
}, },
centeredTextFormatter(cell) { centeredTextFormatter(cell) {
const val = cell.getValue() const val = cell.getValue()
@@ -348,11 +735,6 @@ export const AbgabetoolMitarbeiter = {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' + return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>' '<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
}, },
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) { beurteilungFormatter(cell) {
const val = cell.getValue() const val = cell.getValue()
if(val) { if(val) {
@@ -379,11 +761,13 @@ export const AbgabetoolMitarbeiter = {
return (projekt.typ + projekt.kurzbz)?.toUpperCase() return (projekt.typ + projekt.kurzbz)?.toUpperCase()
}, },
setupData(data){ setupData(data){
this.projektarbeiten = data[0] this.projektarbeiten = data[0]
this.domain = data[1] this.domain = data[1]
this.tableData = data[0]?.retval?.map(projekt => { this.tableData = data[0]?.retval?.map(projekt => {
this.checkAbgabetermineProjektarbeit(projekt)
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
return { return {
@@ -455,6 +839,29 @@ export const AbgabetoolMitarbeiter = {
}, },
}, },
computed: { computed: {
emailItems() {
const menu = []
if(this.BETREUER_SAMMELMAIL_BUTTON_STUDENT){
menu.push({
label: this.$p.t('abgabetool/c4sendEmailStudierendev2', [this.uniqueStudentEmailCount]),
command: this.sammelMailStudent
})
}
return menu
},
uniqueStudentEmailCount() {
const emails = new Set();
this.selectedData.forEach(row => {
if (row.student_uid) {
emails.add(row.student_uid); // actually dont need domain for this
}
});
return emails.size;
},
getAllowedAbgabeTypeOptions() { getAllowedAbgabeTypeOptions() {
return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.paabgabetyp_kurzbz)) return this.abgabeTypeOptions.filter(opt => this.abgabetypenBetreuer.includes(opt.paabgabetyp_kurzbz))
} }
@@ -467,6 +874,7 @@ export const AbgabetoolMitarbeiter = {
this.turnitin_link = res.data?.turnitin_link this.turnitin_link = res.data?.turnitin_link
this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link this.old_abgabe_beurteilung_link = res.data?.old_abgabe_beurteilung_link
this.abgabetypenBetreuer = res.data?.abgabetypenBetreuer this.abgabetypenBetreuer = res.data?.abgabetypenBetreuer
this.BETREUER_SAMMELMAIL_BUTTON_STUDENT = res.data?.BETREUER_SAMMELMAIL_BUTTON_STUDENT
}).catch(e => { }).catch(e => {
this.loading = false this.loading = false
}) })
@@ -515,7 +923,7 @@ export const AbgabetoolMitarbeiter = {
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 align-content-center"> <div class="col-12 col-md-3 align-content-center">
<div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatum') )}}</div> <div class="row fw-bold" style="margin-left: 2px">{{$capitalize( $p.t('abgabetool/c4zieldatumv2') )}}</div>
</div> </div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<VueDatePicker <VueDatePicker
@@ -523,7 +931,8 @@ export const AbgabetoolMitarbeiter = {
v-model="serienTermin.datum" v-model="serienTermin.datum"
:clearable="false" :clearable="false"
:enable-time-picker="false" :enable-time-picker="false"
:format="formatDate" locale="de"
format="dd.MM.yyyy"
:text-input="true" :text-input="true"
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
@@ -557,7 +966,7 @@ export const AbgabetoolMitarbeiter = {
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbz') )}}</div> <div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabekurzbzv2') )}}</div>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea> <Textarea style="margin-bottom: 4px;" v-model="serienTermin.kurzbz" rows="1" class="w-100"></Textarea>
</div> </div>
@@ -578,7 +987,11 @@ export const AbgabetoolMitarbeiter = {
</div> </div>
</template> </template>
<template v-slot:default> <template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit" :isFullscreen="detailIsFullscreen"></AbgabeDetail> <AbgabeDetail
:projektarbeit="selectedProjektarbeit"
:isFullscreen="detailIsFullscreen"
@paUpdated="handlePaUpdated">
</AbgabeDetail>
</template> </template>
</bs-modal> </bs-modal>
@@ -598,6 +1011,7 @@ export const AbgabetoolMitarbeiter = {
@click:new=openAddSeriesModal @click:new=openAddSeriesModal
:tabulator-options="abgabeTableOptions" :tabulator-options="abgabeTableOptions"
:tabulator-events="abgabeTableEventHandlers" :tabulator-events="abgabeTableEventHandlers"
@tableBuilt="handleTableBuilt"
tableOnly tableOnly
:sideMenu="false" :sideMenu="false"
:useSelectionSpan="false" :useSelectionSpan="false"
@@ -613,6 +1027,16 @@ export const AbgabetoolMitarbeiter = {
<i class="fa fa-hourglass-end"></i> <i class="fa fa-hourglass-end"></i>
{{ $p.t('abgabetool/showDeadlines') }} {{ $p.t('abgabetool/showDeadlines') }}
</button> </button>
<button
v-if="emailItems.length"
role="button"
@click="evt => $refs.menu.toggle(evt)"
class="btn btn-outline-secondary dropdown-toggle"
aria-haspopup="true"
>
<i class="fa fa-envelope"></i>
</button>
<tiered-menu ref="menu" :model="emailItems" popup :autoZIndex="false" />
</template> </template>
</core-filter-cmpt> </core-filter-cmpt>
@@ -2,8 +2,8 @@ import AbgabeDetail from "./AbgabeStudentDetail.js";
import ApiAbgabe from '../../../api/factory/abgabe.js' import ApiAbgabe from '../../../api/factory/abgabe.js'
import BsModal from "../../Bootstrap/Modal.js"; import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
const today = new Date()
export const AbgabetoolStudent = { export const AbgabetoolStudent = {
name: "AbgabetoolStudent", name: "AbgabetoolStudent",
components: { components: {
@@ -48,61 +48,6 @@ export const AbgabetoolStudent = {
}; };
}, },
methods: { methods: {
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(Number.isInteger(termin.note)) {
const opt = this.notenOptions.find(opt => opt.note == termin.note)
if(opt.positiv) return 'bestanden'
}
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';
},
checkQualityGatesStrict(termine) { checkQualityGatesStrict(termine) {
let qgate1Passed = false let qgate1Passed = false
let qgate2Passed = false let qgate2Passed = false
@@ -155,7 +100,9 @@ export const AbgabetoolStudent = {
return qgate1positiv && qgate2positiv return qgate1positiv && qgate2positiv
}, },
isPastDate(date) { isPastDate(date) {
return new Date(date) < new Date(Date.now()) const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
return nowInVienna > deadline;
}, },
setDetailComponent(details){ setDetailComponent(details){
this.loading = true this.loading = true
@@ -173,8 +120,8 @@ export const AbgabetoolStudent = {
// old assumed production logic when qgates are required // old assumed production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine) // termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
// new larifari we want qgates but they are optional fhtw mode const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesOptional(pa.abgabetermine) termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes // development purposes
@@ -193,7 +140,7 @@ export const AbgabetoolStudent = {
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz) termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
termin.dateStyle = this.getDateStyleClass(termin) termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
}) })
pa.betreuer = this.buildBetreuer(pa) pa.betreuer = this.buildBetreuer(pa)
@@ -383,7 +330,7 @@ export const AbgabetoolStudent = {
</div> </div>
<Accordion :multiple="true" :activeIndex="activeTabIndex"> <Accordion :multiple="true" :activeIndex="activeTabIndex">
<template v-for="projektarbeit in projektarbeiten"> <template v-for="projektarbeit in projektarbeiten" :key="projektarbeit.projektarbeit_id">
<AccordionTab> <AccordionTab>
<template #header> <template #header>
@@ -409,11 +356,11 @@ export const AbgabetoolStudent = {
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4beurteilung') )}}</div> <div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4beurteilung') )}}</div>
<div class="col-8 col-md-9"> <div class="col-8 col-md-9">
<button v-if="projektarbeit.beurteilung1" @click="handleDownloadBeurteilung1(projektarbeit)" class="btn btn-primary"> <button v-if="projektarbeit.beurteilung1" @click="handleDownloadBeurteilung1(projektarbeit)" class="btn btn-primary">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungErstbetreuer') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a> <a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungErstbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button> </button>
<a v-else>{{$capitalize( $p.t('abgabetool/c4nobeurteilungVorhanden') )}}</a> <a v-else>{{$capitalize( $p.t('abgabetool/c4nobeurteilungVorhanden') )}}</a>
<button v-if="projektarbeit.beurteilung2" @click="handleDownloadBeurteilung2(projektarbeit)" class="btn btn-primary" style="margin-left: 4px;"> <button v-if="projektarbeit.beurteilung2" @click="handleDownloadBeurteilung2(projektarbeit)" class="btn btn-primary" style="margin-left: 4px;">
<a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungZweitbetreuer') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a> <a> {{$capitalize( $p.t('abgabetool/c4downloadBeurteilungZweitbetreuerv2') )}} <i class="fa fa-file-pdf" style="margin-left:4px; cursor: pointer;"></i></a>
</button> </button>
</div> </div>
</div> </div>
@@ -432,13 +379,13 @@ export const AbgabetoolStudent = {
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit?.betreuerart_kurzbz ? $capitalize( $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) ) : $capitalize( $p.t('abgabetool/c4betreuer') ) }}</div> <div class="col-4 col-md-3 fw-bold">{{ projektarbeit?.betreuerart_kurzbz ? $capitalize( $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) ) : $capitalize( $p.t('abgabetool/c4betreuerv2') ) }}</div>
<div class="col-8 col-md-9"> <div class="col-8 col-md-9">
{{ projektarbeit.betreuerart_kurzbz ? projektarbeit.betreuer : '' }} {{ projektarbeit.betreuerart_kurzbz ? projektarbeit.betreuer : '' }}
</div> </div>
</div> </div>
<div class="row mt-2"> <div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontakt') )}}</div> <div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
<div class="col-8 col-md-9"> <div class="col-8 col-md-9">
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a> <a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
</div> </div>
@@ -34,10 +34,10 @@ export const DeadlineOverview = {
layout: 'fitColumns', layout: 'fitColumns',
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')), placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
columns: [ columns: [
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatum'))), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), field: 'datum', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), field: 'fixterminstring', formatter: this.centeredTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabetyp'))), field: 'typ_bezeichnung', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbz'))), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), field: 'kurzbz', formatter: this.centeredTextFormatter, widthGrow: 3},
{title: Vue.computed(() => this.$capitalize(this.$p.t('person/studentIn'))), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2}, {title: Vue.computed(() => this.$capitalize(this.$p.t('person/studentIn'))), field: 'student', formatter: this.centeredTextFormatter, widthGrow: 2},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1} {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'semester', formatter: this.centeredTextFormatter, widthGrow: 1}
@@ -0,0 +1,37 @@
const zone = 'Europe/Vienna';
const today = luxon.DateTime.now().setZone(zone);
export function getDateStyleClass(termin, notenOptions) {
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
const isLate = abgabedatum && abgabedatum > datum;
// GRADE STATUS
if (termin.note) {
const opt = typeof termin.note === 'object' ? termin.note : notenOptions.find(nopt => nopt.note == termin.note)
if (opt?.positiv === true) return 'bestanden';
else if (opt?.positiv === false) 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';
}
@@ -363,6 +363,21 @@ if($result = $db->db_query("SELECT 1 FROM public.tbl_vorlage WHERE vorlage_kurzb
} }
} }
if($result = $db->db_query("SELECT 1 FROM public.tbl_vorlage WHERE vorlage_kurzbz = 'PAANoSigAssSM'"))
{
if($db->db_num_rows($result) === 0)
{
$qry = "INSERT INTO public.tbl_vorlage (vorlage_kurzbz, bezeichnung, anmerkung, mimetype)
VALUES ('PAANoSigAssSM', 'PAANoSigAssSM', null, 'text/html')
ON CONFLICT (vorlage_kurzbz) DO NOTHING;";
if(!$db->db_query($qry))
echo '<strong>system.tbl_vorlage: '.$db->db_last_error().'</strong><br>';
else
echo "<br>system.tbl_vorlage PAANoSigAssSM hinzugefuegt";
}
}
if($result = $db->db_query("SELECT 1 FROM public.tbl_vorlage WHERE vorlage_kurzbz = 'QualGateNegativ'")) if($result = $db->db_query("SELECT 1 FROM public.tbl_vorlage WHERE vorlage_kurzbz = 'QualGateNegativ'"))
{ {
if($db->db_num_rows($result) === 0) if($db->db_num_rows($result) === 0)
+246 -46
View File
@@ -10872,7 +10872,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'First Assessor', 'text' => 'First Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -11512,7 +11512,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'The second assessor’s assessment has been submitted and is part of the final grade.', 'text' => 'The second reviwer’s assessment has been submitted and is part of the final grade.',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -11692,7 +11692,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Second Assessor', 'text' => 'Second Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -11712,7 +11712,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor', 'text' => 'Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12092,7 +12092,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'To assessment of second assessor', 'text' => 'To assessment of second reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12112,7 +12112,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'The assessment of the second assessor is not available yet.', 'text' => 'The assessment of the second reviewer is not available yet.',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12192,7 +12192,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Send token to Second Assessor', 'text' => 'Send token to Second Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12310,7 +12310,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Sending only possible after completion of assessment by Second Assessor', 'text' => 'Sending only possible after completion of assessment by Second Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12550,7 +12550,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor grade', 'text' => 'Reviewer grade',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12570,7 +12570,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor', 'text' => 'Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12610,7 +12610,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor type', 'text' => 'Reviewer type',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -12630,7 +12630,7 @@ Any unusual occurrences
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'secondary assessor', 'text' => 'secondary reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -43420,7 +43420,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4betreuer', 'phrase' => 'c4betreuerv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -43431,7 +43431,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => "Assessor", 'text' => "Reviewer",
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -43700,7 +43700,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4zieldatum', 'phrase' => 'c4zieldatumv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -43711,7 +43711,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => "Target date", 'text' => "Deadline",
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -43740,7 +43740,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4abgabekurzbz', 'phrase' => 'c4abgabekurzbzv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -43751,7 +43751,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => "Short description of the submitted file", 'text' => "Short description of the submitted document",
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -44240,7 +44240,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4betreuerart', 'phrase' => 'c4betreuerartv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -44251,7 +44251,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor type', 'text' => 'Reviewer type',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45016,7 +45016,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4betreuerEmailKontakt', 'phrase' => 'c4betreuerEmailKontaktv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45027,7 +45027,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Email Assessor', 'text' => 'Email Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45056,7 +45056,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4downloadBeurteilungErstbetreuer', 'phrase' => 'c4downloadBeurteilungErstbetreuerv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45067,7 +45067,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Download evaluation of first assesor', 'text' => 'Download evaluation of first reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45076,7 +45076,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4downloadBeurteilungZweitbetreuer', 'phrase' => 'c4downloadBeurteilungZweitbetreuerv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45087,7 +45087,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Download evaluation of second assesor', 'text' => 'Download evaluation of second reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45511,7 +45511,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4erstbetreuer', 'phrase' => 'c4erstbetreuerv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45522,7 +45522,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'First Assessor', 'text' => 'First Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45531,7 +45531,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4zweitbetreuer', 'phrase' => 'c4zweitbetreuerv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45542,7 +45542,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Second Assessor', 'text' => 'Second Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45611,7 +45611,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4sendEmailBetreuerv2', 'phrase' => 'c4sendEmailBetreuerv3',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45622,7 +45622,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Send Email to {0} assessors', 'text' => 'Send Email to {0} reviewers',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45711,7 +45711,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4fehlerMailBegutachter', 'phrase' => 'c4fehlerMailBegutachterv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45722,7 +45722,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Error sending E-Mail to first Assessor!', 'text' => 'Error sending E-Mail to first Reviewer!',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -45731,7 +45731,7 @@ array(
array( array(
'app' => 'core', 'app' => 'core',
'category' => 'abgabetool', 'category' => 'abgabetool',
'phrase' => 'c4fehlerMailZweitBegutachter', 'phrase' => 'c4fehlerMailZweitBegutachterv2',
'insertvon' => 'system', 'insertvon' => 'system',
'phrases' => array( 'phrases' => array(
array( array(
@@ -45742,7 +45742,7 @@ array(
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Error sending E-Mail to second Assessor!', 'text' => 'Error sending E-Mail to second Reviewer!',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -46434,6 +46434,206 @@ array(
) )
) )
), ),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4deadlineExceeded',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Nicht rechtzeitig abgegeben!',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Deadline exceeded!',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4missingSignatureNotification',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Abgabetool: Fehlende Signatur bei Endupload',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Submission tool: Missing signature at final upload',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4erstbetreuerTitelPre',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Titel Pre',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'First Reviewer Title Pre',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4erstbetreuerVorname',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Vorname',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'First Reviewer First Name',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4erstbetreuerNachname',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Nachname',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'First Reviewer Last Name',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4erstbetreuerTitelPost',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Titel Post',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'First Reviewer Title Post',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitbetreuerTitelPre',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Titel Pre',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Second Reviewer Title Pre',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitbetreuerVorname',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Vorname',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Second Reviewer First Name',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitbetreuerNachname',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Nachname',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Second Reviewer Last Name',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitbetreuerTitelPost',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Titel Post',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Second Reviewer Title Post',
'description' => '',
'insertvon' => 'system'
)
)
),
// ABGABETOOL PHRASEN END // ABGABETOOL PHRASEN END
array( array(
'app' => 'core', 'app' => 'core',
@@ -54242,7 +54442,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'assessor', 'text' => 'reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54262,7 +54462,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Assessor', 'text' => 'Reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54282,7 +54482,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'assessor type', 'text' => 'reviewer type',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54482,7 +54682,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Deleting not possible, assessors were already assigned to this projekt work', 'text' => 'Deleting not possible, reviewers were already assigned to this projekt work',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54522,7 +54722,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Invalid project assessors', 'text' => 'Invalid project reviewers',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54542,7 +54742,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Deleting not possible, project assessor has a contract already', 'text' => 'Deleting not possible, project reviewer has a contract already',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -54982,7 +55182,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Edit assessor', 'text' => 'Edit reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -55002,7 +55202,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'Save assessor', 'text' => 'Save reviewer',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -55142,7 +55342,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'project assessor short name', 'text' => 'project reviewer short name',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )
@@ -55182,7 +55382,7 @@ I have been informed that I am under no obligation to consent to the transmissio
), ),
array( array(
'sprache' => 'English', 'sprache' => 'English',
'text' => 'This project assessor is already assigned', 'text' => 'This project reviewer is already assigned',
'description' => '', 'description' => '',
'insertvon' => 'system' 'insertvon' => 'system'
) )