mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-15 14:02:17 +00:00
Merge branch 'cis40_2026-05_ma_rc' into cis40_2026-05_ma_rc_routerlink
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -418,6 +418,10 @@ class LvPlan extends FHCAPI_Controller
|
||||
*/
|
||||
private function fetchMoodleEvents($start_date, $end_date, $uid = null)
|
||||
{
|
||||
if ($uid && $uid !== getAuthUID()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->load->config('calendar');
|
||||
|
||||
$tz = new DateTimeZone($this->config->item('timezone'));
|
||||
|
||||
@@ -32,6 +32,8 @@ class Phrasen extends FHCAPI_Controller
|
||||
'setLanguage' => self::PERM_ANONYMOUS,
|
||||
'getLanguage' => self::PERM_ANONYMOUS,
|
||||
'getAllLanguages' => self::PERM_ANONYMOUS,
|
||||
'getPhrases' => self::PERM_ANONYMOUS,
|
||||
'getTabulatorPhrases' => self::PERM_ANONYMOUS,
|
||||
]);
|
||||
|
||||
$this->load->helper('hlp_language');
|
||||
@@ -92,4 +94,59 @@ class Phrasen extends FHCAPI_Controller
|
||||
$this->terminateWithSuccess($langs);
|
||||
}
|
||||
|
||||
public function getPhrases()
|
||||
{
|
||||
$postParams = $this->getPostJSON();
|
||||
|
||||
$languages = $postParams->languages;
|
||||
if (!$languages || !count($languages)) {
|
||||
$this->load->model('system/Sprache_model', 'sprachenModel');
|
||||
$activeLanguages = $this->sprachenModel->loadWhere(array('content' => true));
|
||||
$activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages);
|
||||
$languages = array_map(
|
||||
function ($languageData) {
|
||||
return $languageData->sprache;
|
||||
},
|
||||
$activeLanguagesData
|
||||
);
|
||||
}
|
||||
|
||||
$this->load->model('system/Phrase_model', 'phraseModel');
|
||||
$phrasesGroupedByCategory = $postParams->phrasesGroupedByCategory;
|
||||
$result = [];
|
||||
foreach ($languages as $language) {
|
||||
$phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language);
|
||||
$result[$language] = $this->getDataOrTerminateWithError($phrases);
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($result);
|
||||
}
|
||||
|
||||
public function getTabulatorPhrases()
|
||||
{
|
||||
$languages = json_decode($this->input->get('languages'));
|
||||
if (!$languages || !count($languages)) {
|
||||
$this->load->model('system/Sprache_model', 'sprachenModel');
|
||||
$activeLanguages = $this->sprachenModel->loadWhere(array('content' => true));
|
||||
$activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages);
|
||||
$languages = array_map(
|
||||
function ($languageData) {
|
||||
return $languageData->sprache;
|
||||
},
|
||||
$activeLanguagesData
|
||||
);
|
||||
}
|
||||
|
||||
$this->load->model('system/Phrase_model', 'phraseModel');
|
||||
$result = [];
|
||||
foreach ($languages as $language) {
|
||||
$tabulatorPhrases = $this->phraseModel->getPhrasesByCategoryAndLanguage(['tabulator'], $language);
|
||||
$result[$language] = $this->getDataOrTerminateWithError($tabulatorPhrases);
|
||||
}
|
||||
|
||||
header('Pragma: private');
|
||||
header('Cache-Control: private, max-age=' . (60 * 60 * 24 * 30));
|
||||
$this->terminateWithSuccess($result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -90,6 +90,15 @@ class Projektarbeit extends FHCAPI_Controller
|
||||
|
||||
if (!isset($projektarbeit_id) || !is_numeric($projektarbeit_id)) return $this->terminateWithError('Projektarbeit Id missing', self::ERROR_TYPE_GENERAL);
|
||||
|
||||
$result = $this->fetchProjektarbeitByID($projektarbeit_id);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess(current($data));
|
||||
}
|
||||
|
||||
private function fetchProjektarbeitById($projektarbeit_id) {
|
||||
$this->ProjektarbeitModel->resetQuery();
|
||||
$this->ProjektarbeitModel->addSelect(
|
||||
'lehre.tbl_projektarbeit.projektarbeit_id, titel, titel_english, themenbereich, projekttyp_kurzbz, lehrveranstaltung_id, lehreinheit_id,
|
||||
firma_id, beginn, ende, gesperrtbis, note, final, freigegeben, tbl_projektarbeit.anmerkung, fa.name AS firma_name'
|
||||
@@ -97,13 +106,10 @@ class Projektarbeit extends FHCAPI_Controller
|
||||
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehreinheit le', 'lehreinheit_id');
|
||||
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehrveranstaltung lv', 'lehrveranstaltung_id');
|
||||
$this->ProjektarbeitModel->addJoin('public.tbl_firma fa', 'firma_id', 'LEFT');
|
||||
$result = $this->ProjektarbeitModel->loadWhere(
|
||||
return $this->ProjektarbeitModel->loadWhere(
|
||||
array('projektarbeit_id' => $projektarbeit_id)
|
||||
);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess(current($data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +138,8 @@ class Projektarbeit extends FHCAPI_Controller
|
||||
);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($this->fetchProjektarbeitById($data));
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
|
||||
@@ -280,10 +287,7 @@ class Projektarbeit extends FHCAPI_Controller
|
||||
*/
|
||||
public function getNoten()
|
||||
{
|
||||
$this->NoteModel->addOrder('notenwert', 'ASC');
|
||||
$this->NoteModel->addOrder('bezeichnung', 'ASC');
|
||||
|
||||
$result = $this->NoteModel->load();
|
||||
$result = $this->NoteModel->getAllActive();
|
||||
|
||||
if (isError($result)) return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -263,6 +263,59 @@ class ProfilLib{
|
||||
$element->mailto = "mailto:" . $element->gruppe_kurzbz . "@" . DOMAIN;
|
||||
return $element;
|
||||
}, $mailverteiler_res);
|
||||
|
||||
$this->ci->load->model("crm/Student_model", "StudentModel");
|
||||
$this->ci->StudentModel->addJoin("tbl_benutzer", "tbl_benutzer.uid = tbl_student.student_uid");
|
||||
$this->ci->StudentModel->addJoin("tbl_person", "tbl_benutzer.person_id = tbl_person.person_id");
|
||||
$this->ci->StudentModel->addJoin("tbl_studiengang", "tbl_student.studiengang_kz = tbl_studiengang.studiengang_kz");
|
||||
$this->ci->StudentModel->addSelect("matr_nr, semester, verband, gruppe, kurzbzlang");
|
||||
|
||||
$studentResult = $this->ci->StudentModel->loadWhere(["student_uid" => $uid]);
|
||||
if (isError($studentResult)) {
|
||||
return error(getData($studentResult));
|
||||
}
|
||||
|
||||
$studentResultData = getData($studentResult);
|
||||
$studentData = null;
|
||||
if (is_array($studentResultData) && count($studentResultData)) {
|
||||
$studentData = $studentResultData[0];
|
||||
}
|
||||
|
||||
if ($studentData && $studentData->matr_nr) {
|
||||
$this->ci->load->library("phrasesLib", ["profil"], "phrases");
|
||||
$standardCourseVerteiler = trim($studentData->kurzbzlang) . "_STD";
|
||||
$mailverteiler_res[] = [
|
||||
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $standardCourseVerteiler,
|
||||
"gruppe_kurzbz" => $standardCourseVerteiler,
|
||||
"mailto" => "mailto:" . strtolower($standardCourseVerteiler) . "@" . DOMAIN,
|
||||
];
|
||||
|
||||
$semesterVerteiler = trim($studentData->kurzbzlang) . trim($studentData->semester);
|
||||
$mailverteiler_res[] = [
|
||||
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $semesterVerteiler,
|
||||
"gruppe_kurzbz" => $semesterVerteiler,
|
||||
"mailto" => "mailto:" . strtolower($semesterVerteiler) . "@" . DOMAIN,
|
||||
];
|
||||
|
||||
if ($studentData->verband && strlen(trim($studentData->verband))) {
|
||||
$verbandVerteiler = $semesterVerteiler . trim($studentData->verband);
|
||||
$mailverteiler_res[] = [
|
||||
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $verbandVerteiler,
|
||||
"gruppe_kurzbz" => $verbandVerteiler,
|
||||
"mailto" => "mailto:" . strtolower($verbandVerteiler) . "@" . DOMAIN,
|
||||
];
|
||||
|
||||
if ($studentData->gruppe && strlen(trim($studentData->gruppe))) {
|
||||
$gruppeVerteiler = $verbandVerteiler . trim($studentData->gruppe);
|
||||
$mailverteiler_res[] = [
|
||||
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $gruppeVerteiler,
|
||||
"gruppe_kurzbz" => $gruppeVerteiler,
|
||||
"mailto" => "mailto:" . strtolower($gruppeVerteiler) . "@" . DOMAIN,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mailverteiler_res;
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,10 @@ class Projektarbeit_model extends DB_Model
|
||||
campus.tbl_paabgabetyp.bezeichnung,
|
||||
campus.tbl_paabgabetyp.benotbar,
|
||||
campus.tbl_paabgabe.abgabedatum,
|
||||
campus.tbl_paabgabe.insertvon
|
||||
campus.tbl_paabgabe.insertvon,
|
||||
campus.tbl_paabgabe.insertamum,
|
||||
campus.tbl_paabgabe.updatevon,
|
||||
campus.tbl_paabgabe.updateamum
|
||||
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
|
||||
WHERE campus.tbl_paabgabe.projektarbeit_id = ?
|
||||
ORDER BY campus.tbl_paabgabe.datum";
|
||||
@@ -210,7 +213,18 @@ class Projektarbeit_model extends DB_Model
|
||||
campus.tbl_paabgabetyp.paabgabetyp_kurzbz,
|
||||
campus.tbl_paabgabetyp.bezeichnung,
|
||||
campus.tbl_paabgabe.abgabedatum,
|
||||
campus.tbl_paabgabe.insertvon
|
||||
campus.tbl_paabgabe.insertvon,
|
||||
campus.tbl_paabgabe.updatevon,
|
||||
campus.tbl_paabgabe.insertamum,
|
||||
campus.tbl_paabgabe.updateamum,
|
||||
(SELECT p.vorname || ' ' || p.nachname
|
||||
FROM public.tbl_benutzer b
|
||||
JOIN public.tbl_person p USING(person_id)
|
||||
WHERE b.uid = campus.tbl_paabgabe.insertvon) AS insertvon_fullname,
|
||||
(SELECT p.vorname || ' ' || p.nachname
|
||||
FROM public.tbl_benutzer b
|
||||
JOIN public.tbl_person p USING(person_id)
|
||||
WHERE b.uid = campus.tbl_paabgabe.updatevon) AS updatevon_fullname
|
||||
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
|
||||
WHERE campus.tbl_paabgabe.projektarbeit_id IN ?
|
||||
ORDER BY campus.tbl_paabgabe.datum";
|
||||
@@ -243,7 +257,8 @@ class Projektarbeit_model extends DB_Model
|
||||
LEFT JOIN public.tbl_benutzer ben USING (person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ma ON ben.uid = ma.mitarbeiter_uid
|
||||
WHERE (ben.aktiv OR ben.aktiv IS NULL)
|
||||
AND projektarbeit_id = ?";
|
||||
AND projektarbeit_id = ?
|
||||
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}')";
|
||||
|
||||
return $this->execReadOnlyQuery($qry, [$projektarbeit_id]);
|
||||
}
|
||||
@@ -297,34 +312,142 @@ class Projektarbeit_model extends DB_Model
|
||||
* Get a List of Projektarbeiten of a mitarbeiter with zuordnung
|
||||
* used by the mitarbeiter cis4 abgabetool.
|
||||
*/
|
||||
public function getMitarbeiterProjektarbeiten($uid, $showAll){
|
||||
$qry = "SELECT
|
||||
*
|
||||
FROM
|
||||
(SELECT tbl_person.vorname, tbl_person.nachname, tbl_studiengang.typ, tbl_studiengang.kurzbz,
|
||||
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung, tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
|
||||
tbl_projektbetreuer.person_id as betreuer_person_id, tbl_projektbetreuer.betreuerart_kurzbz, tbl_betreuerart.beschreibung AS betreuerart_beschreibung,
|
||||
tbl_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid
|
||||
FROM lehre.tbl_projektarbeit
|
||||
LEFT JOIN lehre.tbl_projektbetreuer using(projektarbeit_id)
|
||||
LEFT JOIN lehre.tbl_betreuerart using(betreuerart_kurzbz)
|
||||
LEFT JOIN public.tbl_benutzer on(uid=student_uid)
|
||||
LEFT JOIN public.tbl_student on(public.tbl_benutzer.uid=public.tbl_student.student_uid)
|
||||
LEFT JOIN public.tbl_person on(tbl_benutzer.person_id=tbl_person.person_id)
|
||||
LEFT JOIN lehre.tbl_lehreinheit using(lehreinheit_id)
|
||||
LEFT JOIN lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id)
|
||||
LEFT JOIN public.tbl_studiengang on(lehre.tbl_lehrveranstaltung.studiengang_kz=public.tbl_studiengang.studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
|
||||
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
|
||||
AND tbl_projektbetreuer.person_id IN (SELECT person_id FROM public.tbl_benutzer
|
||||
WHERE public.tbl_benutzer.person_id=lehre.tbl_projektbetreuer.person_id
|
||||
AND public.tbl_benutzer.uid= ? )
|
||||
".($showAll?'':' AND public.tbl_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
|
||||
public function getMitarbeiterProjektarbeiten($person_id, $showAll){
|
||||
$qry = "WITH secondary_betreuer AS (
|
||||
SELECT DISTINCT ON (pb.projektarbeit_id)
|
||||
pb.projektarbeit_id,
|
||||
pb.person_id AS zweitbetreuer_person_id,
|
||||
b.uid AS zweitbetreuer_benutzer_uid,
|
||||
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
|
||||
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
|
||||
p.titelpre AS zweitbetreuer_titelpre,
|
||||
p.vorname AS zweitbetreuer_vorname,
|
||||
p.nachname AS zweitbetreuer_nachname,
|
||||
p.titelpost AS zweitbetreuer_titelpost,
|
||||
trim(
|
||||
COALESCE(p.titelpre, '') || ' ' ||
|
||||
COALESCE(p.vorname, '') || ' ' ||
|
||||
COALESCE(p.nachname, '') || ' ' ||
|
||||
COALESCE(p.titelpost, '')
|
||||
) AS zweitbetreuer_full_name
|
||||
FROM lehre.tbl_projektbetreuer pb
|
||||
JOIN public.tbl_person p ON p.person_id = pb.person_id
|
||||
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
|
||||
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
|
||||
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
|
||||
ORDER BY pb.projektarbeit_id
|
||||
),
|
||||
primary_betreuer AS (
|
||||
SELECT DISTINCT ON (pb.projektarbeit_id)
|
||||
pb.projektarbeit_id,
|
||||
pb.person_id AS betreuer_person_id,
|
||||
pb.betreuerart_kurzbz AS betreuer_betreuerart_kurzbz,
|
||||
ba.beschreibung AS betreuer_betreuerart_beschreibung,
|
||||
p.titelpre AS betreuer_titelpre,
|
||||
p.vorname AS betreuer_vorname,
|
||||
p.nachname AS betreuer_nachname,
|
||||
p.titelpost AS betreuer_titelpost,
|
||||
trim(
|
||||
COALESCE(p.titelpre, '') || ' ' ||
|
||||
COALESCE(p.vorname, '') || ' ' ||
|
||||
COALESCE(p.nachname, '') || ' ' ||
|
||||
COALESCE(p.titelpost, '')
|
||||
) AS betreuer_full_name
|
||||
FROM lehre.tbl_projektbetreuer pb
|
||||
JOIN public.tbl_person p ON p.person_id = pb.person_id
|
||||
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
|
||||
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
|
||||
WHERE pb.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}')
|
||||
ORDER BY pb.projektarbeit_id
|
||||
)
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
(SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
|
||||
student_person.vorname, student_person.nachname,
|
||||
tbl_studiengang.typ, tbl_studiengang.kurzbz,
|
||||
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung,
|
||||
tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
|
||||
student_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid,
|
||||
|
||||
lehre.tbl_projektarbeit.sprache,
|
||||
lehre.tbl_projektarbeit.seitenanzahl,
|
||||
lehre.tbl_projektarbeit.kontrollschlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter_en,
|
||||
lehre.tbl_projektarbeit.abstract,
|
||||
lehre.tbl_projektarbeit.abstract_en,
|
||||
lehre.tbl_projektarbeit.insertamum,
|
||||
|
||||
(
|
||||
SELECT orgform_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = (
|
||||
SELECT prestudent_id FROM public.tbl_student
|
||||
WHERE student_uid = student_benutzer.uid LIMIT 1
|
||||
)
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS organisationsform,
|
||||
(
|
||||
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
|
||||
tbl_prestudentstatus.orgform_kurzbz,
|
||||
tbl_studiengang.orgform_kurzbz)
|
||||
FROM public.tbl_prestudent
|
||||
JOIN public.tbl_prestudentstatus USING (prestudent_id)
|
||||
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
|
||||
JOIN public.tbl_studiengang USING (studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY tbl_prestudentstatus.datum DESC
|
||||
LIMIT 1
|
||||
) AS orgform,
|
||||
(
|
||||
SELECT status_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS studienstatus,
|
||||
|
||||
pb.betreuer_person_id,
|
||||
pb.betreuer_betreuerart_kurzbz,
|
||||
pb.betreuer_betreuerart_beschreibung,
|
||||
pb.betreuer_titelpre,
|
||||
pb.betreuer_vorname,
|
||||
pb.betreuer_nachname,
|
||||
pb.betreuer_titelpost,
|
||||
pb.betreuer_full_name,
|
||||
|
||||
sb.zweitbetreuer_person_id,
|
||||
sb.zweitbetreuer_benutzer_uid,
|
||||
sb.zweitbetreuer_betreuerart_kurzbz,
|
||||
sb.zweitbetreuer_betreuerart_beschreibung,
|
||||
sb.zweitbetreuer_titelpre,
|
||||
sb.zweitbetreuer_vorname,
|
||||
sb.zweitbetreuer_nachname,
|
||||
sb.zweitbetreuer_titelpost,
|
||||
sb.zweitbetreuer_full_name
|
||||
FROM lehre.tbl_projektarbeit
|
||||
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
|
||||
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
|
||||
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
|
||||
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
|
||||
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
|
||||
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
|
||||
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
|
||||
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
|
||||
LEFT JOIN primary_betreuer pb ON pb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
|
||||
AND (pb.betreuer_person_id = ? OR sb.zweitbetreuer_person_id = ?)
|
||||
".($showAll?'':' AND student_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
|
||||
AND betreuerart_kurzbz IN ('Betreuer', 'Begutachter', 'Erstbegutachter', 'Zweitbegutachter', 'Erstbetreuer', 'Senatsvorsitz', 'Senatsmitglied')
|
||||
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
|
||||
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
|
||||
ORDER BY nachname;";
|
||||
|
||||
return $this->execReadOnlyQuery($qry, array($uid));
|
||||
return $this->execReadOnlyQuery($qry, array($person_id, $person_id));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,176 +467,257 @@ class Projektarbeit_model extends DB_Model
|
||||
|
||||
|
||||
public function getProjektarbeitenForStudiengang($studiengang_kz, $benotet) {
|
||||
$new_qry = "SELECT DISTINCT ON(tmp.projektarbeit_id) *, campus.get_betreuer_details(tmp.zweitbetreuer_person_id) as zweitbetreuer_full_name, campus.get_betreuer_details(tmp.betreuer_person_id) as erstbetreuer_full_name
|
||||
FROM(
|
||||
SELECT
|
||||
DISTINCT ON(tbl_projektarbeit.projektarbeit_id)
|
||||
tbl_projektarbeit.projekttyp_kurzbz,
|
||||
tbl_projektarbeit.titel,
|
||||
tbl_projektarbeit.projektarbeit_id,
|
||||
tbl_studiengang.typ, tbl_studiengang.kurzbz,
|
||||
student_benutzer.uid as student_uid,
|
||||
student_person.vorname as student_vorname,
|
||||
student_person.nachname as student_nachname,
|
||||
tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz,
|
||||
betreuer_benutzer.uid as betreuer_benutzer_uid,
|
||||
betreuer_person.titelpre as betreuer_titelpre,
|
||||
betreuer_person.vorname as betreuer_vorname,
|
||||
betreuer_person.nachname as betreuer_nachname,
|
||||
betreuer_person.titelpost as betreuer_titelpost,
|
||||
lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart,
|
||||
lehre.tbl_projektbetreuer.person_id as betreuer_person_id,
|
||||
lehre.tbl_projektarbeit.sprache as sprache,
|
||||
lehre.tbl_projektarbeit.seitenanzahl as seitenanzahl,
|
||||
lehre.tbl_projektarbeit.kontrollschlagwoerter as kontrollschlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter as schlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter_en as schlagwoerter_en,
|
||||
lehre.tbl_projektarbeit.abstract as abstract,
|
||||
lehre.tbl_projektarbeit.abstract_en as abstract_en,
|
||||
lehre.tbl_projektarbeit.insertamum as insertamum,
|
||||
lehre.tbl_projektarbeit.note as note,
|
||||
(
|
||||
SELECT orgform_kurzbz
|
||||
FROM tbl_prestudentstatus
|
||||
WHERE prestudent_id = (SELECT prestudent_id
|
||||
FROM tbl_student
|
||||
WHERE student_uid = student_benutzer.uid
|
||||
LIMIT 1)
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
)
|
||||
as organisationsform,
|
||||
(
|
||||
SELECT person_id
|
||||
FROM lehre.tbl_projektbetreuer
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
AS zweitbetreuer_person_id,
|
||||
(
|
||||
SELECT betreuerart_kurzbz
|
||||
FROM lehre.tbl_projektbetreuer
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
AS zweitbetreuer_betreuerart_kurzbz,
|
||||
(
|
||||
SELECT tbl_betreuerart.beschreibung
|
||||
FROM lehre.tbl_projektbetreuer
|
||||
JOIN lehre.tbl_betreuerart USING (betreuerart_kurzbz)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
AS zweitbetreuer_betreuerart_beschreibung,
|
||||
(
|
||||
SELECT trim(COALESCE(titelpre, '') || ' ' || COALESCE(vorname, '') || ' ' || COALESCE(nachname, '') || ' ' ||
|
||||
COALESCE(titelpost, ''))
|
||||
FROM public.tbl_person
|
||||
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
as zweitbetreuer_full_name,
|
||||
(
|
||||
SELECT titelpre
|
||||
FROM public.tbl_person
|
||||
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
as zweitbetreuer_titelpre,
|
||||
(
|
||||
SELECT vorname
|
||||
FROM public.tbl_person
|
||||
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
as zweitbetreuer_vorname,
|
||||
(
|
||||
SELECT nachname
|
||||
FROM public.tbl_person
|
||||
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
as zweitbetreuer_nachname,
|
||||
(
|
||||
SELECT titelpost
|
||||
FROM public.tbl_person
|
||||
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
|
||||
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
|
||||
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
|
||||
LIMIT 1
|
||||
)
|
||||
as zweitbetreuer_titelpost,
|
||||
(
|
||||
SELECT
|
||||
COALESCE(tbl_studienplan.orgform_kurzbz,
|
||||
tbl_prestudentstatus.orgform_kurzbz, tbl_studiengang.orgform_kurzbz) as
|
||||
orgform
|
||||
FROM
|
||||
public.tbl_prestudent
|
||||
JOIN public.tbl_prestudentstatus USING(prestudent_id)
|
||||
JOIN public.tbl_studiensemester USING(studiensemester_kurzbz)
|
||||
JOIN public.tbl_studiengang USING(studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_studienplan USING(studienplan_id)
|
||||
WHERE
|
||||
prestudent_id=tbl_student.prestudent_id
|
||||
ORDER BY tbl_prestudentstatus.datum DESC LIMIT 1
|
||||
) as orgform,
|
||||
(SELECT status_kurzbz FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id=tbl_student.prestudent_id
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus
|
||||
FROM lehre.tbl_projektarbeit
|
||||
LEFT JOIN public.tbl_benutzer student_benutzer ON (student_benutzer.uid = lehre.tbl_projektarbeit.student_uid)
|
||||
LEFT JOIN public.tbl_person student_person ON (student_benutzer.person_id = student_person.person_id)
|
||||
LEFT JOIN public.tbl_student on(student_benutzer.uid = public.tbl_student.student_uid)
|
||||
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
|
||||
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
|
||||
LEFT JOIN public.tbl_studiengang ON (public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
|
||||
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
|
||||
LEFT JOIN public.tbl_person betreuer_person ON (betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id)
|
||||
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON (betreuer_person.person_id = betreuer_benutzer.person_id)
|
||||
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
|
||||
AND student_benutzer.aktiv AND (
|
||||
lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbegutachter'
|
||||
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Begutachter'
|
||||
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Betreuer'
|
||||
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbetreuer'
|
||||
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Senatsvorsitz'
|
||||
)
|
||||
AND public.tbl_studiengang.studiengang_kz = ?";
|
||||
$new_qry = "WITH secondary_betreuer AS (
|
||||
SELECT DISTINCT ON (pb.projektarbeit_id)
|
||||
pb.projektarbeit_id,
|
||||
pb.person_id AS zweitbetreuer_person_id,
|
||||
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
|
||||
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
|
||||
p.titelpre AS zweitbetreuer_titelpre,
|
||||
p.vorname AS zweitbetreuer_vorname,
|
||||
p.nachname AS zweitbetreuer_nachname,
|
||||
p.titelpost AS zweitbetreuer_titelpost,
|
||||
trim(
|
||||
COALESCE(p.titelpre, '') || ' ' ||
|
||||
COALESCE(p.vorname, '') || ' ' ||
|
||||
COALESCE(p.nachname, '') || ' ' ||
|
||||
COALESCE(p.titelpost, '')
|
||||
) AS zweitbetreuer_full_name
|
||||
FROM lehre.tbl_projektbetreuer pb
|
||||
JOIN public.tbl_person p ON p.person_id = pb.person_id
|
||||
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
|
||||
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
|
||||
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
|
||||
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
|
||||
)
|
||||
|
||||
if($benotet == 0) {
|
||||
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
|
||||
} else if ($benotet == 1) {
|
||||
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
|
||||
}
|
||||
|
||||
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
|
||||
SELECT DISTINCT ON (tmp.projektarbeit_id)
|
||||
*,
|
||||
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
|
||||
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
|
||||
FROM (
|
||||
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
|
||||
tbl_projektarbeit.projekttyp_kurzbz,
|
||||
tbl_projektarbeit.titel,
|
||||
tbl_projektarbeit.projektarbeit_id,
|
||||
tbl_studiengang.typ,
|
||||
tbl_studiengang.kurzbz,
|
||||
student_benutzer.uid AS student_uid,
|
||||
student_person.vorname AS student_vorname,
|
||||
student_person.nachname AS student_nachname,
|
||||
public.tbl_student.matrikelnr,
|
||||
tbl_lehreinheit.studiensemester_kurzbz,
|
||||
betreuer_benutzer.uid AS betreuer_benutzer_uid,
|
||||
betreuer_person.titelpre AS betreuer_titelpre,
|
||||
betreuer_person.vorname AS betreuer_vorname,
|
||||
betreuer_person.nachname AS betreuer_nachname,
|
||||
betreuer_person.titelpost AS betreuer_titelpost,
|
||||
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
|
||||
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
|
||||
lehre.tbl_projektarbeit.sprache,
|
||||
lehre.tbl_projektarbeit.seitenanzahl,
|
||||
lehre.tbl_projektarbeit.kontrollschlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter_en,
|
||||
lehre.tbl_projektarbeit.abstract,
|
||||
lehre.tbl_projektarbeit.abstract_en,
|
||||
lehre.tbl_projektarbeit.insertamum,
|
||||
lehre.tbl_projektarbeit.note,
|
||||
|
||||
sb.zweitbetreuer_person_id,
|
||||
sb.zweitbetreuer_betreuerart_kurzbz,
|
||||
sb.zweitbetreuer_betreuerart_beschreibung,
|
||||
sb.zweitbetreuer_full_name,
|
||||
sb.zweitbetreuer_titelpre,
|
||||
sb.zweitbetreuer_vorname,
|
||||
sb.zweitbetreuer_nachname,
|
||||
sb.zweitbetreuer_titelpost,
|
||||
|
||||
(
|
||||
SELECT orgform_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = (
|
||||
SELECT prestudent_id FROM public.tbl_student
|
||||
WHERE student_uid = student_benutzer.uid LIMIT 1
|
||||
)
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS organisationsform,
|
||||
(
|
||||
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
|
||||
tbl_prestudentstatus.orgform_kurzbz,
|
||||
tbl_studiengang.orgform_kurzbz)
|
||||
FROM public.tbl_prestudent
|
||||
JOIN public.tbl_prestudentstatus USING (prestudent_id)
|
||||
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
|
||||
JOIN public.tbl_studiengang USING (studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY tbl_prestudentstatus.datum DESC
|
||||
LIMIT 1
|
||||
) AS orgform,
|
||||
(
|
||||
SELECT status_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS studienstatus
|
||||
|
||||
FROM lehre.tbl_projektarbeit
|
||||
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
|
||||
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
|
||||
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
|
||||
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
|
||||
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
|
||||
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
|
||||
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
|
||||
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
|
||||
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
|
||||
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
|
||||
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
|
||||
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
|
||||
AND student_benutzer.aktiv
|
||||
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
|
||||
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
|
||||
)
|
||||
AND public.tbl_studiengang.studiengang_kz = ?";
|
||||
|
||||
if($benotet == 0) {
|
||||
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
|
||||
} else if ($benotet == 1) {
|
||||
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
|
||||
}
|
||||
|
||||
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
|
||||
) as tmp";
|
||||
|
||||
return $this->execReadOnlyQuery($new_qry, array($studiengang_kz));
|
||||
}
|
||||
|
||||
public function getProjektarbeitenForStudent($student_uid) {
|
||||
$new_qry = "WITH secondary_betreuer AS (
|
||||
SELECT DISTINCT ON (pb.projektarbeit_id)
|
||||
pb.projektarbeit_id,
|
||||
pb.person_id AS zweitbetreuer_person_id,
|
||||
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
|
||||
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
|
||||
p.titelpre AS zweitbetreuer_titelpre,
|
||||
p.vorname AS zweitbetreuer_vorname,
|
||||
p.nachname AS zweitbetreuer_nachname,
|
||||
p.titelpost AS zweitbetreuer_titelpost,
|
||||
trim(
|
||||
COALESCE(p.titelpre, '') || ' ' ||
|
||||
COALESCE(p.vorname, '') || ' ' ||
|
||||
COALESCE(p.nachname, '') || ' ' ||
|
||||
COALESCE(p.titelpost, '')
|
||||
) AS zweitbetreuer_full_name
|
||||
FROM lehre.tbl_projektbetreuer pb
|
||||
JOIN public.tbl_person p ON p.person_id = pb.person_id
|
||||
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
|
||||
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
|
||||
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
|
||||
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
|
||||
)
|
||||
|
||||
SELECT DISTINCT ON (tmp.projektarbeit_id)
|
||||
*,
|
||||
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
|
||||
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
|
||||
FROM (
|
||||
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
|
||||
tbl_projektarbeit.projekttyp_kurzbz,
|
||||
tbl_projektarbeit.titel,
|
||||
tbl_projektarbeit.projektarbeit_id,
|
||||
tbl_studiengang.typ,
|
||||
tbl_studiengang.kurzbz,
|
||||
student_benutzer.uid AS student_uid,
|
||||
student_person.vorname AS student_vorname,
|
||||
student_person.nachname AS student_nachname,
|
||||
public.tbl_student.matrikelnr,
|
||||
tbl_lehreinheit.studiensemester_kurzbz,
|
||||
betreuer_benutzer.uid AS betreuer_benutzer_uid,
|
||||
betreuer_person.titelpre AS betreuer_titelpre,
|
||||
betreuer_person.vorname AS betreuer_vorname,
|
||||
betreuer_person.nachname AS betreuer_nachname,
|
||||
betreuer_person.titelpost AS betreuer_titelpost,
|
||||
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
|
||||
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
|
||||
lehre.tbl_projektarbeit.sprache,
|
||||
lehre.tbl_projektarbeit.seitenanzahl,
|
||||
lehre.tbl_projektarbeit.kontrollschlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter,
|
||||
lehre.tbl_projektarbeit.schlagwoerter_en,
|
||||
lehre.tbl_projektarbeit.abstract,
|
||||
lehre.tbl_projektarbeit.abstract_en,
|
||||
lehre.tbl_projektarbeit.insertamum,
|
||||
lehre.tbl_projektarbeit.note,
|
||||
|
||||
sb.zweitbetreuer_person_id,
|
||||
sb.zweitbetreuer_betreuerart_kurzbz,
|
||||
sb.zweitbetreuer_betreuerart_beschreibung,
|
||||
sb.zweitbetreuer_full_name,
|
||||
sb.zweitbetreuer_titelpre,
|
||||
sb.zweitbetreuer_vorname,
|
||||
sb.zweitbetreuer_nachname,
|
||||
sb.zweitbetreuer_titelpost,
|
||||
|
||||
(
|
||||
SELECT orgform_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = (
|
||||
SELECT prestudent_id FROM public.tbl_student
|
||||
WHERE student_uid = student_benutzer.uid LIMIT 1
|
||||
)
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS organisationsform,
|
||||
(
|
||||
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
|
||||
tbl_prestudentstatus.orgform_kurzbz,
|
||||
tbl_studiengang.orgform_kurzbz)
|
||||
FROM public.tbl_prestudent
|
||||
JOIN public.tbl_prestudentstatus USING (prestudent_id)
|
||||
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
|
||||
JOIN public.tbl_studiengang USING (studiengang_kz)
|
||||
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY tbl_prestudentstatus.datum DESC
|
||||
LIMIT 1
|
||||
) AS orgform,
|
||||
(
|
||||
SELECT status_kurzbz
|
||||
FROM public.tbl_prestudentstatus
|
||||
WHERE prestudent_id = public.tbl_student.prestudent_id
|
||||
ORDER BY datum DESC, insertamum DESC, ext_id DESC
|
||||
LIMIT 1
|
||||
) AS studienstatus
|
||||
|
||||
FROM lehre.tbl_projektarbeit
|
||||
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
|
||||
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
|
||||
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
|
||||
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
|
||||
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
|
||||
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
|
||||
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
|
||||
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
|
||||
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
|
||||
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
|
||||
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
|
||||
|
||||
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
|
||||
AND student_benutzer.aktiv
|
||||
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
|
||||
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
|
||||
)
|
||||
AND public.tbl_student.student_uid = ?
|
||||
ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
|
||||
) as tmp";
|
||||
|
||||
return $this->execReadOnlyQuery($new_qry, array($student_uid));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -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 ?? '' ?>"
|
||||
|
||||
Generated
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"name": "FHC-Core",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -277,3 +277,7 @@ html.fs_huge {
|
||||
}
|
||||
*/
|
||||
/* slim ende */
|
||||
|
||||
.fhc-xxl-modal {
|
||||
min-width: 80vw;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -41,5 +41,24 @@ export default {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/phrasen/getAllLanguages'
|
||||
};
|
||||
}
|
||||
},
|
||||
getPhrases(phrasesGroupedByCategory, languages = []) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: '/api/frontend/v1/phrasen/getPhrases',
|
||||
params: {
|
||||
phrasesGroupedByCategory,
|
||||
languages,
|
||||
},
|
||||
};
|
||||
},
|
||||
getTabulatorPhrases(languages = []) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: '/api/frontend/v1/phrasen/getTabulatorPhrases',
|
||||
params: {
|
||||
languages: JSON.stringify(languages),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
`
|
||||
});
|
||||
|
||||
@@ -20,11 +20,16 @@ const app = Vue.createApp({
|
||||
provide() {
|
||||
return { // provide injectable & watchable language property
|
||||
language: Vue.computed(() => this.$p.user_language),
|
||||
isMobile: Vue.computed(() => this.windowWidth < 767),
|
||||
isMobile: Vue.computed(() => this.isMobile),
|
||||
isStudent: Vue.computed(() => this.isStudent),
|
||||
isMitarbeiter: Vue.computed(() => this.isMitarbeiter)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isMobile: function() {
|
||||
return (this.windowWidth < 767);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isInternalRoute(href) {
|
||||
const internalBase = window.location.origin
|
||||
|
||||
@@ -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/c4studentAbgabeNotAllowedRegularv2')), 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-down' : 'fa-solid fa-chevron-right'" 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
@@ -5,8 +5,9 @@ import VueDatePicker from '../../vueDatepicker.js.php';
|
||||
import ApiAbgabe from '../../../api/factory/abgabe.js'
|
||||
import FhcOverlay from "../../Overlay/FhcOverlay.js";
|
||||
import { getDateStyleClass } from "./getDateStyleClass.js";
|
||||
import { dateFilter } from '../../../tabulator/filters/Dates.js';
|
||||
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
|
||||
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
|
||||
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
|
||||
|
||||
export const AbgabetoolMitarbeiter = {
|
||||
name: "AbgabetoolMitarbeiter",
|
||||
@@ -33,7 +34,12 @@ export const AbgabetoolMitarbeiter = {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: null,
|
||||
filteredRows: null,
|
||||
count: 0,
|
||||
filteredcount: 0,
|
||||
selectedcount: 0,
|
||||
qgate1FilterSelected: [],
|
||||
qgate2FilterSelected: [],
|
||||
abgabetypenBetreuer: null,
|
||||
detailIsFullscreen: false,
|
||||
phrasenPromise: null,
|
||||
@@ -48,7 +54,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
allowedNotenOptions: null,
|
||||
notenOptionsNonFinal: null,
|
||||
serienTermin: Vue.reactive({
|
||||
datum: new Date(),
|
||||
datum: getViennaTodayISO(),
|
||||
bezeichnung: {
|
||||
paabgabetyp_kurzbz: 'zwischen',
|
||||
bezeichnung: 'Zwischenabgabe'
|
||||
@@ -70,7 +76,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
abgabeTableOptions: {
|
||||
minHeight: 250,
|
||||
index: 'projektarbeit_id',
|
||||
layout: 'fitDataStretch',
|
||||
layout: 'fitData',
|
||||
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
|
||||
selectable: true,
|
||||
selectableCheck: this.selectionCheck,
|
||||
@@ -128,38 +134,82 @@ export const AbgabetoolMitarbeiter = {
|
||||
handleClick: this.selectAllHandler
|
||||
},
|
||||
width: 50,
|
||||
cssClass: 'sticky-col'
|
||||
cssClass: 'sticky-col',
|
||||
visible: true
|
||||
},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), field: 'prevTermin',
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, headerFilter: false, headerSort: false, width: 140, minWidth: 50, visible: true, tooltip: false, cssClass: 'sticky-col'},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, visible: false,tooltip: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 150, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
|
||||
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))),
|
||||
headerFilter: dateFilter,
|
||||
headerFilterFunc: this.headerFilterTerminCol,
|
||||
sorter: this.sortFuncTerminCol,
|
||||
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
|
||||
tooltip: this.toolTipFuncPrevTermin,
|
||||
field: 'prevTermin', formatter: this.abgabeterminFormatter, width: 250, visible: false},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
|
||||
headerFilter: dateFilter,
|
||||
headerFilterFunc: this.headerFilterTerminCol,
|
||||
sorter: this.sortFuncTerminCol,
|
||||
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
|
||||
tooltip: this.toolTipFuncNextTermin,
|
||||
formatter: this.abgabeterminFormatter, width: 250, visible: true},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
|
||||
headerFilter: 'list',
|
||||
headerFilterParams: { valuesLookup: this.getQGateStatusList },
|
||||
field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false},
|
||||
headerFilter: this.qgateHeaderFilterEditor,
|
||||
headerFilterFunc: this.qgateHeaderFilterFunc,
|
||||
headerFilterParams: {},
|
||||
field: 'qgate1Status',
|
||||
formatter: this.centeredTextFormatter,
|
||||
titleFormatter: this.shortLongTitleFormatter,
|
||||
titleFormatterParams: {
|
||||
shortForm: 'QG1'
|
||||
},
|
||||
width: 50,
|
||||
tooltip: (e, cell) => {
|
||||
const data = cell.getData();
|
||||
return data.qgate1Status
|
||||
}
|
||||
},
|
||||
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))),
|
||||
headerFilter: 'list',
|
||||
headerFilterParams: { valuesLookup: this.getQGateStatusList },
|
||||
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}
|
||||
headerFilter: this.qgateHeaderFilterEditor,
|
||||
headerFilterFunc: this.qgateHeaderFilterFunc,
|
||||
headerFilterParams: {},
|
||||
field: 'qgate2Status',
|
||||
formatter: this.centeredTextFormatter,
|
||||
titleFormatter: this.shortLongTitleFormatter,
|
||||
titleFormatterParams: {
|
||||
shortForm: 'QG2'
|
||||
},
|
||||
width: 50,
|
||||
tooltip: (e, cell) => {
|
||||
const data = cell.getData();
|
||||
return data.qgate2Status
|
||||
}
|
||||
}
|
||||
],
|
||||
persistence: false,
|
||||
persistenceID: 'abgabeTableBetreuer2026-02-26'
|
||||
persistenceID: 'abgabeTableBetreuer2026-05-26'
|
||||
},
|
||||
abgabeTableEventHandlers: [{
|
||||
event: "tableBuilt",
|
||||
@@ -190,11 +240,388 @@ export const AbgabetoolMitarbeiter = {
|
||||
})
|
||||
|
||||
this.selectedData = data
|
||||
this.selectedcount = data.length;
|
||||
}
|
||||
},
|
||||
{
|
||||
event: 'dataFiltered',
|
||||
handler: (filters, rows) => {
|
||||
this.filteredRows = rows;
|
||||
this.filteredcount = rows.length;
|
||||
|
||||
if (!this.selectedData.length) return;
|
||||
|
||||
const visibleData = new Set(rows.map(r => r.getData()));
|
||||
const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd));
|
||||
|
||||
if (!filteredOut.length) return;
|
||||
|
||||
const filteredOutSet = new Set(filteredOut);
|
||||
this.$refs.abgabeTable.tabulator.getSelectedRows()
|
||||
.filter(r => filteredOutSet.has(r.getData()))
|
||||
.forEach(r => r.deselect());
|
||||
}
|
||||
}
|
||||
]};
|
||||
},
|
||||
methods: {
|
||||
notenSorter(a, b, aRow, bRow, column, dir, sorterParams) {
|
||||
const aData = aRow.getData()
|
||||
const bData = bRow.getData()
|
||||
return aData.note - bData.note
|
||||
},
|
||||
async openBenotung(type, link) {
|
||||
if(type === 'new') {
|
||||
window.open(link, '_blank')
|
||||
} else if(type === 'old') {
|
||||
if(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4aeltereParbeitBenotenv2'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
|
||||
acceptClass: 'btn btn-danger',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'btn btn-outline-secondary'
|
||||
}) === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
window.open(link, '_blank')
|
||||
} else {
|
||||
// show info text that no endupload with abgabe has been found
|
||||
if(await this.$fhcAlert.confirm({
|
||||
message: this.$p.t('abgabetool/c4keinEnduploadErfolgt'),
|
||||
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
|
||||
acceptClass: 'btn btn-danger',
|
||||
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
|
||||
rejectClass: 'btn btn-outline-secondary'
|
||||
}) === false) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
formAction(cell) {
|
||||
|
||||
const actionButtons = document.createElement('div');
|
||||
actionButtons.className = "d-flex gap-3";
|
||||
actionButtons.style.display = "flex";
|
||||
actionButtons.style.alignItems = "stretch";
|
||||
actionButtons.style.justifyContent = "start";
|
||||
actionButtons.style.height = "100%";
|
||||
|
||||
const val = cell.getValue();
|
||||
const data = cell.getRow().getData()
|
||||
|
||||
const createButton = (iconClass, titleKey, clickHandler) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-outline-secondary';
|
||||
btn.style.display = "flex";
|
||||
btn.style.alignItems = "center"; // center icon vertically
|
||||
btn.style.justifyContent = "center"; // center icon horizontally
|
||||
btn.style.height = "100%"; // fill parent container height
|
||||
btn.style.aspectRatio = "1 / 1"; // keep square shape (optional)
|
||||
btn.style.padding = "0"; // remove extra padding for compactness
|
||||
if(iconClass == 'fa fa-timeline') btn.style.transform = "rotate(90deg)";
|
||||
btn.innerHTML = `<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)
|
||||
@@ -207,7 +634,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
})
|
||||
const uniqueRecipients = [...new Set(recipientList)];
|
||||
const subject = ""; // empty subject line
|
||||
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
|
||||
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p)
|
||||
},
|
||||
getQGateStatusList() {
|
||||
return [
|
||||
@@ -247,7 +674,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
if (val instanceof Date) {
|
||||
dt = luxon.DateTime.fromJSDate(val);
|
||||
} else if (typeof val === "string") {
|
||||
dt = luxon.DateTime.fromISO(val);
|
||||
dt = toViennaDate(val);
|
||||
} else { // fallback
|
||||
dt = luxon.DateTime.fromMillis(Number(val));
|
||||
}
|
||||
@@ -376,6 +803,9 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
this.stateRestored = true
|
||||
|
||||
// ensure that the filterCollapseables thingy has the correct values
|
||||
this.$refs.abgabeTable.setSelectedFields();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
@@ -441,6 +871,32 @@ export const AbgabetoolMitarbeiter = {
|
||||
projekt.qgate2StatusRank = 1
|
||||
}
|
||||
})
|
||||
|
||||
// set shorthand statuscode once real status has been determined
|
||||
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
|
||||
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
|
||||
},
|
||||
mapRankToShortStatus(rank) {
|
||||
switch(rank){
|
||||
case 0: // kein termin vorhanden
|
||||
return '--'
|
||||
break;
|
||||
case 1: // noch nicht stattgefunden
|
||||
return 'o'
|
||||
break;
|
||||
case 2: // noch nicht abgegeben
|
||||
return '?'
|
||||
break;
|
||||
case 3: // noch nicht benotet
|
||||
return '~'
|
||||
break;
|
||||
case 4: // negativ benotet
|
||||
return '-'
|
||||
break;
|
||||
case 5: // positiv benotet
|
||||
return '+'
|
||||
break;
|
||||
}
|
||||
},
|
||||
checkAbgabetermineProjektarbeit(projekt) {
|
||||
const now = luxon.DateTime.now()
|
||||
@@ -450,7 +906,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
// while already looping through each termin, calculate datestyle beforehand
|
||||
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
|
||||
|
||||
const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
|
||||
const date = toViennaDate(termin.datum).endOf('day')
|
||||
termin.luxonDate = date
|
||||
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
|
||||
|
||||
@@ -472,7 +928,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
// seperate check for quality gates
|
||||
this.checkQualityGateStatus(projekt)
|
||||
},
|
||||
abgabterminFormatter(cell) {
|
||||
abgabeterminFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
|
||||
if(val) {
|
||||
@@ -504,14 +960,15 @@ export const AbgabetoolMitarbeiter = {
|
||||
break
|
||||
}
|
||||
|
||||
const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung
|
||||
const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz
|
||||
const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz)
|
||||
|
||||
return '<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>'
|
||||
|
||||
@@ -535,16 +992,19 @@ export const AbgabetoolMitarbeiter = {
|
||||
},
|
||||
selectAllHandler(e, cell) {
|
||||
const table = cell.getTable();
|
||||
const rows = table.getRows();
|
||||
const rows = this.filteredRows ?? table.getRows();
|
||||
|
||||
// custom select all logic
|
||||
const allowed = rows.filter(r => r.getData().selectable);
|
||||
// since betreuerpage acctually has logic behind selectable flag, it is important to go over allowed only here
|
||||
const selected = allowed.every(r => r.isSelected());
|
||||
|
||||
if(selected) {
|
||||
if(selected){
|
||||
allowed.forEach(r => r.deselect());
|
||||
e.target.checked = false;
|
||||
} else {
|
||||
allowed.forEach(r => r.select());
|
||||
e.target.checked = true;
|
||||
}
|
||||
|
||||
// stop built-in handler
|
||||
@@ -555,18 +1015,10 @@ export const AbgabetoolMitarbeiter = {
|
||||
this.detailIsFullscreen = !this.detailIsFullscreen
|
||||
},
|
||||
getOptionLabelAbgabetyp(option){
|
||||
return option.bezeichnung
|
||||
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
|
||||
},
|
||||
formatDate(dateParam) {
|
||||
const date = new Date(dateParam)
|
||||
// handle missing leading 0
|
||||
const padZero = (num) => String(num).padStart(2, '0');
|
||||
|
||||
const month = padZero(date.getMonth() + 1); // Months are zero-based
|
||||
const day = padZero(date.getDate());
|
||||
const year = date.getFullYear();
|
||||
|
||||
return `${day}.${month}.${year}`;
|
||||
return formatISODate(dateParam);
|
||||
},
|
||||
undoSelection(cell) {
|
||||
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
|
||||
@@ -579,6 +1031,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
},
|
||||
selectionCheck(row) {
|
||||
const data = row.getData()
|
||||
|
||||
// zweitbetreuer cant select projektarbeiten for serientermine
|
||||
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
|
||||
return true
|
||||
},
|
||||
@@ -602,7 +1056,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
addSeries() {
|
||||
this.saving = true
|
||||
this.$api.call(ApiAbgabe.postSerientermin(
|
||||
this.serienTermin.datum.toISOString(),
|
||||
this.serienTermin.datum,
|
||||
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
|
||||
this.serienTermin.bezeichnung.bezeichnung,
|
||||
this.serienTermin.kurzbz,
|
||||
@@ -700,7 +1154,7 @@ export const AbgabetoolMitarbeiter = {
|
||||
termin.allowedToSave = paIsBenotet ? false : true
|
||||
|
||||
// lektoren are not allowed to delete deadlines with existing submissions
|
||||
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
|
||||
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum && !termin.note
|
||||
|
||||
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
|
||||
|
||||
@@ -717,28 +1171,37 @@ export const AbgabetoolMitarbeiter = {
|
||||
|
||||
},
|
||||
centeredTextFormatter(cell) {
|
||||
const val = cell.getValue()
|
||||
if(!val) return
|
||||
|
||||
return '<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
|
||||
@@ -753,15 +1216,19 @@ export const AbgabetoolMitarbeiter = {
|
||||
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
|
||||
},
|
||||
setupData(data){
|
||||
|
||||
|
||||
this.projektarbeiten = data[0]
|
||||
this.domain = data[1]
|
||||
|
||||
this.tableData = data[0]?.retval?.map(projekt => {
|
||||
this.projektarbeiten = data[0]?.retval?.map(projekt => {
|
||||
this.checkAbgabetermineProjektarbeit(projekt)
|
||||
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
|
||||
|
||||
if(this.notenOptions && projekt.note) {
|
||||
const opt = this.notenOptions.find(n => n.note == projekt.note)
|
||||
// TODO: mehrsprachig englisch -> nevermind the english field in
|
||||
// notenoption->bezeichnung_mehrsprachig is ALWAYS german
|
||||
projekt.note_bez = opt?.bezeichnung
|
||||
}
|
||||
|
||||
return {
|
||||
...projekt,
|
||||
details: {
|
||||
@@ -777,9 +1244,10 @@ export const AbgabetoolMitarbeiter = {
|
||||
titel: projekt.titel
|
||||
}
|
||||
})
|
||||
this.count = this.projektarbeiten.length
|
||||
|
||||
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
|
||||
this.$refs.abgabeTable.tabulator.setData(this.tableData);
|
||||
this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten);
|
||||
},
|
||||
loadProjektarbeiten(all = false, callback) {
|
||||
this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all))
|
||||
@@ -828,9 +1296,20 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
|
||||
this.serienTermin.upload_allowed = newVal.upload_allowed_default
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <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 = []
|
||||
|
||||
@@ -859,6 +1338,8 @@ export const AbgabetoolMitarbeiter = {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
document.documentElement.classList.add('abgabetool');
|
||||
|
||||
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
|
||||
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
|
||||
// fetch config to avoid hard coded links
|
||||
@@ -900,12 +1381,16 @@ export const AbgabetoolMitarbeiter = {
|
||||
mounted() {
|
||||
this.setupMounted()
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.documentElement.classList.remove('abgabetool');
|
||||
},
|
||||
template: `
|
||||
<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') }}
|
||||
@@ -925,6 +1410,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>
|
||||
@@ -972,7 +1458,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')}}
|
||||
@@ -991,12 +1478,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,15 +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 ApiAuthinfo from "../../../api/factory/authinfo.js";
|
||||
import { validateThesisTitle } from './titleValidation.js'
|
||||
|
||||
export const AbgabetoolStudent = {
|
||||
name: "AbgabetoolStudent",
|
||||
components: {
|
||||
Accordion: primevue.accordion,
|
||||
AccordionTab: primevue.accordiontab,
|
||||
Textarea: primevue.textarea,
|
||||
BsModal,
|
||||
AbgabeDetail,
|
||||
FhcOverlay
|
||||
@@ -18,7 +20,11 @@ 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: {
|
||||
@@ -39,6 +45,12 @@ export const AbgabetoolStudent = {
|
||||
selectedProjektarbeit: null,
|
||||
moodle_link: null,
|
||||
uid: null,
|
||||
title_edit_allowed: null,
|
||||
confetti_on_endupload: null,
|
||||
siginfolink_german: null,
|
||||
siginfolink_english: null,
|
||||
editingTitel: '',
|
||||
editingProjektarbeit: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -50,10 +62,67 @@ export const AbgabetoolStudent = {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
@@ -70,7 +139,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
|
||||
@@ -111,47 +180,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) {
|
||||
@@ -173,8 +230,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()
|
||||
@@ -184,19 +241,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: {
|
||||
@@ -230,16 +285,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) {
|
||||
@@ -247,6 +300,31 @@ export const AbgabetoolStudent = {
|
||||
return 'mailto:'+projektarbeit.email
|
||||
} else return ''
|
||||
},
|
||||
getZweitbetreuerMailLink(projektarbeit) {
|
||||
if(projektarbeit.zweitbetreuer_mail) {
|
||||
return 'mailto:'+projektarbeit.zweitbetreuer_mail
|
||||
} else return ''
|
||||
},
|
||||
hasZweitbetreuer(projektarbeit) {
|
||||
return !!(projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer)
|
||||
},
|
||||
getErstbetreuerEmailLabel(projektarbeit) {
|
||||
// with only one betreuer keep the generic label; with multiple betreuer
|
||||
// distinguish the email rows by their betreuungsart bezeichnung
|
||||
if(!this.hasZweitbetreuer(projektarbeit)) {
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4betreuerEmailKontaktv2'))
|
||||
}
|
||||
const art = projektarbeit.betreuerart_kurzbz
|
||||
? this.$p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz)
|
||||
: this.$p.t('abgabetool/c4betreuerv2')
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
|
||||
},
|
||||
getZweitbetreuerEmailLabel(projektarbeit) {
|
||||
const art = projektarbeit.zweitbetreuer_betreuerart_kurzbz
|
||||
? this.$p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz)
|
||||
: ''
|
||||
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
|
||||
},
|
||||
getNoteBezeichnung(projektarbeit) {
|
||||
if(projektarbeit.note && this.notenOptions) {
|
||||
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
|
||||
@@ -266,12 +344,15 @@ export const AbgabetoolStudent = {
|
||||
this.uid = authIdResponse.data.uid;
|
||||
},
|
||||
},
|
||||
watch: {},
|
||||
async created() {
|
||||
// make sure zoom media query doesnt spill ever to other CIS4 sites
|
||||
document.documentElement.classList.add('abgabetool');
|
||||
|
||||
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]
|
||||
@@ -284,16 +365,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
|
||||
})
|
||||
@@ -303,26 +386,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>
|
||||
|
||||
@@ -332,8 +462,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>
|
||||
@@ -382,7 +516,7 @@ export const AbgabetoolStudent = {
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getErstbetreuerEmailLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
</div>
|
||||
@@ -393,18 +527,46 @@ export const AbgabetoolStudent = {
|
||||
{{ projektarbeit.zweitbetreuer?.first }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{ getZweitbetreuerEmailLabel(projektarbeit) }}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
<a v-if="projektarbeit.zweitbetreuer_mail" :href="getZweitbetreuerMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-2">
|
||||
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
|
||||
<div class="col-8 col-md-9">
|
||||
{{ 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>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -64,6 +64,9 @@ export default {
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
// do not show download links in otherLvPlan Mode
|
||||
return false;
|
||||
|
||||
if (
|
||||
!this.studiensemester_start ||
|
||||
!this.studiensemester_ende ||
|
||||
|
||||
@@ -72,6 +72,9 @@ export default {
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
// do not show download links in StgOrg Mode
|
||||
return false;
|
||||
|
||||
if (
|
||||
!this.studiensemester_start ||
|
||||
!this.studiensemester_ende ||
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
import FhcCalendar from "../../Calendar/LvPlan.js";
|
||||
|
||||
import ApiLvPlan from '../.././../api/factory/lvPlan.js';
|
||||
import ApiAuthinfo from '../../../api/factory/authinfo.js';
|
||||
|
||||
export const DEFAULT_MODE_LVPLAN = 'Week';
|
||||
|
||||
export default {
|
||||
name: 'LvPlanStgOrg',
|
||||
inject: {
|
||||
cisRoot: {
|
||||
from: 'cisRoot'
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FhcCalendar,
|
||||
},
|
||||
props: {
|
||||
viewData: Object,
|
||||
propsViewData: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
studiensemester_kurzbz: null,
|
||||
studiensemester_start: null,
|
||||
studiensemester_ende: null,
|
||||
uid: null,
|
||||
isMitarbeiter: false,
|
||||
isStudent: false,
|
||||
listStg: [],
|
||||
currentStgBezeichnung: null
|
||||
};
|
||||
},
|
||||
computed:{
|
||||
currentDay() {
|
||||
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
|
||||
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
|
||||
return this.propsViewData?.focus_date;
|
||||
},
|
||||
currentMode() {
|
||||
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
|
||||
return DEFAULT_MODE_LVPLAN;
|
||||
return this.propsViewData?.mode;
|
||||
},
|
||||
downloadLinks() {
|
||||
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
|
||||
return false;
|
||||
|
||||
let type = false;
|
||||
type = this.isStudent ? 'student' : type;
|
||||
type = this.isMitarbeiter ? 'lektor' : type;
|
||||
if (false === type)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const opts = { zone: this.viewData.timezone };
|
||||
const start = luxon.DateTime
|
||||
.fromISO(this.studiensemester_start, opts)
|
||||
.toUnixInteger();
|
||||
const ende = luxon.DateTime
|
||||
.fromISO(this.studiensemester_ende, opts)
|
||||
.toUnixInteger();
|
||||
|
||||
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
|
||||
+ 'cis/private/lvplan/stpl_kalender.php'
|
||||
+ '?type=' + type
|
||||
+ '&pers_uid=' + this.uid
|
||||
+ '&begin=' + start
|
||||
+ '&ende=' + ende;
|
||||
|
||||
return [
|
||||
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
|
||||
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
|
||||
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
|
||||
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
|
||||
];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleChangeDate(day, newMode) {
|
||||
return this.handleChangeMode(newMode, day);
|
||||
},
|
||||
handleChangeMode(newMode, day) {
|
||||
const mode = newMode[0].toUpperCase() + newMode.slice(1)
|
||||
const focus_date = day.toISODate();
|
||||
|
||||
this.$router.push({
|
||||
name: "StgOrgLvPlan",
|
||||
params: {
|
||||
mode,
|
||||
focus_date,
|
||||
stgkz: this.propsViewData.stgkz,
|
||||
sem: this.propsViewData.sem,
|
||||
verband: this.propsViewData.verband,
|
||||
gruppe: this.propsViewData.gruppe,
|
||||
}
|
||||
});
|
||||
},
|
||||
updateRange(rangeInterval) {
|
||||
this.$api
|
||||
.call(ApiLvPlan.studiensemesterDateInterval(
|
||||
rangeInterval.end.startOf('week').toISODate()
|
||||
))
|
||||
.then(res => {
|
||||
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
|
||||
this.studiensemester_start = res.data.start;
|
||||
this.studiensemester_ende = res.data.ende;
|
||||
});
|
||||
},
|
||||
getPromiseFunc(start, end) {
|
||||
return [
|
||||
this.$api.call(ApiLvPlan.eventsStgOrg(start.toISODate(), end.toISODate(), this.propsViewData.stgkz, this.propsViewData.sem, this.propsViewData.verband, this.propsViewData.gruppe)),
|
||||
//local for test
|
||||
/* this.$api.call(ApiLvPlan.eventsStgOrg(start.toISODate(), end.toISODate(), this.stgkz, this.sem, this.verband, this.gruppe))*/
|
||||
];
|
||||
},
|
||||
backToDropdown(){
|
||||
this.$router.push({
|
||||
name: "OverviewLvPlan",
|
||||
});
|
||||
}
|
||||
},
|
||||
created(){
|
||||
this.$api
|
||||
.call(ApiAuthinfo.getAuthInfo())
|
||||
.then(res => {
|
||||
this.uid = res.data.uid;
|
||||
this.isMitarbeiter = res.data.isMitarbeiter;
|
||||
this.isStudent = res.data.isStudent;
|
||||
});
|
||||
|
||||
if(this.propsViewData.stgkz) {
|
||||
this.$api
|
||||
.call(ApiLvPlan.getStudiengaenge())
|
||||
.then(result => {
|
||||
const currentStg = result.data.find(
|
||||
item => item.studiengang_kz == this.propsViewData.stgkz
|
||||
);
|
||||
this.currentStgBezeichnung = currentStg.kurzbzlang + " - " + currentStg.bezeichnung;
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
|
||||
<h2>{{ $p.t('LvPlan/headerLvPlanLvVerband') }}</h2>
|
||||
<p v-if="propsViewData.stgkz"><span><strong>{{currentStgBezeichnung}}</strong></span>
|
||||
<span v-if="propsViewData.sem" style="margin-left:10px;"> Semester: {{propsViewData.sem}} </span>
|
||||
<span v-if="propsViewData.verband" style="margin-left:10px;"> Verband: {{propsViewData.verband}} </span>
|
||||
<span v-if="propsViewData.gruppe" style="margin-left:10px;"> Gruppe: {{propsViewData.gruppe}} </span>
|
||||
<button class="py-1 btn btn-outline-secondary" :title="this.$p.t('LvPlan', 'backToDropdown')" style="margin-left:10px;"> <i class="fa fa-arrow-left fa-xs" @click="backToDropdown"></i></button>
|
||||
</p>
|
||||
<p v-else>{{ $p.t('LvPlan/noStgProvided') }}</p>
|
||||
|
||||
<fhc-calendar
|
||||
ref="calendar"
|
||||
:timezone="viewData.timezone"
|
||||
:get-promise-func="getPromiseFunc"
|
||||
:date="currentDay"
|
||||
:mode="currentMode"
|
||||
@update:date="handleChangeDate"
|
||||
@update:mode="handleChangeMode"
|
||||
@update:range="updateRange"
|
||||
class="responsive-calendar"
|
||||
>
|
||||
<div
|
||||
v-if="downloadLinks"
|
||||
class="d-flex gap-1 justify-items-start"
|
||||
>
|
||||
<div v-for="{ title, icon, link } in downloadLinks">
|
||||
<a
|
||||
:href="link"
|
||||
:aria-label="title"
|
||||
class="py-1 btn btn-outline-secondary"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
<i aria-hidden="true" :class="icon"></i>
|
||||
<span style="font-size:.5rem">{{ title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</fhc-calendar>
|
||||
</div>
|
||||
`,
|
||||
};
|
||||
@@ -36,7 +36,6 @@ export default {
|
||||
return {
|
||||
showModal: false,
|
||||
editDataFilter: null,
|
||||
arePhrasesPreloaded: false,
|
||||
// tabulator options
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaProfilFunktionen",
|
||||
@@ -49,6 +48,7 @@ export default {
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -63,7 +63,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('ui/bezeichnung')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "ui/bezeichnung",
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -71,7 +72,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "lehre/organisationseinheit",
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -79,7 +81,8 @@ export default {
|
||||
responsive: 1,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigVon')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigVon",
|
||||
field: "Gültig_von",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -91,7 +94,8 @@ export default {
|
||||
responsive: 4,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('global/gueltigBis')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigBis",
|
||||
field: "Gültig_bis",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -103,7 +107,8 @@ export default {
|
||||
responsive: 3,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/wochenstunden')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/wochenstunden",
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -124,6 +129,7 @@ export default {
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}],
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -138,7 +144,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/entlehnteBetriebsmittel')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/entlehnteBetriebsmittel",
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -146,7 +153,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/inventarnummer')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/inventarnummer",
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -155,7 +163,8 @@ export default {
|
||||
responsive: 2,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/ausgabedatum')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/ausgabedatum",
|
||||
field: "Ausgegeben_am",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -230,10 +239,6 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
setTableColumnTitles() { // reevaluates computed phrasen
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns);
|
||||
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns);
|
||||
},
|
||||
datetimeFormatterParams: function() {
|
||||
const params = {
|
||||
inputFormat:"yyyy-MM-dd",
|
||||
@@ -316,10 +321,6 @@ export default {
|
||||
},
|
||||
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory(["ui","lehre","global","profil"]).then(() => {
|
||||
this.arePhrasesPreloaded = true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
},
|
||||
@@ -330,9 +331,6 @@ export default {
|
||||
'data.mittel'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setData(newVal);
|
||||
},
|
||||
'language.value'(newVal) {
|
||||
this.setTableColumnTitles()
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form" >
|
||||
@@ -454,7 +452,6 @@ export default {
|
||||
<!-- FUNKTIONEN TABELLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="arePhrasesPreloaded"
|
||||
@tableBuilt="funktionenTableBuilt"
|
||||
:title="$p.t('person','funktionen')"
|
||||
ref="funktionenTable"
|
||||
@@ -466,7 +463,6 @@ export default {
|
||||
<!-- BETRIEBSMITTEL TABELLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="arePhrasesPreloaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
|
||||
@@ -20,7 +20,6 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
collapseIconFunktionen: true,
|
||||
arePhrasesPreloaded: false,
|
||||
funktionen_table_options: {
|
||||
persistenceID: "filterTableMaViewProfilFunktionen",
|
||||
persistence: {
|
||||
@@ -31,6 +30,7 @@ export default {
|
||||
responsiveLayout: "collapse",
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
locale: true,
|
||||
columns: [
|
||||
//? option when wanting to hide the collapsed list
|
||||
|
||||
@@ -46,7 +46,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t("ui/bezeichnung")),
|
||||
title: "placeholder",
|
||||
titlePhrase: "ui/bezeichnung",
|
||||
field: "Bezeichnung",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -54,9 +55,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>
|
||||
this.$p.t("lehre/organisationseinheit"),
|
||||
),
|
||||
title: "placeholder",
|
||||
titlePhrase: "lehre/organisationseinheit",
|
||||
field: "Organisationseinheit",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -64,9 +64,8 @@ export default {
|
||||
responsive: 1,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>
|
||||
this.$p.t("global/gueltigVon"),
|
||||
),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigVon",
|
||||
field: "Gültig_von",
|
||||
headerFilterFunc: "dates",
|
||||
headerFilter: dateFilter,
|
||||
@@ -78,9 +77,8 @@ export default {
|
||||
responsive: 4,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>
|
||||
this.$p.t("global/gueltigBis"),
|
||||
),
|
||||
title: "placeholder",
|
||||
titlePhrase: "global/gueltigBis",
|
||||
field: "Gültig_bis",
|
||||
headerFilterFunc: "dates",
|
||||
headerFilter: dateFilter,
|
||||
@@ -92,9 +90,8 @@ export default {
|
||||
responsive: 3,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() =>
|
||||
this.$p.t("profil/wochenstunden"),
|
||||
),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/wochenstunden",
|
||||
field: "Wochenstunden",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -220,11 +217,6 @@ export default {
|
||||
return quickLinks;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => {
|
||||
this.arePhrasesPreloaded = true;
|
||||
});
|
||||
},
|
||||
|
||||
template: /*html*/ `
|
||||
|
||||
@@ -279,7 +271,7 @@ export default {
|
||||
<div class="row">
|
||||
<!-- FIRST TABLE -->
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt v-if="arePhrasesPreloaded" @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
|
||||
</div>
|
||||
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
|
||||
</div>
|
||||
|
||||
@@ -29,13 +29,12 @@ export default {
|
||||
QuickLinks,
|
||||
CalendarSync,
|
||||
},
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
|
||||
inject: ["sortProfilUpdates", "collapseFunction", "isEditable"],
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
collapseIconBetriebsmittel: true,
|
||||
editDataFilter: null,
|
||||
arePhrasesPreloaded: false,
|
||||
// tabulator options
|
||||
zutrittsgruppen_table_options: {
|
||||
persistenceID: "filterTableStudentProfilZutrittsgruppen",
|
||||
@@ -44,9 +43,9 @@ export default {
|
||||
},
|
||||
minHeight: 200,
|
||||
layout: "fitColumns",
|
||||
columns: [
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/zutrittsGruppen')),
|
||||
columns: [{
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/zutrittsGruppen",
|
||||
field: "bezeichnung"
|
||||
}
|
||||
],
|
||||
@@ -62,6 +61,7 @@ export default {
|
||||
responsiveLayoutCollapseUseFormatters: false,
|
||||
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
|
||||
responsiveLayoutCollapseStartOpen: false,
|
||||
locale: true,
|
||||
columns: [
|
||||
{
|
||||
title:
|
||||
@@ -75,7 +75,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(()=>this.$p.t('profil/entlehnteBetriebsmittel')),
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/entlehnteBetriebsmittel",
|
||||
field: "betriebsmittel",
|
||||
headerFilter: true,
|
||||
minWidth: 200,
|
||||
@@ -83,7 +84,8 @@ export default {
|
||||
responsive: 0,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/inventarnummer')) ,
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/inventarnummer",
|
||||
field: "Nummer",
|
||||
headerFilter: true,
|
||||
resizable: true,
|
||||
@@ -92,7 +94,8 @@ export default {
|
||||
responsive: 2,
|
||||
},
|
||||
{
|
||||
title: Vue.computed(() => this.$p.t('profil/ausgabedatum')) ,
|
||||
title: "placeholder",
|
||||
titlePhrase: "profil/ausgabedatum",
|
||||
field: "Ausgegeben_am",
|
||||
headerFilterFunc: 'dates',
|
||||
headerFilter: dateFilter,
|
||||
@@ -259,19 +262,9 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
// preload phrasen
|
||||
this.$p.loadCategory('profil').then(() => {
|
||||
this.arePhrasesPreloaded = true;
|
||||
});
|
||||
//? sorts the profil Updates: pending -> accepted -> rejected
|
||||
this.data.profilUpdates?.sort(this.sortProfilUpdates);
|
||||
},
|
||||
watch: {
|
||||
'language.value'(newVal) {
|
||||
if(this.$refs.betriebsmittelTable) this.$refs.betriebsmittelTable.tabulator.setColumns(this.betriebsmittel_table_options.columns)
|
||||
if(this.$refs.zutrittsgruppenTable) this.$refs.zutrittsgruppenTable.tabulator.setColumns(this.zutrittsgruppen_table_options.columns)
|
||||
}
|
||||
},
|
||||
template: /*html*/ `
|
||||
<div class="container-fluid text-break fhc-form">
|
||||
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal"
|
||||
@@ -395,7 +388,6 @@ export default {
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="arePhrasesPreloaded"
|
||||
@tableBuilt="betriebsmittelTableBuilt"
|
||||
:title="$p.t('profil','entlehnteBetriebsmittel')"
|
||||
ref="betriebsmittelTable"
|
||||
@@ -405,7 +397,6 @@ export default {
|
||||
</div>
|
||||
<div class="col-12 mb-4" >
|
||||
<core-filter-cmpt
|
||||
v-if="arePhrasesPreloaded"
|
||||
@tableBuilt="zutrittsgruppenTableBuilt"
|
||||
:title="$p.t('profil','zutrittsGruppen')"
|
||||
ref="zutrittsgruppenTable"
|
||||
|
||||
@@ -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)
|
||||
|
||||
+42
-59
@@ -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>
|
||||
|
||||
+33
-9
@@ -10,6 +10,7 @@ import Vertrag from "./Vertrag.js";
|
||||
import ApiStvProjektbetreuer from '../../../../../api/factory/stv/projektbetreuer.js';
|
||||
|
||||
export default {
|
||||
name: 'Projektbetreuer',
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
BsModal,
|
||||
@@ -120,7 +121,7 @@ export default {
|
||||
layoutColumnsOnNewData: false,
|
||||
height: 'auto',
|
||||
minHeight: '100',
|
||||
selectableRows: true,
|
||||
selectableRows: 1,
|
||||
index: 'betreuer_id',
|
||||
persistence:{
|
||||
columns: true, //persist column layout
|
||||
@@ -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">
|
||||
|
||||
@@ -22,8 +22,14 @@ import TableDownload from './Table/Download.js';
|
||||
import collapseAutoClose from '../../directives/collapseAutoClose.js';
|
||||
|
||||
import moduleLayoutFitDataStretchFrozen from '../../tabulator/layouts/fitDataStretchFrozen.js';
|
||||
import InternalToExternalEventBroadcastModule from "../../tabulator/customModules/InternalToExternalEventBroadcastModule.js"
|
||||
import MenuExtensionModule from "../../tabulator/customModules/MenuExtensionModule.js"
|
||||
import ResponsiveLayoutExtensionModule from "../../tabulator/customModules/ResponsiveLayoutExtensionModule.js"
|
||||
|
||||
import { debounce } from "../../helpers/DebounceHelper.js";
|
||||
|
||||
import ApiFilter from '../../api/factory/filter.js';
|
||||
import ApiPhrases from '../../api/factory/phrasen.js';
|
||||
|
||||
//
|
||||
const FILTER_COMPONENT_NEW_FILTER = 'Filter Component New Filter';
|
||||
@@ -84,8 +90,9 @@ export const CoreFilterCmpt = {
|
||||
useSelectionSpan: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
},
|
||||
inject: ["language"],
|
||||
data: function() {
|
||||
return {
|
||||
uuid: 0,
|
||||
@@ -120,7 +127,8 @@ export const CoreFilterCmpt = {
|
||||
headerFilter: false,
|
||||
group: false,
|
||||
page: false,
|
||||
}
|
||||
},
|
||||
collapsedHeadingLocalizationTimer: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -183,20 +191,25 @@ export const CoreFilterCmpt = {
|
||||
return columns;
|
||||
},
|
||||
fieldIdsForVisibilty() {
|
||||
if (!this.tableBuilt)
|
||||
return [];
|
||||
return this.tabulator.getColumns().filter(col => {
|
||||
let def = col.getDefinition();
|
||||
return !def.frozen && def.title && def.formatter != "responsiveCollapse";
|
||||
}).map(col => col.getField());
|
||||
},
|
||||
fieldNames() {
|
||||
if (!this.tableBuilt)
|
||||
return {};
|
||||
return this.tabulator.getColumns().reduce((res, col) => {
|
||||
res[col.getField()] = col.getDefinition().title;
|
||||
return res;
|
||||
}, {});
|
||||
if (!this.tableBuilt) return [];
|
||||
|
||||
const localizedColumnTitles = this.tabulator.getLang().columns;
|
||||
const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale;
|
||||
return this.tabulator
|
||||
.getColumns()
|
||||
.filter((col) => {
|
||||
let def = col.getDefinition();
|
||||
let title =
|
||||
isTabulatorLocalized && localizedColumnTitles[def.field]
|
||||
? localizedColumnTitles[def.field]
|
||||
: def.title;
|
||||
return (
|
||||
!def.frozen &&
|
||||
title &&
|
||||
def.formatter != "responsiveCollapse"
|
||||
);
|
||||
})
|
||||
.map((col) => col.getField());
|
||||
},
|
||||
idExtra() {
|
||||
if (!this.uuid)
|
||||
@@ -204,14 +217,30 @@ export const CoreFilterCmpt = {
|
||||
return '-' + this.uuid;
|
||||
},
|
||||
columnsForFilter() {
|
||||
if (!this.filteredColumns || !this.datasetMetadata)
|
||||
return [];
|
||||
const filterTitles = this.filteredColumns.reduce((a,c) => {
|
||||
a[c.field] = c.title;
|
||||
if (!this.filteredColumns || !this.datasetMetadata) return [];
|
||||
const localizedColumnTitles = this.tabulator.getLang().columns;
|
||||
const isTabulatorLocalized = !!this.$props.tabulatorOptions.locale;
|
||||
const filterTitles = this.filteredColumns.reduce((a, c) => {
|
||||
a[c.field] =
|
||||
isTabulatorLocalized && localizedColumnTitles[c.field]
|
||||
? localizedColumnTitles[c.field]
|
||||
: c.title;
|
||||
return a;
|
||||
}, {});
|
||||
return this.datasetMetadata.map(el => ({...el, ...{title: filterTitles[el.name]}}));
|
||||
}
|
||||
return this.datasetMetadata.map((el) => ({
|
||||
...el,
|
||||
...{ title: filterTitles[el.name] },
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"language.value": {
|
||||
handler(newSelectedLanguage) {
|
||||
if (!this.$props.tabulatorOptions.locale) return;
|
||||
|
||||
this.tabulator.setLocale(newSelectedLanguage);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reloadTable() {
|
||||
@@ -220,6 +249,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) {
|
||||
@@ -263,6 +296,10 @@ export const CoreFilterCmpt = {
|
||||
persistence: this.persistence,
|
||||
}, ...(this.tabulatorOptions || {})};
|
||||
|
||||
if (tabulatorOptions.locale) {
|
||||
tabulatorOptions = await this.configureTabulatorLocalizations(tabulatorOptions);
|
||||
}
|
||||
|
||||
// set default height if no height property is set
|
||||
if (tabulatorOptions.height === undefined &&
|
||||
tabulatorOptions.minHeight === undefined &&
|
||||
@@ -291,10 +328,10 @@ export const CoreFilterCmpt = {
|
||||
}
|
||||
|
||||
// Start the tabulator with the build options
|
||||
this.tabulator = new Tabulator(
|
||||
this.$refs.table,
|
||||
tabulatorOptions
|
||||
);
|
||||
this.tabulator = new Tabulator(this.$refs.table, {
|
||||
...tabulatorOptions,
|
||||
debugInvalidOptions: false,
|
||||
});
|
||||
// If event handlers have been provided
|
||||
if (Array.isArray(this.tabulatorEvents) && this.tabulatorEvents.length > 0)
|
||||
{
|
||||
@@ -302,7 +339,10 @@ export const CoreFilterCmpt = {
|
||||
for (let evt of this.tabulatorEvents)
|
||||
this.tabulator.on(evt.event, evt.handler);
|
||||
}
|
||||
this.tabulator.on('tableBuilt', () => {this.tableBuilt = true; this.$emit('tableBuilt');});
|
||||
this.tabulator.on('tableBuilt', () => {
|
||||
this.tableBuilt = true;
|
||||
this.$emit('tableBuilt');
|
||||
});
|
||||
this.tabulator.on("rowSelectionChanged", data => {
|
||||
this.selectedData = data;
|
||||
});
|
||||
@@ -337,7 +377,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();
|
||||
});
|
||||
@@ -348,6 +388,121 @@ export const CoreFilterCmpt = {
|
||||
this.filterActive = filters.length > 0;
|
||||
this.$emit("headerFilterOn", this.filterActive);
|
||||
});
|
||||
this.tabulator.on("localized", () => {
|
||||
this.tabulator.modules.responsiveLayout.generateCollapsedContent();
|
||||
});
|
||||
},
|
||||
async configureTabulatorLocalizations(tabulatorOptions) {
|
||||
tabulatorOptions.locale = this.$p.user_language.value;
|
||||
tabulatorOptions.langs = {};
|
||||
|
||||
const genericTabulatorPhrasesResponse = await this.$api.call(
|
||||
ApiPhrases.getTabulatorPhrases(),
|
||||
);
|
||||
const genericTabulatorPhrasesData = genericTabulatorPhrasesResponse.data;
|
||||
Object.keys(genericTabulatorPhrasesData).forEach((language) => {
|
||||
let genericTabulatorPhraseToTranslationMapper = {};
|
||||
genericTabulatorPhrasesData[language].forEach((phrase) => {
|
||||
genericTabulatorPhraseToTranslationMapper[phrase.phrase] = phrase.text;
|
||||
});
|
||||
|
||||
tabulatorOptions.langs[language] = {
|
||||
data: {
|
||||
loading: genericTabulatorPhraseToTranslationMapper["loading"],
|
||||
error: genericTabulatorPhraseToTranslationMapper["error"],
|
||||
},
|
||||
groups: {
|
||||
item: genericTabulatorPhraseToTranslationMapper["item"],
|
||||
items: genericTabulatorPhraseToTranslationMapper["items"],
|
||||
},
|
||||
pagination: {
|
||||
page_size:
|
||||
genericTabulatorPhraseToTranslationMapper["page_size"],
|
||||
page_title:
|
||||
genericTabulatorPhraseToTranslationMapper["page_title"],
|
||||
first: genericTabulatorPhraseToTranslationMapper["first"],
|
||||
first_title:
|
||||
genericTabulatorPhraseToTranslationMapper["first_title"],
|
||||
last: genericTabulatorPhraseToTranslationMapper["last"],
|
||||
last_title:
|
||||
genericTabulatorPhraseToTranslationMapper["last_title"],
|
||||
prev: genericTabulatorPhraseToTranslationMapper["prev"],
|
||||
prev_title:
|
||||
genericTabulatorPhraseToTranslationMapper["prev_title"],
|
||||
next: genericTabulatorPhraseToTranslationMapper["next"],
|
||||
next_title:
|
||||
genericTabulatorPhraseToTranslationMapper["next_title"],
|
||||
all: genericTabulatorPhraseToTranslationMapper["all"],
|
||||
counter: {
|
||||
showing:
|
||||
genericTabulatorPhraseToTranslationMapper["showing"],
|
||||
of: genericTabulatorPhraseToTranslationMapper["of"],
|
||||
rows: genericTabulatorPhraseToTranslationMapper["rows"],
|
||||
pages: genericTabulatorPhraseToTranslationMapper["pages"],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
let phrasesGroupedByCategoryRequestParam = {};
|
||||
tabulatorOptions.columns.forEach((column) => {
|
||||
if (!column.titlePhrase?.length) return;
|
||||
|
||||
let [category, phrase] = column.titlePhrase.split("/");
|
||||
if (phrasesGroupedByCategoryRequestParam[category]) {
|
||||
phrasesGroupedByCategoryRequestParam[category].push(phrase);
|
||||
} else {
|
||||
phrasesGroupedByCategoryRequestParam[category] = [phrase];
|
||||
}
|
||||
});
|
||||
tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => {
|
||||
let [category, phrase] = menuItemPhrase.split("/");
|
||||
if (phrasesGroupedByCategoryRequestParam[category]) {
|
||||
phrasesGroupedByCategoryRequestParam[category].push(phrase);
|
||||
} else {
|
||||
phrasesGroupedByCategoryRequestParam[category] = [phrase];
|
||||
}
|
||||
});
|
||||
|
||||
if (!Object.keys(phrasesGroupedByCategoryRequestParam).length) {
|
||||
return tabulatorOptions;
|
||||
}
|
||||
|
||||
const phrasesResponse = await this.$api.call(
|
||||
ApiPhrases.getPhrases(phrasesGroupedByCategoryRequestParam),
|
||||
);
|
||||
const phrasesData = phrasesResponse.data;
|
||||
|
||||
Object.keys(phrasesData).forEach((language) => {
|
||||
let phraseToTranslationMapper = {};
|
||||
phrasesData[language].forEach((phrase) => {
|
||||
phraseToTranslationMapper[
|
||||
phrase.category + "/" + phrase.phrase
|
||||
] = phrase.text;
|
||||
});
|
||||
|
||||
let columnFieldToTranslationMapper = {};
|
||||
tabulatorOptions.columns.forEach((column) => {
|
||||
if (!column.titlePhrase?.length) return;
|
||||
|
||||
columnFieldToTranslationMapper[column.field] =
|
||||
phraseToTranslationMapper[column.titlePhrase] ??
|
||||
"<< PHRASE " + column.titlePhrase + " >>";
|
||||
});
|
||||
tabulatorOptions.langs[language].columns =
|
||||
columnFieldToTranslationMapper;
|
||||
|
||||
let menuItemPhraseToTranslationMapper = {};
|
||||
tabulatorOptions.menuItemPhrases?.forEach((menuItemPhrase) => {
|
||||
menuItemPhraseToTranslationMapper[menuItemPhrase] =
|
||||
phraseToTranslationMapper[menuItemPhrase] ??
|
||||
"<< PHRASE " + menuItemPhrase + " >>";
|
||||
});
|
||||
tabulatorOptions.langs[language].menuItems =
|
||||
menuItemPhraseToTranslationMapper;
|
||||
});
|
||||
|
||||
return tabulatorOptions;
|
||||
},
|
||||
updateTabulator() {
|
||||
if (this.tabulator) {
|
||||
@@ -371,6 +526,7 @@ export const CoreFilterCmpt = {
|
||||
});
|
||||
this.tabulator.clearFilter();
|
||||
this.filterActive = false;
|
||||
this.$emit('headerFilterOn', this.filterActive)
|
||||
},
|
||||
_setHeaderFilter()
|
||||
{
|
||||
@@ -654,7 +810,18 @@ export const CoreFilterCmpt = {
|
||||
|
||||
// parent not found
|
||||
return false;
|
||||
}
|
||||
},
|
||||
getColumnNames() {
|
||||
if (!this.tableBuilt) {
|
||||
return {};
|
||||
} else if (this.tabulator.options.locale) {
|
||||
return this.tabulator.getLang().columns;
|
||||
}
|
||||
return this.tabulator.getColumns().reduce((res, col) => {
|
||||
res[col.getField()] = col.getDefinition().title;
|
||||
return res;
|
||||
}, {});
|
||||
},
|
||||
},
|
||||
beforeCreate() {
|
||||
if (!this.tableOnly == !this.filterType)
|
||||
@@ -665,6 +832,10 @@ export const CoreFilterCmpt = {
|
||||
alert('"nwNewEntry" listener is mandatory when sideMenu is true');
|
||||
this.uuid = _uuid++;
|
||||
this.$emit('uuidDefined', this.uuid)
|
||||
|
||||
Tabulator.registerModule(InternalToExternalEventBroadcastModule);
|
||||
Tabulator.registerModule(MenuExtensionModule);
|
||||
Tabulator.registerModule(ResponsiveLayoutExtensionModule);
|
||||
},
|
||||
mounted() {
|
||||
this.initTabulator().then(() => {
|
||||
@@ -732,7 +903,7 @@ export const CoreFilterCmpt = {
|
||||
:data-bs-parent="'#filterCollapsables' + idExtra"
|
||||
:fields="fieldIdsForVisibilty"
|
||||
:selected="selectedFields"
|
||||
:names="fieldNames"
|
||||
:names="getColumnNames()"
|
||||
@hide="tabulator.hideColumn($event)"
|
||||
@show="tabulator.showColumn($event)"
|
||||
v-collapse-auto-close
|
||||
|
||||
@@ -42,7 +42,7 @@ export default {
|
||||
watch: {
|
||||
selected(n) {
|
||||
this.selectedFields = n;
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggle(field) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import dms from "./result/dms.js";
|
||||
import cms from "./result/cms.js";
|
||||
import mergedStudent from "./result/mergedstudent.js";
|
||||
import mergedPerson from "./result/mergedperson.js";
|
||||
import { debounce } from "../../helpers/DebounceHelper.js";
|
||||
|
||||
export default {
|
||||
name: "FhcSearchbar",
|
||||
@@ -57,6 +58,107 @@ export default {
|
||||
settingsDropdown: null,
|
||||
lastQuery: "",
|
||||
isSearchShownInMobileView: false,
|
||||
callsearchapi: debounce(() => {
|
||||
this.error = null;
|
||||
this.searchresult.splice(0, this.searchresult.length);
|
||||
this.searching = true;
|
||||
this.showsearchresult();
|
||||
if (this.searchsettings.types.length === 0) {
|
||||
this.error = this.$p.t("search/error_missing_type");
|
||||
this.searching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.abortController) this.abortController.abort();
|
||||
|
||||
if (!this.searchsettings.searchstr?.length) return;
|
||||
|
||||
this.abortController = new AbortController();
|
||||
|
||||
this.searchfunction(this.searchsettings, {
|
||||
timeout: 50000,
|
||||
signal: this.abortController.signal,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.data) {
|
||||
this.error = this.$p.t("search/error_general");
|
||||
} else {
|
||||
let res = response.data.map((el) =>
|
||||
el.data ? { ...el, ...JSON.parse(el.data) } : el,
|
||||
);
|
||||
this.lastQuery = response.meta.searchstring;
|
||||
if (this.searchoptions.mergeResults) {
|
||||
let counter = 0;
|
||||
let mergeTypes = [];
|
||||
let mergedType = "merged-";
|
||||
let mergeKey = "";
|
||||
|
||||
switch (this.searchoptions.mergeResults) {
|
||||
case "student":
|
||||
mergeTypes = ["student", "prestudent"];
|
||||
mergedType +=
|
||||
this.searchoptions.mergeResults;
|
||||
mergeKey = "uid";
|
||||
break;
|
||||
case "person":
|
||||
mergeTypes = [
|
||||
"person",
|
||||
"employee",
|
||||
"student",
|
||||
"prestudent",
|
||||
];
|
||||
mergedType +=
|
||||
this.searchoptions.mergeResults;
|
||||
mergeKey = "person_id";
|
||||
break;
|
||||
}
|
||||
|
||||
if (mergeTypes.length) {
|
||||
res = Object.values(
|
||||
res.reduce((a, c) => {
|
||||
if (!mergeTypes.includes(c.renderer)) {
|
||||
a["nomerge" + counter++] = c;
|
||||
} else if (c[mergeKey] === null) {
|
||||
a["nomerge" + counter++] = c;
|
||||
} else if (
|
||||
a[c[mergeKey]] === undefined
|
||||
) {
|
||||
a[c[mergeKey]] = {
|
||||
rank: c.rank,
|
||||
renderer: mergedType,
|
||||
type: mergedType,
|
||||
list: [c],
|
||||
};
|
||||
} else {
|
||||
a[c[mergeKey]].list.push(c);
|
||||
if (c.rank > a[c[mergeKey]].rank)
|
||||
a[c[mergeKey]].rank = c.rank;
|
||||
}
|
||||
return a;
|
||||
}, {}),
|
||||
).sort((a, b) => b.rank - a.rank);
|
||||
}
|
||||
}
|
||||
this.searchresult = res;
|
||||
this.searchmode = response.meta.mode;
|
||||
}
|
||||
this.searching = false;
|
||||
this.retry = 0;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code == "ERR_CANCELED") {
|
||||
return (this.retry = 0);
|
||||
}
|
||||
if (error.code == "ECONNABORTED" && this.retry) {
|
||||
this.retry--;
|
||||
return this.callsearchapi();
|
||||
}
|
||||
|
||||
this.error = this.$p.t("search/error_general", error);
|
||||
this.searching = false;
|
||||
this.retry = 0;
|
||||
});
|
||||
}, 500),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -345,122 +447,18 @@ export default {
|
||||
this.abort();
|
||||
if (this.searchsettings.searchstr.length >= 2) {
|
||||
this.calcSearchResultExtent();
|
||||
this.searchtimer = setTimeout(this.callsearchapi, 500);
|
||||
this.callsearchapi();
|
||||
} else {
|
||||
this.showresult = false;
|
||||
}
|
||||
},
|
||||
abort() {
|
||||
if (this.searchtimer !== null) {
|
||||
clearTimeout(this.searchtimer);
|
||||
}
|
||||
if (this.abortController) {
|
||||
this.abortController.abort();
|
||||
this.abortController = null;
|
||||
}
|
||||
this.searchresult = [];
|
||||
},
|
||||
callsearchapi: function () {
|
||||
this.error = null;
|
||||
this.searchresult.splice(0, this.searchresult.length);
|
||||
this.searching = true;
|
||||
this.showsearchresult();
|
||||
if (this.searchsettings.types.length === 0) {
|
||||
this.error = this.$p.t("search/error_missing_type");
|
||||
this.searching = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.abortController) this.abortController.abort();
|
||||
|
||||
if (!this.searchsettings.searchstr?.length) return;
|
||||
|
||||
this.abortController = new AbortController();
|
||||
|
||||
this.searchfunction(this.searchsettings, {
|
||||
timeout: 50000,
|
||||
signal: this.abortController.signal,
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.data) {
|
||||
this.error = this.$p.t("search/error_general");
|
||||
} else {
|
||||
let res = response.data.map((el) =>
|
||||
el.data ? { ...el, ...JSON.parse(el.data) } : el,
|
||||
);
|
||||
this.lastQuery = response.meta.searchstring;
|
||||
if (this.searchoptions.mergeResults) {
|
||||
let counter = 0;
|
||||
let mergeTypes = [];
|
||||
let mergedType = "merged-";
|
||||
let mergeKey = "";
|
||||
|
||||
switch (this.searchoptions.mergeResults) {
|
||||
case "student":
|
||||
mergeTypes = ["student", "prestudent"];
|
||||
mergedType +=
|
||||
this.searchoptions.mergeResults;
|
||||
mergeKey = "uid";
|
||||
break;
|
||||
case "person":
|
||||
mergeTypes = [
|
||||
"person",
|
||||
"employee",
|
||||
"student",
|
||||
"prestudent",
|
||||
];
|
||||
mergedType +=
|
||||
this.searchoptions.mergeResults;
|
||||
mergeKey = "person_id";
|
||||
break;
|
||||
}
|
||||
|
||||
if (mergeTypes.length) {
|
||||
res = Object.values(
|
||||
res.reduce((a, c) => {
|
||||
if (!mergeTypes.includes(c.renderer)) {
|
||||
a["nomerge" + counter++] = c;
|
||||
} else if (c[mergeKey] === null) {
|
||||
a["nomerge" + counter++] = c;
|
||||
} else if (
|
||||
a[c[mergeKey]] === undefined
|
||||
) {
|
||||
a[c[mergeKey]] = {
|
||||
rank: c.rank,
|
||||
renderer: mergedType,
|
||||
type: mergedType,
|
||||
list: [c],
|
||||
};
|
||||
} else {
|
||||
a[c[mergeKey]].list.push(c);
|
||||
if (c.rank > a[c[mergeKey]].rank)
|
||||
a[c[mergeKey]].rank = c.rank;
|
||||
}
|
||||
return a;
|
||||
}, {}),
|
||||
).sort((a, b) => b.rank - a.rank);
|
||||
}
|
||||
}
|
||||
this.searchresult = res;
|
||||
this.searchmode = response.meta.mode;
|
||||
}
|
||||
this.searching = false;
|
||||
this.retry = 0;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code == "ERR_CANCELED") {
|
||||
return (this.retry = 0);
|
||||
}
|
||||
if (error.code == "ECONNABORTED" && this.retry) {
|
||||
this.retry--;
|
||||
return this.callsearchapi();
|
||||
}
|
||||
|
||||
this.error = this.$p.t("search/error_general", error);
|
||||
this.searching = false;
|
||||
this.retry = 0;
|
||||
});
|
||||
},
|
||||
refreshsearch: function () {
|
||||
this.search();
|
||||
this.togglesettings();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const debounce = (callback, wait) => {
|
||||
let timeoutId = null;
|
||||
return (context) => {
|
||||
window.clearTimeout(timeoutId);
|
||||
timeoutId = window.setTimeout(() => callback(), wait);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,49 @@ export function capitalize(string) {
|
||||
|
||||
return '';
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function convertCase(string, from, to) {
|
||||
const possibleCases = ["kebab", "snake", "camel"];
|
||||
if (
|
||||
from === to ||
|
||||
!possibleCases.includes(from) ||
|
||||
!possibleCases.includes(to)
|
||||
)
|
||||
return string;
|
||||
|
||||
const individualWords =
|
||||
from === "kebab"
|
||||
? string.split("-")
|
||||
: from === "snake"
|
||||
? string.split("_")
|
||||
: splitWordsInCamelCase(string);
|
||||
|
||||
const formattedIndividualWords = ["snake", "kebab"].includes(to)
|
||||
? individualWords.map((word) => word.toLowerCase())
|
||||
: individualWords.map((word, index) => {
|
||||
if (!index) return word.toLowerCase();
|
||||
return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase();
|
||||
});
|
||||
|
||||
const joinCharacter = to === "snake" ? "_" : to === "kebab" ? "-" : "";
|
||||
return formattedIndividualWords.join(joinCharacter);
|
||||
}
|
||||
|
||||
function splitWordsInCamelCase(string) {
|
||||
const lastUpperCaseLetterIndex = string
|
||||
.split("")
|
||||
.findLastIndex((character) => {
|
||||
return character.toUpperCase() === character;
|
||||
});
|
||||
|
||||
if (lastUpperCaseLetterIndex < 1) {
|
||||
return [string];
|
||||
}
|
||||
|
||||
let splitWords = splitWordsInCamelCase(
|
||||
string.slice(0, lastUpperCaseLetterIndex),
|
||||
);
|
||||
splitWords.push(string.slice(lastUpperCaseLetterIndex));
|
||||
return splitWords;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// changes within tabulator are often not adequately broadcast to the external component
|
||||
// this module exposes tabulator's internal events
|
||||
// if external event name is not specified, the dispatched external event will be the camelCase version of the kebab-case internal event
|
||||
|
||||
import Module from "../../../../vendor/olifolkerd/tabulator5/src/js/core/Module.js";
|
||||
import { convertCase } from "../../helpers/StringHelpers.js";
|
||||
|
||||
export default class InternalToExternalEventBroadcastModule extends Module {
|
||||
static moduleName = "internalToExternalEventBroadcast";
|
||||
static moduleInitNumber = 1;
|
||||
|
||||
constructor(table) {
|
||||
super(table);
|
||||
|
||||
this.eventsToBroadcast = [
|
||||
// example event
|
||||
// {
|
||||
// internal: "layout-refreshed",
|
||||
// external: null,
|
||||
// },
|
||||
];
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.eventsToBroadcast.forEach((event) => {
|
||||
this.subscribe(event.internal, () => {
|
||||
this.dispatchExternal(event.external ?? convertCase(event.internal, "kebab", "camel"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import Menu from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/Menu/Menu.js";
|
||||
|
||||
export default class MenuExtensionModule extends Menu {
|
||||
static moduleName = "menu";
|
||||
static moduleInitNumber = 1;
|
||||
|
||||
constructor(table) {
|
||||
super(table);
|
||||
}
|
||||
|
||||
loadMenu(e, component, menu, parentEl, parentPopup){
|
||||
const isLocalizationEnabled = component.table.options.locale && component.table.options.locale !== "default";
|
||||
const menuItemTranslations = component.table.getLang().menuItems;
|
||||
if (isLocalizationEnabled && menuItemTranslations) {
|
||||
menu = menu.map((menuItem) => {
|
||||
if (menuItem.phrase && menuItemTranslations[menuItem.phrase]) {
|
||||
menuItem.label = menuItemTranslations[menuItem.phrase];
|
||||
}
|
||||
return menuItem;
|
||||
});
|
||||
}
|
||||
|
||||
super.loadMenu(e, component, menu, parentEl, parentPopup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import ResponsiveLayout from "../../../../vendor/olifolkerd/tabulator5/src/js/modules/ResponsiveLayout/ResponsiveLayout.js";
|
||||
|
||||
export default class ResponsiveLayoutExtensionModule extends ResponsiveLayout {
|
||||
static moduleName = "responsiveLayout";
|
||||
static moduleInitNumber = 1;
|
||||
|
||||
constructor(table) {
|
||||
super(table);
|
||||
}
|
||||
|
||||
generateCollapsedRowData(row) {
|
||||
let result = super.generateCollapsedRowData(row)
|
||||
const isLocalizationEnabled = this.table.options.locale && this.table.options.locale !== "default";
|
||||
const columnHeadingTranslations = this.table?.getLang()?.columns;
|
||||
|
||||
if (isLocalizationEnabled && columnHeadingTranslations) {
|
||||
result = result.map((column) => {
|
||||
if (columnHeadingTranslations[column.field]) {
|
||||
column.title = columnHeadingTranslations[column.field];
|
||||
}
|
||||
return column;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user