mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-10-12 15:04:04 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
339b08537e | ||
|
|
cdcc88c04e | ||
|
|
6a67eb750e | ||
|
|
ef3015dff2 | ||
|
|
1b599b49ed | ||
|
|
f346bc6fcb |
@@ -22,6 +22,10 @@ use CI3_Events as Events;
|
||||
|
||||
class Abgabe extends FHCAPI_Controller
|
||||
{
|
||||
// col limits of lehre.tbl_projektarbeit, exceeding them aborts the zusatzdaten update and lead to unpublishable enduploads
|
||||
const MAX_LEN_SCHLAGWOERTER = 150; // varchar(150)
|
||||
const MAX_LEN_ABSTRACT = 5000; // text, capped to match the ui counter
|
||||
const MAX_SEITENANZAHL = 32767; // smallint
|
||||
|
||||
/**
|
||||
* Object initialization
|
||||
@@ -323,6 +327,13 @@ class Abgabe extends FHCAPI_Controller
|
||||
|
||||
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
|
||||
|
||||
// block uploads for quality gate termine that are already graded
|
||||
$this->checkPaabgabeForGradedStatus($paabgabe_id);
|
||||
|
||||
// the posted typ is client state only - verify it against the row so a stale ui cannot push an
|
||||
// endabgabe through here, which would mark the abgabe without ever writing the zusatzdaten
|
||||
$this->checkPaabgabeTypForZwischenabgabe($paabgabe_id, $paabgabetyp_kurzbz);
|
||||
|
||||
// load the $student_uid by $projektarbeit_id so we dont need any post params
|
||||
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
|
||||
$res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id);
|
||||
@@ -335,11 +346,11 @@ class Abgabe extends FHCAPI_Controller
|
||||
$data = getData($res)[0];
|
||||
$student_uid = $data->uid;
|
||||
$studiengang_kz = $data->studiengang_kz;
|
||||
|
||||
|
||||
if(!$this->checkZuordnung($projektarbeit_id, getAuthUID(), $student_uid, $studiengang_kz)) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent', [getAuthUID(), $student_uid]));
|
||||
}
|
||||
|
||||
|
||||
$path = PAABGABE_PATH.$paabgabe_id.'_'.$student_uid.'.pdf';
|
||||
|
||||
if ((isset($_FILES) and isset($_FILES['file']) and ! $_FILES['file']['error'])) {
|
||||
@@ -400,8 +411,10 @@ class Abgabe extends FHCAPI_Controller
|
||||
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
|
||||
}
|
||||
|
||||
$this->checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl);
|
||||
|
||||
$this->checkPaabgabeDeadline($paabgabe_id);
|
||||
|
||||
|
||||
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
|
||||
|
||||
// load the $student_uid by $projektarbeit_id so we dont need any post params
|
||||
@@ -450,17 +463,25 @@ class Abgabe extends FHCAPI_Controller
|
||||
$this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid);
|
||||
$signaturstatus = $paabgabe->signatur;
|
||||
|
||||
// update projektarbeit cols with zusatzdaten AND abgabedatum!
|
||||
$this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array(
|
||||
'sprache' => $sprache,
|
||||
'seitenanzahl' => $seitenanzahl,
|
||||
'abgabedatum' => date('Y-m-d'),
|
||||
'schlagwoerter_en' => $schlagwoerter_en,
|
||||
'schlagwoerter' => $schlagwoerter,
|
||||
'abstract' => $abstract,
|
||||
'abstract_en' => $abstract_en
|
||||
));
|
||||
// update projektarbeit cols with zusatzdaten AND abgabedatum!
|
||||
$updateRes = $this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array(
|
||||
'sprache' => $sprache,
|
||||
'seitenanzahl' => $seitenanzahl,
|
||||
'abgabedatum' => date('Y-m-d'),
|
||||
'schlagwoerter_en' => $schlagwoerter_en,
|
||||
'schlagwoerter' => $schlagwoerter,
|
||||
'abstract' => $abstract,
|
||||
'abstract_en' => $abstract_en
|
||||
));
|
||||
|
||||
// abort before marking the abgabe - a failed update used to leave the projektarbeit without
|
||||
// abgabedatum and zusatzdaten while the endupload counted as done, blocking the publication
|
||||
if(isError($updateRes)) {
|
||||
$this->logLib->logInfoDB(array('endupload projektarbeit update failed', getError($updateRes),
|
||||
$projektarbeit_id, $paabgabe_id, getAuthUID(), getAuthPersonId()));
|
||||
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4enduploadZusatzdatenSpeichernFehlgeschlagen'), 'general');
|
||||
}
|
||||
|
||||
// update paabgabe datum
|
||||
$res = $this->PaabgabeModel->update($paabgabe_id, array(
|
||||
@@ -947,6 +968,16 @@ class Abgabe extends FHCAPI_Controller
|
||||
$paabgabeArr = $this->getDataOrTerminateWithError($result, 'general');
|
||||
$paabgabe = $paabgabeArr[0];
|
||||
|
||||
// resolve the insert/update user full names so the frontend info string ("saved at X by Y")
|
||||
// renders immediately for freshly created/updated termine without needing a reload.
|
||||
$this->load->model('person/Person_model', 'PersonModel');
|
||||
if(!empty($paabgabe->insertvon)) {
|
||||
$paabgabe->insertvon_fullname = getData($this->PersonModel->getFullName($paabgabe->insertvon));
|
||||
}
|
||||
if(!empty($paabgabe->updatevon)) {
|
||||
$paabgabe->updatevon_fullname = getData($this->PersonModel->getFullName($paabgabe->updatevon));
|
||||
}
|
||||
|
||||
// check if abgabe even has note
|
||||
if($paabgabe->note) {
|
||||
$this->load->model('education/Note_model', 'NoteModel');
|
||||
@@ -1057,6 +1088,22 @@ class Abgabe extends FHCAPI_Controller
|
||||
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
|
||||
}
|
||||
|
||||
// a termin with an upload or a note must keep its typ and datum.
|
||||
// the table only hides those rows client side, so a student upload
|
||||
// after the table was loaded would otherwise still let a zwischenabgabe be retyped to 'end'
|
||||
$existingPaabgabe = $paabgabeArr[0];
|
||||
|
||||
if($existingPaabgabe->note !== null || $existingPaabgabe->abgabedatum !== null) {
|
||||
if(isset($updateFields['paabgabetyp_kurzbz'])
|
||||
&& $updateFields['paabgabetyp_kurzbz'] !== $existingPaabgabe->paabgabetyp_kurzbz) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4abgabetypAendernNichtErlaubt'));
|
||||
}
|
||||
|
||||
if(isset($updateFields['datum']) && $updateFields['datum'] !== $existingPaabgabe->datum) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4datumAendernNichtErlaubt'));
|
||||
}
|
||||
}
|
||||
|
||||
$result = $this->PaabgabeModel->update(
|
||||
$paabgabe_id,
|
||||
array_merge($updateFields, [
|
||||
@@ -1570,7 +1617,9 @@ class Abgabe extends FHCAPI_Controller
|
||||
|
||||
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
|
||||
}
|
||||
|
||||
|
||||
$this->checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl);
|
||||
|
||||
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
|
||||
|
||||
|
||||
@@ -1605,7 +1654,7 @@ class Abgabe extends FHCAPI_Controller
|
||||
}
|
||||
|
||||
// update projektarbeit cols with zusatzdaten only
|
||||
$this->ProjektarbeitModel->update($projektarbeit_id, array(
|
||||
$updateRes = $this->ProjektarbeitModel->update($projektarbeit_id, array(
|
||||
'sprache' => $sprache,
|
||||
'seitenanzahl' => $seitenanzahl,
|
||||
'schlagwoerter_en' => $schlagwoerter_en,
|
||||
@@ -1614,6 +1663,9 @@ class Abgabe extends FHCAPI_Controller
|
||||
'abstract_en' => $abstract_en
|
||||
));
|
||||
|
||||
// dont report success on a failed update, otherwise a repair attempt silently changes nothing
|
||||
$this->getDataOrTerminateWithError($updateRes, 'general');
|
||||
|
||||
$this->logLib->logInfoDB(array('zusatzdatenEditMitarbeiter', array(
|
||||
'updatevon' => getAuthUID(),
|
||||
'updateamum' => date('Y-m-d H:i:s')
|
||||
@@ -1996,13 +2048,59 @@ class Abgabe extends FHCAPI_Controller
|
||||
|
||||
$data = getData($res)[0];
|
||||
if($data->note !== NULL) {
|
||||
// hardcode this error msg cause phrasen arent reliable and people keep bugging why the cant edit old entries they definitely shouldnt update
|
||||
$message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv3');
|
||||
if(strpos($message, "<<") === 0) { // phrase could not be loaded
|
||||
$this->terminateWithError('Die Projektarbeit wurde bereits benotet, Sie dürfen deshalb keine weiteren Termine anlegen oder bearbeiten.', 'general');
|
||||
} else {
|
||||
$this->terminateWithError($message);
|
||||
}
|
||||
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv3'));
|
||||
}
|
||||
}
|
||||
|
||||
// validates the zusatzdaten against the column limits of lehre.tbl_projektarbeit.
|
||||
private function checkZusatzdatenLengths($schlagwoerter, $schlagwoerter_en, $abstract, $abstract_en, $seitenanzahl) {
|
||||
if(mb_strlen((string)$schlagwoerter) > self::MAX_LEN_SCHLAGWOERTER
|
||||
|| mb_strlen((string)$schlagwoerter_en) > self::MAX_LEN_SCHLAGWOERTER) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4schlagwoerterZuLang', array(self::MAX_LEN_SCHLAGWOERTER)), 'general');
|
||||
}
|
||||
|
||||
if(mb_strlen((string)$abstract) > self::MAX_LEN_ABSTRACT
|
||||
|| mb_strlen((string)$abstract_en) > self::MAX_LEN_ABSTRACT) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4abstractZuLang', array(self::MAX_LEN_ABSTRACT)), 'general');
|
||||
}
|
||||
|
||||
if(!is_numeric($seitenanzahl) || (int)$seitenanzahl < 1 || (int)$seitenanzahl > self::MAX_SEITENANZAHL) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4seitenanzahlUngueltig', array(self::MAX_SEITENANZAHL)), 'general');
|
||||
}
|
||||
}
|
||||
|
||||
// zwischenabgabe uploads must never touch an endabgabe termin, to avoid empty zusatzdaten
|
||||
private function checkPaabgabeTypForZwischenabgabe($paabgabe_id, $posted_paabgabetyp_kurzbz) {
|
||||
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
|
||||
$res = $this->PaabgabeModel->load($paabgabe_id);
|
||||
|
||||
if(isError($res) || !hasData($res)) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general');
|
||||
}
|
||||
|
||||
$paabgabe = getData($res)[0];
|
||||
|
||||
if($paabgabe->paabgabetyp_kurzbz === 'end') {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4enduploadUeberZwischenabgabeNichtErlaubt'), 'general');
|
||||
}
|
||||
|
||||
// typ changed between page load and upload -> the client would use the wrong upload path
|
||||
if($paabgabe->paabgabetyp_kurzbz !== $posted_paabgabetyp_kurzbz) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4abgabeterminGeaendertNeuLaden'), 'general');
|
||||
}
|
||||
}
|
||||
|
||||
private function checkPaabgabeForGradedStatus($paabgabe_id) {
|
||||
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
|
||||
$res = $this->PaabgabeModel->load($paabgabe_id);
|
||||
|
||||
if(isError($res) || !hasData($res)) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general');
|
||||
}
|
||||
|
||||
$paabgabe = getData($res)[0];
|
||||
if($paabgabe->note !== NULL) {
|
||||
$this->terminateWithError($this->p->t('abgabetool', 'c4studentAbgabeNotAllowedRegularv3'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,38 @@ class AbgabetoolJob extends JOB_Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* helper to tell apart an actually new/edited abgabetermin from an upload or a benotung, since all
|
||||
* three write updatevon/updateamum on the same paabgabe row and the row itself keeps no change history.
|
||||
* uploads and benotungen have their own notifications, reporting them as changed termine is what made
|
||||
* betreuer/assistenz receive "Termine hinzugefügt oder bearbeitet" after a student uploaded a file.
|
||||
*/
|
||||
private function isNewOrEditedTermin($abgabe) {
|
||||
// never updated, so the termin itself was created within the interval
|
||||
if (empty($abgabe->updatevon)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// the student can only ever write abgabedatum by uploading -> notifyBetreuerMail/notifyStudentMail report those
|
||||
if ($abgabe->student_uid === $abgabe->updatevon) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// upload done by assistenz/betreuer on behalf of the student: abgabedatum is written on the day of the update
|
||||
if (!empty($abgabe->abgabedatum) && !empty($abgabe->updateamum)
|
||||
&& substr($abgabe->abgabedatum, 0, 10) === substr($abgabe->updateamum, 0, 10)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// typ and datum of a benoteter termin are locked (see Abgabe->postProjektarbeitAbgabe), so an update
|
||||
// on it is either the benotung itself or a cosmetic edit - neither is a termin change
|
||||
if ($abgabe->note !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function notifyAssistenzAboutChangedAbgaben() {
|
||||
|
||||
$this->_ci->logInfo('Start job FHC-Core->notifyAssistenzAboutChangedAbgaben');
|
||||
@@ -298,7 +330,7 @@ class AbgabetoolJob extends JOB_Controller
|
||||
$relevantTypes = $this->_ci->config->item('RELEVANT_PAABGABETYPEN_SAMMELMAIL_ASSISTENZ');
|
||||
// get all new or changed termine in interval
|
||||
$result = $this->_ci->PaabgabeModel->findAbgabenNewOrUpdatedSince($interval, $relevantTypes);
|
||||
|
||||
|
||||
$retval = getData($result);
|
||||
|
||||
if(count($retval) == 0) {
|
||||
@@ -314,7 +346,12 @@ class AbgabetoolJob extends JOB_Controller
|
||||
if (isset($newOrChangedAbgabe->projektarbeit_id)) {
|
||||
$projektarbeitId = $newOrChangedAbgabe->projektarbeit_id;
|
||||
|
||||
// If the 'projektarbeit_id' is not yet a key in $projektarbeiten,
|
||||
// uploads and benotungen are no termin changes, they have their own notifications
|
||||
if (!$this->isNewOrEditedTermin($newOrChangedAbgabe)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the 'projektarbeit_id' is not yet a key in $projektarbeiten,
|
||||
// initialize it as an empty array.
|
||||
if (!isset($projektarbeiten[$projektarbeitId])) {
|
||||
$projektarbeiten[$projektarbeitId] = [];
|
||||
@@ -325,6 +362,11 @@ class AbgabetoolJob extends JOB_Controller
|
||||
}
|
||||
}
|
||||
|
||||
if(count($projektarbeiten) == 0) {
|
||||
$this->_ci->logInfo("Keine Emails an Assistenzen über neue oder veränderte Termine versandt");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -515,14 +557,12 @@ class AbgabetoolJob extends JOB_Controller
|
||||
if (isset($newOrChangedAbgabe->projektarbeit_id)) {
|
||||
$projektarbeitId = $newOrChangedAbgabe->projektarbeit_id;
|
||||
|
||||
// check if the updatevon field is NOT the same as the student the projektarbeit is assigned to
|
||||
// since uploading a file to a paabgabe is also putting updateamum & updatevon
|
||||
// we have our own "student has uploaded a file" emailjob anyways
|
||||
if($newOrChangedAbgabe->student_uid === $newOrChangedAbgabe->updatevon) {
|
||||
// uploads and benotungen are no termin changes, they have their own notifications
|
||||
if (!$this->isNewOrEditedTermin($newOrChangedAbgabe)) {
|
||||
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.
|
||||
if (!isset($projektarbeiten[$projektarbeitId])) {
|
||||
$projektarbeiten[$projektarbeitId] = [];
|
||||
@@ -720,61 +760,112 @@ class AbgabetoolJob extends JOB_Controller
|
||||
forEach ($betreuer_uids as $person_id => $abgaben) {
|
||||
// $person_id is from betreuer
|
||||
|
||||
$result = $this->_ci->ProjektarbeitModel->getProjektbetreuerAnrede($person_id);
|
||||
$data = getData($result)[0];
|
||||
// resolve the recipient by the person_id the mail was grouped by. resolving it by
|
||||
// projektarbeit_id + betreuerart of the first abgabe addressed the wrong person as soon as a
|
||||
// projektarbeit had two betreuer of the same art
|
||||
$result = $this->_ci->ProjektbetreuerModel->getBetreuerByPersonId($person_id);
|
||||
$betreuerArr = getData($result);
|
||||
|
||||
$anrede = $data->anrede;
|
||||
$anredeFillString = $data->anrede == "Herr" ? "r" : "";
|
||||
$fullFormattedNameString = $data->first;
|
||||
|
||||
// sorting $abgaben array by datum
|
||||
usort($abgaben, function ($a, $b) {
|
||||
return strtotime($a->datum) <=> strtotime($b->datum);
|
||||
});
|
||||
|
||||
$projektarbeit_titel = $abgaben[0]->titel;
|
||||
|
||||
// initialize the table and headers
|
||||
$abgabenString = '
|
||||
<table style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif; color: #333; margin-top: 15px; margin-bottom: 15px;">
|
||||
<thead>
|
||||
<tr style="background-color: #f2f2f2; text-align: left;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 15%;">Zieldatum</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 25%;">Studierende/r</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px;">Bezeichnung</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 15%;">Abgabedatum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($abgaben as $abgabe) {
|
||||
// format the student name
|
||||
$nameParts = [];
|
||||
if (!empty($abgabe->titelpre)) $nameParts[] = $abgabe->titelpre;
|
||||
$nameParts[] = $abgabe->vorname;
|
||||
$nameParts[] = $abgabe->nachname;
|
||||
if (!empty($abgabe->titelpost)) $nameParts[] = $abgabe->titelpost;
|
||||
$studentFullName = implode(' ', $nameParts);
|
||||
|
||||
// format dates inline
|
||||
$dateEmailFormatted = (new DateTime($abgabe->datum))->format('d.m.Y');
|
||||
$abgabedatumFormatted = (new DateTime($abgabe->abgabedatum))->format('d.m.Y');
|
||||
|
||||
// handle the optional Kurzbezeichnung
|
||||
$kurzbzLine = !empty($abgabe->kurzbz) ? "<br/><small style='color: #666; font-style: italic;'>{$abgabe->kurzbz}</small>" : "";
|
||||
|
||||
$abgabenString .= "
|
||||
<tr>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$dateEmailFormatted}</td>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$studentFullName}</td>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px;'>
|
||||
<strong>{$abgabe->bezeichnung}</strong>{$kurzbzLine}
|
||||
</td>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$abgabedatumFormatted}</td>
|
||||
</tr>";
|
||||
if(!$betreuerArr) {
|
||||
$this->_ci->logInfo('Could not load Betreuer PersonID: "'.$person_id.'".');
|
||||
continue;
|
||||
}
|
||||
|
||||
$abgabenString .= '</tbody></table>';
|
||||
$betreuer = $betreuerArr[0];
|
||||
|
||||
$email = $betreuer->uid ? $betreuer->uid."@".DOMAIN : $betreuer->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: "'.$person_id.'".');
|
||||
continue;
|
||||
}
|
||||
|
||||
$anrede = $betreuer->anrede;
|
||||
$anredeFillString = $betreuer->anrede == "Herr" ? "r" : "";
|
||||
$fullFormattedNameString = $betreuer->first;
|
||||
|
||||
// a betreuer can supervise several projektarbeiten, so group the uploads and give each one its
|
||||
// own section instead of printing one projektarbeit title above uploads of all of them
|
||||
$abgabenProProjektarbeit = [];
|
||||
forEach($abgaben as $abgabe) {
|
||||
$abgabenProProjektarbeit[$abgabe->projektarbeit_id][] = $abgabe;
|
||||
}
|
||||
|
||||
// start the container
|
||||
$abgabenString = '<div style="font-family: Arial, sans-serif; color: #333;">';
|
||||
|
||||
forEach($abgabenProProjektarbeit as $projektarbeit_id => $projektarbeitAbgaben) {
|
||||
|
||||
// sorting $projektarbeitAbgaben array by datum
|
||||
usort($projektarbeitAbgaben, function ($a, $b) {
|
||||
return strtotime($a->datum) <=> strtotime($b->datum);
|
||||
});
|
||||
|
||||
// format the student name
|
||||
$s = $projektarbeitAbgaben[0];
|
||||
$nameParts = [];
|
||||
if (!empty($s->titelpre)) $nameParts[] = $s->titelpre;
|
||||
$nameParts[] = $s->vorname;
|
||||
$nameParts[] = $s->nachname;
|
||||
if (!empty($s->titelpost)) $nameParts[] = $s->titelpost;
|
||||
$studentFullName = implode(' ', $nameParts);
|
||||
|
||||
$projektarbeit_titel = $s->titel ?? 'Kein Titel vergeben';
|
||||
|
||||
// project header section
|
||||
$abgabenString .= "
|
||||
<div style='margin-top: 25px; padding: 12px; background-color: #f8f9fa; border-left: 4px solid #007bff; border-bottom: 1px solid #eee;'>
|
||||
<strong style='font-size: 16px; color: #0056b3;'>Projekt: {$projektarbeit_titel}</strong><br/>
|
||||
<div style='margin-top: 5px; font-size: 14px;'>
|
||||
<strong>Studierende/r:</strong> {$studentFullName}
|
||||
</div>
|
||||
<span style='color: #666; font-size: 12px;'>
|
||||
ID: {$projektarbeit_id} | Rolle: {$s->betreuerart_kurzbz} |
|
||||
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: #eee; text-align: left;">
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 20%;">Zieldatum</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px;">Bezeichnung</th>
|
||||
<th style="padding: 10px; border: 1px solid #ddd; font-size: 13px; width: 20%;">Abgabedatum</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>';
|
||||
|
||||
foreach ($projektarbeitAbgaben as $abgabe) {
|
||||
// format dates inline
|
||||
$dateEmailFormatted = (new DateTime($abgabe->datum))->format('d.m.Y');
|
||||
$abgabedatumFormatted = (new DateTime($abgabe->abgabedatum))->format('d.m.Y');
|
||||
|
||||
// handle the optional Kurzbezeichnung
|
||||
$kurzbzLine = !empty($abgabe->kurzbz) ? "<br/><small style='color: #666; font-style: italic;'>{$abgabe->kurzbz}</small>" : "";
|
||||
|
||||
$abgabenString .= "
|
||||
<tr>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$dateEmailFormatted}</td>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px;'>
|
||||
<strong>{$abgabe->bezeichnung}</strong>{$kurzbzLine}
|
||||
</td>
|
||||
<td style='padding: 10px; border: 1px solid #ddd; font-size: 13px; vertical-align: top;'>{$abgabedatumFormatted}</td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
$abgabenString .= '</tbody></table>';
|
||||
}
|
||||
|
||||
// close container
|
||||
$abgabenString .= '</div>';
|
||||
|
||||
// the template prints one projektarbeit title, which only holds when the mail covers a single one
|
||||
$paTitel = count($abgabenProProjektarbeit) === 1
|
||||
? ($abgaben[0]->titel ?? 'Kein Titel vergeben')
|
||||
: $this->p->t('abgabetool', 'c4mehrereProjektarbeiten');
|
||||
|
||||
$path = $this->_ci->config->item('URL_MITARBEITER');
|
||||
$url = CIS_ROOT.$path;
|
||||
@@ -783,28 +874,17 @@ class AbgabetoolJob extends JOB_Controller
|
||||
'anrede' => $anrede,
|
||||
'anredeFillString' => $anredeFillString,
|
||||
'fullFormattedNameString' => $fullFormattedNameString,
|
||||
'paTitel' => $projektarbeit_titel,
|
||||
'paTitel' => $paTitel,
|
||||
'abgabenString' => $abgabenString,
|
||||
'linkAbgabetool' => $url
|
||||
);
|
||||
|
||||
$result = $this->_ci->ProjektbetreuerModel->getBetreuerOfProjektarbeit($abgaben[0]->projektarbeit_id, $abgaben[0]->betreuerart_kurzbz);
|
||||
$data = getData($result)[0];
|
||||
|
||||
$email = $data->uid ? $data->uid."@".DOMAIN : $data->private_email;
|
||||
|
||||
// in rare cases there are betreuer (often zweitbetreuer) without uid and without private email
|
||||
if(!$email) {
|
||||
$this->_ci->logInfo('Could not send Email for Betreuer PersonID: "'.$data->person_id.'".');
|
||||
continue;
|
||||
}
|
||||
|
||||
// send email with bundled info
|
||||
sendSanchoMail(
|
||||
'PaabgabeUpdatesBetSM',
|
||||
$body_fields,
|
||||
$email,
|
||||
$this->p->t('abgabetool', 'changedAbgabeterminev2')
|
||||
$this->p->t('abgabetool', 'c4newUploadsNotification')
|
||||
);
|
||||
|
||||
$count++;
|
||||
|
||||
@@ -65,7 +65,8 @@ class Paabgabe_model extends DB_Model
|
||||
|
||||
$query = "SELECT projektarbeit_id, paabgabe_id, paabgabetyp_kurzbz, fixtermin, datum, campus.tbl_paabgabe.kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum,
|
||||
campus.tbl_paabgabe.insertvon, campus.tbl_paabgabe.insertamum, campus.tbl_paabgabe.updatevon, campus.tbl_paabgabe.updateamum,
|
||||
campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid, tbl_projektarbeit.note, lehre.tbl_projektarbeit.titel,
|
||||
campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid,
|
||||
lehre.tbl_projektarbeit.note as 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_projektarbeit.studiensemester_kurzbz,
|
||||
public.tbl_person.anrede, public.tbl_person.titelpre, public.tbl_person.vorname, public.tbl_person.nachname, public.tbl_person.titelpost
|
||||
@@ -89,7 +90,8 @@ class Paabgabe_model extends DB_Model
|
||||
$queryParams = [$interval];
|
||||
$query = "SELECT projektarbeit_id, paabgabe_id, paabgabetyp_kurzbz, fixtermin, datum, campus.tbl_paabgabe.kurzbz, campus.tbl_paabgabetyp.bezeichnung, campus.tbl_paabgabe.abgabedatum,
|
||||
campus.tbl_paabgabe.insertvon, campus.tbl_paabgabe.insertamum, campus.tbl_paabgabe.updatevon, campus.tbl_paabgabe.updateamum,
|
||||
campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid, tbl_projektarbeit.note, lehre.tbl_projektarbeit.titel,
|
||||
campus.tbl_paabgabe.note, upload_allowed, beurteilungsnotiz, student_uid,
|
||||
lehre.tbl_projektarbeit.note as 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_projektarbeit.studiensemester_kurzbz,
|
||||
lehre.tbl_projektbetreuer.betreuerart_kurzbz, lehre.tbl_projektbetreuer.person_id,
|
||||
|
||||
@@ -269,4 +269,37 @@ class Projektbetreuer_model extends DB_Model
|
||||
return $this->execQuery($qry, array($projektarbeit_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets anrede, name and mail address of a Betreuer by person_id.
|
||||
* Same account preference as getAllBetreuerOfProjektarbeit, but not bound to a single Projektarbeit.
|
||||
* @param int $person_id
|
||||
* @return array success with one row or error
|
||||
*/
|
||||
public function getBetreuerByPersonId($person_id)
|
||||
{
|
||||
$qry = "SELECT DISTINCT ON (pers.person_id) pers.person_id, anrede,
|
||||
trim(COALESCE(titelpre,'')||' '||COALESCE(vorname,'')||' '||COALESCE(nachname,'')||' '||COALESCE(titelpost,'')) as first,
|
||||
ben.uid,
|
||||
(
|
||||
SELECT kontakt
|
||||
FROM public.tbl_kontakt
|
||||
WHERE kontakttyp = 'email'
|
||||
AND person_id = pers.person_id
|
||||
ORDER BY
|
||||
CASE WHEN zustellung THEN 0 ELSE 1 END,
|
||||
insertamum DESC NULLS LAST
|
||||
LIMIT 1
|
||||
) AS private_email
|
||||
FROM public.tbl_person pers
|
||||
LEFT JOIN public.tbl_benutzer ben USING (person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ma ON ben.uid = ma.mitarbeiter_uid
|
||||
WHERE (ben.aktiv OR ben.aktiv IS NULL)
|
||||
AND pers.person_id = ?
|
||||
ORDER BY pers.person_id, CASE WHEN ma.mitarbeiter_uid IS NULL THEN 1 ELSE 0 END, /*Mitarbeiter account first*/
|
||||
CASE WHEN ben.uid IS NULL THEN 1 ELSE 0 END, /*user with account first*/
|
||||
ben.insertamum";
|
||||
|
||||
return $this->execReadOnlyQuery($qry, array($person_id));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -442,7 +442,12 @@ export const AbgabeMitarbeiterDetail = {
|
||||
return true
|
||||
},
|
||||
allowedToSaveZusatzdaten() {
|
||||
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
|
||||
// upper bounds mirror the column limits of lehre.tbl_projektarbeit, see Abgabe.php
|
||||
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter.length <= 150
|
||||
&& this.form.schlagwoerter_en.length > 0 && this.form.schlagwoerter_en.length <= 150
|
||||
&& this.form.abstract.length > 0 && this.form.abstract.length <= 5000
|
||||
&& this.form.abstract_en.length > 0 && this.form.abstract_en.length <= 5000
|
||||
&& this.form.seitenanzahl > 0 && this.form.seitenanzahl <= 32767
|
||||
},
|
||||
getAllowedAbgabeTypeOptions() {
|
||||
if(this.assistenzMode) {
|
||||
@@ -1003,14 +1008,16 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
|
||||
<Textarea v-model="form.schlagwoerter" maxlength="150" class="w-100"></Textarea>
|
||||
<p>{{ form.schlagwoerter?.length ? form.schlagwoerter.length : 0 }} / 150 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
|
||||
<Textarea v-model="form.schlagwoerter_en" maxlength="150" class="w-100"></Textarea>
|
||||
<p>{{ form.schlagwoerter_en?.length ? form.schlagwoerter_en.length : 0 }} / 150 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1035,7 +1042,7 @@ export const AbgabeMitarbeiterDetail = {
|
||||
<div class="row">
|
||||
<InputNumber
|
||||
v-model="form.seitenanzahl"
|
||||
inputId="seitenanzahlInput" :min="1" :max="100000">
|
||||
inputId="seitenanzahlInput" :min="1" :max="32767">
|
||||
</InputNumber>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -258,7 +258,9 @@ export const AbgabeStudentDetail = {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
|
||||
// use the field from the backend, not the client side mapped bezeichnung object - a failed
|
||||
// lookup there would silently route an endupload through the zwischenabgabe endpoint
|
||||
if(termin.paabgabetyp_kurzbz === 'end') {
|
||||
this.enduploadTermin = termin
|
||||
this.$refs.modalContainerEnduploadZusatzdaten.show()
|
||||
} else {
|
||||
@@ -306,6 +308,22 @@ export const AbgabeStudentDetail = {
|
||||
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
|
||||
return noteOpt ? noteOpt.bezeichnung : ''
|
||||
},
|
||||
hasZweitbetreuer(projektarbeit) {
|
||||
return !!(projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer)
|
||||
},
|
||||
getErstbetreuerLabel(projektarbeit) {
|
||||
return projektarbeit.betreuerart_kurzbz
|
||||
? this.$capitalize(this.$p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz))
|
||||
: this.$capitalize(this.$p.t('abgabetool/c4betreuerv2'))
|
||||
},
|
||||
getZweitbetreuerLabel(projektarbeit) {
|
||||
return projektarbeit.zweitbetreuer_betreuerart_kurzbz
|
||||
? this.$capitalize(this.$p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz))
|
||||
: this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))
|
||||
},
|
||||
getZweitbetreuerName(projektarbeit) {
|
||||
return projektarbeit.zweitbetreuer?.first ?? ''
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
projektarbeit(newVal) {
|
||||
@@ -349,7 +367,12 @@ export const AbgabeStudentDetail = {
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4eidesstattlicheErklaerung'))
|
||||
},
|
||||
allowedToSaveZusatzdaten() {
|
||||
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter_en.length > 0 && this.form.abstract.length > 0 && this.form.abstract_en.length > 0 && this.form.seitenanzahl > 0
|
||||
// upper bounds mirror the column limits of lehre.tbl_projektarbeit, see Abgabe.php
|
||||
return this.form.schlagwoerter.length > 0 && this.form.schlagwoerter.length <= 150
|
||||
&& this.form.schlagwoerter_en.length > 0 && this.form.schlagwoerter_en.length <= 150
|
||||
&& this.form.abstract.length > 0 && this.form.abstract.length <= 5000
|
||||
&& this.form.abstract_en.length > 0 && this.form.abstract_en.length <= 5000
|
||||
&& this.form.seitenanzahl > 0 && this.form.seitenanzahl <= 32767
|
||||
},
|
||||
getAllowedToSendEndupload() {
|
||||
return this.eidAkzeptiert && this.allowedToSaveZusatzdaten
|
||||
@@ -392,7 +415,7 @@ export const AbgabeStudentDetail = {
|
||||
if(this.isViewMode) {
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" }
|
||||
} else {
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegularv2')), class: "custom-tooltip" }
|
||||
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegularv3')), class: "custom-tooltip" }
|
||||
}
|
||||
},
|
||||
getTooltipBeurteilungerforderlich() {
|
||||
@@ -430,7 +453,8 @@ export const AbgabeStudentDetail = {
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
|
||||
<p>{{ getErstbetreuerLabel(projektarbeit) }}: {{ projektarbeit.betreuer }}</p>
|
||||
<p v-if="hasZweitbetreuer(projektarbeit)">{{ getZweitbetreuerLabel(projektarbeit) }}: {{ getZweitbetreuerName(projektarbeit) }}</p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="row">
|
||||
@@ -716,14 +740,16 @@ export const AbgabeStudentDetail = {
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter" class="w-100"></Textarea>
|
||||
<Textarea v-model="form.schlagwoerter" maxlength="150" class="w-100"></Textarea>
|
||||
<p>{{ form.schlagwoerter?.length ? form.schlagwoerter.length : 0 }} / 150 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row mb-3 align-items-start">
|
||||
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterEng') )}}</div>
|
||||
<div class="row">
|
||||
<Textarea v-model="form.schlagwoerter_en" class="w-100"></Textarea>
|
||||
<Textarea v-model="form.schlagwoerter_en" maxlength="150" class="w-100"></Textarea>
|
||||
<p>{{ form.schlagwoerter_en?.length ? form.schlagwoerter_en.length : 0 }} / 150 characters</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -748,7 +774,7 @@ export const AbgabeStudentDetail = {
|
||||
<div class="row">
|
||||
<InputNumber
|
||||
v-model="form.seitenanzahl"
|
||||
inputId="seitenanzahlInput" :min="1" :max="100000">
|
||||
inputId="seitenanzahlInput" :min="1" :max="32767">
|
||||
</InputNumber>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,12 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import { getDateStyleClass } from "./getDateStyleClass.js";
|
||||
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
|
||||
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
|
||||
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
|
||||
import { getViennaTodayISO, toViennaDate } from "./dateUtils.js";
|
||||
import AbgabetoolTableMixin from "./AbgabetoolTableMixin.js";
|
||||
|
||||
export const AbgabetoolMitarbeiter = {
|
||||
name: "AbgabetoolMitarbeiter",
|
||||
mixins: [AbgabetoolTableMixin],
|
||||
components: {
|
||||
BsModal,
|
||||
CoreFilterCmpt,
|
||||
@@ -38,8 +40,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
count: 0,
|
||||
filteredcount: 0,
|
||||
selectedcount: 0,
|
||||
qgate1FilterSelected: [],
|
||||
qgate2FilterSelected: [],
|
||||
// restore flags of the table, filled by initTablePersistence
|
||||
tableState: {},
|
||||
abgabetypenBetreuer: null,
|
||||
detailIsFullscreen: false,
|
||||
phrasenPromise: null,
|
||||
@@ -145,7 +147,12 @@ export const AbgabetoolMitarbeiter = {
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez',
|
||||
headerFilter: this.notenHeaderFilterEditor,
|
||||
headerFilterFunc: this.notenHeaderFilterFunc,
|
||||
headerFilterParams: {},
|
||||
headerFilterFuncParams: { idField: 'note' }, // column shows the bezeichnung, filter compares the note id
|
||||
visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
@@ -265,11 +272,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
notenSorter(a, b, aRow, bRow, column, dir, sorterParams) {
|
||||
const aData = aRow.getData()
|
||||
const bData = bRow.getData()
|
||||
return aData.note - bData.note
|
||||
},
|
||||
async openBenotung(type, link) {
|
||||
if(type === 'new') {
|
||||
window.open(link, '_blank')
|
||||
@@ -360,268 +362,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
if(res.meta.status == 'success') this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4zweitBegutachterTokenMailSuccess'))
|
||||
})
|
||||
},
|
||||
getDateStyleHtml(dateStyle) {
|
||||
const iconMap = {
|
||||
'verspaetet': '<i class="fa-solid fa-triangle-exclamation"></i>',
|
||||
'verpasst': '<i class="fa-solid fa-calendar-xmark"></i>',
|
||||
'abzugeben': '<i class="fa-solid fa-hourglass-half"></i>',
|
||||
'standard': '<i class="fa-solid fa-clock"></i>',
|
||||
'abgegeben': '<i class="fa-solid fa-paperclip"></i>',
|
||||
'beurteilungerforderlich': '<i class="fa-solid fa-list-check"></i>',
|
||||
'bestanden': '<i class="fa-solid fa-check"></i>',
|
||||
'nichtbestanden': '<i class="fa-solid fa-circle-exclamation"></i>',
|
||||
};
|
||||
return iconMap[dateStyle] ?? '';
|
||||
},
|
||||
statusHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
|
||||
const options = [
|
||||
{ label: this.$p.t('abgabetool/c4positivBenotet'), value: 'bestanden', dateStyle: 'bestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4negativBenotet'), value: 'nichtbestanden', dateStyle: 'nichtbestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerspaetet'), value: 'verspaetet', dateStyle: 'verspaetet' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerpasst'), value: 'verpasst', dateStyle: 'verpasst' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbzugeben'), value: 'abzugeben', dateStyle: 'abzugeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbgegeben'), value: 'abgegeben', dateStyle: 'abgegeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich'), value: 'beurteilungerforderlich', dateStyle: 'beurteilungerforderlich' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipStandardv2'), value: 'standard', dateStyle: 'standard' },
|
||||
];
|
||||
|
||||
const field = cell.getField();
|
||||
const stateKey = field + 'FilterSelected'; // e.g. dateStyleFilterSelected
|
||||
let selected = [...(this[stateKey] || [])];
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'position: relative; width: 100%;';
|
||||
|
||||
const display = document.createElement('input');
|
||||
display.readOnly = true;
|
||||
display.placeholder = '';
|
||||
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 220px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
|
||||
|
||||
const updateDisplay = () => {
|
||||
display.value = options
|
||||
.filter(o => selected.includes(o.value))
|
||||
.map(o => o.label)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = 'display: flex; align-items: center; gap: 0; cursor: pointer; white-space: nowrap; padding-right: 8px;';
|
||||
row.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = opt.value;
|
||||
cb.checked = selected.includes(opt.value);
|
||||
cb.style.cssText = 'margin: 0 6px;';
|
||||
cb.addEventListener('change', () => {
|
||||
selected = cb.checked
|
||||
? [...selected, opt.value]
|
||||
: selected.filter(v => v !== opt.value);
|
||||
this[stateKey] = [...selected];
|
||||
updateDisplay();
|
||||
success([...selected]);
|
||||
});
|
||||
|
||||
// icon badge — same look as cell
|
||||
const badge = document.createElement('div');
|
||||
badge.className = opt.dateStyle + '-header';
|
||||
badge.style.cssText = `min-width: 36px; height: 36px; display: flex; align-items: center;
|
||||
justify-content: center; flex-shrink: 0; padding: 0px 17px 0px 17px;`;
|
||||
badge.innerHTML = this.getDateStyleHtml(opt.dateStyle);
|
||||
|
||||
const labelText = document.createElement('span');
|
||||
labelText.textContent = opt.label;
|
||||
labelText.style.cssText = 'margin-left: 6px;';
|
||||
|
||||
row.appendChild(cb);
|
||||
row.appendChild(badge);
|
||||
row.appendChild(labelText);
|
||||
dropdown.appendChild(row);
|
||||
});
|
||||
|
||||
updateDisplay();
|
||||
|
||||
display.addEventListener('click', () => {
|
||||
if (dropdown.style.display === 'none') {
|
||||
const rect = display.getBoundingClientRect();
|
||||
dropdown.style.top = rect.bottom + 'px';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
display.addEventListener('blur', () => {
|
||||
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
wrapper.appendChild(display);
|
||||
cell.getElement().addEventListener('remove', () => dropdown.remove());
|
||||
onRendered(() => display.focus());
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
statusHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
// rowVal is the raw dateStyle string on the flat table
|
||||
return filterVal.some(val => val === rowVal);
|
||||
},
|
||||
qgateHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
|
||||
|
||||
const options = [
|
||||
{ label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' },
|
||||
{ label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' },
|
||||
{ label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' },
|
||||
{ label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' },
|
||||
{ label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' },
|
||||
{ label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' },
|
||||
];
|
||||
|
||||
const field = cell.getField();
|
||||
const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected';
|
||||
let selected = [...(this[stateKey] || [])]; // restore persistence state
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'position: relative; width: 100%;';
|
||||
|
||||
const display = document.createElement('input');
|
||||
display.readOnly = true;
|
||||
display.placeholder = '';
|
||||
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
|
||||
|
||||
options.forEach(opt => {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;';
|
||||
row.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = opt.value;
|
||||
cb.checked = selected.includes(opt.value); // sync with persistence
|
||||
cb.addEventListener('change', () => {
|
||||
if (cb.checked) {
|
||||
selected.push(opt.value);
|
||||
} else {
|
||||
selected = selected.filter(v => v !== opt.value);
|
||||
}
|
||||
this[stateKey] = [...selected]; // sync with persistence
|
||||
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
|
||||
success([...selected]);
|
||||
});
|
||||
|
||||
row.appendChild(cb);
|
||||
row.appendChild(document.createTextNode(opt.label));
|
||||
dropdown.appendChild(row);
|
||||
});
|
||||
|
||||
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
|
||||
|
||||
display.addEventListener('click', () => {
|
||||
if (dropdown.style.display === 'none') {
|
||||
const rect = display.getBoundingClientRect();
|
||||
dropdown.style.top = rect.bottom + 'px';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
display.addEventListener('blur', () => {
|
||||
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
wrapper.appendChild(display);
|
||||
|
||||
cell.getElement().addEventListener('remove', () => dropdown.remove());
|
||||
|
||||
onRendered(() => display.focus());
|
||||
|
||||
return wrapper;
|
||||
},
|
||||
qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
|
||||
const matches = (val) => {
|
||||
switch (val) {
|
||||
case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet');
|
||||
case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet');
|
||||
case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded');
|
||||
case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted');
|
||||
case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet');
|
||||
case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden');
|
||||
default: return true;
|
||||
}
|
||||
};
|
||||
|
||||
// OR logic — row passes if it matches any selected filter
|
||||
return filterVal.some(val => matches(val));
|
||||
},
|
||||
shortLongTitleFormatter(cell, formatterParams, onRendered) {
|
||||
const longForm = cell.getValue()
|
||||
const shortForm = formatterParams?.shortForm
|
||||
|
||||
if(longForm && shortForm) {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>`
|
||||
} else {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>`
|
||||
}
|
||||
|
||||
},
|
||||
toolTipFuncPrevTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle);
|
||||
},
|
||||
toolTipFuncNextTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle);
|
||||
},
|
||||
mapDateStyleToTabulatorTooltip(dateStyleString) {
|
||||
switch(dateStyleString) {
|
||||
case 'bestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipBestanden')
|
||||
break;
|
||||
case 'nichtbestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipNichtBestanden')
|
||||
break;
|
||||
case 'beurteilungerforderlich':
|
||||
return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')
|
||||
break;
|
||||
case 'verspaetet':
|
||||
return this.$p.t('abgabetool/c4tooltipVerspaetet')
|
||||
break;
|
||||
case 'abgegeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbgegeben')
|
||||
break;
|
||||
case 'verpasst':
|
||||
return this.$p.t('abgabetool/c4tooltipVerpasst')
|
||||
break;
|
||||
case 'abzugeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbzugeben')
|
||||
break;
|
||||
case 'standard':
|
||||
return this.$p.t('abgabetool/c4tooltipStandardv2')
|
||||
break;
|
||||
default: return ''
|
||||
}
|
||||
},
|
||||
handlePaUpdated(projektarbeit) {
|
||||
this.checkAbgabetermineProjektarbeit(projektarbeit)
|
||||
this.$refs.abgabeTable.tabulator.redraw(true)
|
||||
@@ -636,267 +376,10 @@ export const AbgabetoolMitarbeiter = {
|
||||
const subject = ""; // empty subject line
|
||||
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, 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 = toViennaDate(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
|
||||
|
||||
// ensure that the filterCollapseables thingy has the correct values
|
||||
this.$refs.abgabeTable.setSelectedFields();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// set shorthand statuscode once real status has been determined
|
||||
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
|
||||
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
|
||||
},
|
||||
mapRankToShortStatus(rank) {
|
||||
switch(rank){
|
||||
case 0: // kein termin vorhanden
|
||||
return '--'
|
||||
break;
|
||||
case 1: // noch nicht stattgefunden
|
||||
return 'o'
|
||||
break;
|
||||
case 2: // noch nicht abgegeben
|
||||
return '?'
|
||||
break;
|
||||
case 3: // noch nicht benotet
|
||||
return '~'
|
||||
break;
|
||||
case 4: // negativ benotet
|
||||
return '-'
|
||||
break;
|
||||
case 5: // positiv benotet
|
||||
return '+'
|
||||
break;
|
||||
}
|
||||
this.initTablePersistence(this.$refs.abgabeTable, this.abgabeTableOptions.persistenceID, this.tableState)
|
||||
},
|
||||
checkAbgabetermineProjektarbeit(projekt) {
|
||||
const now = luxon.DateTime.now()
|
||||
@@ -928,55 +411,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
// seperate check for quality gates
|
||||
this.checkQualityGateStatus(projekt)
|
||||
},
|
||||
abgabeterminFormatter(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 typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz
|
||||
const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz)
|
||||
|
||||
return '<div style="display: flex; height: 100%">' +
|
||||
'<div class=' + val.dateStyle + "-header" + ' style="min-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%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
|
||||
'</div>'+
|
||||
'</div>'
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
},
|
||||
selectHandler(e, cell) {
|
||||
const row = cell.getRow();
|
||||
|
||||
@@ -1011,15 +445,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
},
|
||||
handleToggleFullscreenDetail() {
|
||||
this.detailIsFullscreen = !this.detailIsFullscreen
|
||||
},
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
return formatISODate(dateParam);
|
||||
},
|
||||
undoSelection(cell) {
|
||||
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
|
||||
// but actually just revert it after the fact
|
||||
@@ -1050,9 +475,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
openAddSeriesModal() {
|
||||
this.$refs.modalContainerAddSeries.show()
|
||||
},
|
||||
addSeries() {
|
||||
this.saving = true
|
||||
this.$api.call(ApiAbgabe.postSerientermin(
|
||||
@@ -1093,21 +515,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
|
||||
this.$refs.modalContainerAddSeries.hide()
|
||||
},
|
||||
createInfoString(data) {
|
||||
let str = '';
|
||||
|
||||
data.forEach(name => {
|
||||
str += name
|
||||
str += '; '
|
||||
})
|
||||
|
||||
return str
|
||||
},
|
||||
isPastDate(date) {
|
||||
const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day');
|
||||
const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna');
|
||||
return nowInVienna > deadline;
|
||||
},
|
||||
setDetailComponent(details){
|
||||
this.loading=true
|
||||
|
||||
@@ -1170,51 +577,16 @@ export const AbgabetoolMitarbeiter = {
|
||||
this.loading = false
|
||||
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const longForm = cell.getValue()
|
||||
if(!longForm) return
|
||||
const data = cell.getData()
|
||||
const entry = Object.entries(data).find(entry => entry[1] == longForm)
|
||||
|
||||
// shortFormKey must have same keyname as longForm but with 'Short' appended
|
||||
const shortForm = data[entry[0]+'Short']
|
||||
|
||||
if(shortForm && longForm) {
|
||||
return `<div style="display: flex; justify-content: start; align-items: center; height: 100%; width: 100%;">
|
||||
<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">'+longForm+'</p></div>'
|
||||
}
|
||||
},
|
||||
detailFormatter(cell) {
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
|
||||
},
|
||||
pkzTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<a style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+val+'</a></div>'
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
buildMailToLink(abgabe) {
|
||||
return 'mailto:' + abgabe.uid +'@'+ this.domain
|
||||
},
|
||||
buildPKZ(projekt) {
|
||||
return `${projekt.uid} / ${projekt.matrikelnr}`
|
||||
},
|
||||
buildStg(projekt) {
|
||||
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
|
||||
},
|
||||
setupData(data){
|
||||
this.domain = data[1]
|
||||
|
||||
@@ -1259,17 +631,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
})
|
||||
},
|
||||
loadAbgaben(details) {
|
||||
return new Promise((resolve) => {
|
||||
this.$api.call(ApiAbgabe.getStudentProjektabgaben(details))
|
||||
.then(res => {
|
||||
resolve(res)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
},
|
||||
calcMaxTableHeight() {
|
||||
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
|
||||
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
|
||||
@@ -1299,17 +660,6 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
},
|
||||
emailItems() {
|
||||
const menu = []
|
||||
|
||||
|
||||
@@ -187,7 +187,9 @@ export const AbgabetoolStudent = {
|
||||
termin.file = []
|
||||
termin.allowedToUpload = false
|
||||
|
||||
if(termin.paabgabetyp_kurzbz == 'end') {
|
||||
if(termin.note) { // quality gate termin already has note
|
||||
termin.allowedToUpload = false
|
||||
} else if(termin.paabgabetyp_kurzbz == 'end') {
|
||||
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
|
||||
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
|
||||
} else if(termin.fixtermin) {
|
||||
@@ -308,6 +310,19 @@ export const AbgabetoolStudent = {
|
||||
hasZweitbetreuer(projektarbeit) {
|
||||
return !!(projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer)
|
||||
},
|
||||
getErstbetreuerLabel(projektarbeit) {
|
||||
return projektarbeit.betreuerart_kurzbz
|
||||
? this.$capitalize(this.$p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz))
|
||||
: this.$capitalize(this.$p.t('abgabetool/c4betreuerv2'))
|
||||
},
|
||||
getZweitbetreuerLabel(projektarbeit) {
|
||||
return projektarbeit.zweitbetreuer_betreuerart_kurzbz
|
||||
? this.$capitalize(this.$p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz))
|
||||
: this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))
|
||||
},
|
||||
getZweitbetreuerName(projektarbeit) {
|
||||
return projektarbeit.zweitbetreuer?.first ?? ''
|
||||
},
|
||||
getErstbetreuerEmailLabel(projektarbeit) {
|
||||
// with only one betreuer keep the generic label; with multiple betreuer
|
||||
// distinguish the email rows by their betreuungsart bezeichnung
|
||||
@@ -510,9 +525,9 @@ export const AbgabetoolStudent = {
|
||||
</div>
|
||||
</div>
|
||||
<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/c4betreuerv2') ) }}</div>
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getErstbetreuerLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
{{ projektarbeit.betreuerart_kurzbz ? projektarbeit.betreuer : '' }}
|
||||
{{ projektarbeit.betreuer }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
@@ -521,13 +536,13 @@ export const AbgabetoolStudent = {
|
||||
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{ projektarbeit.zweitbetreuer_betreuerart_kurzbz ? $p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz) : '' }}</div>
|
||||
<div v-if="hasZweitbetreuer(projektarbeit)" class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getZweitbetreuerLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
{{ projektarbeit.zweitbetreuer?.first }}
|
||||
{{ getZweitbetreuerName(projektarbeit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
|
||||
<div v-if="hasZweitbetreuer(projektarbeit)" class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getZweitbetreuerEmailLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<a v-if="projektarbeit.zweitbetreuer_mail" :href="getZweitbetreuerMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
import { multiSelectHeaderFilter } from '../../../tabulator/filters/multiSelectHeaderFilter.js';
|
||||
import { formatISODate, toViennaDate } from './dateUtils.js';
|
||||
|
||||
// Shared projektarbeiten table logic of the Assistenz & Betreuer view: column formatters, sorters,
|
||||
// header filters, quality gate status and the localStorage persistence of the table state.
|
||||
export const AbgabetoolTableMixin = {
|
||||
data() {
|
||||
return {
|
||||
// selected values of the multiselect header filters, keyed by column field
|
||||
headerFilterSelections: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
headerFilterSelection(field) {
|
||||
return {
|
||||
get: () => this.headerFilterSelections[field] ?? [],
|
||||
set: (values) => { this.headerFilterSelections[field] = [...values] }
|
||||
}
|
||||
},
|
||||
getStatusFilterOptions() {
|
||||
return [
|
||||
{ label: this.$p.t('abgabetool/c4positivBenotet'), value: 'bestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4negativBenotet'), value: 'nichtbestanden' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerspaetet'), value: 'verspaetet' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipVerpasst'), value: 'verpasst' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbzugeben'), value: 'abzugeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipAbgegeben'), value: 'abgegeben' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich'), value: 'beurteilungerforderlich' },
|
||||
{ label: this.$p.t('abgabetool/c4tooltipStandardv2'), value: 'standard' },
|
||||
].map(opt => ({
|
||||
...opt,
|
||||
badge: { cssClass: opt.value + '-header', html: this.getDateStyleHtml(opt.value) }
|
||||
}));
|
||||
},
|
||||
statusHeaderFilterEditor(cell, onRendered, success, cancel) {
|
||||
return multiSelectHeaderFilter(cell, onRendered, success, cancel, {
|
||||
options: () => this.getStatusFilterOptions(),
|
||||
selected: this.headerFilterSelection(cell.getField()),
|
||||
minWidth: '220px'
|
||||
});
|
||||
},
|
||||
statusHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
// rowVal is the raw dateStyle string on the flat table
|
||||
return filterVal.some(val => val === rowVal);
|
||||
},
|
||||
getQgateFilterOptions() {
|
||||
return [
|
||||
{ label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' },
|
||||
{ label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' },
|
||||
{ label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' },
|
||||
{ label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' },
|
||||
{ label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' },
|
||||
{ label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' },
|
||||
];
|
||||
},
|
||||
qgateHeaderFilterEditor(cell, onRendered, success, cancel) {
|
||||
return multiSelectHeaderFilter(cell, onRendered, success, cancel, {
|
||||
options: () => this.getQgateFilterOptions(),
|
||||
selected: this.headerFilterSelection(cell.getField())
|
||||
});
|
||||
},
|
||||
qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
|
||||
const matches = (val) => {
|
||||
switch (val) {
|
||||
case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet');
|
||||
case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet');
|
||||
case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded');
|
||||
case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted');
|
||||
case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet');
|
||||
case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden');
|
||||
default: return true;
|
||||
}
|
||||
};
|
||||
|
||||
// OR logic — row passes if it matches any selected filter
|
||||
return filterVal.some(val => matches(val));
|
||||
},
|
||||
getNotenFilterOptions(field) {
|
||||
// mapping evaluated at render time, not at column definition time
|
||||
const fieldOptionsMap = {
|
||||
'pa_note': this.notenOptions,
|
||||
'note': this.allowedNotenOptions,
|
||||
};
|
||||
const options = fieldOptionsMap[field] ?? this.notenOptions;
|
||||
|
||||
return (options ?? []).map(opt => ({ value: opt.note, label: opt.bezeichnung }));
|
||||
},
|
||||
notenHeaderFilterEditor(cell, onRendered, success, cancel) {
|
||||
return multiSelectHeaderFilter(cell, onRendered, success, cancel, {
|
||||
options: () => this.getNotenFilterOptions(cell.getField()),
|
||||
selected: this.headerFilterSelection(cell.getField())
|
||||
});
|
||||
},
|
||||
notenHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
|
||||
if (!filterVal || !filterVal.length) return true;
|
||||
// columns showing the bezeichnung (note_bez) keep the note id in another field
|
||||
const value = filterParams?.idField ? rowData?.[filterParams.idField] : rowVal;
|
||||
// rowVal is the raw integer note id or a note object
|
||||
const noteId = typeof value === 'object' ? value?.note : value;
|
||||
return filterVal.some(val => val == noteId); // loose equality: filter vals are numbers, noteId might be string
|
||||
},
|
||||
notenSorter(a, b, aRow, bRow, column, dir, sorterParams) {
|
||||
const aData = aRow.getData()
|
||||
const bData = bRow.getData()
|
||||
return aData.note - bData.note
|
||||
},
|
||||
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 = toViennaDate(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
|
||||
},
|
||||
getDateStyleHtml(dateStyle) {
|
||||
const iconMap = {
|
||||
'verspaetet': '<i class="fa-solid fa-triangle-exclamation"></i>',
|
||||
'verpasst': '<i class="fa-solid fa-calendar-xmark"></i>',
|
||||
'abzugeben': '<i class="fa-solid fa-hourglass-half"></i>',
|
||||
'standard': '<i class="fa-solid fa-clock"></i>',
|
||||
'abgegeben': '<i class="fa-solid fa-paperclip"></i>',
|
||||
'beurteilungerforderlich': '<i class="fa-solid fa-list-check"></i>',
|
||||
'bestanden': '<i class="fa-solid fa-check"></i>',
|
||||
'nichtbestanden': '<i class="fa-solid fa-circle-exclamation"></i>',
|
||||
};
|
||||
return iconMap[dateStyle] ?? '';
|
||||
},
|
||||
abgabeterminFormatter(cell, formatterParams, onRendered) {
|
||||
const val = cell.getValue()
|
||||
const dateStyle = val?.dateStyle ?? val
|
||||
|
||||
|
||||
if(val) {
|
||||
let icon = ''
|
||||
switch(dateStyle) {
|
||||
case 'verspaetet':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-triangle-exclamation"></i>'
|
||||
break
|
||||
case 'verpasst':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-calendar-xmark"></i>'
|
||||
break
|
||||
case 'abzugeben':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-hourglass-half"></i>'
|
||||
break
|
||||
case 'standard':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-clock"></i>'
|
||||
break
|
||||
case 'abgegeben':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-paperclip"></i>'
|
||||
break
|
||||
case 'beurteilungerforderlich':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-list-check"></i>'
|
||||
break
|
||||
case 'bestanden':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-check"></i>'
|
||||
break
|
||||
case 'nichtbestanden':
|
||||
icon = '<i style="color: #000000 !important;" class="fa-solid fa-circle-exclamation"></i>'
|
||||
break
|
||||
}
|
||||
|
||||
const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz
|
||||
const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz)
|
||||
|
||||
if(formatterParams?.iconOnly) {
|
||||
return '<div style="display: flex; height: 20px;">' +
|
||||
'<div class=' + dateStyle + "-header" + ' style="min-width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
|
||||
icon +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
}
|
||||
|
||||
return '<div style="display: flex; height: 20px;">' +
|
||||
'<div class=' + dateStyle + "-header" + ' style="min-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%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
|
||||
'</div>'+
|
||||
'</div>'
|
||||
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const longForm = cell.getValue()
|
||||
if(!longForm) return
|
||||
const data = cell.getData()
|
||||
const entry = Object.entries(data).find(entry => entry[1] == longForm)
|
||||
|
||||
// shortFormKey must have same keyname as longForm but with 'Short' appended
|
||||
const shortForm = data[entry[0]+'Short']
|
||||
|
||||
if(shortForm && longForm) {
|
||||
return `<div style="display: flex; justify-content: start; align-items: center; height: 100%; width: 100%;">
|
||||
<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>
|
||||
</div>`;
|
||||
} else {
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">'+longForm+'</p></div>'
|
||||
}
|
||||
},
|
||||
pkzTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
|
||||
'<a style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+val+'</a></div>'
|
||||
},
|
||||
shortLongTitleFormatter(cell, formatterParams, onRendered) {
|
||||
const longForm = cell.getValue()
|
||||
const shortForm = formatterParams?.shortForm
|
||||
|
||||
if(longForm && shortForm) {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>
|
||||
<span class="short-text" style="font-weight: bold; display: none;">
|
||||
${shortForm}
|
||||
</span>`
|
||||
} else {
|
||||
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
|
||||
${longForm}
|
||||
</span>`
|
||||
}
|
||||
|
||||
},
|
||||
toolTipFuncPrevTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
if(!data.prevTermin) return ''
|
||||
return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle);
|
||||
},
|
||||
toolTipFuncNextTermin(e, cell, onRendered) {
|
||||
const data = cell.getData();
|
||||
if(!data.nextTermin) return ''
|
||||
return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle);
|
||||
},
|
||||
mapDateStyleToTabulatorTooltip(dateStyleString) {
|
||||
switch(dateStyleString) {
|
||||
case 'bestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipBestanden')
|
||||
break;
|
||||
case 'nichtbestanden':
|
||||
return this.$p.t('abgabetool/c4tooltipNichtBestanden')
|
||||
break;
|
||||
case 'beurteilungerforderlich':
|
||||
return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')
|
||||
break;
|
||||
case 'verspaetet':
|
||||
return this.$p.t('abgabetool/c4tooltipVerspaetet')
|
||||
break;
|
||||
case 'abgegeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbgegeben')
|
||||
break;
|
||||
case 'verpasst':
|
||||
return this.$p.t('abgabetool/c4tooltipVerpasst')
|
||||
break;
|
||||
case 'abzugeben':
|
||||
return this.$p.t('abgabetool/c4tooltipAbzugeben')
|
||||
break;
|
||||
case 'standard':
|
||||
return this.$p.t('abgabetool/c4tooltipStandardv2')
|
||||
break;
|
||||
default: return ''
|
||||
}
|
||||
},
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
// set shorthand statuscode once real status has been determined
|
||||
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
|
||||
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
|
||||
},
|
||||
mapRankToShortStatus(rank) {
|
||||
switch(rank){
|
||||
case 0: // kein termin vorhanden
|
||||
return '--'
|
||||
break;
|
||||
case 1: // noch nicht stattgefunden
|
||||
return 'o'
|
||||
break;
|
||||
case 2: // noch nicht abgegeben
|
||||
return '?'
|
||||
break;
|
||||
case 3: // noch nicht benotet
|
||||
return '~'
|
||||
break;
|
||||
case 4: // negativ benotet
|
||||
return '-'
|
||||
break;
|
||||
case 5: // positiv benotet
|
||||
return '+'
|
||||
break;
|
||||
}
|
||||
},
|
||||
loadTableState(persistenceID) {
|
||||
return JSON.parse(localStorage.getItem(persistenceID) || "null");
|
||||
},
|
||||
saveTableState(table, persistenceID, state) {
|
||||
// avoid storing state after first restore part happened
|
||||
if(!state.stateRestored) return
|
||||
const rawLayout = table.getColumnLayout();
|
||||
const savedState = {
|
||||
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(persistenceID, JSON.stringify(savedState));
|
||||
},
|
||||
// attaches the localStorage persistence (column layout, sorters, filters) to a table.
|
||||
// state holds the restore flags of that table, since a component can hold multiple tables
|
||||
initTablePersistence(tableCmpt, persistenceID, state) {
|
||||
const table = tableCmpt.tabulator
|
||||
|
||||
Object.assign(state, {
|
||||
stateRestored: false,
|
||||
colLayoutRestored: false,
|
||||
filtersRestored: false,
|
||||
headerFiltersRestored: false,
|
||||
sortRestored: false
|
||||
})
|
||||
|
||||
const save = () => this.saveTableState(table, persistenceID, state);
|
||||
|
||||
table.on("columnMoved", save);
|
||||
table.on("columnResized", save);
|
||||
table.on("columnVisibilityChanged", save);
|
||||
table.on("filterChanged", save);
|
||||
table.on("headerFilterChanged", save);
|
||||
table.on("dataSorted", save);
|
||||
table.on("columnSorted", save);
|
||||
table.on("sortersChanged", save);
|
||||
|
||||
const saved = this.loadTableState(persistenceID);
|
||||
|
||||
table.on("renderComplete", () => {
|
||||
if(state.stateRestored) return
|
||||
|
||||
if (saved?.columns && !state.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);
|
||||
|
||||
state.colLayoutRestored = true;
|
||||
}
|
||||
|
||||
if (saved?.filters && !state.filtersRestored) {
|
||||
state.filtersRestored = true // instantly avoid retriggers
|
||||
table.setFilter(saved.filters);
|
||||
}
|
||||
|
||||
if (saved?.headerFilters && !state.headerFiltersRestored) {
|
||||
state.headerFiltersRestored = true // instantly avoid retriggers
|
||||
for (let hf of saved.headerFilters) {
|
||||
// keep the checkboxes of the multiselect header filters in sync with the restored value
|
||||
if (Array.isArray(hf.value)) this.headerFilterSelections[hf.field] = [...hf.value];
|
||||
table.setHeaderFilterValue(hf.field, hf.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (saved?.sort?.length && !state.sortRestored) {
|
||||
state.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);
|
||||
}
|
||||
state.stateRestored = true
|
||||
|
||||
// ensure that the filterCollapseables thingy has the correct values
|
||||
tableCmpt.setSelectedFields();
|
||||
});
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
return formatISODate(dateParam);
|
||||
},
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
|
||||
},
|
||||
buildStg(projekt) {
|
||||
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
|
||||
},
|
||||
handleToggleFullscreenDetail() {
|
||||
this.detailIsFullscreen = !this.detailIsFullscreen
|
||||
},
|
||||
openAddSeriesModal() {
|
||||
this.$refs.modalContainerAddSeries.show()
|
||||
},
|
||||
tableResolve(resolve) {
|
||||
this.tableBuiltResolve = resolve
|
||||
},
|
||||
handleUuidDefined(uuid) {
|
||||
this.tabulatorUuid = uuid
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default AbgabetoolTableMixin;
|
||||
@@ -0,0 +1,106 @@
|
||||
// Tabulator headerFilter editor: readonly input opening a checkbox list which is attached to the body,
|
||||
// so it is not clipped by the table header. Filter value is the array of selected option values.
|
||||
//
|
||||
// params:
|
||||
// options () => [{ value, label, badge: { cssClass, html } }] - resolved on every open, so
|
||||
// asynchronously loaded lists (noten, abgabetypen) are picked up even if they were
|
||||
// not available yet when the header filter was rendered
|
||||
// selected { get, set } - holds the selected values outside of the editor, since tabulator
|
||||
// rebuilds the editor on every column redraw
|
||||
// minWidth min width of the dropdown
|
||||
export function multiSelectHeaderFilter(cell, onRendered, success, cancel, params = {}) {
|
||||
let selected = [...(params.selected?.get() ?? [])];
|
||||
let options = [];
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.cssText = 'position: relative; width: 100%;';
|
||||
|
||||
const display = document.createElement('input');
|
||||
display.readOnly = true;
|
||||
display.placeholder = '';
|
||||
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: '
|
||||
+ (params.minWidth ?? '180px') + '; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
|
||||
|
||||
const updateDisplay = () => {
|
||||
display.value = options
|
||||
.filter(o => selected.includes(o.value))
|
||||
.map(o => o.label)
|
||||
.join(', ');
|
||||
};
|
||||
|
||||
const buildOptionRow = (opt) => {
|
||||
const row = document.createElement('label');
|
||||
row.style.cssText = opt.badge
|
||||
? 'display: flex; align-items: center; gap: 0; cursor: pointer; white-space: nowrap; padding-right: 8px;'
|
||||
: 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;';
|
||||
row.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = opt.value;
|
||||
checkbox.checked = selected.includes(opt.value);
|
||||
checkbox.style.cssText = 'margin: 0 6px;';
|
||||
checkbox.addEventListener('change', () => {
|
||||
selected = checkbox.checked
|
||||
? [...selected, opt.value]
|
||||
: selected.filter(v => v !== opt.value);
|
||||
params.selected?.set([...selected]);
|
||||
updateDisplay();
|
||||
success([...selected]);
|
||||
});
|
||||
row.appendChild(checkbox);
|
||||
|
||||
if (opt.badge) {
|
||||
// icon badge — same look as the cell
|
||||
const badge = document.createElement('div');
|
||||
badge.className = opt.badge.cssClass;
|
||||
badge.style.cssText = `min-width: 36px; height: 36px; display: flex; align-items: center;
|
||||
justify-content: center; flex-shrink: 0; padding: 0px 17px 0px 17px;`;
|
||||
badge.innerHTML = opt.badge.html;
|
||||
row.appendChild(badge);
|
||||
}
|
||||
|
||||
const labelText = document.createElement('span');
|
||||
labelText.textContent = opt.label;
|
||||
if (opt.badge) labelText.style.cssText = 'margin-left: 6px;';
|
||||
row.appendChild(labelText);
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
const renderOptions = () => {
|
||||
options = params.options?.() ?? [];
|
||||
dropdown.replaceChildren(...options.map(buildOptionRow));
|
||||
updateDisplay();
|
||||
};
|
||||
|
||||
renderOptions();
|
||||
|
||||
display.addEventListener('click', () => {
|
||||
if (dropdown.style.display === 'none') {
|
||||
renderOptions();
|
||||
const rect = display.getBoundingClientRect();
|
||||
dropdown.style.top = rect.bottom + 'px';
|
||||
dropdown.style.left = rect.left + 'px';
|
||||
dropdown.style.display = 'block';
|
||||
} else {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
display.addEventListener('blur', () => {
|
||||
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
|
||||
});
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
wrapper.appendChild(display);
|
||||
cell.getElement().addEventListener('remove', () => dropdown.remove());
|
||||
onRendered(() => display.focus());
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
export default multiSelectHeaderFilter;
|
||||
+163
-3
@@ -46565,7 +46565,7 @@ array(
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4studentAbgabeNotAllowedRegularv2',
|
||||
'phrase' => 'c4studentAbgabeNotAllowedRegularv3',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
@@ -46574,7 +46574,7 @@ array(
|
||||
|
||||
Um einen Endupload durchführen zu können, müssen Sie ein positiv benotetes Quality Gate 1 & Quality Gate 2 in der relevanten Projektarbeit absolviert haben.
|
||||
|
||||
Uploads sind für bereits benotete Projektarbeiten gesperrt. Sollten Sie trotzdem etwas hochladen wollen, wenden Sie sich bitte an Ihre Studiengangsassistenz.",
|
||||
Uploads sind für bereits benotete Projektarbeiten beziehungsweise Quality Gates gesperrt. Sollten Sie trotzdem etwas hochladen wollen, wenden Sie sich bitte an Ihre Studiengangsassistenz.",
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
@@ -46584,7 +46584,7 @@ array(
|
||||
|
||||
To be able to complete a final upload, you must have successfully completed Quality Gate 1 and Quality Gate 2 for the relevant project work.
|
||||
|
||||
Uploads are blocked for already graded project work. If you still wish to upload something, please contact your program assistant.',
|
||||
Uploads are blocked for already graded project work aswell as quality gates. If you still wish to upload something, please contact your program assistant.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
@@ -47709,6 +47709,46 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4newUploadsNotification',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Neue Abgaben: Bachelor-/Masterarbeiten',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => "New submissions: Bachelor's-/Master's Thesis",
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4mehrereProjektarbeiten',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Mehrere Projektarbeiten (siehe unten)',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Several theses (see below)',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
// PAABGABETYP PHRASEN BEGIN
|
||||
array(
|
||||
'app' => 'core',
|
||||
@@ -48895,6 +48935,126 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4schlagwoerterZuLang',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Schlagwörter dürfen maximal {0} Zeichen lang sein.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Keywords may not exceed {0} characters.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4abstractZuLang',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Der Abstract darf maximal {0} Zeichen lang sein.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The abstract may not exceed {0} characters.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4seitenanzahlUngueltig',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Seitenanzahl muss zwischen 1 und {0} liegen.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The number of pages must be between 1 and {0}.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4enduploadZusatzdatenSpeichernFehlgeschlagen',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Die Zusatzdaten konnten nicht gespeichert werden, der Upload wurde daher nicht abgeschlossen. Bitte wenden Sie sich an Ihre Studiengangsassistenz.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The additional data could not be saved, therefore the upload was not completed. Please contact your degree programme assistant.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4enduploadUeberZwischenabgabeNichtErlaubt',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Dieser Termin ist eine Endabgabe und kann nur über den Endupload inklusive Zusatzdaten hochgeladen werden. Bitte laden Sie die Seite neu.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'This deadline is a final submission and can only be uploaded via the final upload including the additional data. Please reload the page.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'abgabetool',
|
||||
'phrase' => 'c4abgabeterminGeaendertNeuLaden',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Der Abgabetermin wurde zwischenzeitlich geändert. Bitte laden Sie die Seite neu.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'The submission deadline has been changed in the meantime. Please reload the page.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
// ABGABETOOL PHRASEN END
|
||||
array(
|
||||
'app' => 'core',
|
||||
|
||||
Reference in New Issue
Block a user