diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 43dc18d1c..d6f3c29a8 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -35,9 +35,12 @@ class Abgabe extends FHCAPI_Controller 'getStudentProjektabgaben' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw', 'basis/abgabe_lektor:rw'), 'postStudentProjektarbeitZwischenabgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'postStudentProjektarbeitEndupload' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), + 'postStudentProjektarbeitTitel' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_student:rw'), 'getMitarbeiterProjektarbeiten' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'postProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'patchProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'deleteProjektarbeitAbgabe' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), + 'deleteProjektarbeitAbgabeMultiple' => array('basis/abgabe_assistenz:rw'), 'postSerientermin' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'fetchDeadlines' => array('basis/abgabe_assistenz:rw', 'basis/abgabe_lektor:rw'), 'getPaAbgabetypen' => self::PERM_LOGGED, @@ -448,7 +451,194 @@ class Abgabe extends FHCAPI_Controller } } - + + /** + * POST METHOD + * allows a student (or assistenz on their behalf) to update the titel of their own projektarbeit. + * blocked once the projektarbeit has been graded (checkProjektarbeitForFinishedStatus). + */ + public function postStudentProjektarbeitTitel() + { + $projektarbeit_id = $this->input->post('projektarbeit_id'); + $titel = $this->input->post('titel'); + + if ($projektarbeit_id === NULL || trim((string)$projektarbeit_id) === '' + || $titel === NULL || trim((string)$titel) === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + + // Verify the projektarbeit actually belongs to the supplied student_uid + $res = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($res) || !hasData($res)) { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektarbeitNichtGefunden'), 'general'); + } + $assignedStudentUid = getData($res)[0]->uid; + + // A student may only update their own title; assistenz is covered by checkZuordnung + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + if (getAuthUID() !== $assignedStudentUid && !$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $data = getData($result); + + $oldTitle = $data[0]->titel ?? ''; + + $result = $this->ProjektarbeitModel->update( + $projektarbeit_id, + array( + 'titel' => trim($titel), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + + $this->logLib->logInfoDB(array( + 'titelUpdate', + array( + 'projektarbeit_id' => $projektarbeit_id, + 'titel' => trim($titel), + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ), + getAuthUID(), + getAuthPersonId() + )); + + $this->sendTitelChangedEmail( + $projektarbeit_id, + trim($titel), + $oldTitle, + $assignedStudentUid + ); + + $result = $this->ProjektarbeitModel->load($projektarbeit_id); + $this->terminateWithSuccess($result); + } + + /** + * Notifies all betreuer of a projektarbeit and all assistenzen responsible for its studiengang + * when a student updates the titel of their projektarbeit. + * + * Betreuer retrieval mirrors AbgabetoolJob->notifyBetreuerAboutChangedAbgaben. + * Assistenz retrieval mirrors AbgabetoolJob->notifyAssistenzAboutChangedAbgaben. + * + * @param int $projektarbeit_id + * @param string $new_titel The titel as it was saved + * @param string $student_uid + */ + private function sendTitelChangedEmail($projektarbeit_id, $new_titel, $old_titel, $student_uid) + { + $this->load->model('education/Projektarbeit_model', 'ProjektarbeitModel'); + $this->load->model('education/Projektbetreuer_model', 'ProjektbetreuerModel'); + $this->load->model('organisation/Studiengang_model', 'StudiengangModel'); + $this->load->model('organisation/Organisationseinheit_model', 'OrganisationseinheitModel'); + $this->load->model('person/Person_model', 'PersonModel'); + + $this->load->model('person/Person_model', 'PersonModel'); + $studentNameResult = $this->PersonModel->getFullName($student_uid); + $studentFullName = hasData($studentNameResult) ? getData($studentNameResult) : $student_uid; + + $studentInfoResult = $this->ProjektarbeitModel->getStudentInfoForProjektarbeitId($projektarbeit_id); + if (isError($studentInfoResult) || !hasData($studentInfoResult)) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: student info not found', $projektarbeit_id)); + return; + } + $studentInfo = getData($studentInfoResult)[0]; + $studiengang_kz = $studentInfo->studiengang_kz; + + $stgResult = $this->StudiengangModel->load($studiengang_kz); + $oe_kurzbz = null; + if (!isError($stgResult) && hasData($stgResult)) { + $oe_kurzbz = getData($stgResult)[0]->oe_kurzbz ?? null; + } + + // build shared mail data + $base_mail_data = array( + 'studentFullName' => $studentFullName, + 'new_titel' => $new_titel, + 'old_titel' => $old_titel + ); + + // notify all betreuer + $betreuerResult = $this->ProjektbetreuerModel->getAllBetreuerOfProjektarbeit($projektarbeit_id); + if (!isError($betreuerResult) && hasData($betreuerResult)) { + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_MITARBEITER'); + + foreach (getData($betreuerResult) as $betreuer) { + $email = $betreuer->uid ? $betreuer->uid . '@' . DOMAIN : ($betreuer->private_email ?? null); + if (!$email) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no email for betreuer', $betreuer->person_id)); + continue; + } + + $anredeResult = $this->ProjektarbeitModel->getProjektbetreuerAnrede($betreuer->person_id); + $anrede = (!isError($anredeResult) && hasData($anredeResult)) ? getData($anredeResult)[0] : null; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => ($anrede->anrede ?? '') === 'Herr' ? 'r' : '', + 'anrede' => $anrede->anrede ?? '', + 'fullFormattedNameString' => $anrede->first ?? $email, + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // notify assistenz for the studiengang OE + if (!$oe_kurzbz) { + $this->logLib->logInfoDB(array('sendTitelChangedEmail: no oe_kurzbz resolved, skipping assistenz', $studiengang_kz)); + return; + } + + $assistenzResult = $this->OrganisationseinheitModel->getAssistenzForOE($oe_kurzbz); + if (isError($assistenzResult) || !hasData($assistenzResult)) { + return; + } + + $linkAbgabetool = APP_ROOT . $this->config->item('URL_ASSISTENZ'); + + // similar pattern as job uses via the assistenzMap + $sentTo = []; + foreach (getData($assistenzResult) as $assistenz) { + if (in_array($assistenz->person_id, $sentTo)) { + continue; + } + $sentTo[] = $assistenz->person_id; + + $email = $assistenz->uid . '@' . DOMAIN; + + $mail_data = array_merge($base_mail_data, array( + 'anredeFillString' => $assistenz->anrede === 'Herr' ? 'r' : '', + 'anrede' => $assistenz->anrede ?? '', + 'fullFormattedNameString' => $assistenz->first ?? ($assistenz->uid . '@' . DOMAIN), + 'linkAbgabetool' => $linkAbgabetool, + )); + + sendSanchoMail( + 'PATitleUpdated', + $mail_data, + $email, + $this->p->t('abgabetool', 'c4PATitleChanged') + ); + } + } + + // validate paabgabe deadline against servertime just in case a student spoofs their local clock and thus // unlocks the upload ui private function checkPaabgabeDeadline($paabgabe_id) { @@ -687,6 +877,99 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithSuccess([$paabgabe, $existingPaabgabe]); } + /** + * called by abgabetool/assistenz when bulk-editing multiple abgabetermine via the flat termine table view + * only fields present in the payload are updated - absent fields are left untouched + */ + public function patchProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + // collect only fields that were actually sent + $updateFields = []; + + $datum = $this->input->post('datum'); + if ($datum !== NULL && trim((string)$datum) !== '') { + $updateFields['datum'] = $datum; + } + + $paabgabetyp_kurzbz = $this->input->post('paabgabetyp_kurzbz'); + if ($paabgabetyp_kurzbz !== NULL && trim((string)$paabgabetyp_kurzbz) !== '') { + $updateFields['paabgabetyp_kurzbz'] = $paabgabetyp_kurzbz; + } + + $kurzbz = $this->input->post('kurzbz'); + if ($kurzbz !== NULL) { + $updateFields['kurzbz'] = $kurzbz; + } + + // booleans: only include if explicitly posted + $upload_allowed = $this->input->post('upload_allowed'); + if ($upload_allowed !== NULL) { + $updateFields['upload_allowed'] = filter_var($upload_allowed, FILTER_VALIDATE_BOOLEAN); + } + + $fixtermin = $this->input->post('fixtermin'); + if ($fixtermin !== NULL) { + $updateFields['fixtermin'] = filter_var($fixtermin, FILTER_VALIDATE_BOOLEAN); + } + + if (empty($updateFields)) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $projektarbeit_id = $this->getProjektarbeitIDForPaabgabeID($paabgabe_id); + + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); + + $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->update( + $paabgabe_id, + array_merge($updateFields, [ + 'updatevon' => getAuthUID(), + 'updateamum' => date('Y-m-d H:i:s') + ]) + ); + + $this->getDataOrTerminateWithError($result, 'general'); + $results[] = getData($this->PaabgabeModel->load($paabgabe_id))[0]; + + $this->logLib->logInfoDB(array( + 'paabgabe bulk updated', + $paabgabe_id, + $updateFields, + getAuthUID(), + getAuthPersonId() + )); + } + + $this->terminateWithSuccess($results); + } + /** * called by abgabetool/mitarbeiter in mitarbeiterdetail.js when deleting an abgabetermin * deletion is only possible if user is assistenz OR betreuer deletes their own custom termin @@ -719,11 +1002,55 @@ class Abgabe extends FHCAPI_Controller $result = $this->PaabgabeModel->delete($paabgabe_id); $result = $this->getDataOrTerminateWithError($result, 'general'); - // TODO: consider this in nightly email job $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); $this->terminateWithSuccess($result); } + /** + * called by abgabetool/assistenz when deleting multiple abgabetermine via the flat termine table view + */ + public function deleteProjektarbeitAbgabeMultiple() { + $paabgabe_ids = $this->input->post('paabgabe_ids'); + + if ($paabgabe_ids === NULL || !is_array($paabgabe_ids) || count($paabgabe_ids) === 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $results = []; + foreach ($paabgabe_ids as $paabgabe_id) { + $paabgabe_id = trim((string)$paabgabe_id); + + if ($paabgabe_id === '') { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $this->checkProjektarbeitForFinishedStatus($this->getProjektarbeitIDForPaabgabeID($paabgabe_id)); + + $zugeordnet = $this->checkZuordnungByPaabgabe($paabgabe_id, getAuthUID()); + + if (!$zugeordnet) { + $this->terminateWithError($this->p->t('abgabetool', 'c4noZuordnungBetreuerStudent'), 'general'); + } + + $paabgabeResult = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($paabgabeResult, 'general'); + + if (count($paabgabeArr) == 0) { + $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); + } + + $result = $this->PaabgabeModel->delete($paabgabe_id); + $result = $this->getDataOrTerminateWithError($result, 'general'); + $results[] = $result; + + $this->logLib->logInfoDB(array($paabgabeArr[0], getAuthUID(), getAuthPersonId())); + } + + $this->terminateWithSuccess($results); + } + /** * endpoint for adding the same paabgabe for multiple projektarbeiten * can be slow for large n since it queries twice per projektarbeit_id @@ -1411,7 +1738,13 @@ class Abgabe extends FHCAPI_Controller $data = getData($res)[0]; if($data->note !== NULL) { - $this->terminateWithError($this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'), 'general'); + // hardcode this error msg cause phrasen arent reliable and people keep bugging why the cant edit old entries they definitely shouldnt update + $message = $this->p->t('abgabetool','c4fehlerAktualitaetProjektarbeit'); + if(strpos($message, "<<") === 0) { // phrase could not be loaded + $this->terminateWithError('Die Projektarbeit wurde bereits benotet, Sie dürfen deshalb keine weiteren Termine anlegen oder bearbeiten.', 'general'); + } else { + $this->terminateWithError($message); + } } } diff --git a/application/models/education/Projektarbeit_model.php b/application/models/education/Projektarbeit_model.php index 3b1ea55e5..ef1759154 100644 --- a/application/models/education/Projektarbeit_model.php +++ b/application/models/education/Projektarbeit_model.php @@ -341,172 +341,130 @@ 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 -- ← THE NEW LINE + + 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)); diff --git a/application/views/Cis/Abgabetool.php b/application/views/Cis/Abgabetool.php index 86e8721f2..a0621b1f9 100644 --- a/application/views/Cis/Abgabetool.php +++ b/application/views/Cis/Abgabetool.php @@ -38,7 +38,7 @@ $includesArray = array( $this->load->view('templates/FHC-Header', $includesArray); ?> -
+
uid= student_uid_prop="" stg_kz_prop="" diff --git a/public/css/components/abgabetool/abgabe.css b/public/css/components/abgabetool/abgabe.css index d70481805..12e0e82ee 100644 --- a/public/css/components/abgabetool/abgabe.css +++ b/public/css/components/abgabetool/abgabe.css @@ -305,4 +305,45 @@ /* 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 */ -} \ No newline at end of file +} + +.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; + } + +} + diff --git a/public/js/api/factory/abgabe.js b/public/js/api/factory/abgabe.js index c6f229973..7621b548b 100644 --- a/public/js/api/factory/abgabe.js +++ b/public/js/api/factory/abgabe.js @@ -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,14 @@ 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}, + }; } }; \ No newline at end of file diff --git a/public/js/components/Bootstrap/Modal.js b/public/js/components/Bootstrap/Modal.js index e320d4429..0f2504da6 100644 --- a/public/js/components/Bootstrap/Modal.js +++ b/public/js/components/Bootstrap/Modal.js @@ -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 {
{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}
-
- +
+
@@ -874,6 +876,7 @@ export const AbgabeMitarbeiterDetail = { :disabled="true" locale="de" format="dd.MM.yyyy" + model-type="yyyy-MM-dd" >
@@ -907,7 +910,7 @@ export const AbgabeMitarbeiterDetail = {
- diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index a7931f307..c9d5280cb 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -3,6 +3,7 @@ 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"; export const AbgabeStudentDetail = { name: "AbgabeStudentDetail", @@ -31,12 +32,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 +52,52 @@ export const AbgabeStudentDetail = { } }, methods: { + openTitelEdit() { + this.editingTitel = this.projektarbeit.titel ?? ''; + this.$refs.modalTitelEdit.show(); + }, + async saveTitel() { + const trimmed = this.editingTitel.trim(); + if (!trimmed) { + this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningEmptyField'))); + return; + } + + 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, + trimmed + ) + ).then(res => { + if (res.meta.status === 'success') { + this.projektarbeit.titel = trimmed; + this.$emit('titel-updated', { + projektarbeit_id: this.projektarbeit.projektarbeit_id, + titel: trimmed + }); + 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 +111,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 +123,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 +140,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]); } @@ -110,38 +156,27 @@ export const AbgabeStudentDetail = { .then(res => { this.handleUploadRes(res, this.enduploadTermin) }).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 +186,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 +196,7 @@ export const AbgabeStudentDetail = { .then(res => { this.handleUploadRes(res, termin) }).finally(()=> { - this.loading = false + this.loading = false }) } }, @@ -169,21 +204,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 +227,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 +234,13 @@ export const AbgabeStudentDetail = { this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.seitenanzahl = newVal.seitenanzahl ?? 1 - } }, computed: { 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,85 +273,47 @@ export const AbgabeStudentDetail = { }) return qgatefound }, + isTitelEditAllowed() { + // blocked once the projektarbeit has a note (finished) - mirrors backend guard + return !this.isViewMode && !this.projektarbeit?.note; + }, getTooltipVerspaetet() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" } }, getTooltipVerpasst() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), class: "custom-tooltip" } }, getTooltipAbzugeben() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), class: "custom-tooltip" } }, getTooltipStandard() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), class: "custom-tooltip" } }, getTooltipAbgegeben() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), class: "custom-tooltip" } }, getTooltipFixtermin() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), class: "custom-tooltip" } }, getTooltipAbgabeDetected() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), class: "custom-tooltip" } }, getTooltipNotAllowedToUpload() { if(this.isViewMode) { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" } } else { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')), class: "custom-tooltip" } } }, getTooltipBeurteilungerforderlich() { - return { - value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), - class: "custom-tooltip" - } + return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), class: "custom-tooltip" } }, getTooltipBestanden() { - return { - value: this.$p.t('abgabetool/c4tooltipBestanden'), - class: "custom-tooltip" - } + return { value: this.$p.t('abgabetool/c4tooltipBestanden'), class: "custom-tooltip" } }, getTooltipNichtBestanden() { - return { - value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), - class: "custom-tooltip" - } + return { value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), class: "custom-tooltip" } }, - }, - created() { - - }, - mounted() { - }, template: ` @@ -332,9 +323,24 @@ export const AbgabeStudentDetail = {
{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}
-

{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}

-

{{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}

-

{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}

+

{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}

+ +

+ {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}} + +

+ +

{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}

{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }} @@ -357,7 +363,6 @@ export const AbgabeStudentDetail = { -

{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }} @@ -408,8 +413,6 @@ export const AbgabeStudentDetail = {
- -
@@ -481,9 +484,6 @@ export const AbgabeStudentDetail = { {{ $p.t('abgabetool/c4keineSignatur') }} {{ $p.t('abgabetool/c4signaturServerError') }}
- - -
@@ -542,8 +542,49 @@ export const AbgabeStudentDetail = {
{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}
-
- +
+ + + + - `, }; diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 0d6ec1ab0..b6610cba3 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -10,6 +10,7 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" import { getDateStyleClass} from "./getDateStyleClass.js"; import { dateFilter } from '../../../tabulator/filters/Dates.js'; +import { compareISODateValues, formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js"; export const AbgabetoolAssistenz = { name: "AbgabetoolAssistenz", @@ -45,9 +46,23 @@ export const AbgabetoolAssistenz = { }, data() { return { - tableData: null, + flatDataDirty: true, + mode: 'perProjectView', + qgate1FilterSelected: [], + qgate2FilterSelected: [], + pa_noteFilterSelected: [], + noteFilterSelected: [], + count: 0, + filteredcount: 0, + selectedcount: 0, + countFlat: 0, + filteredcountFlat: 0, + selectedcountFlat: 0, + filteredRows: null, + filteredRowsFlat: null, studiensemesterOptions: null, allSem: null, + allSemOption: null, curSem: null, notenOptionFilter: null, inplaceToggle: false, @@ -56,6 +71,11 @@ export const AbgabetoolAssistenz = { colLayoutRestored: false, sortRestored: false, stateRestored: false, + headerFiltersRestoredFlat: false, + filtersRestoredFlat: false, + colLayoutRestoredFlat: false, + sortRestoredFlat: false, + stateRestoredFlat: false, timelineProjekt: null, selectedStudiengangOption: null, studiengaengeOptions: null, @@ -74,8 +94,24 @@ export const AbgabetoolAssistenz = { allowedNotenFilterOptions: null, allowedNotenOptions: null, notenOptionsNonFinal: null, + serienEdit: Vue.reactive({ + datum: null, + bezeichnung: null, + kurzbz: null, + upload_allowed: null, + fixtermin: null, + invertedFixtermin: null, + }), + // track which fields should actually be applied + serienEditFields: { + datum: false, + bezeichnung: false, + kurzbz: false, + upload_allowed: false, + fixtermin: false, + }, serienTermin: Vue.reactive({ - datum: new Date(), + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -87,7 +123,9 @@ export const AbgabetoolAssistenz = { }), showAll: false, tabulatorUuid: Vue.ref(0), + tabulatorUuidFlat: Vue.ref(0), selectedData: [], + selectedDataFlat: [], domain: '', student_uid: null, detail: null, @@ -96,6 +134,8 @@ export const AbgabetoolAssistenz = { selectedProjektarbeit: null, tableBuiltResolve: null, tableBuiltPromise: null, + tableBuiltResolveFlat: null, + tableBuiltPromiseFlat: null, abgabeTableOptions: { minHeight: 250, index: 'projektarbeit_id', @@ -103,7 +143,6 @@ export const AbgabetoolAssistenz = { placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), selectable: true, selectableCheck: this.selectionCheck, - rowHeight: 40, renderVerticalBuffer: 2000, columns: [ { @@ -158,64 +197,78 @@ export const AbgabetoolAssistenz = { handleClick: this.selectAllHandler }, width: 50, - cssClass: 'sticky-col' + cssClass: 'sticky-col', + visible: true }, - // { - // field: 'rowSelection', - // formatter: 'rowSelection', - // titleFormatter: 'rowSelection', - // titleFormatterParams: { - // rowRange: "active" // Only toggle the values of the active filtered rows - // }, - // hozAlign:"center", - // headerSort: false, - // frozen: true, - // width: 40 - // }, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 100, 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: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgform'))), field: 'orgform', 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/c4note'))), field: 'note_bez', headerFilter: true, - formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', headerFilter: false, headerSort: false, formatter: this.formAction, tooltip:false, minWidth: 130, visible: true, cssClass: 'sticky-col'}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, tooltip: false, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_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: 150, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, minWidth: 200, formatter: this.centeredTextFormatter}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, visible: false, formatter: this.centeredTextFormatter, minWidth: 100}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'erstbetreuer', 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, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/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', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer', 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, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + field: 'prevTermin', formatter: this.abgabterminFormatter, width: 250, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabterminFormatter, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate1Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate2Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + }, ], persistence: false, persistenceID: "abgabetool_2026_03_16" @@ -234,14 +287,762 @@ export const AbgabetoolAssistenz = { }) 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()); + } + }], + abgabeTableOptionsFlat: { + minHeight: 250, + height: 700, + index: 'paabgabe_id', + layout: 'fitColumns', + placeholder: Vue.computed(() => this.$capitalize(this.$p.t('global/noDataAvailable'))), + selectable: true, + renderVerticalBuffer: 400, + columns: [ + { + formatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + if(!cell.getRow().getData().selectable) return + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle select manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + cell.getRow().getData().checkbox = checkbox + + let wrapper = document.createElement("div"); + wrapper.style.cssText = "display: flex; justify-content: center; align-items: center; height: 100%; width: 100%;"; + + wrapper.appendChild(checkbox); + + return wrapper; + }, + titleFormatter: function (cell, formatterParams, onRendered) { + // create the built-in checkbox + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + + // Handle "select all" manually + checkbox.addEventListener("click", (e) => { + e.stopPropagation(); + + // call our function + if (formatterParams && formatterParams.handleClick) { + formatterParams.handleClick(e, cell); + } + }); + + return checkbox; + }, + hozAlign: "center", + headerSort: false, + formatterParams: { + handleClick: this.selectHandler + }, + titleFormatterParams: { + handleClick: this.selectAllHandlerFlat + }, + width: 50, + cssClass: 'sticky-col' + }, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, tooltip: false, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'student_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'student_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/c4abgabetyp'))), + field: 'paabgabetyp_kurzbz', + headerFilter: true, + formatter: this.paabgabetypFormatter, + + minWidth: 120, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabekurzbzv2'))), + field: 'kurzbz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 120 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zieldatumv2'))), + field: 'datum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminColISO, + sorter: compareISODateValues, + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 100 + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4abgabedatum'))), + field: 'abgabedatum', + headerFilter: dateFilter, + headerFilterFunc: this.headerFilterTerminColISO, + sorter: compareISODateValues, + formatter: (cell) => this.formatDate(cell.getValue()), + minWidth: 100 + }, + { + title: 'Status', + field: 'dateStyle', + headerSort: false, + headerFilter: this.statusHeaderFilterEditor, + headerFilterFunc: this.statusHeaderFilterFunc, + headerFilterParams: {}, + formatter: this.abgabterminFormatter, + formatterParams: { iconOnly: true }, + width: 70, + tooltip: (e, cell) => this.mapDateStyleToTabulatorTooltip(cell.getValue()) + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4noteprojektarbeit'))), + field: 'pa_note', + formatter: (cell) => { + const val = cell.getValue(); + if (!val) return ''; + return val?.bezeichnung ?? this.notenOptions?.find(n => n.note == val)?.bezeichnung ?? val; + }, + minWidth: 100, + tooltip: false, + headerFilter: this.notenHeaderFilterEditor, + headerFilterFunc: this.notenHeaderFilterFunc, + headerFilterParams: {}, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notetermin'))), + field: 'note', + formatter: (cell) => { + const val = cell.getValue(); + if (!val) return ''; + return val?.bezeichnung ?? this.notenOptions?.find(n => n.note == val)?.bezeichnung ?? val; + }, + minWidth: 100, + headerFilter: this.notenHeaderFilterEditor, + headerFilterFunc: this.notenHeaderFilterFunc, + headerFilterParams: {}, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4notizQualGatev2'))), + field: 'beurteilungsnotiz', + headerFilter: true, + formatter: this.centeredTextFormatter, + minWidth: 150, visible: false + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4fixterminv4'))), + field: 'fixtermin', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + { + title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4upload_allowed'))), + field: 'upload_allowed', + hozAlign: 'center', + formatter: 'tickCross', + width: 80, + headerFilter: 'tickCross', + headerFilterParams: { tristate: true }, + }, + ], + persistence: false, + persistenceID: "abgabetoolflat_2026_05_05" + }, + abgabeTableEventHandlersFlat: [ + { + event: "rowSelectionChanged", + handler: async(data) => + { + this.selectedDataFlat.filter(sd => !data.includes(sd)).forEach(fsd => { + if(fsd.checkbox) fsd.checkbox.checked = false + }) + + data.forEach(d => { + if(d.checkbox) d.checkbox.checked = true + }) + + this.selectedDataFlat = data + this.selectedcountFlat = data.length; + } + }, + { + event: 'dataFiltered', + handler: (filters, rows) => { + this.filteredRowsFlat = rows; + this.filteredcountFlat = rows.length; + + if (!this.selectedDataFlat.length) return; + + const visibleData = new Set(rows.map(r => r.getData())); + const filteredOut = this.selectedDataFlat.filter(sd => !visibleData.has(sd)); + + if (!filteredOut.length) return; + + const filteredOutSet = new Set(filteredOut); + this.$refs.abgabeTableFlat.tabulator.getSelectedRows() + .filter(r => filteredOutSet.has(r.getData())) + .forEach(r => r.deselect()); + } + } + ] + }; }, methods: { + notenHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) { + if (!this.notenOptions) return; + + const field = cell.getField(); + const stateKey = field + 'FilterSelected'; + 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: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);'; + + + // mapping evaluated at render time, not at column definition time + const fieldOptionsMap = { + 'pa_note': this.notenOptions, + 'note': this.allowedNotenOptions, + }; + const options = fieldOptionsMap[cell.getField()] ?? this.notenOptions; + if (!options) return; + + const updateDisplay = () => { + display.value = options + .filter(o => selected.includes(o.note)) + .map(o => o.bezeichnung) + .join(', '); + }; + 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.note; + cb.checked = selected.includes(opt.note); + cb.style.cssText = 'margin: 0 6px;'; + cb.addEventListener('change', () => { + selected = cb.checked + ? [...selected, opt.note] + : selected.filter(v => v !== opt.note); + this[stateKey] = [...selected]; + updateDisplay(); + success([...selected]); + }); + + const labelText = document.createElement('span'); + labelText.textContent = opt.bezeichnung; + + row.appendChild(cb); + 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; + }, + + notenHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) { + if (!filterVal || !filterVal.length) return true; + // rowVal is the raw integer note id or a note object + const noteId = typeof rowVal === 'object' ? rowVal?.note : rowVal; + return filterVal.some(val => val == noteId); // loose equality: filter vals are numbers, noteId might be string + }, + handleFilterActiveChanged(active) { + if(!active && this.allSemOption) { + this.curSem = this.allSemOption + } + }, + reloadData() { + this.loadProjektarbeiten() + }, + openEditModal() { + // reset + this.serienEditFields = { + datum: false, + bezeichnung: false, + kurzbz: false, + upload_allowed: false, + fixtermin: false, + } + this.serienEdit.datum = getViennaTodayISO() + this.serienEdit.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === 'zwischen') + this.serienEdit.kurzbz = '' + this.serienEdit.upload_allowed = false + this.serienEdit.invertedFixtermin = true + + this.$refs.modalContainerEditSeries.show() + }, + async handleEditSelectedTermine() { + const activeFields = Object.keys(this.serienEditFields).filter(k => this.serienEditFields[k]) + if (!activeFields.length) { + this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4noFieldsSelected')) + return + } + + if (await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4confirm_edit_n_termine', [this.selectedDataFlat.length]), + acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')), + acceptClass: 'p-button-primary', + rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')), + rejectClass: 'p-button-secondary' + }) === false) return + + this.$refs.modalContainerEditSeries.hide() + this.editSelectedTermine(this.selectedDataFlat) + }, + editSelectedTermine(termine) { + const paabgabeIDS = termine.map(t => t.paabgabe_id) + + // only send fields that were checked + const payload = { paabgabe_ids: paabgabeIDS } + if (this.serienEditFields.datum) payload.datum = this.serienEdit.datum + if (this.serienEditFields.bezeichnung) payload.paabgabetyp_kurzbz = this.serienEdit.bezeichnung.paabgabetyp_kurzbz + if (this.serienEditFields.kurzbz) payload.kurzbz = this.serienEdit.kurzbz + if (this.serienEditFields.upload_allowed) payload.upload_allowed = this.serienEdit.upload_allowed + if (this.serienEditFields.fixtermin) payload.fixtermin = !this.serienEdit.invertedFixtermin + + this.saving = true + this.$api.call(ApiAbgabe.patchProjektarbeitAbgabeMultiple(payload)).then(res => { + if (res?.meta?.status == 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('ui/gespeichert')) + + // patch local data structure + termine.forEach(t => { + const pa = this.projektarbeiten.find(pa => pa.projektarbeit_id == t.projektarbeit_id) + const termin = pa.abgabetermine.find(termin => termin.paabgabe_id === t.paabgabe_id) + if (!termin) return + + if (this.serienEditFields.datum) termin.datum = this.serienEdit.datum + if (this.serienEditFields.bezeichnung) termin.paabgabetyp_kurzbz = this.serienEdit.bezeichnung.paabgabetyp_kurzbz + if (this.serienEditFields.kurzbz) termin.kurzbz = this.serienEdit.kurzbz + if (this.serienEditFields.upload_allowed) termin.upload_allowed = this.serienEdit.upload_allowed + if (this.serienEditFields.fixtermin) termin.fixtermin = !this.serienEdit.invertedFixtermin + }) + + const updatedProjektarbeiten = new Set(termine.map(t => t.projektarbeit_id)) + updatedProjektarbeiten.forEach(pa_id => { + const projektarbeit = this.projektarbeiten.find(pa => pa.projektarbeit_id == pa_id) + this.checkAbgabetermineProjektarbeit(projektarbeit) + }) + + this.redrawTableScrollSave() + this.selectedDataFlat = [] + this.selectedcountFlat = 0 + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + + } else if (res?.meta?.status == 'error') { + this.$fhcAlert.alertError() + } + }).finally(() => { + this.saving = false + }) + }, + deleteSelectedTermine(termine) { + const paabgabeIDS = termine.map(t => t.paabgabe_id) + this.$api.call(ApiAbgabe.deleteProjektarbeitAbgabeMultiple(paabgabeIDS)).then( (res) => { + if(res?.meta?.status == 'success') { + this.$fhcAlert.alertSuccess(this.$p.t('ui/genericDeleted', [this.$p.t('abgabetool/c4abgaben_n', [paabgabeIDS.length])])) + + termine.forEach(t => { + const pa = this.projektarbeiten.find(pa => pa.projektarbeit_id == t.projektarbeit_id) + const deletedTerminIndex = pa.abgabetermine.findIndex(termin => t.paabgabe_id === termin.paabgabe_id) + pa.abgabetermine.splice(deletedTerminIndex, 1) + }) + + const updatedProjektarbeiten = new Set(termine.map(t => t.projektarbeit_id)) + + updatedProjektarbeiten.forEach(pa_id => { + const projektarbeit = this.projektarbeiten.find(pa => pa.projektarbeit_id == pa_id) + this.checkAbgabetermineProjektarbeit(projektarbeit) + }) + + this.redrawTableScrollSave() + + // update flat table with fresh computed data and clear selection + this.selectedDataFlat = [] + this.selectedcountFlat = 0 + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + + } else if(res?.meta?.status == 'error'){ + this.$fhcAlert.alertError() + } + }) + }, + async handleDeleteSelectedTermine() { + // TODO: check if every selected termin is actually "allowed to delete" + + + if(await this.$fhcAlert.confirm({ + message: this.$p.t('abgabetool/c4confirm_delete_n_termine', [this.selectedDataFlat.length]), + acceptLabel: 'Löschen', + acceptClass: 'p-button-danger', + rejectLabel: 'Zurück', + rejectClass: 'p-button-secondary' + }) === false) { + return false + } else { + this.deleteSelectedTermine(this.selectedDataFlat) + } + + }, + async switchMode() { + if(this.mode == 'perProjectView') { + this.mode = 'flatView' + + await this.tableBuiltPromiseFlat; + + if(this.flatDataDirty) { + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine); + this.flatDataDirty = false + } + + } else { + this.mode = 'perProjectView' + } + }, + getDateStyleHtml(dateStyle) { + const iconMap = { + 'verspaetet': '', + 'verpasst': '', + 'abzugeben': '', + 'standard': '', + 'abgegeben': '', + 'beurteilungerforderlich': '', + 'bestanden': '', + 'nichtbestanden': '', + }; + 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)); + }, + redrawTableScrollSave() { + const table = this.$refs.abgabeTable.tabulator; + const scrollX = table.rowManager.scrollLeft; + const scrollY = table.rowManager.scrollTop; + this.$refs.abgabeTable.tabulator.redraw(true) + + Vue.nextTick(()=> { + const tableholder = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(tableholder) { + tableholder.scrollLeft = scrollX; + tableholder.scrollTop = scrollY; + } + }) + }, + shortLongTitleFormatter(cell, formatterParams, onRendered) { + const longForm = cell.getValue() + const shortForm = formatterParams?.shortForm + + if(longForm && shortForm) { + return ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + toolTipFuncPrevTermin(e, cell, onRendered) { + const data = cell.getData(); + if(!data.prevTermin) return '' + return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle); + }, + toolTipFuncNextTermin(e, cell, onRendered) { + const data = cell.getData(); + if(!data.nextTermin) return '' + return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle); + }, + mapDateStyleToTabulatorTooltip(dateStyleString) { + switch(dateStyleString) { + case 'bestanden': + return this.$p.t('abgabetool/c4tooltipBestanden') + break; + case 'nichtbestanden': + return this.$p.t('abgabetool/c4tooltipNichtBestanden') + break; + case 'beurteilungerforderlich': + return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich') + break; + case 'verspaetet': + return this.$p.t('abgabetool/c4tooltipVerspaetet') + break; + case 'abgegeben': + return this.$p.t('abgabetool/c4tooltipAbgegeben') + break; + case 'verpasst': + return this.$p.t('abgabetool/c4tooltipVerpasst') + break; + case 'abzugeben': + return this.$p.t('abgabetool/c4tooltipAbzugeben') + break; + case 'standard': + return this.$p.t('abgabetool/c4tooltipStandardv2') + break; + default: return '' + } + }, handlePaUpdated(projektarbeit) { this.checkAbgabetermineProjektarbeit(projektarbeit) - this.$refs.abgabeTable.tabulator.redraw(true) + this.redrawTableScrollSave() }, getQGateStatusList() { return [ @@ -268,6 +1069,41 @@ export const AbgabetoolAssistenz = { // just in case someone reuses this return Math.abs(b.diffMs) - Math.abs(a.diffMs) }, + headerFilterTerminColISO(filterVal, rowVal) { + if (!rowVal) { + return false; + } + + const toLuxon = (val) => { + if (!val) return null; + let dt; + if (val instanceof Date) { + dt = luxon.DateTime.fromJSDate(val); + } else if (typeof val === "string") { + dt = toViennaDate(val); + } else { // fallback + dt = luxon.DateTime.fromMillis(Number(val)); + } + + return dt.isValid ? dt : null; + }; + + const rowDate = toLuxon(rowVal); + const von = toLuxon(filterVal[0]); + const bis = toLuxon(filterVal[1]); + + // specific day + if (von && !bis) { + return rowDate.hasSame(von, "day"); + } + + // range case + if (von && bis) { + return rowDate >= von.startOf("day") && rowDate <= bis.endOf("day"); + } + + return false + }, headerFilterTerminCol(filterVal, rowVal) { if (!rowVal || !rowVal.luxonDate || !rowVal.luxonDate.isValid) { return false; @@ -281,7 +1117,7 @@ export const AbgabetoolAssistenz = { 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)); } @@ -313,20 +1149,31 @@ export const AbgabetoolAssistenz = { const uniqueRecipients = [...new Set(recipientList)]; const subject = this.$p.t('abgabetool/c4sammelmailStudentBetreff', [this.selectedStudiengangOption?.bezeichnung]); - splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p) }, sammelMailBetreuer(param) { - const recipientList = []; this.selectedData.forEach(row => { if (row.betreuer_mail) recipientList.push(row.betreuer_mail); if (row.zweitbetreuer_mail) recipientList.push(row.zweitbetreuer_mail); }); - // actually not necessary for email clients but looks better for assistenz if we avoid duplicates here const uniqueRecipients = [...new Set(recipientList)]; const subject = this.$p.t('abgabetool/c4sammelmailBetreuerBetreff', [this.selectedStudiengangOption?.bezeichnung]); - splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p) + + // dedupe by student_uid, then build one line per student + const seenUids = new Set(); + const bodyLines = []; + this.selectedData.forEach(row => { + if (seenUids.has(row.student_uid)) return; + seenUids.add(row.student_uid); + const name = `${row.student_vorname ?? ''} ${row.student_nachname ?? ''}`.trim(); + const titel = row.titel ? ` - ${row.titel}` : ''; + bodyLines.push(`${name}${titel}`); + }); + + const body = bodyLines.join('\n'); + splitMailsHelper(uniqueRecipients, param.originalEvent, subject, body, this.$fhcAlert, this.$p) }, selectHandler(e, cell) { const row = cell.getRow(); @@ -343,11 +1190,31 @@ export const AbgabetoolAssistenz = { }, 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); - const selected = allowed.every(r => r.isSelected()); + const selected = rows.every(r => r.isSelected()); + + 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 + e.stopPropagation(); + return false; + }, + selectAllHandlerFlat(e, cell) { + const table = cell.getTable(); + const rows = this.filteredRowsFlat ?? table.getRows(); + + // custom select all logic + const allowed = rows.filter(r => r.getData().selectable); + const selected = rows.every(r => r.isSelected()); if(selected){ allowed.forEach(r => r.deselect()); @@ -420,6 +1287,32 @@ export const AbgabetoolAssistenz = { 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; + } }, getItemBezeichnung(item){ if(!item.bezeichnung) return '' @@ -450,7 +1343,6 @@ export const AbgabetoolAssistenz = { if(this.$refs.abgabeTable.tabulator) { const table = this.$refs.abgabeTable.tabulator - // TODO: maybe check if existing synergy really works with many filters const existing = table.getFilters().filter(f => f.field != 'studiensemester_kurzbz'); const compVal = e.value.studiensemester_kurzbz == this.$p.t('abgabetool/c4all') ? '' : e.value.studiensemester_kurzbz @@ -473,7 +1365,7 @@ export const AbgabetoolAssistenz = { // 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 @@ -559,7 +1451,7 @@ export const AbgabetoolAssistenz = { table.on("renderComplete", () => { if(!this.stateRestored) { - + if (saved?.columns && !this.colLayoutRestored) { const layout = saved.columns.map(col => ({ field: col.field, @@ -580,6 +1472,8 @@ export const AbgabetoolAssistenz = { if (saved?.headerFilters && !this.headerFiltersRestored) { this.headerFiltersRestored = true // instantly avoid retriggers for (let hf of saved.headerFilters) { + if (hf.field === 'qgate1Status') this.qgate1FilterSelected = hf.value || []; + if (hf.field === 'qgate2Status') this.qgate2FilterSelected = hf.value || []; table.setHeaderFilterValue(hf.field, hf.value); } } @@ -600,11 +1494,129 @@ export const AbgabetoolAssistenz = { }, 100); } this.stateRestored = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); } }); }, + handleTableBuiltFlat() { + const table = this.$refs.abgabeTableFlat.tabulator + + this.tableBuiltResolveFlat() + + table.on("columnMoved", () => { + this.saveStateFlat(table); + }); + + table.on("columnResized", () => { + this.saveStateFlat(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveStateFlat(table); + }); + + table.on("filterChanged", () => { + this.saveStateFlat(table); + }); + + table.on("headerFilterChanged", () => { + this.saveStateFlat(table); + }); + + table.on("dataSorted", () => { + this.saveStateFlat(table); + }); + + table.on("columnSorted", () => { + this.saveStateFlat(table); + }); + + table.on("sortersChanged", () => { + this.saveStateFlat(table); + }); + + const saved = this.loadStateFlat(); + + table.on("renderComplete", () => { + if(!this.stateRestoredFlat) { + + if (saved?.columns && !this.colLayoutRestoredFlat) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestoredFlat = true; + } + + if (saved?.filters && !this.filtersRestoredFlat) { + this.filtersRestoredFlat = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestoredFlat) { + this.headerFiltersRestoredFlat = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + if (hf.field === 'note') this.noteFilterSelected = hf.value || []; + if (hf.field === 'pa_note') this.pa_noteFilterSelected = hf.value || []; + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestoredFlat) { + this.sortRestoredFlat = true; + + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + if (!col) { + return null; + } + return { column: col, dir: s.dir }; + }).filter(Boolean); + + table.setSort(sortList); + }, 100); + } + this.stateRestoredFlat = true + + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTableFlat.setSelectedFields(); + + } + + }); + }, + loadStateFlat() { + return JSON.parse(localStorage.getItem(this.abgabeTableOptionsFlat.persistenceID) || "null"); + }, + saveStateFlat(table) { + // avoid storing state after first restore part happened + if(!this.stateRestoredFlat) return + const rawLayout = table.getColumnLayout(); + const state = { + columns: rawLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: table.getSorters().map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: table.getFilters(), + headerFilters: table.getHeaderFilters() + }; + + localStorage.setItem(this.abgabeTableOptionsFlat.persistenceID, JSON.stringify(state)); + }, handleToggleFullscreenDetail() { this.detailIsFullscreen = !this.detailIsFullscreen }, @@ -621,24 +1633,15 @@ export const AbgabetoolAssistenz = { return option.bezeichnung }, formatDate(dateParam) { - if(dateParam === null) return '' - 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); }, formAction(cell) { const actionButtons = document.createElement('div'); - actionButtons.className = "d-flex gap-3"; // you can keep Bootstrap gap if loaded + actionButtons.className = "d-flex gap-3"; actionButtons.style.display = "flex"; - actionButtons.style.alignItems = "stretch"; // buttons stretch to full height + actionButtons.style.alignItems = "stretch"; actionButtons.style.justifyContent = "start"; - actionButtons.style.height = "100%"; // full grid cell height + actionButtons.style.height = "100%"; const val = cell.getValue(); @@ -693,7 +1696,9 @@ export const AbgabetoolAssistenz = { }, selectionCheck(row) { const data = row.getData() - if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false + + // currently assistenz is allowed to select everything in projektarbeit table + return true }, showDeadlines(){ @@ -712,7 +1717,7 @@ export const AbgabetoolAssistenz = { this.saving = true this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.$api.call(ApiAbgabe.postSerientermin( - this.serienTermin.datum.toISOString(), + this.serienTermin.datum, this.serienTermin.bezeichnung.paabgabetyp_kurzbz, this.serienTermin.bezeichnung.bezeichnung, this.serienTermin.kurzbz, @@ -737,27 +1742,18 @@ export const AbgabetoolAssistenz = { abgabe.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz == abgabe.paabgabetyp_kurzbz) pa.abgabetermine.push(abgabe) - pa.abgabetermine.sort((a, b) => new Date(a.datum) - new Date(b.datum)) + pa.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum)) }) - // reset selection to empty - // this.$refs.abgabeTable.tabulator.deselectRow() - const table = this.$refs.abgabeTable.tabulator; - const scrollX = table.rowManager.scrollLeft; - const scrollY = table.rowManager.scrollTop; - - const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + this.projektarbeiten = this.mapProjekteToTableData(this.projektarbeiten) - table.setData(mappedData) - table.redraw(true) + this.redrawTableScrollSave() - Vue.nextTick(()=> { - const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') - if(table) { - table.scrollLeft = scrollX; - table.scrollTop = scrollY; - } - }) + // in case pesky user creates a series and instantly switches viewmode + this.flatDataDirty = true + if (this.mode === 'flatView') { + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + } }).finally(()=>{ this.saving = false @@ -786,7 +1782,7 @@ export const AbgabetoolAssistenz = { } const latestTerminWithUpload = this.findLatestTerminWithUpload(projekt) - + return { ...projekt, abgabetermine: projekt.abgabetermine, @@ -808,8 +1804,10 @@ export const AbgabetoolAssistenz = { }) }, findLatestTerminWithUpload(projekt) { - const withAbgabedatumSorted = projekt?.abgabetermine?.filter(t => t.abgabedatum != null)?.sort((a,b) => a < b) - + const withAbgabedatumSorted = projekt?.abgabetermine + ?.filter(t => t.abgabedatum != null) + ?.sort((a, b) => compareISODateValues(b.abgabedatum, a.abgabedatum)); + if(withAbgabedatumSorted.length) { return withAbgabedatumSorted[0] } @@ -865,7 +1863,7 @@ export const AbgabetoolAssistenz = { termin.allowedToSave = paIsBenotet ? false : true // assistenz are not allowed to delete deadlines with existing submissions - termin.allowedToDelete = paIsBenotet ? false : !termin.abgabedatum + termin.allowedToDelete = paIsBenotet ? false : !termin.abgabedatum && !termin.note }) @@ -892,45 +1890,51 @@ export const AbgabetoolAssistenz = { this.timelineProjekt = projekt this.$refs.drawer.show() }, + paabgabetypFormatter(cell) { + const key = cell.getValue() + return this.$p.t('abgabetool/c4paatyp' + key) + }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return + const longForm = cell.getValue() + if(!longForm) return + const data = cell.getData() + const entry = Object.entries(data).find(entry => entry[1] == longForm) - return '
' + - '

'+val+'

' + // shortFormKey must have same keyname as longForm but with 'Short' appended + const shortForm = data[entry[0]+'Short'] + + if(shortForm && longForm) { + return `
+ + ${longForm} + + +
`; + } else { + return '
' + + '

'+longForm+'

' + } }, detailFormatter(cell) { - return '
' + + return '
' + '
' }, - mailFormatter(cell) { - const val = cell.getValue() - return '
' + - '
' - }, - timelineFormatter() { - return '
' + - '
' - }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' - }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, - abgabterminFormatter(cell) { + abgabterminFormatter(cell, formatterParams, onRendered,) { const val = cell.getValue() - + const dateStyle = val?.dateStyle ?? val + + if(val) { let icon = '' - switch(val.dateStyle) { + switch(dateStyle) { case 'verspaetet': icon = '' break @@ -959,15 +1963,23 @@ export const AbgabetoolAssistenz = { const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung - return '
' + - '
' + + if(formatterParams?.iconOnly) { + return '
' + + '
' + + icon + + '
' + + '
' + } + + return '
' + + '
' + icon + '
' + '
' + - '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + + '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + '
'+ '
' - + } else { return '' } @@ -976,6 +1988,9 @@ export const AbgabetoolAssistenz = { tableResolve(resolve) { this.tableBuiltResolve = resolve }, + tableResolveFlat(resolve) { + this.tableBuiltResolveFlat = resolve + }, buildMailToLink(projekt) { return 'mailto:' + projekt.student_uid +'@'+ this.domain }, @@ -993,14 +2008,29 @@ export const AbgabetoolAssistenz = { return projekt.zweitbetreuer_full_name ?? '' }, async setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - - this.tableData = this.mapProjekteToTableData(this.projektarbeiten) + this.projektarbeiten = this.mapProjekteToTableData(data[0]) + this.count = this.projektarbeiten.length + await this.tableBuiltPromise - this.$refs.abgabeTable.tabulator.setData(this.tableData); + this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten); + + // apply preselected semester filter now that table + data are ready + this.semesterChanged({ value: this.curSem }) + + // keep flat table in sync + this.flatDataDirty = true + if (this.mode === 'flatView') { + await this.tableBuiltPromiseFlat + this.$refs.abgabeTableFlat.tabulator.setData(this.getAllTermine) + this.flatDataDirty = false + } + + // reset flat selection since the underlying data changed entirely + this.selectedDataFlat = [] + this.selectedcountFlat = 0 }, loadProjektarbeiten(all = false, callback) { this.loading = true @@ -1015,7 +2045,7 @@ export const AbgabetoolAssistenz = { callback() } }).finally(()=>{ - this.loading=false + this.loading = false }) }, loadAbgaben(details) { @@ -1029,23 +2059,32 @@ export const AbgabetoolAssistenz = { handleUuidDefined(uuid) { this.tabulatorUuid = uuid }, + handleUuidDefinedFlat(uuid) { + this.tabulatorUuidFlat = uuid + }, calcMaxTableHeight() { const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : '' const tableDataSet = document.getElementById('filterTableDataset' + tableID); if(!tableDataSet) return const rect = tableDataSet.getBoundingClientRect(); - this.abgabeTableOptions.height = window.visualViewport.height - rect.top - 80 + + this.abgabeTableOptions.height = Math.round(window.visualViewport.height - rect.top) this.$refs.abgabeTable.tabulator.setHeight(this.abgabeTableOptions.height) + + // same thing for 2nd tabulator which would exceed in size massively + this.abgabeTableOptionsFlat.height = Math.round(window.visualViewport.height - rect.top) + }, async setupMounted() { this.tableBuiltPromise = new Promise(this.tableResolve) + this.tableBuiltPromiseFlat = new Promise(this.tableResolveFlat); await this.tableBuiltPromise await this.allConfigPromise // called through notenOptionFilter/selectedStudiengangOption watcher on startup - // this.loadProjektarbeiten() + this.loadProjektarbeiten() this.calcMaxTableHeight() }, @@ -1054,6 +2093,61 @@ export const AbgabetoolAssistenz = { }, }, computed: { + getDisableDeleteForSelectedFlat() { + return this.selectedDataFlat.some(s => s.allowedToDelete === false) + }, + getDisableSaveForSelectedFlat(){ + return this.selectedDataFlat.some(s => s.allowedToSave === false) + }, + getAllTermine() { + if (!this.projektarbeiten) return []; + return this.projektarbeiten.flatMap(pa => + pa.abgabetermine.map(termin => { + const allowedToSave = pa.note !== null ? false : true + const allowedToDelete = pa.note !== null ? false : !termin.abgabedatum && !termin.note + return { + allowedToSave, + allowedToDelete, + selectable: allowedToSave || allowedToDelete, + ...termin, + student_uid: pa.student_uid, + student_vorname: pa.student_vorname, + student_nachname: pa.student_nachname, + titel: pa.titel, + pa_note: pa.note, + projektarbeit_id: pa.projektarbeit_id, + stg: pa.stg, + pkz: pa.pkz, + studienstatus: pa.studienstatus, + orgform: pa.orgform + } + } + + ) + ); + }, + countsToHTMLFlat() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcountFlat || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.countFlat || 0) + ''; + }, + countsToHTML() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, emailItems() { const menu = [] @@ -1104,6 +2198,8 @@ export const AbgabetoolAssistenz = { this.serienTermin.upload_allowed = newVal.upload_allowed_default }, selectedStudiengangOption(newVal, oldVal) { + if(this.loading == true) return + // implicitely avoids juggling around promises for created api calls, // since we need note & stg flags for loadProjektarbeiten if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { @@ -1111,6 +2207,8 @@ export const AbgabetoolAssistenz = { } }, notenOptionFilter(newVal) { + if(this.loading == true) return + // that single where clause is worth a decent load time so rather not filter tabulator but just // adapt the qry if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { @@ -1133,10 +2231,18 @@ export const AbgabetoolAssistenz = { const cb = row.getElement().children[0]?.children[0]?.children[0] if(cb) cb.checked = true }) - + }, + projektarbeiten: { + handler() { + this.flatDataDirty = true + }, + deep: true } }, created() { + // make sure zoom media query doesnt spill ever to other CIS4 sites + document.documentElement.classList.add('abgabetool'); + this.loading = true this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global']) this.phrasenPromise.then(()=> {this.phrasenResolved = true}) @@ -1179,8 +2285,18 @@ export const AbgabetoolAssistenz = { const res = results[2].value; this.allSem = res.data[0]; const all = { studiensemester_kurzbz: this.$p.t('abgabetool/c4all') }; - this.curSem = all; + + this.allSemOption = all this.studiensemesterOptions = [all, ...this.allSem]; + + const currentSemObj = res.data[1]; + + // find the matching entry from studiensemesterOptions so PrimeVue + // can match by reference for the dropdown preselection + const foundRef = this.studiensemesterOptions.find( + s => s.studiensemester_kurzbz === currentSemObj.studiensemester_kurzbz + ) + this.curSem = foundRef ?? all; } // 4. Noten @@ -1218,10 +2334,112 @@ export const AbgabetoolAssistenz = { mounted() { this.setupMounted() }, + beforeUnmount() { + document.documentElement.classList.remove('abgabetool'); + }, template: ` `, diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 0acb46bb6..00f81be36 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -7,6 +7,7 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { getDateStyleClass } from "./getDateStyleClass.js"; import { dateFilter } from '../../../tabulator/filters/Dates.js'; import {splitMailsHelper} from "../../../helpers/EmailHelpers.js"; +import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js"; export const AbgabetoolMitarbeiter = { name: "AbgabetoolMitarbeiter", @@ -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,65 @@ 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.detailFormatter, headerFilter: false, headerSort: false, 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/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/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/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, visible: true, minWidth: 100, width: 200}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncPrevTermin, + field: 'prevTermin', formatter: this.abgabterminFormatter, width: 250, visible: false}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminCol, sorter: this.sortFuncTerminCol, - formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false}, + tooltip: this.toolTipFuncNextTermin, + formatter: this.abgabterminFormatter, width: 250, visible: true}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate1Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG1' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate1Status + } + }, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), - headerFilter: 'list', - headerFilterParams: { valuesLookup: this.getQGateStatusList }, - field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} + headerFilter: this.qgateHeaderFilterEditor, + headerFilterFunc: this.qgateHeaderFilterFunc, + headerFilterParams: {}, + field: 'qgate2Status', + formatter: this.centeredTextFormatter, + titleFormatter: this.shortLongTitleFormatter, + titleFormatterParams: { + shortForm: 'QG2' + }, + width: 50, + tooltip: (e, cell) => { + const data = cell.getData(); + return data.qgate2Status + } + } ], persistence: false, - persistenceID: 'abgabeTableBetreuer2026-02-26' + persistenceID: 'abgabeTableBetreuer2026-05-26' }, abgabeTableEventHandlers: [{ event: "tableBuilt", @@ -190,11 +223,293 @@ 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: { + getDateStyleHtml(dateStyle) { + const iconMap = { + 'verspaetet': '', + 'verpasst': '', + 'abzugeben': '', + 'standard': '', + 'abgegeben': '', + 'beurteilungerforderlich': '', + 'bestanden': '', + 'nichtbestanden': '', + }; + 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 ` + ${longForm} + + ` + } else { + return ` + ${longForm} + ` + } + + }, + 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 +522,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 +562,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 +691,9 @@ export const AbgabetoolMitarbeiter = { } this.stateRestored = true + // ensure that the filterCollapseables thingy has the correct values + this.$refs.abgabeTable.setSelectedFields(); + } }); @@ -441,6 +759,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 +794,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 @@ -507,11 +851,11 @@ export const AbgabetoolMitarbeiter = { const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung return '
' + - '
' + - icon + + '
' + + icon + '
' + '
' + - '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + + '

'+bezeichnung+' - '+ this.formatDate(val.datum)+'

' + '
'+ '
' @@ -535,16 +879,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 @@ -558,15 +905,7 @@ export const AbgabetoolMitarbeiter = { return option.bezeichnung }, formatDate(dateParam) { - const date = new Date(dateParam) - // handle missing leading 0 - const padZero = (num) => String(num).padStart(2, '0'); - - const month = padZero(date.getMonth() + 1); // Months are zero-based - const day = padZero(date.getDate()); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; + return formatISODate(dateParam); }, undoSelection(cell) { // checks if cells row is selected and unselects -> imitates columns which dont trigger row selection @@ -579,6 +918,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 +943,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 +1041,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 +1058,37 @@ export const AbgabetoolMitarbeiter = { }, centeredTextFormatter(cell) { - const val = cell.getValue() - if(!val) return - - return '
' + - '

'+val+'

' + 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 `
+ + ${longForm} + + +
`; + } else { + return '
' + + '

'+longForm+'

' + } }, detailFormatter(cell) { - return '
' + + return '
' + '
' }, - beurteilungFormatter(cell) { - const val = cell.getValue() - if(val) { - return '
' + - '
' - } else return '-' - }, pkzTextFormatter(cell) { const val = cell.getValue() - return '
' + - ''+val+'
' + return '
' + + ''+val+'
' }, tableResolve(resolve) { this.tableBuiltResolve = resolve @@ -755,10 +1105,9 @@ export const AbgabetoolMitarbeiter = { setupData(data){ - this.projektarbeiten = data[0] this.domain = data[1] - this.tableData = data[0]?.retval?.map(projekt => { + this.projektarbeiten = data[0]?.retval?.map(projekt => { this.checkAbgabetermineProjektarbeit(projekt) projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' @@ -777,9 +1126,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)) @@ -831,6 +1181,17 @@ export const AbgabetoolMitarbeiter = { }, }, computed: { + countsToHTML() { + return this.$p.t('global/ausgewaehlt') + + ': ' + (this.selectedcount || 0) + '' + + ' | ' + + this.$p.t('global/gefiltert') + + ': ' + + '' + (this.filteredcount || 0) + '' + + ' | ' + + this.$p.t('global/gesamt') + + ': ' + (this.count || 0) + ''; + }, emailItems() { const menu = [] @@ -859,6 +1220,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,6 +1263,9 @@ export const AbgabetoolMitarbeiter = { mounted() { this.setupMounted() }, + beforeUnmount() { + document.documentElement.classList.remove('abgabetool'); + }, template: `