Compare commits

..

40 Commits

Author SHA1 Message Date
Johann Hoffmann d1b1ea29eb stronger visual queues for external zweitbegutachter if their beurteilung is just saved or saved and sent; 2026-06-15 16:48:32 +02:00
Johann Hoffmann 263b04e67f reworked assistenz timeline feature; now shows all projektarbeiten of associated student in a more compact form; 2026-06-15 14:37:47 +02:00
Johann Hoffmann 7c594bc4f9 stv added back the old 'save betreuer' button as it makes sense for finishing a single betreuer form and then continuing with editing the projektarbeit form user will get used to it 100%; updated some gendering phrases from StudentIn -> Student*In because thats apparently the current way to do it; 2026-06-12 12:55:06 +02:00
Johann Hoffmann 3f99a2ce81 can send zweitbetreuer token mail on demand from betreuer abgabetool detail buttons, if it is external zweitbetreuer without benutzer 2026-06-12 12:17:00 +02:00
Johann Hoffmann e574998897 fetch both erst & zweitbetreuer in abgabetool betreueransicht; fix ProjektarbeitModel->getProjektbetreuerEmail by explicitely only querying erstbetreuer, which fixes rare cases where endupload crashes when the zweitbetreuer has no contact email as private mail AND randomly gets retrieved as first row by the query; fix race condition on the semester filter default value in assistenz view; block all edit/delete operations in flat table for any termin which already has a file uplaoded (abgabetermin != null) or has a note (due to quality gate), to avoid confusing UI implementations; fix STV Projektarbeit bug where betreuer form options where not loading correctly when editing existing projektarbeiten; 2026-06-11 17:10:33 +02:00
Johann Hoffmann e088b529f0 alternate dateFilter which allows for manual input, as the primevue3 datepicker overlay aggressively works against this behaviour by stealing away the focus of its own input field. this one enables manual typing but the overlay only opens when clicking the icon so it is in its own way inconsistent...; 2026-06-05 14:51:22 +02:00
Johann Hoffmann 61349c7ca3 show updated/inserted by/at on every abgabetermin in mitarbeiter detail view; 2026-06-05 12:15:11 +02:00
Johann Hoffmann c75e1f9416 WIP 2026-06-05 10:12:03 +02:00
Johann Hoffmann 66e1e6ad8a studierendenbereich phrasenkey change so it gets inserted as a new phrase and not ignored like our phrasesupdate.php tends to do 2026-06-01 15:59:38 +02:00
Johann Hoffmann d0d590ff89 block the change of paabgabetyp if the termin has an upload associated (abgabedatum !== null) or has a note; TODO: block the same in multiedit! 2026-06-01 12:56:55 +02:00
Johann Hoffmann 60e36bb4b9 change updateProjektarbeit call so that the abgabedatum actually only gets set when doing the endupload and not also when zusatzdaten are being edited, which can certainly be an earlier or even later date; 2026-06-01 11:30:36 +02:00
Johann Hoffmann ae71517ceb expanded title validation in frontend aswell before validation for the same patterns in backend just in case; phrasen alert messages & use backend response instead of frontend validated title 2026-05-29 11:47:46 +02:00
Johann Hoffmann 9f462fe3d6 "hasContent" guardrail in abgabetoolJob to avoid sending empty email templates when every possible relevant change has been filtered by the job; 2026-05-29 10:21:44 +02:00
Johann Hoffmann 004bcc43c3 merged stv saveProjektarbeit/saveBetreuer buttons; benoten button for betreuer is now a table action button; remove viewData.uid and call student_uid via authinfo endpoint; digital signature info links in student abgabetool; show projektarbeit note in overview inside the accordions content aswell as flex-end in the header; smaller vertical padding on all abgabetool modals from 3rem to 1.5 rem aka py-5 to py-4; 2026-05-29 09:50:37 +02:00
Johann Hoffmann 6d79288f33 Merge remote-tracking branch 'origin/master' into feature-76657/AbgabetoolFinetuning
# Conflicts:
#	system/phrasesupdate.php
2026-05-27 16:11:33 +02:00
Johann Hoffmann 2eb2c36d5b replace + in email helpers with spaces; configs for title edit, multi edit table & confetti on endupload; empty student projektarbeiten check; only show dropdown labels on large displays; new phrasen & phrasen fixes; 2026-05-26 17:06:39 +02:00
Johann Hoffmann 22eed92c86 Merge branch 'feature-75838/UXImproveProjektarbeitSTVSprint247' into feature-76657/AbgabetoolFinetuning 2026-05-26 12:58:57 +02:00
Johann Hoffmann 557e43e19c extracted date handling with luxon into dateUtils file and only work with those functions to completely avoid anymore timezone bugs due to js Dates. formatDate was still affected by timezone issues and was showing off by one dates in rare cases. 2026-05-22 11:17:37 +02:00
Johann Hoffmann 790568e1e0 timezone save date format function in student detail view via getUTCDate() instead of getDate() 2026-05-22 09:55:25 +02:00
Johann Hoffmann dfc3096587 rewrote findlatestTerminWithUpload sort to guarantee sorting by abgabedatum DESC 2026-05-20 13:52:32 +02:00
Johann Hoffmann c0192e9e00 adjusted flatTable index to paabgabe_id since projektarbeit_id is not unique per row there; fixed splitMailHelpers call in Mitarbeiter page; emailHelper changes so it actually works with bcc aswell; 2026-05-18 10:23:41 +02:00
Johann Hoffmann 9b823dbaa1 scope the zoom media query to abgabetool document selector to avoid spillover to CIS4 pages unintentionally; added noten headerfilter & persistence in flat table; 2026-05-08 12:44:07 +02:00
Johann Hoffmann 398e3aa139 block deletion of benotet qulity gates; sync filter clear & semester dropdown; setSelecetedFields for flat table; WIP testing & refinement of everything; 2026-05-07 16:18:58 +02:00
Johann Hoffmann 553a33aa42 adapt Betreuer Page with changes of assistenzpage; set select all checkbox flag in selectAll handler to always be in sync with selectedData; 2026-05-06 18:54:19 +02:00
Johann Hoffmann 05e9d948ea deselect filtered rows; 2026-05-06 17:18:35 +02:00
Johann Hoffmann a5d343268f fix deselect condition on filtered datasets; added new phrases for confirm & edit dialog; fixed emailSplitter phrasen & confirm; fixed modal fullscreen button alignment & color; 2026-05-06 16:49:35 +02:00
Johann Hoffmann 9e061de9bd fix deselect condition on filtered datasets; added new phrases for confirm & edit dialog; 2026-05-06 15:26:02 +02:00
Johann Hoffmann d0adf2dfc3 improved assistenz subqueries for zweitbetreuer infos by using common table expression instead of 8 subqueries; adapted splitMailsHelper function to take in body parameter to set default email text by parameter; dateStyles adapted so "in 12 days" also applies to termine without uploads; titleEdit modal in student & studentDetail component; send email to relevant assistenzen & projektarbeit betreuer about change from old to new title; 2nd flat table in AbgabetoolAssistenz that provides a list of all projektarbeittermine so it can be filtered & multiselected; multi delete & multi edit on these selected termine; tried to introduce a media query for zoomed in desktop users that shrinks fontsize and tabulator rows/cells; standard assistenz table column definitions have sensible minWidths & most columns are default invisible; fancy multiselect headerfilter on qualitygate 1/2 status column; actually figured out a vue watcher race codnition triggering loadProjektarbeiten twice unnecessarily; added a reload Button in case one observes a faulty reactivity somewhere in the table; fancy multiselect headerfilter on termin status column for flat table; Preselect current Semester & autoapply Filter for it; WIP refine new table & hunt for bugz; WIP working on the exact custom select handler Handling with filtered datasets; fixed root element style on legacy php view for abgabetool in old cis; WIP define more accurate allowed to delete & allowed to Edit conditions for abgabetermine -> currently benotet quality gates can be edited/deleted!!!!! 2026-05-06 13:46:26 +02:00
Johann Hoffmann f51d0f72c6 setSelectedFields() method in filter cmpt to keep custom persistence with visible fields; distinct titles assistenz/betreuer page; fix select logic to respect the possibility of dataset being filtered; beurteilungsnotiz required if note of that termin is negative; multiselect filter on qgate status columns + persistence of those; WIP switching mode to flat table showing all abgabetermine of all projektarbeiten to multi edit/delete/whatever with them selected 2026-04-28 17:27:19 +02:00
Johann Hoffmann c62c3c23f6 minimal column setup by default, added sensible minWidths 2026-04-27 16:37:36 +02:00
Johann Hoffmann b22375bd21 Merge remote-tracking branch 'origin/feature-61164/AbgabetoolQualityGates' into feature-76657/AbgabetoolFinetuning
# Conflicts:
#	public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js
2026-04-27 16:24:06 +02:00
Johann Hoffmann f9550c99bb remove newAbgabetermin from SpeedDial Items 2026-04-27 16:13:31 +02:00
Johann Hoffmann df369c7149 Merge branch 'feature-75996/AbgabetoolFeedbackSprint247' into feature-76657/AbgabetoolFinetuning 2026-04-27 16:11:11 +02:00
Johann Hoffmann 6868e68145 define datepickers to use ISO strings internally with model-type="yyyy-MM-dd" to avoid construction of new js dateobjects when deleting the value via text input, which could lead to a off by one day/timezone error when saving the date 2026-04-09 15:42:05 +02:00
Johann Hoffmann 44e43d9479 phrasenkey fix 'c4fehlerAktualitaetProjektarbeit ' => 'c4fehlerAktualitaetProjektarbeit' 2026-04-08 15:47:17 +02:00
Johann Hoffmann 079a8e3ec1 added selected: x | filtered: y | total: z count to assistenz table 2026-03-24 15:19:36 +01:00
Johann Hoffmann 6ddb064c9e qgateStatus tooltips 2026-03-23 10:09:46 +01:00
Johann Hoffmann 9b8b1b2532 cellFormatter & TitleFormatter breakpoints at 100px to display longform or shortform text; phrasenupdate; lefthandside formatting per default; curated tabulator tooltips for complex cells WIP 2026-03-20 12:21:47 +01:00
Johann Hoffmann aba4bc2909 merge projektarbeit details & betreuer form and handle several UX changes regarding formData of both 2026-03-19 11:12:14 +01:00
Johann Hoffmann f6d2b55f75 redrawTableScrollSave method instead of manual reset 2026-03-16 11:26:31 +01:00
40 changed files with 4549 additions and 1302 deletions
+12 -1
View File
@@ -29,7 +29,7 @@ $config['ALLOWED_NOTEN_ABGABETOOL'] = [10, 14]; // tbl_note pk
// benotete projektarbeiten sperren weitere terminanlage & bearbeitung, diese noten sind ausnahmen dieser Regel
// wie zB "Nicht beurteilt" & "Noch nicht eingetragen"
$config['NONFINAL_NOTEN_ABGABETOOL'] = [9];
$config['beurteilung_link_fallback'] = 'addons/fhtw/content/projektbeurteilung/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?';
$config['beurteilung_link_fallback'] = 'cis/private/lehre/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?';
$config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter';
$config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ZWEITBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter';
@@ -38,8 +38,19 @@ $config['SIGNATUR_CHECK_PAABGABETYPEN'] = ['end'];
// to be used as "https://moodle.technikum-wien.at/course/view.php?idnumber=dl{$stg_kz}" for stg specific moodle routing
$config['STG_MOODLE_LINK'] = 'https://moodle.technikum-wien.at/course/view.php?idnumber=dl';
// TODO: check if these links change if the file changes and how to better retrieve the link?
$config['SIGNATUR_INFO_LINK_GERMAN'] = 'https://cis.technikum-wien.at/cms/dms.php?id=214779';
$config['SIGNATUR_INFO_LINK_ENGLISH'] = 'https://cis.technikum-wien.at/cms/dms.php?id=264256';
$config['ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'] = true;
$config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true;
$config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true;
$config['MULTIEDIT_TABLE'] = true;
$config['STUDENT_EDIT_PROJEKTARBEIT_TITLE'] = true;
$config['CONFETTI_ON_ENDUPLOAD'] = true;
@@ -35,9 +35,12 @@ class Abgabe extends FHCAPI_Controller
'getStudentProjektabgaben' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw', 'basis/abgabe_lektor:rw'),
'postStudentProjektarbeitZwischenabgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'),
'postStudentProjektarbeitEndupload' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'),
'postStudentProjektarbeitTitel' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'),
'getMitarbeiterProjektarbeiten' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'),
'postProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'),
'patchProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'),
'deleteProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'),
'deleteProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'),
'postSerientermin' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'),
'fetchDeadlines' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'),
'getPaAbgabetypen' => self::PERM_LOGGED,
@@ -46,7 +49,9 @@ class Abgabe extends FHCAPI_Controller
'getStudiengaenge' => array('basis/abgabe_assistenz:rw'),
'getStudentProjektarbeitAbgabeFile' => array('basis/abgabe_student:rw', 'basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'),
'postStudentProjektarbeitZusatzdaten' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'),
'getSignaturStatusForProjektarbeitAbgaben' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw')
'getSignaturStatusForProjektarbeitAbgaben' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'),
'sendZweitbetreuerTokenMail' => array('basis/abgabe_lektor:rw', 'basis/abgabe_assistenz:rw'),
'fetchProjektarbeitenHistory' => array('basis/abgabe_assistenz:rw')
]);
$this->load->library('PhrasesLib');
@@ -90,6 +95,7 @@ class Abgabe extends FHCAPI_Controller
$ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT');
$ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER = $this->config->item('ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER');
$BETREUER_SAMMELMAIL_BUTTON_STUDENT = $this->config->item('BETREUER_SAMMELMAIL_BUTTON_STUDENT');
$MULTIEDIT_TABLE = $this->config->item('MULTIEDIT_TABLE');
$ret = array(
'old_abgabe_beurteilung_link' => $old_abgabe_beurteilung_link,
@@ -97,7 +103,7 @@ class Abgabe extends FHCAPI_Controller
'abgabetypenBetreuer' => $abgabetypenBetreuer,
'ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT' => $ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT,
'ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER' => $ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER,
'BETREUER_SAMMELMAIL_BUTTON_STUDENT' => $BETREUER_SAMMELMAIL_BUTTON_STUDENT,
'MULTIEDIT_TABLE' => $MULTIEDIT_TABLE,
);
$this->terminateWithSuccess($ret);
@@ -107,10 +113,18 @@ class Abgabe extends FHCAPI_Controller
* loads config related to abgabetool for students to avoid handing out links reserved for employees
*/
public function getConfigStudent() {
$moodle_link =$this->config->item('STG_MOODLE_LINK');
$moodle_link = $this->config->item('STG_MOODLE_LINK');
$title_edit_allowed = $this->config->item('STUDENT_EDIT_PROJEKTARBEIT_TITLE');
$confetti_on_endupload = $this->config->item('CONFETTI_ON_ENDUPLOAD');
$siginfolink_german = $this->config->item('SIGNATUR_INFO_LINK_GERMAN');
$siginfolink_english = $this->config->item('SIGNATUR_INFO_LINK_ENGLISH');
$ret = array(
'moodle_link' => $moodle_link,
'title_edit_allowed' => $title_edit_allowed,
'confetti_on_endupload' => $confetti_on_endupload,
'siginfolink_german' => $siginfolink_german,
'siginfolink_english' => $siginfolink_english
);
$this->terminateWithSuccess($ret);
@@ -187,8 +201,8 @@ class Abgabe extends FHCAPI_Controller
} else {
$result = $this->ProjektarbeitModel->getStudentProjektarbeitenWithBetreuer(getAuthUID());
}
$projektarbeiten = getData($result);
$projektarbeiten = hasData($result) ? getData($result) : array();
if(count($projektarbeiten)) {
foreach($projektarbeiten as $pa) {
@@ -410,9 +424,16 @@ class Abgabe extends FHCAPI_Controller
$this->checkAbgabeSignatur($paabgabe, $projektarbeit->student_uid);
$signaturstatus = $paabgabe->signatur;
// update projektarbeit cols
$this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id, $sprache, $abstract, $abstract_en
, $schlagwoerter, $schlagwoerter_en, $seitenanzahl);
// update projektarbeit cols with zusatzdaten AND abgabedatum!
$this->ProjektarbeitModel->update($projektarbeit->projektarbeit_id, array(
'sprache' => $sprache,
'seitenanzahl' => $seitenanzahl,
'abgabedatum' => date('Y-m-d'),
'schlagwoerter_en' => $schlagwoerter_en,
'schlagwoerter' => $schlagwoerter,
'abstract' => $abstract,
'abstract_en' => $abstract_en
));
// update paabgabe datum
@@ -448,7 +469,218 @@ class Abgabe extends FHCAPI_Controller
}
}
/**
* POST METHOD
* allows a student (or assistenz on their behalf) to update the titel of their own projektarbeit.
* blocked once the projektarbeit has been graded (checkProjektarbeitForFinishedStatus).
*/
public function postStudentProjektarbeitTitel()
{
if(!$this->config->item('STUDENT_EDIT_PROJEKTARBEIT_TITLE')) {
$this->terminateWithError($this->p->t('global', 'c4studentEditNotAllowed'), 'general');
};
$projektarbeit_id = $this->input->post('projektarbeit_id');
$titel = $this->input->post('titel');
if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === ''
|| $titel === NULL || trim((string)$titel) === '') {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
// strip all HTML tags to prevent XSS in mail bodies, table views and Projektarbeitsbenotung
$titel = trim(strip_tags($titel));
if ($titel === '') {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
// Reject emojis and pictographs
// allows foreign letters, math symbols, accents, and standard punctuation.
$emojiPattern = '/[\x{1F300}-\x{1F5FF}\x{1F600}-\x{1F64F}\x{1F680}-\x{1F6FF}\x{1F900}-\x{1FAFF}\x{23E9}-\x{23EF}\x{2b50}\x{2700}-\x{27BF}]/u';
// i would like this much more but our server does not recognize this utf-8 character range this way, so hexcodes it is
// if (preg_match('/\p{Extended_Pictographic}/u', $titel)) {
if (preg_match($emojiPattern, $titel)) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
// Verify the projektarbeit actually belongs to the supplied student_uid
$res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id);
if (isError($res) || !hasData($res)) {
$this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general');
}
$assignedStudentUid = getData($res)[0]->uid;
// A student may only update their own title; assistenz is covered by checkZuordnung
$zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID());
if (getAuthUID() !== $assignedStudentUid && !$zugeordnet) {
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general');
}
$result = $this->ProjektarbeitModel->load($projektarbeit_id);
$data = getData($result);
$oldTitle = $data[0]->titel ?? '';
$result = $this->ProjektarbeitModel->update(
$projektarbeit_id,
array(
'titel' => $titel,
'updatevon' => getAuthUID(),
'updateamum' => date('Y-m-d H:i:s')
)
);
$this->getDataOrTerminateWithError($result, 'general');
$this->logLib->logInfoDB(array(
'titelUpdate',
array(
'projektarbeit_id' => $projektarbeit_id,
'titel' => $titel,
'updatevon' => getAuthUID(),
'updateamum' => date('Y-m-d H:i:s')
),
getAuthUID(),
getAuthPersonId()
));
$this->sendTitelChangedEmail(
$projektarbeit_id,
$titel,
$oldTitle,
$assignedStudentUid
);
$result = $this->ProjektarbeitModel->load($projektarbeit_id);
$titel = hasData($result) ? getData($result)[0]->titel : $titel;
$this->terminateWithSuccess($titel);
}
/**
* Notifies all betreuer of a projektarbeit and all assistenzen responsible for its studiengang
* when a student updates the titel of their projektarbeit.
*
* Betreuer retrieval mirrors AbgabetoolJob->notifyBetreuerAboutChangedAbgaben.
* Assistenz retrieval mirrors AbgabetoolJob->notifyAssistenzAboutChangedAbgaben.
*
* @param int $projektarbeit_id
* @param string $new_titel The titel as it was saved
* @param string $student_uid
*/
private function sendTitelChangedEmail($projektarbeit_id, $new_titel, $old_titel, $student_uid)
{
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel');
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel');
$this->load->model('person/Person_model', 'PersonModel');
$this->load->model('person/Person_model', 'PersonModel');
$studentNameResult = $this->PersonModel->getFullName($student_uid);
$studentFullName = hasData($studentNameResult) ? getData($studentNameResult) : $student_uid;
$studentInfoResult = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id);
if (isError($studentInfoResult) || !hasData($studentInfoResult)) {
$this->logLib->logInfoDB(array('sendTitelChangedEmail: student info not found', $projektarbeit_id));
return;
}
$studentInfo = getData($studentInfoResult)[0];
$studiengang_kz = $studentInfo->studiengang_kz;
$stgResult = $this->StudiengangModel->load($studiengang_kz);
$oe_kurzbz = null;
if (!isError($stgResult) && hasData($stgResult)) {
$oe_kurzbz = getData($stgResult)[0]->oe_kurzbz ?? null;
}
// build shared mail data
$base_mail_data = array(
'studentFullName' => $studentFullName,
'new_titel' => $new_titel,
'old_titel' => $old_titel
);
// notify all betreuer
$betreuerResult = $this->ProjektbetreuerModel->getAllBetreuerOfProjektarbeit($projektarbeit_id);
if (!isError($betreuerResult) && hasData($betreuerResult)) {
$linkAbgabetool = APP_ROOT . $this->config->item('URL_MITARBEITER');
foreach (getData($betreuerResult) as $betreuer) {
$email = $betreuer->uid ? $betreuer->uid . '@' . DOMAIN : ($betreuer->private_email ?? null);
if (!$email) {
$this->logLib->logInfoDB(array('sendTitelChangedEmail: no email for betreuer', $betreuer->person_id));
continue;
}
$anredeResult = $this->ProjektarbeitModel->getProjektbetreuerAnrede($betreuer->person_id);
$anrede = (!isError($anredeResult) && hasData($anredeResult)) ? getData($anredeResult)[0] : null;
$mail_data = array_merge($base_mail_data, array(
'anredeFillString' => ($anrede->anrede ?? '') === 'Herr' ? 'r' : '',
'anrede' => $anrede->anrede ?? '',
'fullFormattedNameString' => $anrede->first ?? $email,
'linkAbgabetool' => $linkAbgabetool,
));
sendSanchoMail(
'PATitleUpdated',
$mail_data,
$email,
$this->p->t('abgabetool', 'c4PATitleChanged')
);
}
}
// notify assistenz for the studiengang OE
if (!$oe_kurzbz) {
$this->logLib->logInfoDB(array('sendTitelChangedEmail: no oe_kurzbz resolved, skipping assistenz', $studiengang_kz));
return;
}
$assistenzResult = $this->OrganisationseinheitModel->getAssistenzForOE($oe_kurzbz);
if (isError($assistenzResult) || !hasData($assistenzResult)) {
return;
}
$linkAbgabetool = APP_ROOT . $this->config->item('URL_ASSISTENZ');
// similar pattern as job uses via the assistenzMap
$sentTo = [];
foreach (getData($assistenzResult) as $assistenz) {
if (in_array($assistenz->person_id, $sentTo)) {
continue;
}
$sentTo[] = $assistenz->person_id;
$email = $assistenz->uid . '@' . DOMAIN;
$mail_data = array_merge($base_mail_data, array(
'anredeFillString' => $assistenz->anrede === 'Herr' ? 'r' : '',
'anrede' => $assistenz->anrede ?? '',
'fullFormattedNameString' => $assistenz->first ?? ($assistenz->uid . '@' . DOMAIN),
'linkAbgabetool' => $linkAbgabetool,
));
sendSanchoMail(
'PATitleUpdated',
$mail_data,
$email,
$this->p->t('abgabetool', 'c4PATitleChanged')
);
}
}
// validate paabgabe deadline against servertime just in case a student spoofs their local clock and thus
// unlocks the upload ui
private function checkPaabgabeDeadline($paabgabe_id) {
@@ -504,7 +736,7 @@ class Abgabe extends FHCAPI_Controller
$showAllBool = false;
}
$projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool);
$projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthPersonId(), $showAllBool);
$mapFunc = function($projektarbeit) {
@@ -519,18 +751,15 @@ class Abgabe extends FHCAPI_Controller
forEach($projektarbeiten->retval as $pa) {
$result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id);
$anredeArr = $this->getDataOrTerminateWithError($result, 'general');
$pa->betreuer = $anredeArr[0];
$oldLink = ''; // show this when paIsCurrent == false -> moodle course template
$newLink = ''; // get curated path for betreuer type
$returnFunc = function ( $resultOld, $resultNew) use (&$oldLink, &$newLink) {
$returnFunc = function ($resultOld, $resultNew) use (&$oldLink, &$newLink) {
$newLink = $resultNew;
$oldLink = $resultOld;
};
Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc);
$own_betreuerart_kurzbz = $pa->betreuer_person_id == getAuthPersonId() ? $pa->betreuer_betreuerart_kurzbz : $pa->zweitbetreuer_betreuerart_kurzbz;
Events::trigger('projektbeurteilung_formular_link', $own_betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc);
$pa->beurteilungLinkNew = $newLink;
$pa->beurteilungLinkOld = $oldLink;
@@ -618,12 +847,22 @@ class Abgabe extends FHCAPI_Controller
// load existing entry of paabgabe and check if note has changed to negativ, to avoid sending when
// only notiz has changed.
// TODO: what if paabgabe is a qualgate1, is benotet negativ and then its type is changed to gate2?
$existingResult = $this->PaabgabeModel->load($paabgabe_id);
$existingPaabgabeArr = getData($existingResult);
if(count($existingPaabgabeArr) > 0) $existingPaabgabe = $existingPaabgabeArr[0];
if($existingPaabgabe->note !== null || $existingPaabgabe->abgabedatum !== null) {
// check if a change of paabgabetyp is being attempted -> not allowed at this point
if($paabgabetyp_kurzbz !== $existingPaabgabe->paabgabetyp_kurzbz) {
$this->terminateWithError($this->p->t('abgabetool', 'c4abgabetypAendernNichtErlaubt'));
}
// check if a change of deadline aka datum is being attempted -> notallowed at this point
if($datum !== $existingPaabgabe->datum) {
$this->terminateWithError($this->p->t('abgabetool', 'c4datumAendernNichtErlaubt'));
}
}
$result = $this->PaabgabeModel->update(
$paabgabe_id,
array(
@@ -687,6 +926,99 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithSuccess([$paabgabe, $existingPaabgabe]);
}
/**
* called by abgabetool/assistenz when bulk-editing multiple abgabetermine via the flat termine table view
* only fields present in the payload are updated - absent fields are left untouched
*/
public function patchProjektarbeitAbgabeMultiple() {
$paabgabe_ids = $this->input->post('paabgabe_ids');
if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
// collect only fields that were actually sent
$updateFields = [];
$datum = $this->input->post('datum');
if ($datum !== NULL && trim((string)$datum) !== '') {
$updateFields['datum'] = $datum;
}
$paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz');
if ($paabgabetyp_kurzbz !== NULL && trim((string)$paabgabetyp_kurzbz) !== '') {
$updateFields['paabgabetyp_kurzbz'] = $paabgabetyp_kurzbz;
}
$kurzbz = $this->input->post('kurzbz');
if ($kurzbz !== NULL) {
$updateFields['kurzbz'] = $kurzbz;
}
// booleans: only include if explicitly posted
$upload_allowed = $this->input->post('upload_allowed');
if ($upload_allowed !== NULL) {
$updateFields['upload_allowed'] = filter_var($upload_allowed, FILTER_VALIDATE_BOOLEAN);
}
$fixtermin = $this->input->post('fixtermin');
if ($fixtermin !== NULL) {
$updateFields['fixtermin'] = filter_var($fixtermin, FILTER_VALIDATE_BOOLEAN);
}
if (empty($updateFields)) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
$results = [];
foreach ($paabgabe_ids as $paabgabe_id) {
$paabgabe_id = trim((string)$paabgabe_id);
if ($paabgabe_id === '') {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id);
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
$zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID());
if (!$zugeordnet) {
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general');
}
$paabgabeResult = $this->PaabgabeModel->load($paabgabe_id);
$paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general');
if (count($paabgabeArr) === 0) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$result = $this->PaabgabeModel->update(
$paabgabe_id,
array_merge($updateFields, [
'updatevon' => getAuthUID(),
'updateamum' => date('Y-m-d H:i:s')
])
);
$this->getDataOrTerminateWithError($result, 'general');
$results[] = getData($this->PaabgabeModel->load($paabgabe_id))[0];
$this->logLib->logInfoDB(array(
'paabgabe bulk updated',
$paabgabe_id,
$updateFields,
getAuthUID(),
getAuthPersonId()
));
}
$this->terminateWithSuccess($results);
}
/**
* called by abgabetool/mitarbeiter in mitarbeiterdetail.js when deleting an abgabetermin
* deletion is only possible if user is assistenz OR betreuer deletes their own custom termin
@@ -719,11 +1051,55 @@ class Abgabe extends FHCAPI_Controller
$result = $this->PaabgabeModel->delete($paabgabe_id);
$result = $this->getDataOrTerminateWithError($result, 'general');
// TODO: consider this in nightly email job
$this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId()));
$this->terminateWithSuccess($result);
}
/**
* called by abgabetool/assistenz when deleting multiple abgabetermine via the flat termine table view
*/
public function deleteProjektarbeitAbgabeMultiple() {
$paabgabe_ids = $this->input->post('paabgabe_ids');
if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->load->model('education/Paabgabe_model', 'PaabgabeModel');
$results = [];
foreach ($paabgabe_ids as $paabgabe_id) {
$paabgabe_id = trim((string)$paabgabe_id);
if ($paabgabe_id === '') {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id));
$zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID());
if (!$zugeordnet) {
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general');
}
$paabgabeResult = $this->PaabgabeModel->load($paabgabe_id);
$paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general');
if (count($paabgabeArr) == 0) {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$result = $this->PaabgabeModel->delete($paabgabe_id);
$result = $this->getDataOrTerminateWithError($result, 'general');
$results[] = $result;
$this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId()));
}
$this->terminateWithSuccess($results);
}
/**
* endpoint for adding the same paabgabe for multiple projektarbeiten
* can be slow for large n since it queries twice per projektarbeit_id
@@ -847,10 +1223,10 @@ class Abgabe extends FHCAPI_Controller
private function getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id) {
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$result = $this->ProjektarbeitModel->getProjektbetreuerEmail($projektarbeit_id);
if(count($result->retval) > 0) {
if(hasData($result)) {
$email = getData($result);
return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email;
} else return '';
} else return null;
}
@@ -861,9 +1237,12 @@ class Abgabe extends FHCAPI_Controller
private function getProjektbetreuerEmailByPersonID($person_id) {
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$result = $this->ProjektarbeitModel->getProjektbetreuerEmailByPersonID($person_id);
$email = $this->getDataOrTerminateWithError($result, 'general');
return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email;
if(hasData($result)) {
$email = getData($result);
return $email[0]->uid ? $email[0]->uid.'@'.DOMAIN : $email[0]->private_email;
} else {
return null;
}
}
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
@@ -1106,9 +1485,15 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general');
}
// update projektarbeit cols
$this->ProjektarbeitModel->updateProjektarbeit($projektarbeit_id,$sprache,$abstract,$abstract_en
,$schlagwoerter, $schlagwoerter_en, $seitenanzahl);
// update projektarbeit cols with zusatzdaten only
$this->ProjektarbeitModel->update($projektarbeit_id, array(
'sprache' => $sprache,
'seitenanzahl' => $seitenanzahl,
'schlagwoerter_en' => $schlagwoerter_en,
'schlagwoerter' => $schlagwoerter,
'abstract' => $abstract,
'abstract_en' => $abstract_en
));
$this->logLib->logInfoDB(array('zusatzdatenEditMitarbeiter', array(
'updatevon' => getAuthUID(),
@@ -1144,6 +1529,58 @@ class Abgabe extends FHCAPI_Controller
$this->terminateWithSuccess($data);
}
public function sendZweitbetreuerTokenMail() {
$projektarbeit_id = $this->input->post('projektarbeit_id');
$bperson_id = $this->input->post('bperson_id');
$student_uid = $this->input->post('student_uid');
if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === ''
|| $bperson_id === NULL || trim((string)$bperson_id) === ''
|| $student_uid === NULL || trim((string)$student_uid) === '') {
$this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general');
}
$this->checkProjektarbeitForFinishedStatus($projektarbeit_id);
$zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID());
if(!$zugeordnet) {
$this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'));
}
$this->sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $student_uid);
}
// basically what getStudentProjektarbeiten intends to do for the student page but
// designed for the assistenz timeline component -> more extensive loading upfront
// with every abgabetermin fetched here aswell
public function fetchProjektarbeitenHistory() {
$student_uid = $this->input->post('student_uid');
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$projektarbeiten = $this->ProjektarbeitModel->getProjektarbeitenForStudent($student_uid);
$mapFunc = function($projektarbeit) {
return $projektarbeit->projektarbeit_id;
};
$projektarbeiten_ids = array_map($mapFunc, $projektarbeiten->retval);
if(count($projektarbeiten_ids) > 0) {
$ret = $this->ProjektarbeitModel->getProjektarbeitenAbgabetermine($projektarbeiten_ids);
$projektabgaben = $this->getDataOrTerminateWithError($ret, 'general');
}
forEach($projektarbeiten->retval as $pa) {
$filterFunc = function($projektabgabe) use ($pa) {
return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id;
};
$pa->abgabetermine = array_values(array_filter($projektabgaben, $filterFunc));
}
$this->terminateWithSuccess($projektarbeiten->retval);
}
/**
* helper function to check the signature status of uploaded files for zwischenabgabe & endupload
*/
@@ -1206,14 +1643,7 @@ class Abgabe extends FHCAPI_Controller
};
Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc);
if(!$projektarbeitIsCurrent) {
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general');
}
// Link to Abgabetool
if (defined('CIS4') && CIS4) {
$ci3BootstrapFilePath = "cis.php";
} else {
$ci3BootstrapFilePath = "index.ci.php";
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv2'), 'general');
}
$path = $this->config->item('URL_MITARBEITER');
@@ -1227,7 +1657,6 @@ class Abgabe extends FHCAPI_Controller
// 1. Begutachter mail ohne Token
$mail_baselink = APP_ROOT.$this->config->item('PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER');
// $mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter";
$mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$studentUser->uid;
$projekttyp_kurzbz = $projektarbeit->projekttyp_kurzbz;
$subject = $projektarbeit->projekttyp_kurzbz == 'Diplom' ? 'Masterarbeitsbetreuung' : 'Bachelorarbeitsbetreuung';
@@ -1247,7 +1676,7 @@ class Abgabe extends FHCAPI_Controller
$email = $this->getProjektbetreuerEmailByProjektarbeitID($projektarbeit_id);
if(!$email) $this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general');
$mailres = sendSanchoMail(
'ParbeitsbeurteilungEndupload',
$maildata,
@@ -1265,66 +1694,101 @@ class Abgabe extends FHCAPI_Controller
// 2. Begutachter mail, wenn Endabgabe, mit Token wenn extern
if ($paabgabetyp_kurzbz == 'end')
{
// Zweitbegutachter holen
$this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel');
$zweitbegutachterRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $studentUser->uid));
$this->sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $studentUser->uid);
if ($zweitbegutachterRetval && count($zweitbegutachterRetval) > 0)
}
}
}
private function sendUploadEmailZweitbegutachterToken($bperson_id, $projektarbeit_id, $student_uid) {
$this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel');
$projektarbeitArr = $this->getDataOrTerminateWithError($this->ProjektarbeitModel->load($projektarbeit_id));
if(count($projektarbeitArr) > 0) {
$projektarbeit = $projektarbeitArr[0];
} else {
$this->terminateWithError($this->p->t('abgabetool','c4projektarbeitNichtGefunden'), 'general');
}
// Zweitbegutachter holen
$this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel');
$zweitbegutachterRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $student_uid));
$projektarbeitIsCurrent = false;
$returnFunc = function ($result) use (&$projektarbeitIsCurrent) {
$projektarbeitIsCurrent = $result;
};
Events::trigger('projektarbeit_is_current', $projektarbeit_id, $returnFunc);
if(!$projektarbeitIsCurrent) {
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv2'), 'general');
}
if ($zweitbegutachterRetval && count($zweitbegutachterRetval) > 0)
{
foreach ($zweitbegutachterRetval as $begutachter)
{
// token generieren, wenn noch nicht vorhanden und notwendig (wird in methode überprüft)
$tokenGenRes = $this->ProjektbetreuerModel->generateZweitbegutachterToken($begutachter->person_id, $projektarbeit_id);
if (!$tokenGenRes)
{
foreach ($zweitbegutachterRetval as $begutachter)
{
// token generieren, wenn noch nicht vorhanden und notwendig (wird in methode überprüft)
$tokenGenRes = $this->ProjektbetreuerModel->generateZweitbegutachterToken($begutachter->person_id, $projektarbeit_id);
if (!$tokenGenRes)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
}
$begutachterMitTokenRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $studentUser->uid, $begutachter->person_id));
if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
}
$begutachterMitToken = $begutachterMitTokenRetval[0];
$path = $begutachterMitToken->betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter';
$mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/$path";
$mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$studentUser->uid;
$intern = isset($begutachterMitToken->uid);
$mail_link = $intern ? $mail_fulllink : $mail_baselink;
$zweitbetmaildata = array();
$zweitbetmaildata['geehrt'] = "geehrte" . ($begutachterMitToken->anrede == "Herr" ? "r" : "");
$zweitbetmaildata['anrede'] = $begutachterMitToken->anrede;
$zweitbetmaildata['betreuer_voller_name'] = $begutachterMitToken->voller_name;
$zweitbetmaildata['student_anrede'] = $maildata['student_anrede'];
$zweitbetmaildata['student_voller_name'] = $maildata['student_voller_name'];
$zweitbetmaildata['abgabetyp'] = $abgabetyp;
$zweitbetmaildata['parbeituebersichtlink'] = $intern ? $maildata['parbeituebersichtlink'] : "";
$zweitbetmaildata['bewertunglink'] = $projektarbeitIsCurrent ? "<p><a href='$mail_link'>Zur Beurteilung der Arbeit</a></p>" : "";
$zweitbetmaildata['token'] = $projektarbeitIsCurrent && isset($begutachterMitToken->zugangstoken) && !$intern ? "<p>Zugangstoken: " . $begutachterMitToken->zugangstoken . "</p>" : "";
$mailres = sendSanchoMail(
'ParbeitsbeurteilungEndupload',
$zweitbetmaildata,
$begutachterMitToken->email,
$subject,
'sancho_header_min_bw.jpg',
'sancho_footer_min_bw.jpg',
get_uid()."@".DOMAIN
);
if (!$mailres)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general');
}
}
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
}
$begutachterMitTokenRetval = getData($this->ProjektbetreuerModel->getZweitbegutachterWithToken($bperson_id, $projektarbeit_id, $student_uid, $begutachter->person_id));
if (!$begutachterMitTokenRetval && count($begutachterMitTokenRetval) <= 0)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailZweitBegutachterv2'), 'general');
}
$begutachterMitToken = $begutachterMitTokenRetval[0];
$studentUser = $this->ProjektarbeitModel->getProjektarbeitBenutzer($student_uid)->retval[0];
$path = $begutachterMitToken->betreuerart_kurzbz == 'Zweitbegutachter' ? 'ProjektarbeitsbeurteilungZweitbegutachter' : 'ProjektarbeitsbeurteilungErstbegutachter';
$mail_baselink = APP_ROOT."index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/$path";
$mail_fulllink = "$mail_baselink?projektarbeit_id=".$projektarbeit_id."&uid=".$student_uid;
$intern = isset($begutachterMitToken->uid);
$mail_link = $intern ? $mail_fulllink : $mail_baselink;
// automatic email ensures that, client only exposes this method if that happened already
$paabgabetyp_kurzbz = 'end';
$abgabetyp = $paabgabetyp_kurzbz == 'end' ? 'Endabgabe' : 'Zwischenabgabe';
$path = $this->config->item('URL_MITARBEITER');
$url = APP_ROOT.$path;
$subject = $projektarbeit->projekttyp_kurzbz == 'Diplom' ? 'Masterarbeitsbetreuung' : 'Bachelorarbeitsbetreuung';
$zweitbetmaildata = array();
$zweitbetmaildata['geehrt'] = "geehrte" . ($begutachterMitToken->anrede == "Herr" ? "r" : "");
$zweitbetmaildata['anrede'] = $begutachterMitToken->anrede;
$zweitbetmaildata['betreuer_voller_name'] = $begutachterMitToken->voller_name;
$zweitbetmaildata['student_anrede'] = $studentUser->anrede;
$zweitbetmaildata['student_voller_name'] = trim($studentUser->titelpre." ".$studentUser->vorname." ".$studentUser->nachname." ".$studentUser->titelpost);
$zweitbetmaildata['abgabetyp'] = $abgabetyp;
$zweitbetmaildata['parbeituebersichtlink'] = $intern ? "<p><a href='$url'>Zur Projektarbeitsübersicht</a></p>" : "";
$zweitbetmaildata['bewertunglink'] = $projektarbeitIsCurrent ? "<p><a href='$mail_link'>Zur Beurteilung der Arbeit</a></p>" : "";
$zweitbetmaildata['token'] = $projektarbeitIsCurrent && isset($begutachterMitToken->zugangstoken) && !$intern ? "<p>Zugangstoken: " . $begutachterMitToken->zugangstoken . "</p>" : "";
$this->addMeta('$zweitbetmaildata', $zweitbetmaildata);
$mailres = sendSanchoMail(
'ParbeitsbeurteilungEndupload',
$zweitbetmaildata,
$begutachterMitToken->email,
$subject,
'sancho_header_min_bw.jpg',
'sancho_footer_min_bw.jpg',
get_uid()."@".DOMAIN
);
if (!$mailres)
{
$this->terminateWithError($this->p->t('abgabetool', 'c4fehlerMailBegutachterv2'), 'general');
}
}
}
}
@@ -1411,7 +1875,13 @@ class Abgabe extends FHCAPI_Controller
$data = getData($res)[0];
if($data->note !== NULL) {
$this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general');
// hardcode this error msg cause phrasen arent reliable and people keep bugging why the cant edit old entries they definitely shouldnt update
$message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeitv2');
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);
}
}
}
@@ -90,6 +90,15 @@ class Projektarbeit extends FHCAPI_Controller
if (!isset($projektarbeit_id) || !is_numeric($projektarbeit_id)) return $this->terminateWithError('Projektarbeit Id missing', self::ERROR_TYPE_GENERAL);
$result = $this->fetchProjektarbeitByID($projektarbeit_id);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($data));
}
private function fetchProjektarbeitById($projektarbeit_id) {
$this->ProjektarbeitModel->resetQuery();
$this->ProjektarbeitModel->addSelect(
'lehre.tbl_projektarbeit.projektarbeit_id, titel, titel_english, themenbereich, projekttyp_kurzbz, lehrveranstaltung_id, lehreinheit_id,
firma_id, beginn, ende, gesperrtbis, note, final, freigegeben, tbl_projektarbeit.anmerkung, fa.name AS firma_name'
@@ -97,13 +106,10 @@ class Projektarbeit extends FHCAPI_Controller
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehreinheit le', 'lehreinheit_id');
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehrveranstaltung lv', 'lehrveranstaltung_id');
$this->ProjektarbeitModel->addJoin('public.tbl_firma fa', 'firma_id', 'LEFT');
$result = $this->ProjektarbeitModel->loadWhere(
return $this->ProjektarbeitModel->loadWhere(
array('projektarbeit_id' => $projektarbeit_id)
);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($data));
}
/**
@@ -132,7 +138,8 @@ class Projektarbeit extends FHCAPI_Controller
);
$data = $this->getDataOrTerminateWithError($result);
$data = $this->getDataOrTerminateWithError($this->fetchProjektarbeitById($data));
$this->terminateWithSuccess($data);
}
@@ -358,7 +358,8 @@ class AbgabetoolJob extends JOB_Controller
foreach($assistenzMap as $assistenz_person_id => $tupelArr) {
$abgabenString = '<div style="font-family: Arial, sans-serif; color: #333;">';
$hasContent = false;
foreach($tupelArr as $tupel) {
$projektarbeit_id = $tupel[0];
$assistenzRow = $tupel[1];
@@ -377,6 +378,7 @@ class AbgabetoolJob extends JOB_Controller
if(count($relevantAbgaben) == 0) {
continue;
}
$hasContent = true;
// Format the Student Name
$s = $relevantAbgaben[0];
@@ -447,7 +449,12 @@ class AbgabetoolJob extends JOB_Controller
}
$abgabenString .= '</div>';
// skip send entirely
if (!$hasContent) {
continue;
}
// done with building the change list, now send it
$assistenzRow = $tupelArr[0][1];
$anrede = $assistenzRow->anrede;
@@ -187,7 +187,10 @@ class Projektarbeit_model extends DB_Model
campus.tbl_paabgabetyp.bezeichnung,
campus.tbl_paabgabetyp.benotbar,
campus.tbl_paabgabe.abgabedatum,
campus.tbl_paabgabe.insertvon
campus.tbl_paabgabe.insertvon,
campus.tbl_paabgabe.insertamum,
campus.tbl_paabgabe.updatevon,
campus.tbl_paabgabe.updateamum
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
WHERE campus.tbl_paabgabe.projektarbeit_id = ?
ORDER BY campus.tbl_paabgabe.datum";
@@ -207,7 +210,18 @@ class Projektarbeit_model extends DB_Model
campus.tbl_paabgabetyp.paabgabetyp_kurzbz,
campus.tbl_paabgabetyp.bezeichnung,
campus.tbl_paabgabe.abgabedatum,
campus.tbl_paabgabe.insertvon
campus.tbl_paabgabe.insertvon,
campus.tbl_paabgabe.updatevon,
campus.tbl_paabgabe.insertamum,
campus.tbl_paabgabe.updateamum,
(SELECT p.vorname || ' ' || p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_person p USING(person_id)
WHERE b.uid = campus.tbl_paabgabe.insertvon) AS insertvon_fullname,
(SELECT p.vorname || ' ' || p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_person p USING(person_id)
WHERE b.uid = campus.tbl_paabgabe.updatevon) AS updatevon_fullname
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
WHERE campus.tbl_paabgabe.projektarbeit_id IN ?
ORDER BY campus.tbl_paabgabe.datum";
@@ -240,7 +254,8 @@ class Projektarbeit_model extends DB_Model
LEFT JOIN public.tbl_benutzer ben USING (person_id)
LEFT JOIN public.tbl_mitarbeiter ma ON ben.uid = ma.mitarbeiter_uid
WHERE (ben.aktiv OR ben.aktiv IS NULL)
AND projektarbeit_id = ?";
AND projektarbeit_id = ?
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz = ANY('{Erstbetreuer,Erstbegutachter,Senatsvorsitz}')";
return $this->execReadOnlyQuery($qry, [$projektarbeit_id]);
}
@@ -294,34 +309,134 @@ class Projektarbeit_model extends DB_Model
* Get a List of Projektarbeiten of a mitarbeiter with zuordnung
* used by the mitarbeiter cis4 abgabetool.
*/
public function getMitarbeiterProjektarbeiten($uid, $showAll){
$qry = "SELECT
*
FROM
(SELECT tbl_person.vorname, tbl_person.nachname, tbl_studiengang.typ, tbl_studiengang.kurzbz,
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung, tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
tbl_projektbetreuer.person_id as betreuer_person_id, tbl_projektbetreuer.betreuerart_kurzbz, tbl_betreuerart.beschreibung AS betreuerart_beschreibung,
tbl_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid
FROM lehre.tbl_projektarbeit
LEFT JOIN lehre.tbl_projektbetreuer using(projektarbeit_id)
LEFT JOIN lehre.tbl_betreuerart using(betreuerart_kurzbz)
LEFT JOIN public.tbl_benutzer on(uid=student_uid)
LEFT JOIN public.tbl_student on(public.tbl_benutzer.uid=public.tbl_student.student_uid)
LEFT JOIN public.tbl_person on(tbl_benutzer.person_id=tbl_person.person_id)
LEFT JOIN lehre.tbl_lehreinheit using(lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang on(lehre.tbl_lehrveranstaltung.studiengang_kz=public.tbl_studiengang.studiengang_kz)
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
AND tbl_projektbetreuer.person_id IN (SELECT person_id FROM public.tbl_benutzer
WHERE public.tbl_benutzer.person_id=lehre.tbl_projektbetreuer.person_id
AND public.tbl_benutzer.uid= ? )
".($showAll?'':' AND public.tbl_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
public function getMitarbeiterProjektarbeiten($person_id, $showAll){
$qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
b.uid AS zweitbetreuer_benutzer_uid,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id
),
primary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS betreuer_person_id,
pb.betreuerart_kurzbz AS betreuer_betreuerart_kurzbz,
ba.beschreibung AS betreuer_betreuerart_beschreibung,
p.titelpre AS betreuer_titelpre,
p.vorname AS betreuer_vorname,
p.nachname AS betreuer_nachname,
p.titelpost AS betreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS betreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Erstbetreuer,Erstbegutachter,Senatsvorsitz}')
ORDER BY pb.projektarbeit_id
)
SELECT
*
FROM
(SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
student_person.vorname, student_person.nachname,
tbl_studiengang.typ, tbl_studiengang.kurzbz,
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung,
tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
student_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus,
pb.betreuer_person_id,
pb.betreuer_betreuerart_kurzbz,
pb.betreuer_betreuerart_beschreibung,
pb.betreuer_titelpre,
pb.betreuer_vorname,
pb.betreuer_nachname,
pb.betreuer_titelpost,
pb.betreuer_full_name,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_benutzer_uid,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
sb.zweitbetreuer_full_name
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN primary_betreuer pb ON pb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
AND (pb.betreuer_person_id = ? OR sb.zweitbetreuer_person_id = ?)
".($showAll?'':' AND student_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
AND betreuerart_kurzbz IN ('Betreuer', 'Begutachter', 'Erstbegutachter', 'Zweitbegutachter', 'Erstbetreuer', 'Senatsvorsitz', 'Senatsmitglied')
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
ORDER BY nachname;";
return $this->execReadOnlyQuery($qry, array($uid));
return $this->execReadOnlyQuery($qry, array($person_id, $person_id));
}
/**
@@ -341,176 +456,257 @@ class Projektarbeit_model extends DB_Model
public function getProjektarbeitenForStudiengang($studiengang_kz, $benotet) {
$new_qry = "SELECT DISTINCT ON(tmp.projektarbeit_id) *, campus.get_betreuer_details(tmp.zweitbetreuer_person_id) as zweitbetreuer_full_name, campus.get_betreuer_details(tmp.betreuer_person_id) as erstbetreuer_full_name
FROM(
SELECT
DISTINCT ON(tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ, tbl_studiengang.kurzbz,
student_benutzer.uid as student_uid,
student_person.vorname as student_vorname,
student_person.nachname as student_nachname,
tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid as betreuer_benutzer_uid,
betreuer_person.titelpre as betreuer_titelpre,
betreuer_person.vorname as betreuer_vorname,
betreuer_person.nachname as betreuer_nachname,
betreuer_person.titelpost as betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart,
lehre.tbl_projektbetreuer.person_id as betreuer_person_id,
lehre.tbl_projektarbeit.sprache as sprache,
lehre.tbl_projektarbeit.seitenanzahl as seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter as kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter as schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en as schlagwoerter_en,
lehre.tbl_projektarbeit.abstract as abstract,
lehre.tbl_projektarbeit.abstract_en as abstract_en,
lehre.tbl_projektarbeit.insertamum as insertamum,
lehre.tbl_projektarbeit.note as note,
(
SELECT orgform_kurzbz
FROM tbl_prestudentstatus
WHERE prestudent_id = (SELECT prestudent_id
FROM tbl_student
WHERE student_uid = student_benutzer.uid
LIMIT 1)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
)
as organisationsform,
(
SELECT person_id
FROM lehre.tbl_projektbetreuer
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_person_id,
(
SELECT betreuerart_kurzbz
FROM lehre.tbl_projektbetreuer
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_betreuerart_kurzbz,
(
SELECT tbl_betreuerart.beschreibung
FROM lehre.tbl_projektbetreuer
JOIN lehre.tbl_betreuerart USING (betreuerart_kurzbz)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_betreuerart_beschreibung,
(
SELECT trim(COALESCE(titelpre, '') || ' ' || COALESCE(vorname, '') || ' ' || COALESCE(nachname, '') || ' ' ||
COALESCE(titelpost, ''))
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_full_name,
(
SELECT titelpre
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_titelpre,
(
SELECT vorname
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_vorname,
(
SELECT nachname
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_nachname,
(
SELECT titelpost
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_titelpost,
(
SELECT
COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz, tbl_studiengang.orgform_kurzbz) as
orgform
FROM
public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING(prestudent_id)
JOIN public.tbl_studiensemester USING(studiensemester_kurzbz)
JOIN public.tbl_studiengang USING(studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING(studienplan_id)
WHERE
prestudent_id=tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC LIMIT 1
) as orgform,
(SELECT status_kurzbz FROM public.tbl_prestudentstatus
WHERE prestudent_id=tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON (student_benutzer.uid = lehre.tbl_projektarbeit.student_uid)
LEFT JOIN public.tbl_person student_person ON (student_benutzer.person_id = student_person.person_id)
LEFT JOIN public.tbl_student on(student_benutzer.uid = public.tbl_student.student_uid)
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON (public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz)
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON (betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id)
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON (betreuer_person.person_id = betreuer_benutzer.person_id)
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv AND (
lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbegutachter'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Begutachter'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Betreuer'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbetreuer'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Senatsvorsitz'
)
AND public.tbl_studiengang.studiengang_kz = ?";
$new_qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
)
if($benotet == 0) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
} else if ($benotet == 1) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
}
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
SELECT DISTINCT ON (tmp.projektarbeit_id)
*,
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
FROM (
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ,
tbl_studiengang.kurzbz,
student_benutzer.uid AS student_uid,
student_person.vorname AS student_vorname,
student_person.nachname AS student_nachname,
public.tbl_student.matrikelnr,
tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid AS betreuer_benutzer_uid,
betreuer_person.titelpre AS betreuer_titelpre,
betreuer_person.vorname AS betreuer_vorname,
betreuer_person.nachname AS betreuer_nachname,
betreuer_person.titelpost AS betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
lehre.tbl_projektarbeit.sprache,
lehre.tbl_projektarbeit.seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en,
lehre.tbl_projektarbeit.abstract,
lehre.tbl_projektarbeit.abstract_en,
lehre.tbl_projektarbeit.insertamum,
lehre.tbl_projektarbeit.note,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_full_name,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
)
AND public.tbl_studiengang.studiengang_kz = ?";
if($benotet == 0) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
} else if ($benotet == 1) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
}
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
) as tmp";
return $this->execReadOnlyQuery($new_qry, array($studiengang_kz));
}
public function getProjektarbeitenForStudent($student_uid) {
$new_qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
)
SELECT DISTINCT ON (tmp.projektarbeit_id)
*,
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
FROM (
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ,
tbl_studiengang.kurzbz,
student_benutzer.uid AS student_uid,
student_person.vorname AS student_vorname,
student_person.nachname AS student_nachname,
public.tbl_student.matrikelnr,
tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid AS betreuer_benutzer_uid,
betreuer_person.titelpre AS betreuer_titelpre,
betreuer_person.vorname AS betreuer_vorname,
betreuer_person.nachname AS betreuer_nachname,
betreuer_person.titelpost AS betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
lehre.tbl_projektarbeit.sprache,
lehre.tbl_projektarbeit.seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en,
lehre.tbl_projektarbeit.abstract,
lehre.tbl_projektarbeit.abstract_en,
lehre.tbl_projektarbeit.insertamum,
lehre.tbl_projektarbeit.note,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_full_name,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
)
AND public.tbl_student.student_uid = ?
ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
) as tmp";
return $this->execReadOnlyQuery($new_qry, array($student_uid));
}
/**
*
+1 -1
View File
@@ -38,7 +38,7 @@ $includesArray = array(
$this->load->view('templates/FHC-Header', $includesArray);
?>
<div id="abgabetoolroot" class="h-100" style="max-width: 95%;" route=<?php echo json_encode($route) ?>
<div id="abgabetoolroot" class="h-100" style="max-width: 99%" route=<?php echo json_encode($route) ?>
uid=<?php echo $uid ?>
student_uid_prop="<?php echo $student_uid_prop ?? '' ?>"
stg_kz_prop="<?php echo $stg_kz_prop ?? '' ?>"
@@ -315,15 +315,22 @@
WHERE tpl.app = '.$APP.'
) pl USING(person_id)
LEFT JOIN (
SELECT DISTINCT ON (tbl_rueckstellung.person_id)
SELECT
tbl_rueckstellung.person_id,
tbl_rueckstellung.datum_bis,
tbl_rueckstellung.status_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung
FROM public.tbl_rueckstellung
JOIN public.tbl_rueckstellung_status USING(status_kurzbz)
WHERE tbl_rueckstellung.datum_bis >= NOW()
ORDER BY tbl_rueckstellung.person_id, tbl_rueckstellung.datum_bis DESC
JOIN public.tbl_person sp ON tbl_rueckstellung.person_id = sp.person_id
WHERE tbl_rueckstellung.rueckstellung_id =
(
SELECT srueck.rueckstellung_id
FROM public.tbl_rueckstellung srueck
WHERE srueck.person_id = tbl_rueckstellung.person_id
AND datum_bis >= NOW()
ORDER BY srueck.datum_bis DESC LIMIT 1
)
) rueck ON rueck.person_id = p.person_id
WHERE
EXISTS (
@@ -24,15 +24,22 @@ $query = '
WHERE tpl.app = '.$APP.'
) pl ON p.person_id = pl.person_id
LEFT JOIN (
SELECT DISTINCT ON (tbl_rueckstellung.person_id)
SELECT
tbl_rueckstellung.person_id,
tbl_rueckstellung.datum_bis,
tbl_rueckstellung.status_kurzbz,
array_to_json(bezeichnung_mehrsprachig::varchar[])->>0 as bezeichnung
FROM public.tbl_rueckstellung
JOIN public.tbl_rueckstellung_status USING(status_kurzbz)
WHERE tbl_rueckstellung.datum_bis >= NOW()
ORDER BY tbl_rueckstellung.person_id, tbl_rueckstellung.datum_bis DESC
JOIN public.tbl_rueckstellung_status USING(status_kurzbz)
JOIN public.tbl_person sp ON tbl_rueckstellung.person_id = sp.person_id
WHERE tbl_rueckstellung.rueckstellung_id =
(
SELECT srueck.rueckstellung_id
FROM public.tbl_rueckstellung srueck
WHERE srueck.person_id = tbl_rueckstellung.person_id
AND datum_bis >= NOW()
ORDER BY srueck.datum_bis DESC LIMIT 1
)
) rueck ON rueck.person_id = p.person_id
WHERE p.person_id NOT IN (SELECT person_id FROM public.tbl_prestudent)';
+11 -75
View File
@@ -80,17 +80,9 @@ echo '
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml; charset=UTF-8" />
<link rel="stylesheet" href="../../vendor/components/jqueryui/themes/base/jquery-ui.min.css" type="text/css" />
<link rel="stylesheet" href="../../vendor/twbs/bootstrap3/dist/css/bootstrap.min.css" type="text/css"/>
<link href="../../skin/style.css.php" rel="stylesheet" type="text/css" />
<style>
.ui-dialog-titlebar-close
{
visibility: hidden !important;
}
</style>
<script type="text/javascript" src="../../vendor/components/jquery/jquery.min.js"></script>
<script type="text/javascript" src="../../vendor/components/jqueryui/jquery-ui.min.js"></script>
<script type="text/javascript" src="../../vendor/twbs/bootstrap3/dist/js/bootstrap.min.js"></script>
<script language="Javascript" type="text/javascript">
//<![CDATA[
@@ -139,78 +131,22 @@ echo '
}
}
function GebietStarten(bezeichnung, stunde, minute, sekunde, gebiet_id)
function GebietStarten(bezeichnung,stunde,minute,sekunde,gebiet_id)
{
let message = <?php echo "'".$p->t('testtool/okKlickenUmZuStarten')."'"?> + ' ' + stunde + 'h ' + minute + 'm ' + sekunde + 's';
let title = <?php echo "'".$p->t('testtool/startGebiet')."'"?>;
let abbrechen = <?php echo "'".$p->t('testtool/abbrechen')."'"?>;
if ($('#gebiet-dialog').length === 0)
{
$('body').append(
'<div id="gebiet-dialog" title="' + title + '">' +
'<p id="gebiet-dialog-msg">' + message + '</p>' +
'</div>'
);
var check = confirm(<?php echo "'".$p->t('testtool/okKlickenUmZuStarten')."'"?>+' '+stunde+'h '+minute+'m '+sekunde+'s');
if (check == true) {
var sprache_user = <?php echo "'".$sprache_user."'"?>;
document.location.href = 'frage.php?gebiet_id='+gebiet_id+'&start=true';
}
else {
return false;
}
$('#gebiet-dialog').dialog({
modal: true,
width: 400,
resizable: false,
buttons: [
{
text: 'OK',
click: function() {
$(this).dialog('close');
document.location.href = 'frage.php?gebiet_id=' + gebiet_id + '&start=true';
}
},
{
text: abbrechen,
click: function() {
$(this).dialog('close');
}
}
]
});
}
let letzteFrageBestaetigt = false;
function letzteFrage()
{
if (letzteFrageBestaetigt)
return true;
let message = <?php echo "'".$p->t("testtool/alleFragenBeantwortet")."'"?>;
if ($('#fertig-dialog').length === 0)
{
$('body').append(
'<div id="fertig-dialog">' +
'<p>' + message + '</p>' +
'</div>'
);
}
$('#fertig-dialog').dialog({
modal: true,
width: 400,
resizable: false,
buttons: [
{
text: 'OK',
click: function() {
$(this).dialog('close');
letzteFrageBestaetigt = true;
$('[name="submitantwort"]').click();
}
}
]
});
return false;
alert(<?php echo "'".$p->t("testtool/alleFragenBeantwortet")."'"?>);
return true;
}
$(document).ready(function () {
@@ -711,7 +647,7 @@ if($frage->frage_id!='')
}
$letzte = $frage->getNextFrage($gebiet_id, $_SESSION['pruefling_id'], $frage_id, $demo);
echo "<form action=\"$PHP_SELF?gebiet_id=$gebiet_id&amp;frage_id=$frage->frage_id\" method=\"POST\" ".(!$letzte && !$levelgebiet?"onsubmit=\"return letzteFrage()\"":"").">";
echo "<form action=\"$PHP_SELF?gebiet_id=$gebiet_id&amp;frage_id=$frage->frage_id\" method=\"POST\" ".(!$letzte && !$levelgebiet?"onsubmit=\"letzteFrage()\"":"").">";
echo '
<div class="row text-center">
<table class="table" style="width: 600px; margin-left: auto; margin-right: auto;">
-22
View File
@@ -44,27 +44,6 @@ if (isset($_GET['sprache_user']) && !empty($_GET['sprache_user']))
$sprache_user = (isset($_SESSION['sprache_user']) && !empty($_SESSION['sprache_user'])) ? $_SESSION['sprache_user'] : DEFAULT_LANGUAGE;
$p = new phrasen($sprache_user);
$showInfo = false;
if (isset($_SESSION['alleGebiete']))
{
$alleGebiete = array_map('intval', $_SESSION['alleGebiete']);
$pruefling_id = $_SESSION['pruefling_id'];
$qry = "SELECT COUNT(DISTINCT gebiet_id) as anzahl
FROM testtool.tbl_pruefling_frage
JOIN testtool.tbl_frage USING(frage_id)
WHERE gebiet_id IN (". implode(',', $alleGebiete) .")
AND pruefling_id = ". $pruefling_id ."
";
$result = $db->db_query($qry);
$anzahlGebiete = $db->db_fetch_object($result);
if ((int)$anzahlGebiete->anzahl === count($alleGebiete))
$showInfo = true;
}
?>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
@@ -90,7 +69,6 @@ if (isset($_SESSION['alleGebiete']))
<body>
<br><br><br><br><br>
<center><h2><?php echo $p->t('testtool/zeitAbgelaufen');?></h2>
<h3><?php echo ($showInfo ? ($p->t('testtool/alleGebietGestartet') . "<br />" . $p->t('testtool/alleGebieteGestartetInfo')) : ''); ?></h3>
</center>
</body>
</html>
+2 -11
View File
@@ -142,9 +142,7 @@ if (isset($_REQUEST['prestudent']))
}
if ($reihungstest_id != '' && $rt->load($reihungstest_id))
{
$pruefling_exist = new Pruefling();
$alreadyInRT = $pruefling_exist->personAlreadyInRT($ps->person_id, $rt->reihungstest_id, $ps->prestudent_id);
if ($rt->freigeschaltet && !$alreadyInRT)
if ($rt->freigeschaltet)
{
// regenerate Session ID after Login
session_regenerate_id();
@@ -284,14 +282,7 @@ if (isset($_REQUEST['prestudent']))
}
else
{
if ($alreadyInRT)
{
$alertmsg .= '<div class="alert alert-danger">'.$p->t('testtool/reihungstestNichtRegistriert').'</div>';
}
else
{
$alertmsg .= '<div class="alert alert-danger">'.$p->t('testtool/reihungstestNichtFreigeschalten').'</div>';
}
$alertmsg .= '<div class="alert alert-danger">'.$p->t('testtool/reihungstestNichtFreigeschalten').'</div>';
}
}
else
-24
View File
@@ -187,7 +187,6 @@ else if (isset($_SESSION['pruefling_id']))
}
$qry .= "
AND ps_status.bewerbung_abgeschicktamum IS NOT NULL
/* Order to get last semester when using distinct on */
ORDER BY
@@ -406,29 +405,6 @@ else if (isset($_SESSION['pruefling_id']))
echo '</table>';
}
if (isset($_SESSION['pruefling_id']) && !empty($_SESSION['alleGebiete']))
{
$alleGebiete = array_map('intval', $_SESSION['alleGebiete']);
$pruefling_id = (int)$_SESSION['pruefling_id'];
$qry = "SELECT COUNT(DISTINCT gebiet_id) AS anzahl
FROM testtool.tbl_pruefling_frage
JOIN testtool.tbl_frage USING(frage_id)
WHERE gebiet_id IN (". implode(',', $alleGebiete) .")
AND pruefling_id = ". $pruefling_id;
$result_check = $db->db_query($qry);
$row_check = $db->db_fetch_object($result_check);
if ((int)$row_check->anzahl === count($alleGebiete))
{
echo '<tr><td>
<div class="alert alert-success small" style="margin-left: 20px; width: 170px; margin-top: 3px;" role="alert">
<strong>'.$p->t('testtool/alleGebietGestartet').'</strong>
</div>
</td></tr>';
}
}
// Link zum Logout
echo '<tr><td class="ItemTesttool" style="margin-left: 20px;" nowrap>
-26
View File
@@ -584,32 +584,6 @@ class pruefling extends basis_db
$qry .= " LIMIT 1";
if($result = $this->db_query($qry))
{
if ($this->db_num_rows($result) == 0)
return false;
else
return true;
}
else
{
$this->errormsg = 'Fehler bei einer Abfrage';
return false;
}
}
public function personAlreadyInRT($person_id, $reihungstest_id, $prestudent_id)
{
$qry = "SELECT tbl_prestudent.prestudent_id
FROM public.tbl_rt_person
JOIN public.tbl_prestudent ON tbl_prestudent.person_id = tbl_rt_person.person_id
JOIN public.tbl_prestudentstatus ON tbl_prestudent.prestudent_id = tbl_prestudentstatus.prestudent_id AND status_kurzbz = 'Bewerber'
AND tbl_prestudentstatus.studienplan_id = tbl_rt_person.studienplan_id
WHERE tbl_rt_person.person_id = " . $this->db_add_param($person_id) . "
AND tbl_rt_person.rt_id = " . $this->db_add_param($reihungstest_id) . "
AND tbl_prestudent.prestudent_id != " . $this->db_add_param($prestudent_id) . "
AND get_rolle_prestudent(tbl_prestudent.prestudent_id, NULL) = 'Bewerber'
LIMIT 1";
if($result = $this->db_query($qry))
{
if ($this->db_num_rows($result) == 0)
-5
View File
@@ -17,7 +17,6 @@ $this->phrasen['testtool/basic']='Basic';
$this->phrasen['testtool/basisgebiete']='Basisgebiete';
$this->phrasen['testtool/semester']='Semester';
$this->phrasen['testtool/reihungstestNichtFreigeschalten']='Der zuteilte Reihungstest ist noch nicht freigeschaltet';
$this->phrasen['testtool/reihungstestNichtRegistriert']='Sie sind für den Reihungstest nicht registriert';
$this->phrasen['testtool/reihungstestKannNichtGeladenWerden']='Der Reihungstest dem Sie zugeteilt sind, kann nicht geladen werden. Melden Sie sich bitte bei der Reihungstestaufsicht.';
$this->phrasen['testtool/geburtsdatumStimmtNichtUeberein']='Ihr Geburtsdatum stimmt nicht mit unseren Daten überein. Bitte wenden Sie sich an die Aufsichtsperson';
$this->phrasen['testtool/home']='Home';
@@ -32,14 +31,10 @@ $this->phrasen['testtool/keineAntwort']='Keine Antwort';
$this->phrasen['testtool/speichernUndWeiter']='Speichern und weiter';
$this->phrasen['testtool/alleFragenBeantwortet']='GLÜCKWUNSCH! \n\nSie haben alle Fragen in der zur Verfügung stehenden Zeit beantwortet. \nNutzen Sie die verbleibende Zeit, um Ihre Antworten zu kontrollieren oder fahren Sie mit dem nächsten Teilgebiet fort.';
$this->phrasen['testtool/zeitAbgelaufen']='Die Maximalzeit für dieses Gebiet ist abgelaufen, oder alle Fragen wurden beantwortet';
$this->phrasen['testtool/alleGebietGestartet']='Sie haben alle Gebiete bearbeitet.';
$this->phrasen['testtool/alleGebieteGestartetInfo']='Sie können sich nun ausloggen und den Browser schließen.';
$this->phrasen['testtool/spracheDerTestfragen']='Gewünschte Sprache der Testfragen';
$this->phrasen['testtool/einleitung']='Einleitung';
$this->phrasen['testtool/blaettern']='Blättern';
$this->phrasen['testtool/demo']='Demobeispiel ansehen';
$this->phrasen['testtool/abbrechen']='Abbrechen';
$this->phrasen['testtool/startGebiet']='Gebiet starten';
$this->phrasen['testtool/okKlickenUmZuStarten']='Klicken Sie OK um dieses Gebiet zu starten. \nSie haben für die Bearbeitung ein Zeitlimit von';
$this->phrasen['testtool/bitteZuerstAnmelden']='Bitte zuerst anmelden!';
$this->phrasen['testtool/fehlerBeimGenerierenDesFragenpools']='Fehler beim generieren des Fragenpools';
-5
View File
@@ -17,7 +17,6 @@ $this->phrasen['testtool/basic']='Basic';
$this->phrasen['testtool/basisgebiete']='Basic test';
$this->phrasen['testtool/semester']='Semester';
$this->phrasen['testtool/reihungstestNichtFreigeschalten']='The entrance examination assigned has not yet been activated.';
$this->phrasen['testtool/reihungstestNichtRegistriert']='You are not registered for the placement test.';
$this->phrasen['testtool/reihungstestKannNichtGeladenWerden']='The placement test you are assigned to could not be loaded. Please contact the placement test supervisior.';
$this->phrasen['testtool/geburtsdatumStimmtNichtUeberein']='Your date of birth does not correspond to the data we have. Please speak to the supervisor. ';
$this->phrasen['testtool/home']='Home';
@@ -32,14 +31,10 @@ $this->phrasen['testtool/keineAntwort']='No Answer';
$this->phrasen['testtool/speichernUndWeiter']='Save and next';
$this->phrasen['testtool/alleFragenBeantwortet']='CONGRATULATIONS!\n\nYou have answered all the questions in the time allowed.\n Use the remaining time to check your answers or continue to the next section.';
$this->phrasen['testtool/zeitAbgelaufen']='The time for this part has expired or you have answered all the questions.';
$this->phrasen['testtool/alleGebietGestartet']='You have worked on all sections.';
$this->phrasen['testtool/alleGebieteGestartetInfo']='You can now log out and close the browser.';
$this->phrasen['testtool/spracheDerTestfragen']='Desired language of questions';
$this->phrasen['testtool/einleitung']='Introduction';
$this->phrasen['testtool/blaettern']='Browse';
$this->phrasen['testtool/demo']='See an example';
$this->phrasen['testtool/abbrechen']='Cancel';
$this->phrasen['testtool/startGebiet']='Start the section';
$this->phrasen['testtool/okKlickenUmZuStarten']='Click OK to start this section. \nYou have a timelimit of';
$this->phrasen['testtool/bitteZuerstAnmelden']='Please log in first!';
$this->phrasen['testtool/fehlerBeimGenerierenDesFragenpools']='Error in generating the pool of questions.';
+4
View File
@@ -277,3 +277,7 @@ html.fs_huge {
}
*/
/* slim ende */
.fhc-xxl-modal {
min-width: 80vw;
}
+101 -1
View File
@@ -305,4 +305,104 @@
/* If you use hover rows, you need to ensure the sticky cell matches the hover color */
#abgabetable .tabulator-row:hover .tabulator-cell.sticky-col {
background-color: #ccc; /* Match your existing hover color */
}
}
.tabulator-cell {
container-type: inline-size;
}
.tabulator-col-title {
container-type: inline-size;
}
@container (max-width: 100px) {
.full-text {
display: none !important;
}
.short-text {
display: inline-block !important;
width: 100%;
}
}
/*conditional tooltips fix*/
.p-tooltip.custom-tooltip {
z-index: 8001 !important;
}
/* Shrinks font and table rows for desktop users who have zoomed in their browser (150%+).
Does not affect mobile/touchscreen devices, which use touch input instead of a mouse. */
@media (pointer: fine) and (min-resolution: 1.5dppx) {
html.abgabetool {
font-size: 0.5rem;
}
.abgabetool .tabulator-cell,
.abgabetool .tabulator-row {
height: 20px;
max-height: 20px;
}
}
/*confetti celebration on endupload - impossible to miss*/
#confetti-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.confetti-piece {
position: absolute;
opacity: 0;
will-change: top, transform, opacity;
}
/* Background Rain */
@keyframes fallAndSpin {
0% {
top: var(--start-y);
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg);
opacity: 1;
}
100% {
top: 105vh;
transform: translate3d(var(--drift), 0, 0) rotateX(720deg) rotateY(360deg);
opacity: 0.3;
}
}
/* Corner Cannons*/
@keyframes cannonBlast {
0% {
transform: translate3d(0, 0, 0) scale(0.3) rotate(0deg);
opacity: 1;
animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1);
}
30% {
transform: translate3d(var(--blast-x), var(--blast-y), 0) scale(1.2) rotate(270deg);
opacity: 1;
animation-timing-function: linear;
}
100% {
transform: translate3d(calc(var(--blast-x) * 1.4), 15vh, 0) scale(0.4) rotate(630deg);
opacity: 0;
}
}
.p-timeline-event-opposite {
flex: 0 0 90px;
max-width: 90px;
}
.p-timeline-event-content {
flex: 1 1 auto;
min-width: 0;
}
+38
View File
@@ -77,6 +77,13 @@ export default {
}
};
},
patchProjektarbeitAbgabeMultiple(payload) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/patchProjektarbeitAbgabeMultiple',
params: payload
};
},
deleteProjektarbeitAbgabe(paabgabe_id) {
return {
method: 'post',
@@ -84,6 +91,13 @@ export default {
params: { paabgabe_id }
};
},
deleteProjektarbeitAbgabeMultiple(paabgabe_ids) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabeMultiple',
params: { paabgabe_ids }
};
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) {
return {
method: 'post',
@@ -139,6 +153,30 @@ export default {
url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben',
params: {paabgabe_ids, student_uid},
};
},
postStudentProjektarbeitTitel(projektarbeit_id, titel) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitTitel',
params: {projektarbeit_id, titel},
};
},
sendZweitbetreuerTokenMail(projektarbeit_id, betreuer_person_id, student_uid) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/sendZweitbetreuerTokenMail',
params: {projektarbeit_id, bperson_id: betreuer_person_id, student_uid},
};
},
fetchProjektarbeitenHistory(student_uid) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/fetchProjektarbeitenHistory',
params: {student_uid},
};
}
};
+4 -7
View File
@@ -25,9 +25,6 @@ const app = Vue.createApp({
},
computed: {
viewData() {
return { uid: this.uid}
},
student_uid_computed() {
return this.student_uid ?? this.uid
},
@@ -55,10 +52,10 @@ const app = Vue.createApp({
},
template: `
<template v-if="comp && uid">
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :viewData="viewData" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'" :viewData="viewData"></AbgabetoolMitarbeiter>
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :viewData="viewData" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
<DeadlineOverview v-if="comp == 'DeadlinesOverview'" :viewData="viewData"></DeadlineOverview>
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'"></AbgabetoolMitarbeiter>
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
<DeadlineOverview v-if="comp == 'DeadlinesOverview'"></DeadlineOverview>
</template>
`
});
+31 -2
View File
@@ -4,7 +4,9 @@ export default {
name: 'BootstrapModal',
data: () => ({
modal: null,
fullscreen: false
fullscreen: false,
expandBtnHovered: false,
expandBtnFocused: false,
}),
props: {
backdrop: {
@@ -70,6 +72,29 @@ export default {
this.$emit('toggleFullscreen')
}
},
computed: {
getExpandButtonStyles() {
const hovered = this.expandBtnHovered;
const focused = this.expandBtnFocused;
return `display: flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
padding: 0;
border: 0;
background: transparent;
font-size: 1em;
opacity: 0.5;
color: inherit;
cursor: pointer;
line-height: 1;
transition: opacity 0.15s ease;
opacity: ${focused ? '1' : hovered ? '0.75' : '0.5'};
outline: ${focused ? '1px solid currentColor' : 'none'};
outline-offset: 2px;`
}
},
mounted() {
if (this.$refs.modal)
this.modal = new bootstrap.Modal(this.$refs.modal, {
@@ -140,9 +165,13 @@ export default {
<div class="d-flex align-items-center ms-auto gap-2">
<button
type="button"
class="btn mb-1"
:style="getExpandButtonStyles"
v-if="allowFullscreenExpand"
@click="toggleFullscreen"
@mouseenter="expandBtnHovered = true"
@mouseleave="expandBtnHovered = false"
@focusin="expandBtnFocused = true"
@focusout="expandBtnFocused = false"
:aria-label="fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'"
>
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
@@ -2,6 +2,8 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import { getDateStyleClass } from "./getDateStyleClass.js";
import {compareISODateValues, formatDateTime, formatISODate, getViennaTodayISO} from "./dateUtils.js";
export const AbgabeMitarbeiterDetail = {
name: "AbgabeMitarbeiterDetail",
@@ -46,12 +48,7 @@ export const AbgabeMitarbeiterDetail = {
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
speedDialItems: [{
label: Vue.computed(() => this.$p.t('abgabetool/c4newAbgabetermin')),
icon: "fa fa-plus",
command: this.openCreateNewAbgabeModal,
disabled: Vue.computed(() => !this.getAllowedToCreateNewTermin)
},
speedDialItems: [
{
label: Vue.computed(() => this.$p.t('abgabetool/c4benoten')),
icon: "fa fa-user-check",
@@ -81,6 +78,22 @@ export const AbgabeMitarbeiterDetail = {
}
},
methods: {
getSavedTerminInfoString(termin) {
const isUpdate = termin.updateamum != null;
const fullname = isUpdate
? termin.updatevon_fullname
: termin.insertvon_fullname;
const datetime = isUpdate
? termin.updateamum
: termin.insertamum;
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
},
terminIsInvalid(termin) {
return termin.note?.positiv == false && !termin.beurteilungsnotiz
},
getNoteBezeichnung(termin){
if(termin.noteBackend?.bezeichnung) {
return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
@@ -113,6 +126,8 @@ export const AbgabeMitarbeiterDetail = {
if(newTerminRes.note) {
newTerminRes.note = noteOpt
newTerminRes.noteBackend = noteOpt // certain UI elements should only reflect persisted state
termin.allowedToDelete = false
newTerminRes.allowedToDelete = false
}
newTerminRes.invertedFixtermin = !newTerminRes.fixtermin
const existingTerminRes = res.data[1]
@@ -132,13 +147,13 @@ export const AbgabeMitarbeiterDetail = {
} else {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting
termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz
termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
}
this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
this.projektarbeit.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum))
const index = this.projektarbeit.abgabetermine.findIndex(t => termin.paabgabe_id == t.paabgabe_id)
@@ -159,7 +174,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '', // todo kurzbz textfield value vorschlag für qualgates
'datum': new Date().toISOString().split('T')[0],
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -337,16 +352,7 @@ export const AbgabeMitarbeiterDetail = {
}
},
formatDate(dateParam) {
// unsafe for datepickers, dont use there
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`
return formatISODate(dateParam)
},
openCreateNewAbgabeModal() {
if(this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter') {
@@ -364,7 +370,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': new Date().toISOString().split('T')[0],
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -398,7 +404,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': new Date().toISOString().split('T')[0],
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -446,7 +452,7 @@ export const AbgabeMitarbeiterDetail = {
}
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
// adjust outer spacing and internal padding to appear similar to download button in size
return {
root: {
style: {
@@ -561,6 +567,12 @@ export const AbgabeMitarbeiterDetail = {
class: "custom-tooltip"
}
},
getTooltipBeurteilungsnotiz() {
return {
value: this.$p.t('abgabetool/c4beurteilungsnotizBeiNegNote'),
class: "custom-tooltip"
}
},
getProjektarbeitTitel() {
if(this.projektarbeit?.titel) return this.$capitalize(this.$p.t('abgabetool/c4titel')) + ': ' + this.projektarbeit.titel
@@ -591,7 +603,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': new Date().toISOString().split('T')[0],
'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -627,6 +639,7 @@ export const AbgabeMitarbeiterDetail = {
dialogClass="bordered-modal modal-lg"
:backdrop="true"
@hideBsModal="showAutomagicModalPhrase=false;"
bodyClass="px-4 py-4"
>
<template v-slot:title>
<div>
@@ -656,6 +669,7 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -806,6 +820,7 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -816,7 +831,7 @@ export const AbgabeMitarbeiterDetail = {
<div class="col-12 col-md-9">
<Dropdown
:style="{'width': '100%'}"
:disabled="!termin.allowedToSave"
:disabled="!termin.allowedToSave || termin.abgabedatum !== null || termin.noteBackend"
:placeholder="getPlaceholderTermin(termin)"
v-model="termin.bezeichnung"
@change="handleChangeAbgabetyp(termin)"
@@ -851,8 +866,10 @@ export const AbgabeMitarbeiterDetail = {
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
<div class="col-12 col-md-9" v-tooltip.right="terminIsInvalid(termin) && getTooltipBeurteilungsnotiz">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz"
:class="{ 'p-invalid': terminIsInvalid(termin) }"
rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
</div>
</div>
@@ -874,6 +891,7 @@ export const AbgabeMitarbeiterDetail = {
:disabled="true"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
>
</VueDatePicker>
</div>
@@ -889,9 +907,6 @@ export const AbgabeMitarbeiterDetail = {
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4keineSignatur')) }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4signaturServerError')) }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</div>
</template>
@@ -904,10 +919,10 @@ export const AbgabeMitarbeiterDetail = {
<div class="col-12 col-md-3 fw-bold align-content-center">
{{ $capitalize( $p.t('abgabetool/c4actions') )}}
</div>
<div class="col-12 col-md-9">
<div class="col-12 col-md-5">
<div class="row">
<div class="col-auto">
<button v-if="termin.allowedToSave" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
<button v-if="termin.allowedToSave && !terminIsInvalid(termin)" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
{{ $capitalize( $p.t('abgabetool/c4save') )}}
<i class="fa-solid fa-floppy-disk"></i>
</button>
@@ -932,6 +947,9 @@ export const AbgabeMitarbeiterDetail = {
</div>
</div>
</div>
<div class="col-12 col-md-4 align-content-center text-end text-muted small">
{{getSavedTerminInfoString(termin)}}
</div>
</div>
</AccordionTab>
</template>
@@ -944,7 +962,8 @@ export const AbgabeMitarbeiterDetail = {
<bs-modal
ref="modalContainerZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg">
dialogClass="bordered-modal modal-lg"
bodyClass="px-4 py-4">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
@@ -3,6 +3,9 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { formatISODate, getViennaTodayISO } from "./dateUtils.js";
import { validateThesisTitle } from './titleValidation.js'
export const AbgabeStudentDetail = {
name: "AbgabeStudentDetail",
@@ -20,7 +23,16 @@ export const AbgabeStudentDetail = {
VueDatePicker,
FhcOverlay
},
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'],
inject: [
'notenOptions',
'isMobile',
'isViewMode',
'moodle_link',
'confetti_on_endupload',
'title_edit_allowed',
'siginfolink_german',
'siginfolink_english'
],
props: {
projektarbeit: {
type: Object,
@@ -31,12 +43,14 @@ export const AbgabeStudentDetail = {
default: false
}
},
emits: ['titel-updated'],
data() {
return {
loading: false,
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
editingTitel: '',
form: Vue.reactive({
sprache: '',
abstract: '',
@@ -49,9 +63,120 @@ export const AbgabeStudentDetail = {
}
},
methods: {
confettiCannons() {
const container = document.getElementById('confetti-container');
if (!container) return;
const colors = ['#FFC107', '#FF5722', '#E91E63', '#00BCD4', '#4CAF50', '#9C27B0'];
const shapes = ['50%', '0%'];
const fragment = document.createDocumentFragment();
// Corner Cannons - Slowed Down)
const cannonCount = 150;
for (let i = 0; i < cannonCount; i++) {
const leftConfetti = document.createElement('div');
leftConfetti.classList.add('confetti-piece');
leftConfetti.style.left = '0px';
leftConfetti.style.top = '100%';
const rightConfetti = document.createElement('div');
rightConfetti.classList.add('confetti-piece');
rightConfetti.style.left = '100vw';
rightConfetti.style.top = '100%';
const colorL = colors[Math.floor(Math.random() * colors.length)];
const colorR = colors[Math.floor(Math.random() * colors.length)];
const shapeL = shapes[Math.floor(Math.random() * shapes.length)];
const shapeR = shapes[Math.floor(Math.random() * shapes.length)];
// Left Styles
leftConfetti.style.background = colorL;
leftConfetti.style.borderRadius = shapeL;
leftConfetti.style.width = `${Math.random() * 10 + 6}px`;
leftConfetti.style.height = `${Math.random() * 14 + 6}px`;
leftConfetti.style.setProperty('--blast-x', `${Math.random() * 50 + 10}vw`);
leftConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Right Styles
rightConfetti.style.background = colorR;
rightConfetti.style.borderRadius = shapeR;
rightConfetti.style.width = `${Math.random() * 10 + 6}px`;
rightConfetti.style.height = `${Math.random() * 14 + 6}px`;
rightConfetti.style.setProperty('--blast-x', `-${Math.random() * 50 + 10}vw`);
rightConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Increased durations to 3s - 5s for a floating gravity effect
const durationL = Math.random() * 2 + 3;
const durationR = Math.random() * 2 + 3;
const delayL = Math.random() * 0.2;
const delayR = Math.random() * 0.2;
leftConfetti.style.animation = `cannonBlast ${durationL}s linear ${delayL}s both`;
rightConfetti.style.animation = `cannonBlast ${durationR}s linear ${delayR}s both`;
fragment.appendChild(leftConfetti);
fragment.appendChild(rightConfetti);
setTimeout(() => leftConfetti.remove(), (delayL + durationL) * 1000);
setTimeout(() => rightConfetti.remove(), (delayR + durationR) * 1000);
}
container.appendChild(fragment);
},
openTitelEdit() {
this.editingTitel = this.projektarbeit.titel ?? '';
this.$refs.modalTitelEdit.show();
},
async saveTitel() {
const validation = validateThesisTitle(this.editingTitel);
if (!validation.isValid) {
if (validation.error === 'empty') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
} else if (validation.error === 'invalid_characters') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
}
return false;
}
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
acceptClass: 'p-button-primary',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'p-button-secondary'
});
if (confirmed === false) return;
this.loading = true;
this.$api.call(
ApiAbgabe.postStudentProjektarbeitTitel(
this.projektarbeit.projektarbeit_id,
validation.cleanedTitle
)
).then(res => {
if (res.meta.status === 'success') {
this.projektarbeit.titel = res.data;
this.$emit('titel-updated', {
projektarbeit_id: this.projektarbeit.projektarbeit_id,
titel: res.data
});
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
this.$refs.modalTitelEdit.hide();
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
}
}).finally(() => {
this.loading = false;
});
},
getNoteBezeichnung(termin){
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
if(noteOpt?.bezeichnung) {
return noteOpt?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
} else if(noteOpt?.benotbar === true && !termin.note) {
@@ -65,7 +190,7 @@ export const AbgabeStudentDetail = {
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile')));
return false
}
if(endupload) {
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/confirmEnduploadSpeichern'),
@@ -77,16 +202,16 @@ export const AbgabeStudentDetail = {
return false
}
}
return true;
},
async triggerEndupload() {
if (!await this.validate(this.enduploadTermin, true))
{
return false;
}
// post endabgabe
const formData = new FormData();
formData.append('paabgabetyp_kurzbz', this.enduploadTermin.paabgabetyp_kurzbz)
@@ -94,14 +219,14 @@ export const AbgabeStudentDetail = {
formData.append('paabgabe_id', this.enduploadTermin.paabgabe_id)
formData.append('student_uid', this.projektarbeit.student_uid)
formData.append('bperson_id', this.projektarbeit.bperson_id)
formData.append('sprache', this.form['sprache'].sprache)
formData.append('abstract', this.form['abstract'])
formData.append('abstract_en', this.form['abstract_en'])
formData.append('schlagwoerter', this.form['schlagwoerter'])
formData.append('schlagwoerter_en', this.form['schlagwoerter_en'])
formData.append('seitenanzahl', this.form['seitenanzahl'])
for (let i = 0; i < this.enduploadTermin.file.length; i++) {
formData.append('file', this.enduploadTermin.file[i]);
}
@@ -109,39 +234,31 @@ export const AbgabeStudentDetail = {
this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData))
.then(res => {
this.handleUploadRes(res, this.enduploadTermin)
if(this.confetti_on_endupload && res.meta.status == "success") {
this.confettiCannons()
}
}).finally(()=> {
this.loading = false
this.loading = false
})
this.$refs.modalContainerEnduploadZusatzdaten.hide()
},
downloadAbgabe(termin) {
const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${termin.paabgabe_id}&student_uid=${this.projektarbeit.student_uid}&projektarbeit_id=${this.projektarbeit.projektarbeit_id}`;
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
},
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`
return formatISODate(dateParam)
},
async upload(termin) {
// only do this on endupload
if (! await this.validate(termin))
{
return false;
}
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
// open endupload form modal for further inputs
this.enduploadTermin = termin
this.$refs.modalContainerEnduploadZusatzdaten.show()
} else {
@@ -151,7 +268,7 @@ export const AbgabeStudentDetail = {
formData.append('paabgabe_id', termin.paabgabe_id)
formData.append('student_uid', this.projektarbeit.student_uid)
formData.append('bperson_id', this.projektarbeit.bperson_id)
for (let i = 0; i < termin.file.length; i++) {
formData.append('file', termin.file[i]);
}
@@ -161,7 +278,7 @@ export const AbgabeStudentDetail = {
.then(res => {
this.handleUploadRes(res, termin)
}).finally(()=> {
this.loading = false
this.loading = false
})
}
},
@@ -169,21 +286,18 @@ export const AbgabeStudentDetail = {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
termin.abgabedatum = new Date().toISOString().split('T')[0];
termin.abgabedatum = getViennaTodayISO();
if(res?.data?.signatur !== undefined) {
termin.signatur = res.data.signatur
}
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
}
if(res.meta.signaturInfo) {
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
}
},
getOptionLabel(option) {
return option.sprache
@@ -195,7 +309,6 @@ export const AbgabeStudentDetail = {
},
watch: {
projektarbeit(newVal) {
// default select german if projektarbeit sprache was null
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
this.form.abstract = newVal.abstract ?? ''
this.form.abstract_en = newVal.abstract_en ?? ''
@@ -203,15 +316,22 @@ export const AbgabeStudentDetail = {
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
this.form.seitenanzahl = newVal.seitenanzahl ?? 1
}
},
computed: {
getSignaturInfoLink() {
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return this.siginfolink_german
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return this.siginfolink_english
},
getSignaturInfoAvailable() {
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return true
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return true
else return false
},
getMoodleLink() {
return this.moodle_link + this.projektarbeit.studiengang_kz
return this.moodle_link + this.projektarbeit.studiengang_kz
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
root: {
style: {
@@ -244,102 +364,84 @@ export const AbgabeStudentDetail = {
})
return qgatefound
},
isTitelEditAllowed() {
return this.title_edit_allowed && !this.isViewMode && !this.projektarbeit?.note;
},
getTooltipVerspaetet() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" }
},
getTooltipVerpasst() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), class: "custom-tooltip" }
},
getTooltipAbzugeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), class: "custom-tooltip" }
},
getTooltipStandard() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), class: "custom-tooltip" }
},
getTooltipAbgegeben() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), class: "custom-tooltip" }
},
getTooltipFixtermin() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), class: "custom-tooltip" }
},
getTooltipAbgabeDetected() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), class: "custom-tooltip" }
},
getTooltipNotAllowedToUpload() {
if(this.isViewMode) {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" }
} else {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')), class: "custom-tooltip" }
}
},
getTooltipBeurteilungerforderlich() {
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')),
class: "custom-tooltip"
}
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), class: "custom-tooltip" }
},
getTooltipBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipBestanden'),
class: "custom-tooltip"
}
return { value: this.$p.t('abgabetool/c4tooltipBestanden'), class: "custom-tooltip" }
},
getTooltipNichtBestanden() {
return {
value: this.$p.t('abgabetool/c4tooltipNichtBestanden'),
class: "custom-tooltip"
}
return { value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), class: "custom-tooltip" }
},
},
created() {
},
mounted() {
},
template: `
<FhcOverlay :active="loading"></FhcOverlay>
<div v-if="projektarbeit">
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereichv2') )}}</h5>
<div class="row">
<div class="col-8">
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
<p>{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p class="d-flex align-items-center gap-2 mb-2" style="min-width: 0;">
<span
:title="projektarbeit.titel"
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 480px;"
>{{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</span>
<button
v-if="isTitelEditAllowed"
class="btn btn-sm btn-outline-secondary border-0 p-1"
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
@click="openTitelEdit"
>
<i class="fa-solid fa-pen"></i>
</button>
</p>
<p>{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
</div>
<div class="col-4">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
<div class="row">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
</div>
<div class="row" v-if="getSignaturInfoAvailable">
<a :href="getSignaturInfoLink" target="_blank">{{$p.t('abgabetool/c4signaturinfo')}} <i class="fa-solid fa-circle-info"></i></a>
</div>
</div>
</div>
@@ -357,7 +459,6 @@ export const AbgabeStudentDetail = {
<i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
<i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
<i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="text-start px-2" style="min-width: 150px; max-width: 300px; margin-left: 40px">
<span>{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}</span>
@@ -408,8 +509,6 @@ export const AbgabeStudentDetail = {
</div>
</template>
</Inplace>
</div>
<div class="row mt-2">
@@ -430,7 +529,7 @@ export const AbgabeStudentDetail = {
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
@@ -466,7 +565,7 @@ export const AbgabeStudentDetail = {
<div class="col-12 col-md-9">
<template v-if="termin?.abgabedatum">
<div class="row">
<div style="width:100px; align-content: center;">
<div style="width:100px; align-content: end;">
<h6>{{ termin.abgabedatum?.split("-").reverse().join(".") }}</h6>
</div>
@@ -481,9 +580,6 @@ export const AbgabeStudentDetail = {
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4keineSignatur') }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturServerError') }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</template>
</div>
</template>
@@ -542,25 +638,66 @@ export const AbgabeStudentDetail = {
<h5>{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}</h5>
</div>
</div>
</div>
<div v-if="confetti_on_endupload" id="confetti-container"></div>
<bs-modal
ref="modalTitelEdit"
class="bootstrap-prompt"
dialogClass="bordered-modal"
bodyClass="px-4 py-4"
>
<template v-slot:title>
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
</template>
<template v-slot:default>
<div class="mb-2">
<label class="form-label fw-bold">
{{$capitalize( $p.t('abgabetool/c4titel') )}}
</label>
<Textarea
v-model="editingTitel"
rows="2"
maxlength="1024"
class="form-control w-100"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
</template>
<template v-slot:footer>
<button
class="btn btn-secondary"
@click="$refs.modalTitelEdit.hide()"
>
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
</button>
<button
class="btn btn-primary"
:disabled="!editingTitel.trim()"
@click="saveTitel"
>
<i class="fa-solid fa-floppy-disk me-1"></i>
{{$capitalize( $p.t('ui/speichern') )}}
</button>
</template>
</bs-modal>
<bs-modal
ref="modalContainerEnduploadZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg">
dialogClass="bordered-modal modal-lg"
bodyClass="px-4 py-4">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">Student UID: {{ projektarbeit?.student_uid}}</p>
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
</div>
</template>
<template v-slot:default>
@@ -576,15 +713,6 @@ export const AbgabeStudentDetail = {
</div>
</div>
<!-- lektor fills these out-->
<!-- <div class="row mb-3 align-items-start">-->
<!-- <div class="row">Kontrollierte Schlagwörter</div>-->
<!-- <div class="row">-->
<!-- <Textarea v-model="form.kontrollschlagwoerter"></Textarea>-->
<!-- </div>-->
<!-- -->
<!-- -->
<!-- </div>-->
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
@@ -631,7 +759,6 @@ export const AbgabeStudentDetail = {
<div class="col-9"></div>
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
<div class="col-1">
<Checkbox
v-model="eidAkzeptiert"
:binary="true"
@@ -647,8 +774,8 @@ export const AbgabeStudentDetail = {
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
`,
};
@@ -0,0 +1,251 @@
import AbgabeterminStatusLegende from "./StatusLegende.js";
import {formatDateTime} from "./dateUtils.js";
export const AbgabeStudentTimeline = {
name: "AbgabeStudentTimeline",
components: {
AbgabeterminStatusLegende,
Timeline: primevue.timeline,
},
props: {
projekte: { type: Array, default: () => [] },
notenOptions: { type: Array, default: () => [] },
formatDateFn: { type: Function, required: true }
},
data() {
return {
legendExpanded: false,
expandedProjects: {}
}
},
computed: {
student() {
return this.projekte?.[0] ?? null
}
},
watch: {
projekte: {
immediate: true,
handler(val) {
// open all projects by default whenever the student changes
const state = {}
val?.forEach(p => { state[p.projektarbeit_id] = true })
this.expandedProjects = state
this.legendExpanded = false
}
}
},
methods: {
getNoteBezeichnung(projektarbeit) {
if(projektarbeit.note && this.notenOptions) {
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
return noteOpt?.bezeichnung
} else {
return ''
}
},
getSavedTerminInfoString(termin) {
const isUpdate = termin.updateamum != null;
const fullname = isUpdate
? termin.updatevon_fullname
: termin.insertvon_fullname;
const datetime = isUpdate
? termin.updateamum
: termin.insertamum;
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
},
getItemBezeichnung(item) {
if (!item?.bezeichnung) return ''
return item.bezeichnung?.bezeichnung ?? item.bezeichnung
},
getItemNote(item) {
if (!item?.note) return ''
if (item.note?.bezeichnung) return item.note.bezeichnung
return this.notenOptions?.find(n => n.note == item.note)?.bezeichnung ?? String(item.note)
},
getIconClass(dateStyle) {
return ({
verspaetet: 'fa-solid fa-triangle-exclamation',
verpasst: 'fa-solid fa-calendar-xmark',
abzugeben: 'fa-solid fa-hourglass-half',
standard: 'fa-solid fa-clock',
abgegeben: 'fa-solid fa-paperclip',
beurteilungerforderlich: 'fa-solid fa-list-check',
bestanden: 'fa-solid fa-check',
nichtbestanden: 'fa-solid fa-circle-exclamation',
})[dateStyle] ?? ''
},
getBetreuerLabel(projekt) {
return projekt.erstbetreuer_full_name
|| (projekt.betreuer_vorname ? `${projekt.betreuer_vorname} ${projekt.betreuer_nachname}`.trim() : null)
},
toggleProject(id) {
this.expandedProjects[id] = !this.expandedProjects[id]
}
},
template: `
<div v-if="student">
<div class="d-flex align-items-baseline gap-2 mb-3 pb-2 border-bottom">
<span class="fw-bold fs-6">{{ student.student_vorname }} {{ student.student_nachname }}</span>
<span class="text-muted small">{{ student.student_uid }}</span>
<span v-if="student.matrikelnr" class="text-muted small ms-auto">{{ student.matrikelnr }}</span>
</div>
<div v-for="projekt in projekte" :key="projekt.projektarbeit_id" class="mb-2">
<button
class="btn btn-sm w-100 text-start d-flex align-items-center gap-2 rounded bg-light border-0 py-2 px-3"
@click="toggleProject(projekt.projektarbeit_id)"
>
<i
:class="expandedProjects[projekt.projektarbeit_id] ? 'fa-solid fa-chevron-down' : 'fa-solid fa-chevron-right'"
style="width: 12px; flex-shrink: 0;"
></i>
<span class="fw-semibold text-truncate flex-grow-1" :title="projekt.titel">
{{ projekt.titel || projekt.projekttyp_kurzbz || projekt.projektarbeit_id }}
</span>
<span
v-if="projekt.note"
class="small fw-semibold mx-2 flex-shrink-0"
>
{{ getNoteBezeichnung(projekt) }}
</span>
<span class="text-muted small me-1 flex-shrink-0">{{ projekt.studiensemester_kurzbz }}</span>
<span class="badge bg-secondary flex-shrink-0">{{ projekt.abgabetermine?.length ?? 0 }}</span>
</button>
<div v-show="expandedProjects[projekt.projektarbeit_id]" class="px-2 pt-1">
<div v-if="getBetreuerLabel(projekt)" class="text-muted small px-1 mb-2">
{{ projekt.betreuerart || $capitalize($p.t('abgabetool/c4erstbetreuerv2')) }}:
{{ getBetreuerLabel(projekt) }}
</div>
<Timeline :value="projekt.abgabetermine" align="right">
<template #marker="slotProps">
<!-- padding:0 overrides the 34px accordion left-padding from the -header CSS class -->
<div
:class="slotProps.item.dateStyle + '-header'"
style="height: 26px; width: 26px; padding: 0; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"
>
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size: 0.75rem;"></i>
</div>
</template>
<template #opposite="slotProps">
<div class="text-end small text-nowrap text-muted">
{{ formatDateFn(slotProps.item.datum) }}
</div>
</template>
<template #content="slotProps">
<div class="small pb-1">
<div class="d-flex align-items-center gap-1 fw-semibold">
<div
:class="slotProps.item.dateStyle + '-header'"
style="height:20px;width:20px;padding:0;border-radius:3px;display:flex;align-items:center;justify-content:center;flex-shrink:0;"
>
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size:.65rem;"></i>
</div>
<span>
{{ getItemBezeichnung(slotProps.item) }}
</span>
<i
v-if="slotProps.item.fixtermin"
class="fa-solid fa-lock text-muted"
title="Fixtermin"
></i>
<i
v-if="slotProps.item.abgabedatum"
class="fa-solid fa-file text-muted"
:title="$capitalize($p.t('abgabetool/c4abgabedatum'))"
></i>
<div class="flex-grow-1 text-end">
<span
v-if="slotProps.item.noteBackend?.bezeichnung"
class="fw-bold"
>
{{ slotProps.item.noteBackend?.positiv
? $capitalize($p.t('abgabetool/c4positivBenotet'))
: $capitalize($p.t('abgabetool/c4negativBenotet'))
}}
</span>
<span
v-else-if="slotProps.item.bezeichnung?.benotbar"
class="fw-bold text-muted"
>
{{ $capitalize($p.t('abgabetool/c4notYetGraded')) }}
</span>
</div>
</div>
<div
v-if="slotProps.item.kurzbz"
class="text-muted fst-italic ms-4"
>
{{ slotProps.item.kurzbz }}
</div>
<div
v-if="slotProps.item.abgabedatum"
class="small text-muted ms-4"
>
{{ $capitalize($p.t('abgabetool/c4abgabedatum')) }}:
{{ formatDateFn(slotProps.item.abgabedatum) }}
</div>
<div
v-if="slotProps.item.beurteilungsnotiz"
class="small ms-4 mt-1 text-muted"
>
{{ slotProps.item.beurteilungsnotiz }}
</div>
<div
v-if="slotProps.item.insertamum"
class="small text-muted ms-4 mt-1"
style="font-size: .72rem;"
>
<i class="fa-solid fa-clock-rotate-left me-1"></i>
{{ getSavedTerminInfoString(slotProps.item) }}
</div>
</div>
</template>
</Timeline>
</div>
</div>
<div class="mt-3 border-top pt-2">
<button
class="btn btn-link btn-sm p-0 text-muted text-decoration-none d-flex align-items-center gap-1"
@click="legendExpanded = !legendExpanded"
>
<i :class="legendExpanded ? 'fa-solid fa-chevron-up' : 'fa-solid fa-chevron-down'" style="font-size: 0.7rem;"></i>
Legende
</button>
<div v-show="legendExpanded" class="mt-2">
<AbgabeterminStatusLegende />
</div>
</div>
</div>
`
}
export default AbgabeStudentTimeline;
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,7 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass } from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/Dates.js';
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter",
@@ -31,19 +32,14 @@ export const AbgabetoolMitarbeiter = {
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
}
},
props: {
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards
}
}
},
data() {
return {
tableData: null,
filteredRows: null,
count: 0,
filteredcount: 0,
selectedcount: 0,
qgate1FilterSelected: [],
qgate2FilterSelected: [],
abgabetypenBetreuer: null,
detailIsFullscreen: false,
phrasenPromise: null,
@@ -58,7 +54,7 @@ export const AbgabetoolMitarbeiter = {
allowedNotenOptions: null,
notenOptionsNonFinal: null,
serienTermin: Vue.reactive({
datum: new Date(),
datum: getViennaTodayISO(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
@@ -80,7 +76,7 @@ export const AbgabetoolMitarbeiter = {
abgabeTableOptions: {
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitDataStretch',
layout: 'fitData',
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
selectable: true,
selectableCheck: this.selectionCheck,
@@ -138,38 +134,81 @@ export const AbgabetoolMitarbeiter = {
handleClick: this.selectAllHandler
},
width: 50,
cssClass: 'sticky-col'
cssClass: 'sticky-col',
visible: true
},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), field: 'prevTermin',
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, headerFilter: false, headerSort: false, minWidth: 140, visible: true, tooltip: false, cssClass: 'sticky-col'},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, visible: false,tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 150, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, minWidth: 200, formatter: this.centeredTextFormatter},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))),
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
tooltip: this.toolTipFuncPrevTermin,
field: 'prevTermin', formatter: this.abgabterminFormatter, width: 250, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
tooltip: this.toolTipFuncNextTermin,
formatter: this.abgabterminFormatter, width: 250, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false},
headerFilter: this.qgateHeaderFilterEditor,
headerFilterFunc: this.qgateHeaderFilterFunc,
headerFilterParams: {},
field: 'qgate1Status',
formatter: this.centeredTextFormatter,
titleFormatter: this.shortLongTitleFormatter,
titleFormatterParams: {
shortForm: 'QG1'
},
width: 50,
tooltip: (e, cell) => {
const data = cell.getData();
return data.qgate1Status
}
},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))),
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}
headerFilter: this.qgateHeaderFilterEditor,
headerFilterFunc: this.qgateHeaderFilterFunc,
headerFilterParams: {},
field: 'qgate2Status',
formatter: this.centeredTextFormatter,
titleFormatter: this.shortLongTitleFormatter,
titleFormatterParams: {
shortForm: 'QG2'
},
width: 50,
tooltip: (e, cell) => {
const data = cell.getData();
return data.qgate2Status
}
}
],
persistence: false,
persistenceID: 'abgabeTableBetreuer2026-02-26'
persistenceID: 'abgabeTableBetreuer2026-05-26'
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
@@ -200,11 +239,383 @@ export const AbgabetoolMitarbeiter = {
})
this.selectedData = data
this.selectedcount = data.length;
}
},
{
event: 'dataFiltered',
handler: (filters, rows) => {
this.filteredRows = rows;
this.filteredcount = rows.length;
if (!this.selectedData.length) return;
const visibleData = new Set(rows.map(r => r.getData()));
const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd));
if (!filteredOut.length) return;
const filteredOutSet = new Set(filteredOut);
this.$refs.abgabeTable.tabulator.getSelectedRows()
.filter(r => filteredOutSet.has(r.getData()))
.forEach(r => r.deselect());
}
}
]};
},
methods: {
async openBenotung(type, link) {
if(type === 'new') {
window.open(link, '_blank')
} else if(type === 'old') {
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4aeltereParbeitBenotenv2'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
window.open(link, '_blank')
} else {
// show info text that no endupload with abgabe has been found
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4keinEnduploadErfolgt'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
}
},
formAction(cell) {
const actionButtons = document.createElement('div');
actionButtons.className = "d-flex gap-3";
actionButtons.style.display = "flex";
actionButtons.style.alignItems = "stretch";
actionButtons.style.justifyContent = "start";
actionButtons.style.height = "100%";
const val = cell.getValue();
const data = cell.getRow().getData()
const createButton = (iconClass, titleKey, clickHandler) => {
const btn = document.createElement('button');
btn.className = 'btn btn-outline-secondary';
btn.style.display = "flex";
btn.style.alignItems = "center"; // center icon vertically
btn.style.justifyContent = "center"; // center icon horizontally
btn.style.height = "100%"; // fill parent container height
btn.style.aspectRatio = "1 / 1"; // keep square shape (optional)
btn.style.padding = "0"; // remove extra padding for compactness
if(iconClass == 'fa fa-timeline') btn.style.transform = "rotate(90deg)";
btn.innerHTML = `<i class="${iconClass}" style="color:#00649C; font-size:1.1rem;"></i>`;
btn.title = this.$capitalize(this.$p.t(titleKey));
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
clickHandler();
});
return btn;
};
actionButtons.append(
createButton('fa fa-folder-open', 'abgabetool/c4details', () => this.setDetailComponent(val)),
);
if(data.isCurrent && data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkNew) {
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('new', data.beurteilungLinkNew)))
} else if(data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkOld) {
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('old', data.beurteilungLinkOld)))
}
if(this.checkForZweitbetreuerTokenMailAvailability(data)) {
actionButtons.append(createButton('fa fa-envelope-open-text', 'abgabetool/c4zweitBegutachterTokenMailSenden', () => this.sendZweitbetreuerToken(data)))
}
return actionButtons;
},
checkForZweitbetreuerTokenMailAvailability(data) {
const hasEndabgabeWithUpload = !!data.abgabetermine.find(termin => termin.abgabedatum !== null && termin.paabgabetyp_kurzbz == 'end')
const hasZweitbetreuerWithoutBenutzerUid = data.zweitbetreuer_person_id !== null && data.zweitbetreuer_benutzer_uid === null
return hasEndabgabeWithUpload && hasZweitbetreuerWithoutBenutzerUid
},
sendZweitbetreuerToken(data) {
this.$api.call(ApiAbgabe.sendZweitbetreuerTokenMail(data.projektarbeit_id, data.betreuer_person_id, data.student_uid))
.then(res => {
if(res.meta.status == 'success') this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4zweitBegutachterTokenMailSuccess'))
})
},
getDateStyleHtml(dateStyle) {
const iconMap = {
'verspaetet': '<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)
@@ -217,7 +628,7 @@ export const AbgabetoolMitarbeiter = {
})
const uniqueRecipients = [...new Set(recipientList)];
const subject = ""; // empty subject line
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p)
},
getQGateStatusList() {
return [
@@ -257,7 +668,7 @@ export const AbgabetoolMitarbeiter = {
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = luxon.DateTime.fromISO(val);
dt = toViennaDate(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
@@ -386,6 +797,9 @@ export const AbgabetoolMitarbeiter = {
}
this.stateRestored = true
// ensure that the filterCollapseables thingy has the correct values
this.$refs.abgabeTable.setSelectedFields();
}
});
@@ -451,6 +865,32 @@ export const AbgabetoolMitarbeiter = {
projekt.qgate2StatusRank = 1
}
})
// set shorthand statuscode once real status has been determined
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
},
mapRankToShortStatus(rank) {
switch(rank){
case 0: // kein termin vorhanden
return '--'
break;
case 1: // noch nicht stattgefunden
return 'o'
break;
case 2: // noch nicht abgegeben
return '?'
break;
case 3: // noch nicht benotet
return '~'
break;
case 4: // negativ benotet
return '-'
break;
case 5: // positiv benotet
return '+'
break;
}
},
checkAbgabetermineProjektarbeit(projekt) {
const now = luxon.DateTime.now()
@@ -460,7 +900,7 @@ export const AbgabetoolMitarbeiter = {
// while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
const date = toViennaDate(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
@@ -517,11 +957,11 @@ export const AbgabetoolMitarbeiter = {
const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung
return '<div style="display: flex; height: 100%">' +
'<div class=' + val.dateStyle + "-header" + ' style="width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
icon +
'<div 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%; word-wrap: break-word; white-space: normal;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
'</div>'+
'</div>'
@@ -545,16 +985,19 @@ export const AbgabetoolMitarbeiter = {
},
selectAllHandler(e, cell) {
const table = cell.getTable();
const rows = table.getRows();
const rows = this.filteredRows ?? table.getRows();
// custom select all logic
const allowed = rows.filter(r => r.getData().selectable);
// since betreuerpage acctually has logic behind selectable flag, it is important to go over allowed only here
const selected = allowed.every(r => r.isSelected());
if(selected) {
if(selected){
allowed.forEach(r => r.deselect());
e.target.checked = false;
} else {
allowed.forEach(r => r.select());
e.target.checked = true;
}
// stop built-in handler
@@ -568,15 +1011,7 @@ export const AbgabetoolMitarbeiter = {
return option.bezeichnung
},
formatDate(dateParam) {
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`;
return formatISODate(dateParam);
},
undoSelection(cell) {
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
@@ -589,6 +1024,8 @@ export const AbgabetoolMitarbeiter = {
},
selectionCheck(row) {
const data = row.getData()
// zweitbetreuer cant select projektarbeiten for serientermine
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
return true
},
@@ -612,7 +1049,7 @@ export const AbgabetoolMitarbeiter = {
addSeries() {
this.saving = true
this.$api.call(ApiAbgabe.postSerientermin(
this.serienTermin.datum.toISOString(),
this.serienTermin.datum,
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
@@ -710,7 +1147,7 @@ export const AbgabetoolMitarbeiter = {
termin.allowedToSave = paIsBenotet ? false : true
// lektoren are not allowed to delete deadlines with existing submissions
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum && !termin.note
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
@@ -727,28 +1164,37 @@ export const AbgabetoolMitarbeiter = {
},
centeredTextFormatter(cell) {
const val = cell.getValue()
if(!val) return
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<p style="max-width: 100%; width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal; margin: 0px; text-align: center">'+val+'</p></div>'
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: center; align-items: center; height: 100%">' +
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>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
if(val) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
} else return '-'
},
pkzTextFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</a></div>'
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
@@ -765,10 +1211,9 @@ export const AbgabetoolMitarbeiter = {
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
this.tableData = data[0]?.retval?.map(projekt => {
this.projektarbeiten = data[0]?.retval?.map(projekt => {
this.checkAbgabetermineProjektarbeit(projekt)
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
@@ -787,9 +1232,10 @@ export const AbgabetoolMitarbeiter = {
titel: projekt.titel
}
})
this.count = this.projektarbeiten.length
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(this.tableData);
this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten);
},
loadProjektarbeiten(all = false, callback) {
this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all))
@@ -838,9 +1284,20 @@ export const AbgabetoolMitarbeiter = {
}
this.serienTermin.upload_allowed = newVal.upload_allowed_default
},
}
},
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 = []
@@ -869,6 +1326,8 @@ export const AbgabetoolMitarbeiter = {
}
},
created() {
document.documentElement.classList.add('abgabetool');
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
// fetch config to avoid hard coded links
@@ -910,12 +1369,16 @@ export const AbgabetoolMitarbeiter = {
mounted() {
this.setupMounted()
},
beforeUnmount() {
document.documentElement.classList.remove('abgabetool');
},
template: `
<template v-if="phrasenResolved">
<FhcOverlay :active="loading || saving"></FhcOverlay>
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
dialogClass="modal-lg">
dialogClass="modal-lg"
bodyClass="px-4 py-4">
<template v-slot:title>
<div>
{{ $p.t('abgabetool/neueTerminserie') }}
@@ -935,6 +1398,7 @@ export const AbgabetoolMitarbeiter = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -982,7 +1446,8 @@ export const AbgabetoolMitarbeiter = {
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true"
@toggle-fullscreen="handleToggleFullscreenDetail">
@toggle-fullscreen="handleToggleFullscreenDetail"
bodyClass="px-4 py-4">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
@@ -1001,12 +1466,13 @@ export const AbgabetoolMitarbeiter = {
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div id="abgabetable" style="max-height:40vw;">
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<h2>{{$p.t('abgabetool/abgabetoolTitleBetreuer')}}</h2>
<hr>
<core-filter-cmpt
:title="''"
@uuidDefined="handleUuidDefined"
ref="abgabeTable"
:description="countsToHTML"
:newBtnShow="true"
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
:newBtnDisabled="!selectedData.length"
@@ -1,14 +1,17 @@
import AbgabeDetail from "./AbgabeStudentDetail.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
import { validateThesisTitle } from './titleValidation.js'
export const AbgabetoolStudent = {
name: "AbgabetoolStudent",
components: {
Accordion: primevue.accordion,
AccordionTab: primevue.accordiontab,
Textarea: primevue.textarea,
BsModal,
AbgabeDetail,
FhcOverlay
@@ -17,20 +20,16 @@ export const AbgabetoolStudent = {
return {
notenOptions: Vue.computed(() => this.notenOptions),
isViewMode: Vue.computed(() => this.isViewMode),
moodle_link: Vue.computed(() => this.moodle_link)
moodle_link: Vue.computed(() => this.moodle_link),
title_edit_allowed: Vue.computed(() => this.title_edit_allowed),
confetti_on_endupload: Vue.computed(() => this.confetti_on_endupload),
siginfolink_german: Vue.computed(() => this.siginfolink_german),
siginfolink_english: Vue.computed(() => this.siginfolink_english)
}
},
props: {
student_uid_prop: {
default: null
},
viewData: {
type: Object,
required: true,
default: () => ({uid: ''}),
validator(value) {
return value && value.uid
}
}
},
data() {
@@ -44,14 +43,78 @@ export const AbgabetoolStudent = {
detail: null,
projektarbeiten: null,
selectedProjektarbeit: null,
moodle_link: null
moodle_link: null,
title_edit_allowed: null,
confetti_on_endupload: null,
siginfolink_german: null,
siginfolink_english: null,
editingTitel: '',
editingProjektarbeit: null,
uid: null
};
},
methods: {
openTitelEdit(projektarbeit, event) {
// stop the click from toggling the accordion tab
event.stopPropagation();
this.editingProjektarbeit = projektarbeit;
this.editingTitel = projektarbeit.titel ?? '';
this.$refs.modalTitelEdit.show();
},
async saveTitel() {
const validation = validateThesisTitle(this.editingTitel);
if (!validation.isValid) {
if (validation.error === 'empty') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
} else if (validation.error === 'invalid_characters') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
}
return false;
}
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
acceptClass: 'p-button-primary',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'p-button-secondary'
});
if (confirmed === false) return;
this.loading = true;
this.$api.call(
ApiAbgabe.postStudentProjektarbeitTitel(
this.editingProjektarbeit.projektarbeit_id,
validation.cleanedTitle
)
).then(res => {
if (res.meta.status === 'success') {
// update the local list entry in-place so the accordion header reflects it immediately
this.editingProjektarbeit.titel = res.data;
// keep the open detail modal in sync if it happens to be showing this projektarbeit
if (this.selectedProjektarbeit?.projektarbeit_id === this.editingProjektarbeit.projektarbeit_id) {
this.selectedProjektarbeit.titel = res.data;
}
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
this.$refs.modalTitelEdit.hide();
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
}
}).finally(() => {
this.loading = false;
});
},
handleTitelUpdated(projektarbeit_id, titel) {
const pa = this.projektarbeiten?.find(p => p.projektarbeit_id === projektarbeit_id);
if (pa) pa.titel = titel;
},
checkQualityGatesStrict(termine) {
let qgate1Passed = false
let qgate2Passed = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
@@ -68,7 +131,7 @@ export const AbgabetoolStudent = {
checkQualityGatesOptional(termine) {
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
let qgate1positiv = true
if(qgate1found) {
qgate1positiv = false
@@ -109,47 +172,35 @@ export const AbgabetoolStudent = {
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
const paIsBenotet = pa.note !== null
pa.abgabetermine.forEach(termin => {
termin.file = []
termin.allowedToUpload = false
if(termin.paabgabetyp_kurzbz == 'end') {
// old assumed production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = true
} else if(termin.fixtermin) {
termin.allowedToUpload = !this.isPastDate(termin.datum)
} else {
// this could confuse people since we should dont show people this flag
termin.allowedToUpload = termin.upload_allowed
termin.allowedToUpload = termin.upload_allowed
}
// blocks client upload button if projektarbeitet is already beurteilt und thus further abgaben on any termin should be blocked
if(paIsBenotet) termin.allowedToUpload = false
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
})
pa.betreuer = this.buildBetreuer(pa)
pa.student_uid = this.student_uid
this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show()
}).finally(()=>{this.loading=false})
},
centeredTextFormatter(cell) {
@@ -171,8 +222,8 @@ export const AbgabetoolStudent = {
},
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
@@ -182,19 +233,17 @@ export const AbgabetoolStudent = {
} else return '-'
},
buildMailToLink(projekt) {
// should always be "projekt.mitarbeiter_uid +'@'+ this.domain", built in backend
return 'mailto:' + projekt.email
},
buildBetreuer(abgabe) {
return (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
},
async setupData(data){
// this.projektarbeiten = data[0]
const projektarbeiten = data[0] ?? null
if(!projektarbeiten) return
this.projektarbeiten = projektarbeiten.map(projekt => {
let mode = 'detailTermine'
return {
...projekt,
details: {
@@ -228,16 +277,14 @@ export const AbgabetoolStudent = {
.then(res => {
resolve(res)
})
})
})
},
async setupMounted() {
this.loadProjektarbeiten()
},
getAccTabHeaderForProjektarbeit(projektarbeit) {
let title = ''
title += projektarbeit.titel ?? this.$p.t('abgabetool/keinTitel')
return title
},
getMailLink(projektarbeit) {
@@ -260,23 +307,25 @@ export const AbgabetoolStudent = {
window.open(projektarbeit.beurteilung2)
}
},
watch: {
},
watch: {},
computed: {
isViewMode() {
return this.student_uid !== this.viewData.uid
return this.student_uid !== this.uid
},
student_uid() {
return this.student_uid_prop || this.viewData?.uid || null
return this.student_uid_prop || this.uid || null
}
},
async created() {
// make sure zoom media query doesnt spill ever to other CIS4 sites
document.documentElement.classList.add('abgabetool');
this.$api.call(ApiAuthinfo.getAuthUID()).then(res => this.uid = res.data.uid)
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
this.loading = true
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
@@ -289,16 +338,18 @@ export const AbgabetoolStudent = {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
this.moodle_link = res.data?.moodle_link
this.title_edit_allowed = res.data?.title_edit_allowed
this.confetti_on_endupload = res.data?.confetti_on_endupload
this.siginfolink_german = res.data?.siginfolink_german
this.siginfolink_english = res.data?.siginfolink_english
}).catch(e => {
this.loading = false
})
@@ -306,26 +357,73 @@ export const AbgabetoolStudent = {
mounted() {
this.setupMounted()
},
beforeUnmount() {
document.documentElement.classList.remove('abgabetool');
},
template: `
<template v-if="phrasenResolved">
<FhcOverlay :active="loading"></FhcOverlay>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true">
dialogClass="modal-xl" :allowFullscreenExpand="true" bodyClass="px-4 py-4">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
</div>
</template>
<template v-slot:default>
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
<AbgabeDetail
:projektarbeit="selectedProjektarbeit"
@titel-updated="handleTitelUpdated"
></AbgabeDetail>
</template>
</bs-modal>
<bs-modal
ref="modalTitelEdit"
class="bootstrap-prompt"
dialogClass="bordered-modal"
bodyClass="px-4 py-4"
>
<template v-slot:title>
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
</template>
<template v-slot:default>
<div class="mb-2">
<label class="form-label fw-bold">
{{$capitalize( $p.t('abgabetool/c4titel') )}}
</label>
<Textarea
v-model="editingTitel"
rows="2"
maxlength="1024"
class="form-control w-100"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
</template>
<template v-slot:footer>
<button
class="btn btn-secondary"
@click="$refs.modalTitelEdit.hide()"
>
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
</button>
<button
class="btn btn-primary"
:disabled="!editingTitel.trim()"
@click="saveTitel"
>
<i class="fa-solid fa-floppy-disk me-1"></i>
{{$capitalize( $p.t('ui/speichern') )}}
</button>
</template>
</bs-modal>
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null">
<div v-if="projektarbeiten === null || projektarbeiten?.length == 0">
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
@@ -335,8 +433,12 @@ export const AbgabetoolStudent = {
<template #header>
<div class="d-flex row w-100">
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'">
<span>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'"
style="min-width: 0;">
<span
:title="getAccTabHeaderForProjektarbeit(projektarbeit)"
style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 600px;"
>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
</div>
<div class="col-6 text-end">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
@@ -402,12 +504,33 @@ export const AbgabetoolStudent = {
{{ projektarbeit.projekttypbezeichnung }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.titel }}
<div class="col-8 col-md-9 d-flex align-items-center gap-2" style="min-width: 0;">
<span
:title="projektarbeit.titel"
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>{{ projektarbeit.titel }}</span>
<button
v-if="title_edit_allowed && !isViewMode && projektarbeit.note == null"
class="btn btn-sm btn-outline-secondary border-0 p-1"
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
@click="openTitelEdit(projektarbeit, $event)"
>
<i class="fa-solid fa-pen"></i>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-8 col-md-9">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
</div>
</div>
</AccordionTab>
</template>
</Accordion>
@@ -415,4 +538,4 @@ export const AbgabetoolStudent = {
`,
};
export default AbgabetoolStudent;
export default AbgabetoolStudent;
@@ -0,0 +1,33 @@
const zone = 'Europe/Vienna';
export function getViennaTodayISO() {
return luxon.DateTime.now().setZone(zone).toISODate();
}
export function formatISODate(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromISO(String(dateParam), { zone });
return date.isValid ? date.toFormat('dd.MM.yyyy') : '';
}
export function formatDateTime(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromSQL(dateParam);
return date.isValid ? date.toFormat("dd.MM.yyyy HH:mm") : '';
}
export function toViennaDate(dateParam) {
if (!dateParam) return null;
return luxon.DateTime.fromISO(String(dateParam), { zone });
}
export function compareISODateValues(a, b) {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return String(a).localeCompare(String(b));
}
@@ -1,8 +1,8 @@
const zone = 'Europe/Vienna';
const today = luxon.DateTime.now().setZone(zone);
export function getDateStyleClass(termin, notenOptions) {
const today = luxon.DateTime.now().setZone(zone);
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
@@ -28,10 +28,11 @@ export function getDateStyleClass(termin, notenOptions) {
// no submission yet
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
// GENERIC STATUS
return datum < today ? 'verpasst' : 'standard';
}
// GENERIC STATUS — applies to all termine
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
@@ -0,0 +1,25 @@
/**
* Validates the thesis title on the frontend
* @param {string} title - The raw input from the title field
* @returns {object} Validation result containing status and cleaned title
*/
export function validateThesisTitle(title) {
if (!title) {
return { isValid: false, error: 'empty' };
}
// Replicate strip_tags / trim
const cleanedTitle = title.replace(/<\/?[^>]+(>|$)/g, "").trim();
if (cleanedTitle === '') {
return { isValid: false, error: 'empty' };
}
// Replicate the emoji/pictograph rejection
const emojiRegex = /\p{Extended_Pictographic}/u;
if (emojiRegex.test(cleanedTitle)) {
return { isValid: false, error: 'invalid_characters' };
}
return { isValid: true, cleanedTitle: cleanedTitle };
}
@@ -26,13 +26,13 @@ export default {
internMail(event) {
if (this.internMails.length)
{
splitMailsHelper(this.internMails, event, null, this.$fhcAlert, this.$p)
splitMailsHelper(this.internMails, event, null, null, this.$fhcAlert, this.$p)
}
},
privateMail(event) {
if (this.privateMails.length)
{
splitMailsHelper(this.privateMails, event, null, this.$fhcAlert, this.$p)
splitMailsHelper(this.privateMails, event, null,null, this.$fhcAlert, this.$p)
}
}
},
@@ -5,6 +5,7 @@ import PvAutoComplete from "../../../../../../../index.ci.php/public/js/componen
import ApiStvProjektarbeit from '../../../../../api/factory/stv/projektarbeit.js';
export default {
name: 'ProjektarbeitDetails',
components: {
FormForm,
FormInput,
@@ -110,6 +111,10 @@ export default {
this.formData.anmerkung = null;
this.$refs.formDetails.clearValidation();
},
setFormData(projektarbeit) {
this.formData = projektarbeit;
if (this.formData.firma_id) this.formData.firma = {firma_id: this.formData.firma_id, name: this.formData.firma_name};
},
getFormData(newProjektarbeit, studiensemester_kurzbz, additional_lehrveranstaltung_id) {
this.additional_lehrveranstaltung_id = additional_lehrveranstaltung_id;
@@ -148,8 +153,7 @@ export default {
return this.$api
.call(ApiStvProjektarbeit.loadProjektarbeit(projektarbeit_id))
.then(result => {
this.formData = result.data;
if (this.formData.firma_id) this.formData.firma = {firma_id: this.formData.firma_id, name: this.formData.firma_name};
this.setFormData(result.data)
return result;
})
.catch(this.$fhcAlert.handleSystemError)
@@ -9,6 +9,7 @@ import ProjektarbeitDetails from "./Details.js";
import Projektbetreuer from "./Projektbetreuer.js";
export default {
name: 'Projektarbeit',
components: {
CoreFilterCmpt,
BsModal,
@@ -213,17 +214,6 @@ export default {
});
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-users"></i>';
button.title = this.$p.t('projektarbeit', 'betreuerBearbeiten');
button.addEventListener('click', (event) => {
let data = cell.getData();
this.editedProjektarbeit = data;
this.actionEditBetreuer();
});
container.append(button);
button = document.createElement('button');
button.className = 'btn btn-outline-secondary btn-action';
button.innerHTML = '<i class="fa fa-xmark"></i>';
@@ -264,6 +254,7 @@ export default {
actionEditProjektarbeit() {
this.statusNew = false;
this.toggleMenu('details');
this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz);
this.$refs.projektarbeitModal.show();
},
actionEditBetreuer() {
@@ -280,18 +271,26 @@ export default {
.then(this.deleteProjektarbeit)
.catch(this.$fhcAlert.handleSystemError);
},
saveProjektarbeit() {
if(this.statusNew) this.addNewProjektarbeit()
else this.updateProjektarbeit()
},
addNewProjektarbeit() {
this.$refs.projektarbeitDetails.addNewProjektarbeit()
.then((result) => {
if(result?.data?.length) {
this.editedProjektarbeit = result.data[0]
this.$refs.projektarbeitDetails.setFormData(this.editedProjektarbeit)
this.toggleMenu('betreuer');
}
this.projektarbeitSaved();
})
.catch(this.$fhcAlert.handleSystemError);
},
updateProjektarbeit() {
this.$refs.projektarbeitDetails.updateProjektarbeit()
.then((result) => {
this.projektarbeitSaved();
})
.then(() => this.$refs.projektbetreuer.saveIfOpen())
.then(() => this.projektarbeitSaved())
.catch(this.$fhcAlert.handleSystemError);
},
deleteProjektarbeit(projektarbeit_id) {
@@ -308,7 +307,8 @@ export default {
projektarbeitSaved() {
this.reload();
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
this.hideModal('projektarbeitModal');
if(!this.statusNew) this.hideModal('projektarbeitModal');
else this.statusNew = false
},
setDefaultStunden(projekttyp_kurzbz) {
this.$refs.projektbetreuer.setDefaultStunden(projekttyp_kurzbz);
@@ -321,22 +321,22 @@ export default {
},
toggleMenu(tabId) {
this.activeTab = tabId;
if (this.statusNew == false) {
switch(tabId) {
case 'details':
this.$refs.projektarbeitDetails.getFormData(
this.statusNew, this.editedProjektarbeit?.studiensemester_kurzbz, this.editedProjektarbeit?.lehrveranstaltung_id
);
this.$refs.projektarbeitDetails.loadProjektarbeit(this.editedProjektarbeit?.projektarbeit_id);
break;
case 'betreuer':
this.$refs.projektbetreuer.getFormData(
this.editedProjektarbeit ? this.editedProjektarbeit.projekttyp_kurzbz : null
);
this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz);
break;
}
if (this.statusNew == false && tabId == 'details') {
this.$refs.projektarbeitDetails.getFormData(
this.statusNew, this.editedProjektarbeit?.studiensemester_kurzbz, this.editedProjektarbeit?.lehrveranstaltung_id
);
this.$refs.projektarbeitDetails.loadProjektarbeit(this.editedProjektarbeit?.projektarbeit_id);
} else if(tabId == 'betreuer') {
this.$refs.projektbetreuer.getFormData(
this.editedProjektarbeit ? this.editedProjektarbeit.projekttyp_kurzbz : null
);
this.$refs.projektbetreuer.getProjektbetreuer(this.editedProjektarbeit?.projektarbeit_id, this.editedProjektarbeit?.studiensemester_kurzbz);
}
},
resetFormData() {
this.$refs.projektarbeitDetails.resetForm()
this.$refs.projektbetreuer.resetForm()
}
},
template: `
@@ -358,46 +358,29 @@ export default {
</core-filter-cmpt>
<!--Modal: projektarbeitModal-->
<bs-modal ref="projektarbeitModal" dialog-class="modal-xl modal-dialog-scrollable" header-class="flex-wrap pb-0">
<bs-modal ref="projektarbeitModal" :dialog-class="(statusNew ? 'modal-xl ' : 'fhc-xxl-modal ' ) + 'modal-dialog-scrollable'"
header-class="flex-wrap pb-0"
@hideBsModal="resetFormData"
>
<template #title>
<p v-if="statusNew" class="fw-bold mt-3">{{$p.t('projektarbeit', 'projektarbeitAnlegen')}}</p>
<p v-else class="fw-bold mt-3">{{$p.t('projektarbeit', 'projektarbeitBearbeiten')}}</p>
</template>
<template #modal-header-content v-if="!statusNew">
<ul class="nav nav-tabs w-100 mt-3 msg_preview" id="pa_tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link" :class="activeTab == 'details' ? 'active' : ''" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="true" @click="toggleMenu('details')">Details</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" :class="activeTab == 'betreuer' ? 'active' : ''" id="betreuer-tab" data-bs-toggle="tab" data-bs-target="#betreuer" type="button" role="tab" aria-controls="betreuer" aria-selected="false" @click="toggleMenu('betreuer')">{{$p.t('projektarbeit', 'betreuerGross')}}</button>
</li>
</ul>
</template>
<div class="tab-content" id="pa_content">
<div class="tab-pane fade show" :class="activeTab == 'details' ? 'active' : ''" id="details" role="tabpanel" aria-labelledby="details-tab">
<div class="row">
<div class="col-12">
<projektarbeit-details ref="projektarbeitDetails" :student="student" @projekttyp-changed="setDefaultStunden">
</projektarbeit-details>
</div>
</div>
<div class="row" >
<div :class="statusNew ? 'col-12' : 'col-6'">
<projektarbeit-details ref="projektarbeitDetails" :student="student" @projekttyp-changed="setDefaultStunden">
</projektarbeit-details>
</div>
<div class="tab-pane fade show" :class="activeTab == 'betreuer' ? 'active' : ''" id="betreuer" role="tabpanel" aria-labelledby="betreuer-tab">
<div class="row">
<div class="col-12">
<projektbetreuer ref="projektbetreuer" :config="config" @betreuer-saved="reload"></projektbetreuer>
</div>
</div>
<div :class="statusNew ? '' : 'col-6'" :style="statusNew ? 'display: none' : ''">
<projektbetreuer ref="projektbetreuer" :config="config" @betreuer-saved="reload"></projektbetreuer>
</div>
</div>
<template #footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{$p.t('ui', 'abbrechen')}}</button>
<button v-if="statusNew" class="btn btn-primary" @click="addNewProjektarbeit()"> {{$p.t('ui', 'speichern')}}</button>
<button v-if="!statusNew && activeTab == 'details'" class="btn btn-primary" @click="updateProjektarbeit()"> {{$p.t('ui', 'speichern')}}</button>
<button type="button" class="btn btn-secondary" @click="resetFormData" data-bs-dismiss="modal">{{$p.t('ui', 'abbrechen')}}</button>
<button class="btn btn-primary" @click="saveProjektarbeit()"> {{$p.t('ui', 'speichern')}}</button>
</template>
</bs-modal>
@@ -10,6 +10,7 @@ import Vertrag from "./Vertrag.js";
import ApiStvProjektbetreuer from '../../../../../api/factory/stv/projektbetreuer.js';
export default {
name: 'Projektbetreuer',
components: {
CoreFilterCmpt,
BsModal,
@@ -292,21 +293,44 @@ export default {
this.$refs.projektbetreuerTable.tabulator.replaceData(this.addIds(result.data));
})
.catch(this.$fhcAlert.handleSystemError);
// get other initial data
this.$api
.call(ApiStvProjektbetreuer.getBetreuerarten())
.then(result => {
this.arrBetreuerart = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
this.$api
.call(ApiStvProjektbetreuer.getNoten())
.then(result => {
this.arrNoten = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
} else {
this.emptyBetreuerList();
}
},
saveProjektbetreuer() {
this.$refs.formProjektbetreuer.call(
_doSaveBetreuer() {
return this.$refs.formProjektbetreuer.call(
ApiStvProjektbetreuer.saveProjektbetreuer(this.projektarbeit_id, this.getFormDataWithBetreuer())
)
.then(result => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
).then(result => {
this.getProjektbetreuer(this.projektarbeit_id, this.studiensemester_kurzbz);
this.resetModes();
this.$emit('betreuerSaved');
})
.catch(this.$fhcAlert.handleSystemError);
return result;
});
},
// called by combined save button
saveIfOpen() {
if (!this.betreuerFormOpened) return Promise.resolve(null);
return this._doSaveBetreuer();
},
saveProjektbetreuer() {
this._doSaveBetreuer()
.then(() => this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')))
.catch(this.$fhcAlert.handleSystemError);
},
searchBetreuer(event) {
if (this.abortController.betreuer) {
@@ -539,7 +563,7 @@ export default {
</form-form>
<button class="btn btn-primary" v-show="betreuerFormOpened" @click="saveProjektbetreuer">
{{ $p.t('projektarbeit', 'betreuerSpeichern') }}
{{ $p.t('projektarbeit', 'betreuerSpeichernv2') }}
</button>
<!-- <div class = "mt-5" v-if="beurteilungDownloadLink !== null">
<div class="mb-1">
+6 -1
View File
@@ -220,6 +220,10 @@ export const CoreFilterCmpt = {
else
this.getFilter();
},
setSelectedFields() {
const cols = this.tabulator.getColumns();
this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField());
},
async initTabulator() {
let placeholder = '< Phrasen Plugin not loaded! >';
if (this.$p) {
@@ -337,7 +341,7 @@ export const CoreFilterCmpt = {
this.tabulator.on('tableBuilt', () => {
const cols = this.tabulator.getColumns();
this.fields = cols.map(col => col.getField());
this.selectedFields = cols.filter(col => col.isVisible()).map(col => col.getField());
this.setSelectedFields();
if (this.tabulator.options.persistence.headerFilter)
this._setHeaderFilter();
});
@@ -371,6 +375,7 @@ export const CoreFilterCmpt = {
});
this.tabulator.clearFilter();
this.filterActive = false;
this.$emit('headerFilterOn', this.filterActive)
},
_setHeaderFilter()
{
+50 -29
View File
@@ -1,45 +1,66 @@
export async function splitMailsHelper(mails, event, subject, alertPluginRef, phrasenPluginRef) {
export async function splitMailsHelper(mails, event, subject, body, alertPluginRef, phrasenPluginRef) {
await phrasenPluginRef.loadCategory('ui');
let splititem = ",";
let maillist = mails.join(splititem);
let mailto = "";
// take subject line length + '?subject=' length into account
const subjectlength = subject && typeof subject === 'string' ? subject.length + 9 : 0
if (maillist.length > 2024)
{
if (await alertPluginRef.confirm({message: phrasenPluginRef.t('stv', 'zuvieleEMails') }) === false)
return;
let useBcc = event?.ctrlKey || event?.metaKey;
// build query parameters using URLSearchParams to get encoding
const urlParams = new URLSearchParams();
if (subject && typeof subject === 'string') {
urlParams.append('subject', subject);
}
if (body && typeof body === 'string') {
urlParams.append('body', body);
}
// initial overhead: "mailto:?bcc=" -> 12 chars, "mailto:" -> 7 chars
const baseOverhead = useBcc ? 12 : 7;
let queryString = urlParams.toString().replace(/\+/g, '%20');;
let overhead = baseOverhead + (queryString ? 1 + queryString.length : 0); // +1 accounts for '?' or '&'
// calculate overhead with body to exceed the limit
if (overhead > 2024) {
await alertPluginRef.alertWarning(phrasenPluginRef.t('ui', 'bodyZuLang'));
urlParams.delete('body').replace(/\+/g, '%20');;
queryString = urlParams.toString();
overhead = baseOverhead + (queryString ? 1 + queryString.length : 0);
}
let firstrun = true;
let useBcc = event?.ctrlKey || event?.metaKey;
while (maillist.length > 0)
{
if (maillist.length + subjectlength > 2024)
{
let splitposition = maillist.lastIndexOf(splititem, 1900);
while (maillist.length > 0) {
let mailto = "";
if (maillist.length + overhead > 2024) {
let splitposition = maillist.lastIndexOf(splititem, 2024 - overhead);
// Fallback guard: if a single email address is somehow longer than the remaining space
if (splitposition === -1) {
splitposition = maillist.indexOf(splititem);
if (splitposition === -1) splitposition = maillist.length;
}
mailto = maillist.substring(0, splitposition);
maillist = maillist.substring(splitposition + 1);
}
else
{
} else {
mailto = maillist;
maillist = "";
}
// construct the clean mailLink
let mailLink = useBcc ? `mailto:?bcc=${mailto}` : `mailto:${mailto}`;
if(subject && typeof subject === 'string') mailLink += `?subject=${subject}`
if (firstrun)
{
window.location.href = mailLink;
firstrun = false;
}
else
{
if (await alertPluginRef.confirm({message: phrasenPluginRef.t('stv', 'weitereEMail')}) === true)
{
window.location.href = mailLink;
}
if (queryString) {
// If using BCC, the string already has a '?', so append with '&'. Otherwise, start with '?'
mailLink += useBcc ? `&${queryString}` : `?${queryString}`;
}
if (firstrun) {
window.location.href = mailLink;
firstrun = false;
} else {
if (await alertPluginRef.confirm({message: phrasenPluginRef.t('ui', 'weitereEMail')}) === true) {
window.location.href = mailLink;
} else {
break; // Stop processing further batches if the user cancels
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
// DatesManual.js: implemented custom input handling for tabulator date header filter,
// since primevue3 calendar manual input seems to be broken in this case. Main difference
// to normal Dates.js headerfilter is that this one does not automatically open the calendar
// overlay, as it tries to aggressively steal the focus away from the input field. try it out!
// Custom evaluation logic so Tabulator knows how to filter the array of Date objects
Tabulator.extendModule('filter', 'filters', {
"dates": (headerValue, rowValue) => {
if (!headerValue) return true;
let rowDate = new Date(rowValue);
if (Array.isArray(headerValue)) {
let startDate = new Date(headerValue[0]);
if (headerValue[1]) {
let endDate = new Date(headerValue[1]);
endDate.setHours(23, 59, 59, 999);
return rowDate >= startDate && rowDate <= endDate;
}
return rowDate.toDateString() === startDate.toDateString();
}
let singleDate = new Date(headerValue);
return rowDate.toDateString() === singleDate.toDateString();
}
});
export function dateFilter(cell, onRendered, success) {
let div = document.createElement('div');
let initialValue = null;
let val = cell.getValue();
if (Array.isArray(val)) {
const start = val[0] ? new Date(val[0]) : null;
const end = val[1] ? new Date(val[1]) : null;
initialValue = [start, end];
}
// Manual parser needed since we are bypassing PrimeVue's broken manualInput mode
function parseDMY(str) {
const m = str.trim().match(/^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$/);
if (!m) return null;
let year = parseInt(m[3]);
if (year < 100) year += 2000;
const d = new Date(year, parseInt(m[2]) - 1, parseInt(m[1]));
return isNaN(d.getTime()) ? null : d;
}
// String formatter to sync the raw text field when dates are picked via the calendar UI
function formatRange(dates) {
if (!dates) return '';
const fmt = d => d
? `${String(d.getDate()).padStart(2,'0')}.${String(d.getMonth()+1).padStart(2,'0')}.${d.getFullYear()}`
: '';
if (Array.isArray(dates)) {
return fmt(dates[0]) + (dates[1] ? ` - ${fmt(dates[1])}` : '');
}
return fmt(dates);
}
Vue.createApp({
components: { PrimevueCalendar: primevue.calendar },
data() {
return {
// Split state into a proper Date object (calendar) and a raw string (text input)
calVal: initialValue,
textVal: formatRange(initialValue)
};
},
watch: {
calVal(n) {
// Centralized synchronization: updates text representation
// AND safely notifies Tabulator of the value change
this.textVal = formatRange(n);
success(n);
}
},
methods: {
// Translates the typed string back into Date objects for Tabulator's filter
onTextChange() {
if (!this.textVal) {
this.calVal = null; // Triggers watcher -> success(null)
return;
}
const parts = this.textVal.split(/\s*-\s*/);
const start = parseDMY(parts[0]);
const end = parts[1] ? parseDMY(parts[1]) : null;
if (start) {
// Changing calVal automatically triggers the watcher,
// which handles executing success() exactly once.
this.calVal = [start, end];
}
}
},
// Native HTML input handles typing, PrimeVue calendar input is hidden (icon-only)
// Placeholder removed to match the rest of the application's header filters
template: `
<div style="display:flex;align-items:center;width:100%">
<input
type="text"
v-model="textVal"
@change="onTextChange"
@keydown.stop
@keypress.stop
@keyup.stop
@mousedown.stop
class="p-inputtext p-component"
style="flex:1;min-width:0"
/>
<primevue-calendar
v-model="calVal"
selection-mode="range"
show-button-bar
:showIcon="true"
:input-style="{display:'none'}"
dateFormat="dd.mm.yy">
</primevue-calendar>
</div>`
}).use(primevue.config.default).mount(div);
return div;
}
-76
View File
@@ -705,72 +705,7 @@ if (isset($_REQUEST["xmlformat"]) && $_REQUEST["xmlformat"] == "xml")
}
echo ' </anrechnungen>';
//Berufliche Kompetenzen
$studienplan = new studienplan();
$studienplan->loadStudienplan($studienplan_id);
$regelstudiendauer = $studienplan->regelstudiendauer;
$studienplan_ects = $studienplan->ects_stpl;
$ects_berufliche_kompetenzen = 0;
//bei masterlehrgängen und $studienplan_ects >= 120 ECTS: Andruck der beruflichen Kompetenzen, wenn die Lv angerechnet wurde
//TODO(Manu) check if rule still valid
if ($row->typ == 'l' && $regelstudiendauer >= 4)
{
$ects_berufliche_kompetenzen = 0;
echo '<berufliche_kompetenzen>';
echo '<header_berufliche_kompetenz>Validierung von beruflich erworbenen Kompetenzen</header_berufliche_kompetenz>';
$qry_sem_0="
SELECT
lehrveranstaltung_id,
lehrform_kurzbz,
sws,
lehre.tbl_lehrveranstaltung.bezeichnung,
bezeichnung_english,
ects,
benotungsdatum,
note,
positiv,
offiziell,
note.anmerkung
FROM
lehre.tbl_zeugnisnote zeugnis
JOIN lehre.tbl_note note USING(note)
JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
JOIN public.tbl_student student USING(student_uid)
WHERE
student_uid =".$db->db_add_param($uid_arr[$i])."
AND
lehre.tbl_lehrveranstaltung.semester = '0'
";
if($result_sem_0 = $db->db_query($qry_sem_0))
{
while ($row_sem_0 = $db->db_fetch_object($result_sem_0))
{
$benotungsdatum = $datum->formatDatum($row_sem_0->benotungsdatum, 'd/m/Y');
$note = $db->db_parse_bool($row_sem_0->offiziell) ? $row_sem_0->anmerkung : $row_sem_0->note;
$ects_berufliche_kompetenzen += $row_sem_0->ects;
echo '<lv_sem0>
<lv_id>' . $row_sem_0->lehrveranstaltung_id . '</lv_id>
<lehrform_kurzbz>' . $row_sem_0->lehrform_kurzbz . '</lehrform_kurzbz>
<bezeichnung><![CDATA[' . $row_sem_0->bezeichnung . ']]></bezeichnung>
<bezeichnung_englisch><![CDATA[' . $row_sem_0->bezeichnung_english . ']]></bezeichnung_englisch>
<sws_lv>'.$row_sem_0->sws.'</sws_lv>
<ects>'.$row_sem_0->ects.'</ects>
<note_positiv>'.$db->db_parse_bool($row_sem_0->positiv).'</note_positiv>
<note>'.$note.'</note>
<benotungsdatum>'.$benotungsdatum.'</benotungsdatum>
</lv_sem0>';
}
}
echo '<ects_berufliche_kompetenz>'.$ects_berufliche_kompetenzen.'</ects_berufliche_kompetenz>';
echo '</berufliche_kompetenzen>';
}
echo "<studiensemester>";
for($start = $semesterNumberStart; $start <= $semesterNumberEnd; $start++)
{
$semester_ects = 0;
@@ -793,7 +728,6 @@ if (isset($_REQUEST["xmlformat"]) && $_REQUEST["xmlformat"] == "xml")
AND zeugnis = true
AND status.ausbildungssemester = ".$db->db_add_param($start)."
AND status.status_kurzbz NOT IN('Unterbrecher', 'Interessent','Bewerber','Aufgenommener','Abgewiesener','Wartender')
--AND lehre.tbl_lehrveranstaltung.semester != '0'
ORDER BY datum ASC";
$semester_kurzbz = array();
@@ -842,7 +776,6 @@ if (isset($_REQUEST["xmlformat"]) && $_REQUEST["xmlformat"] == "xml")
WHERE
student_uid = ".$db->db_add_param($uid_arr[$i])."
AND zeugnis = true
AND lehre.tbl_lehrveranstaltung.semester != '0'
AND studiensemester_kurzbz in (".$sqlStudent->implode4SQL($aktuellesSemester).")";
if (defined('ZEUGNISNOTE_NICHT_ANZEIGEN'))
@@ -1201,19 +1134,10 @@ if (isset($_REQUEST["xmlformat"]) && $_REQUEST["xmlformat"] == "xml")
}
}
}
echo '<ects_gesamt>'.$semester_ects.'</ects_gesamt>';
echo '<ects_gesamt_positiv>'.$semester_ects_positiv.'</ects_gesamt_positiv>';
echo "</semesters>";
}
//TODO(Manu) check if rule still valid
if ($row->typ == 'l' && $regelstudiendauer >= 4)
{
$ects_total += $ects_berufliche_kompetenzen;
$ects_total_positiv += $ects_berufliche_kompetenzen;
}
echo "</studiensemester>";
echo " <ects_total>$ects_total</ects_total>";
echo " <ects_total_positiv>$ects_total_positiv</ects_total_positiv>";
-1
View File
@@ -94,7 +94,6 @@ require_once('dbupdate_3.4/71399_dashboard_update_widget_paths.php');
require_once('dbupdate_3.4/71645_studvw_messagetab_ladezeit.php');
require_once('dbupdate_3.4/71566_studienordnungsdokument_neuer_organisationseinheitstyp_programm.php');
require_once('dbupdate_3.4/70376_lohnguide.php');
require_once('dbupdate_3.4/75888_reihungstest_mehrfachdurchfuehrung.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';
@@ -1,15 +0,0 @@
<?php
if (! defined('DB_NAME')) exit('No direct script access allowed');
if ($result = $db->db_query("SELECT * FROM pg_class WHERE relname='idx_tbl_benutzerfunktion_uid'"))
{
if ($db->db_num_rows($result) == 0)
{
$qry = "CREATE INDEX idx_tbl_benutzerfunktion_uid ON public.tbl_benutzerfunktion USING btree (uid)";
if (! $db->db_query($qry))
echo '<strong>idx_tbl_benutzerfunktion_uid: ' . $db->db_last_error() . '</strong><br>';
else
echo 'Index idx_tbl_benutzerfunktion_uid angelegt<br>';
}
}
+267 -67
View File
@@ -1036,7 +1036,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn möglicherweise vorhanden',
'text' => 'Bewerber*In möglicherweise vorhanden',
'description' => '',
'insertvon' => 'system'
),
@@ -2134,6 +2134,26 @@ $phrases = array(
)
)
),
array(
'app' => 'core',
'category' => 'ui',
'phrase' => 'savedAtByV3',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Gespeichert am {0} von {1}',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Saved on {0} by {1}',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'anrechnung',
@@ -3771,7 +3791,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LektorIn',
'text' => 'Lektor*In',
'description' => '',
'insertvon' => 'system'
),
@@ -4400,7 +4420,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn',
'text' => 'Bewerber*In',
'description' => '',
'insertvon' => 'system'
),
@@ -5300,7 +5320,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn parken',
'text' => 'Bewerber*In parken',
'description' => '',
'insertvon' => 'system'
),
@@ -5320,7 +5340,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn ausparken',
'text' => 'Bewerber*In ausparken',
'description' => '',
'insertvon' => 'system'
),
@@ -5400,7 +5420,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn geparkt bis',
'text' => 'Bewerber*In geparkt bis',
'description' => '',
'insertvon' => 'system'
),
@@ -5460,7 +5480,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn zurückstellen',
'text' => 'Bewerber*In zurückstellen',
'description' => '',
'insertvon' => 'system'
),
@@ -5500,7 +5520,7 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BewerberIn zurückgestellt bis',
'text' => 'Bewerber*In zurückgestellt bis',
'description' => '',
'insertvon' => 'system'
),
@@ -5580,8 +5600,8 @@ $phrases = array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Geparkte und zurückgestellte BewerberInnen werden von der Bearbeitung temporär ausgenommen.
Geparkte BewerberInnen werden zum angegebenen Datum automatisch entparkt, während zurückgestellte BewerberInnen nur manuell durch Drücken des Buttons den Zurückgestellt-Status verlieren.
'text' => 'Geparkte und zurückgestellte Bewerber*Innen werden von der Bearbeitung temporär ausgenommen.
Geparkte Bewerber*Innen werden zum angegebenen Datum automatisch entparkt, während zurückgestellte Bewerber*Innen nur manuell durch Drücken des Buttons den Zurückgestellt-Status verlieren.
Bei einer Zurückstellung dient das Datum nur der Erinnerung.',
'description' => '',
'insertvon' => 'system'
@@ -8811,7 +8831,7 @@ The invoice will be sent to you again. <u><strong>The amount is only to be trans
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ANNEHMEN<br>(LektorIn)',
'text' => 'ANNEHMEN<br>(Lektor*In)',
'description' => '',
'insertvon' => 'system'
),
@@ -12922,6 +12942,46 @@ Any unusual occurrences
)
)
),
array(
'app' => 'projektarbeitsbeurteilung',
'category' => 'projektarbeitsbeurteilung',
'phrase' => 'statusZwischengespeichert',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Zwischengespeichert (Noch nicht an Erstbetreuer*In abgeschickt)',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Temporarily saved (Not yet sent to first reviewer)',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'projektarbeitsbeurteilung',
'category' => 'projektarbeitsbeurteilung',
'phrase' => 'statusAbgeschickt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Abgeschickt am',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Sent on',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'anrechnung',
@@ -13682,7 +13742,7 @@ Any unusual occurrences
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'StudentIn',
'text' => 'Student*In',
'description' => '',
'insertvon' => 'system'
),
@@ -15858,7 +15918,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Alle anzeigen, die auf Empfehlung von LektorIn warten.",
'text' => "Alle anzeigen, die auf Empfehlung von Lektor*In warten.",
'description' => '',
'insertvon' => 'system'
),
@@ -25145,7 +25205,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'StudentIn ({prestudent_id})',
'text' => 'Student*In ({prestudent_id})',
'description' => '',
'insertvon' => 'system'
),
@@ -28188,7 +28248,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'MitarbeiterIn',
'text' => 'Mitarbeiter*In',
'description' => '',
'insertvon' => 'system'
),
@@ -29191,7 +29251,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'MitarbeiterIn',
'text' => 'Mitarbeiter*In',
'description' => '',
'insertvon' => 'system'
),
@@ -29231,7 +29291,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Mitarbeiter Information',
'text' => 'Mitarbeiter*In Information',
'description' => '',
'insertvon' => 'system'
),
@@ -29251,7 +29311,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'StudentIn',
'text' => 'Student*In',
'description' => '',
'insertvon' => 'system'
),
@@ -29271,7 +29331,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Student Information',
'text' => 'Student*In Information',
'description' => '',
'insertvon' => 'system'
),
@@ -34342,7 +34402,7 @@ array(
'sprache' => 'German',
'text' => 'Sie können eine Entschuldigung für einen Zeitraum von bis zu {0} Tagen in die Vergangenheit und bis zum Ende des aktuellen Semesters hochladen. Die erlaubten Dateitypen für das Dokument sind .pdf, .png und .jpg.
Sobald Sie eine Entschuldigung hochladen wird die zugehörige Studiengangsassistenz informiert und Ihr Anliegen überprüft. Solange Ihre Entschuldigung noch keinen akzeptierten oder abgelehnten Status erhalten hat, steht es Ihnen frei diese inklusive Datei zu löschen. Sobald sie entweder akzeptiert oder abgelehnt wurde können Sie den Eintrag nichtmehr löschen.
Sobald Sie eine Entschuldigung hochladen wird die zugehörige Studiengangsassistenz informiert und Ihr Anliegen überprüft. Solange Ihre Entschuldigung noch keinen akzeptierten oder abgelehnten Status erhalten hat, steht es Ihnen frei diese inklusive Datei zu löschen. Sobald sie entweder akzeptiert oder abgelehnt wurde können Sie den Eintrag nicht mehr löschen.
Bei einer akzeptierten Entschuldigung werden sämtliche digitalen Anwesenheiten in diesem Zeitraum als positiv gewertet. Eine abgelehnte Entschuldigung hat keine Auswirkungen auf ihre Anwesenheitsquote.',
'description' => '',
@@ -37429,7 +37489,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'In welches Semester sollen diese {count} PrestudentInnen ({status})verschoben werden?',
'text' => 'In welches Semester sollen diese {count} Prestudent*Innen ({status})verschoben werden?',
'description' => '',
'insertvon' => 'system'
),
@@ -39654,7 +39714,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'InteressentIn',
'text' => 'Interessent*In',
'description' => '',
'insertvon' => 'system'
),
@@ -39834,7 +39894,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'PreStudentIn',
'text' => 'PreStudent*In',
'description' => '',
'insertvon' => 'system'
),
@@ -40798,7 +40858,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LektorIn',
'text' => 'Lektor*In',
'description' => '',
'insertvon' => 'system'
),
@@ -40938,7 +40998,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'StudentIn hat keinen Status in diesem Semester',
'text' => 'Student*In hat keinen Status in diesem Semester',
'description' => '',
'insertvon' => 'system'
),
@@ -41761,12 +41821,12 @@ array(
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4fehlerAktualitaetProjektarbeit',
'phrase' => 'c4fehlerAktualitaetProjektarbeitv2',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Projektarbeit ist nichtmehr aktuell",
'text' => "Projektarbeit ist nicht mehr aktuell",
'description' => '',
'insertvon' => 'system'
),
@@ -41846,7 +41906,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Dokumentabgabe - MitarbeiterInbereich",
'text' => "Dokumentabgabe - Mitarbeiter*Inbereich",
'description' => '',
'insertvon' => 'system'
),
@@ -43652,7 +43712,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "BetreuerIn",
'text' => "Betreuer*In",
'description' => '',
'insertvon' => 'system'
),
@@ -43727,7 +43787,7 @@ array(
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4abgabeStudentenbereich',
'phrase' => 'c4abgabeStudentenbereichv2',
'insertvon' => 'system',
'phrases' => array(
array(
@@ -44692,7 +44752,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Projektarbeit Detailansicht MitarbeiterIn",
'text' => "Projektarbeit Detailansicht Mitarbeiter*In",
'description' => '',
'insertvon' => 'system'
),
@@ -45248,7 +45308,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "E-Mail BetreuerIn",
'text' => "E-Mail Betreuer*In",
'description' => '',
'insertvon' => 'system'
),
@@ -45288,7 +45348,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "ErstbetreuerIn Beurteilung herunterladen",
'text' => "Erstbetreuer*In Beurteilung herunterladen",
'description' => '',
'insertvon' => 'system'
),
@@ -45308,7 +45368,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "ZweitbetreuerIn Beurteilung herunterladen",
'text' => "Zweitbetreuer*In Beurteilung herunterladen",
'description' => '',
'insertvon' => 'system'
),
@@ -45488,7 +45548,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Fehler beim Laden der Projektarbeit - StudentIn Zuordnung",
'text' => "Fehler beim Laden der Projektarbeit - Student*In Zuordnung",
'description' => '',
'insertvon' => 'system'
),
@@ -45508,7 +45568,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => "Kein zugeteilter StudentIn gefunden für Projektarbeit",
'text' => "Kein zugeteilter Student*In gefunden für Projektarbeit",
'description' => '',
'insertvon' => 'system'
),
@@ -45783,7 +45843,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn',
'text' => 'Erstbetreuer*In',
'description' => '',
'insertvon' => 'system'
),
@@ -45803,7 +45863,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn',
'text' => 'Zweitbetreuer*In',
'description' => '',
'insertvon' => 'system'
),
@@ -46015,6 +46075,46 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitBegutachterTokenMailSenden',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Access Token E-Mail an Zweitbegutachter*In senden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Send access token email to second reviewer',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4zweitBegutachterTokenMailSuccess',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Access Token E-Mail an Zweitbegutachter*In wurde erfolgreich versandt.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Access token email has been successfully sent to second reviewer.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
@@ -46468,7 +46568,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BetreuerIn Berufspraktikum oder Projektarbeit',
'text' => 'Betreuer*In Berufspraktikum oder Projektarbeit',
'description' => '',
'insertvon' => 'system'
),
@@ -46749,7 +46849,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Titel Pre',
'text' => 'Erstbetreuer*In Titel Pre',
'description' => '',
'insertvon' => 'system'
),
@@ -46769,7 +46869,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Vorname',
'text' => 'Erstbetreuer*In Vorname',
'description' => '',
'insertvon' => 'system'
),
@@ -46789,7 +46889,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Nachname',
'text' => 'Erstbetreuer*In Nachname',
'description' => '',
'insertvon' => 'system'
),
@@ -46809,7 +46909,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ErstbetreuerIn Titel Post',
'text' => 'Erstbetreuer*In Titel Post',
'description' => '',
'insertvon' => 'system'
),
@@ -46829,7 +46929,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Titel Pre',
'text' => 'Zweitbetreuer*In Titel Pre',
'description' => '',
'insertvon' => 'system'
),
@@ -46849,7 +46949,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Vorname',
'text' => 'Zweitbetreuer*In Vorname',
'description' => '',
'insertvon' => 'system'
),
@@ -46869,7 +46969,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Nachname',
'text' => 'Zweitbetreuer*In Nachname',
'description' => '',
'insertvon' => 'system'
),
@@ -46889,7 +46989,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ZweitbetreuerIn Titel Post',
'text' => 'Zweitbetreuer*In Titel Post',
'description' => '',
'insertvon' => 'system'
),
@@ -46921,6 +47021,106 @@ array(
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4signaturinfo',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Digitale Signatur Leitfaden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Guidelines for digital signatures',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4emptyThesisTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Projektarbeit Titel darf nicht leer sein.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Projekt work title is not allowed to be empty.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4invalidCharactersThesisTitle',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Ungültige Zeichen im Titel der Projektarbeit gefunden.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Invalid characters detected in thesis title.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4abgabetypAendernNichtErlaubt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Ändern des Abgabetyps ist nach erfolgtem Upload oder einer erfolgten Benotung nicht erlaubt.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'You are not allowed to change the submission type after the upload is complete or after grades have been assigned.',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'abgabetool',
'phrase' => 'c4datumAendernNichtErlaubt',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Ändern des Termin Zieldatums ist nach erfolgtem Upload oder einer erfolgten Benotung nicht erlaubt.',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'You are not allowed to change the deadline date after the upload is complete or after grades have been assigned.',
'description' => '',
'insertvon' => 'system'
)
)
),
// ABGABETOOL PHRASEN END
array(
'app' => 'core',
@@ -47276,7 +47476,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'PrüferIn 1',
'text' => 'Prüfer*In 1',
'description' => '',
'insertvon' => 'system'
),
@@ -47296,7 +47496,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'PrüferIn 2',
'text' => 'Prüfer*In 2',
'description' => '',
'insertvon' => 'system'
),
@@ -47316,7 +47516,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'PrüferIn 3',
'text' => 'Prüfer*In 3',
'description' => '',
'insertvon' => 'system'
),
@@ -48420,7 +48620,7 @@ array(
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'PrestudentIn ID',
'text' => 'Prestudent*In ID',
'description' => '',
'insertvon' => 'system'
),
@@ -51086,7 +51286,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'LektorIn hinzufügen',
'text' => 'Lektor*In hinzufügen',
'description' => '',
'insertvon' => 'system'
),
@@ -51468,7 +51668,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'SenderIn',
'text' => 'Sender*In',
'description' => '',
'insertvon' => 'system'
),
@@ -51488,7 +51688,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'EmpfängerIn',
'text' => 'Empfänger*In',
'description' => '',
'insertvon' => 'system'
),
@@ -51528,7 +51728,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'EmpfängerIn ID',
'text' => 'Empfänger*In ID',
'description' => '',
'insertvon' => 'system'
),
@@ -53565,7 +53765,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Bitte StudentIn auswählen!',
'text' => 'Bitte Student*In auswählen!',
'description' => '',
'insertvon' => 'system'
),
@@ -54030,7 +54230,7 @@ and represent the current state of research on the topic. The prescribed citatio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'MitarbeiterIn',
'text' => 'Mitarbeiter*In',
'description' => '',
'insertvon' => 'system'
),
@@ -55466,7 +55666,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BetreuerIn',
'text' => 'Betreuer*In',
'description' => '',
'insertvon' => 'system'
),
@@ -55486,7 +55686,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BetreuerIn',
'text' => 'Betreuer*In',
'description' => '',
'insertvon' => 'system'
),
@@ -55706,7 +55906,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Löschen nicht möglich, dieser Projektarbeit sind bereits BetreuerInnen zugewiesen',
'text' => 'Löschen nicht möglich, dieser Projektarbeit sind bereits Betreuer*Innen zugewiesen',
'description' => '',
'insertvon' => 'system'
),
@@ -55746,7 +55946,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'ProjektbetreuerIn ungültig',
'text' => 'Projektbetreuer*In ungültig',
'description' => '',
'insertvon' => 'system'
),
@@ -55766,7 +55966,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Löschen nicht möglich, ProjektbetreuerIn hat bereits einen Vertrag',
'text' => 'Löschen nicht möglich, Projektbetreuer*In hat bereits einen Vertrag',
'description' => '',
'insertvon' => 'system'
),
@@ -56206,7 +56406,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BetreuerIn bearbeiten',
'text' => 'Betreuer*In bearbeiten',
'description' => '',
'insertvon' => 'system'
),
@@ -56221,12 +56421,12 @@ I have been informed that I am under no obligation to consent to the transmissio
array(
'app' => 'core',
'category' => 'projektarbeit',
'phrase' => 'betreuerSpeichern',
'phrase' => 'betreuerSpeichernv2',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'BetreuerIn speichern',
'text' => 'Betreuer*In speichern',
'description' => '',
'insertvon' => 'system'
),
@@ -57363,7 +57563,7 @@ I have been informed that I am under no obligation to consent to the transmissio
),
array(
'sprache' => 'English',
'text' => 'Saved on {date} von {name}',
'text' => 'Saved on {date} by {name}',
'description' => '',
'insertvon' => 'system'
)
@@ -58083,7 +58283,7 @@ I have been informed that I am under no obligation to consent to the transmissio
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'StudentIn ist in diesem Semester keinem Lehrverband zugeteilt',
'text' => 'Student*In ist in diesem Semester keinem Lehrverband zugeteilt',
'description' => '',
'insertvon' => 'system'
),