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 {
diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js
index 011e75145..abd647af2 100644
--- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js
+++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js
@@ -2,6 +2,7 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import { getDateStyleClass } from "./getDateStyleClass.js";
+import { compareISODateValues, formatISODate, getViennaTodayISO } from "./dateUtils.js";
export const AbgabeMitarbeiterDetail = {
name: "AbgabeMitarbeiterDetail",
@@ -46,12 +47,7 @@ export const AbgabeMitarbeiterDetail = {
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
- speedDialItems: [{
- label: Vue.computed(() => this.$p.t('abgabetool/c4newAbgabetermin')),
- icon: "fa fa-plus",
- command: this.openCreateNewAbgabeModal,
- disabled: Vue.computed(() => !this.getAllowedToCreateNewTermin)
- },
+ speedDialItems: [
{
label: Vue.computed(() => this.$p.t('abgabetool/c4benoten')),
icon: "fa fa-user-check",
@@ -81,6 +77,9 @@ export const AbgabeMitarbeiterDetail = {
}
},
methods: {
+ terminIsInvalid(termin) {
+ return termin.note?.positiv == false && !termin.beurteilungsnotiz
+ },
getNoteBezeichnung(termin){
if(termin.noteBackend?.bezeichnung) {
return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
@@ -113,6 +112,8 @@ export const AbgabeMitarbeiterDetail = {
if(newTerminRes.note) {
newTerminRes.note = noteOpt
newTerminRes.noteBackend = noteOpt // certain UI elements should only reflect persisted state
+ termin.allowedToDelete = false
+ newTerminRes.allowedToDelete = false
}
newTerminRes.invertedFixtermin = !newTerminRes.fixtermin
const existingTerminRes = res.data[1]
@@ -132,13 +133,13 @@ export const AbgabeMitarbeiterDetail = {
} else {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting
-
+
termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz
termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
}
- this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
+ this.projektarbeit.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum))
const index = this.projektarbeit.abgabetermine.findIndex(t => termin.paabgabe_id == t.paabgabe_id)
@@ -159,7 +160,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '', // todo kurzbz textfield value vorschlag für qualgates
- 'datum': new Date().toISOString().split('T')[0],
+ 'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -337,16 +338,7 @@ export const AbgabeMitarbeiterDetail = {
}
},
formatDate(dateParam) {
- // unsafe for datepickers, dont use there
- const date = new Date(dateParam)
- // handle missing leading 0
- const padZero = (num) => String(num).padStart(2, '0');
-
- const month = padZero(date.getMonth() + 1); // Months are zero-based
- const day = padZero(date.getDate());
- const year = date.getFullYear();
-
- return `${day}.${month}.${year}`
+ return formatISODate(dateParam)
},
openCreateNewAbgabeModal() {
if(this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter') {
@@ -364,7 +356,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
- 'datum': new Date().toISOString().split('T')[0],
+ 'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -398,7 +390,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
- 'datum': new Date().toISOString().split('T')[0],
+ 'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -446,7 +438,7 @@ export const AbgabeMitarbeiterDetail = {
}
},
getMessagePtStyle() {
- // adjust outer spacing and internal padding to appear similar to doenload button in size
+ // adjust outer spacing and internal padding to appear similar to download button in size
return {
root: {
style: {
@@ -561,6 +553,12 @@ export const AbgabeMitarbeiterDetail = {
class: "custom-tooltip"
}
},
+ getTooltipBeurteilungsnotiz() {
+ return {
+ value: this.$p.t('abgabetool/c4beurteilungsnotizBeiNegNote'),
+ class: "custom-tooltip"
+ }
+ },
getProjektarbeitTitel() {
if(this.projektarbeit?.titel) return this.$capitalize(this.$p.t('abgabetool/c4titel')) + ': ' + this.projektarbeit.titel
@@ -591,7 +589,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
- 'datum': new Date().toISOString().split('T')[0],
+ 'datum': getViennaTodayISO(),
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -656,6 +654,7 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
+ model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
@@ -806,6 +805,7 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
+ model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
@@ -851,8 +851,10 @@ export const AbgabeMitarbeiterDetail = {
{{$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 = {
-
+
{{ $capitalize( $p.t('abgabetool/c4save') )}}
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') )}}
-
-
+
+
+
+
+ {{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
+
+
+
+
+ {{$capitalize( $p.t('abgabetool/c4titel') )}}
+
+
+
{{ editingTitel.length }} / 1024
+
+
+
+
+ {{$capitalize( $p.t('abgabetool/c4Cancel') )}}
+
+
+
+ {{$capitalize( $p.t('ui/speichern') )}}
+
+
+
+
-
Student UID: {{ projektarbeit?.student_uid}}
-
-
{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}
-
@@ -576,15 +613,6 @@ export const AbgabeStudentDetail = {
-
-
-
-
-
-
-
-
-
{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}
@@ -631,7 +659,6 @@ export const AbgabeStudentDetail = {
{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}
-
{{$capitalize( $p.t('ui/hochladen') )}}
-
`,
};
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}
+
+
+ ${shortForm}
+ `
+ } 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 ''
+ // shortFormKey must have same keyname as longForm but with 'Short' appended
+ const shortForm = data[entry[0]+'Short']
+
+ if(shortForm && longForm) {
+ return `
+
+ ${longForm}
+
+
+ ${shortForm}
+
+
`;
+ } else {
+ return ''
+ }
},
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 '
'
+ return '
'
},
- 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 '
' +
- '