From ee7254a9642ca405502be1f19ad52b6f63a42078 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 17 Feb 2026 16:22:26 +0100 Subject: [PATCH 01/14] assistenz preserve table state (selection, scroll) when adding serientermin; update isPastDate() function to luxon timezone safe logic; --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 53 ++++++++++++++++--- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 4 +- .../Cis/Abgabetool/AbgabetoolStudent.js | 4 +- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 34ddd3fc2..e09171c25 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -212,7 +212,8 @@ export const AbgabetoolAssistenz = { abgabeTableEventHandlers: [ { event: "rowSelectionChanged", - handler: async(data) => { + handler: async(data) => + { this.selectedData.filter(sd => !data.includes(sd)).forEach(fsd => { if(fsd.checkbox) fsd.checkbox.checked = false }) @@ -220,7 +221,7 @@ export const AbgabetoolAssistenz = { data.forEach(d => { if(d.checkbox) d.checkbox.checked = true }) - + this.selectedData = data } } @@ -612,6 +613,9 @@ export const AbgabetoolAssistenz = { }, addSeries() { const pids = this.selectedData?.map(projekt => projekt.projektarbeit_id) + + const preserveSelected = [...this.selectedData] + this.saving = true this.serienTermin.fixtermin = !this.serienTermin.invertedFixtermin this.$api.call(ApiAbgabe.postSerientermin( @@ -644,14 +648,27 @@ export const AbgabetoolAssistenz = { }) // reset selection to empty - this.$refs.abgabeTable.tabulator.deselectRow() - - const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + // this.$refs.abgabeTable.tabulator.deselectRow() + const table = this.$refs.abgabeTable.tabulator; + const scrollX = table.rowManager.element.scrollLeft; + const scrollY = table.rowManager.element.scrollTop; + + const mappedData = this.mapProjekteToTableData(this.projektarbeiten) + + table.setData(mappedData) + table.redraw(true) + + + requestAnimationFrame(() => { + table.rowManager.element.scrollLeft = scrollX; + table.rowManager.element.scrollTop = scrollY; + }); + + - this.$refs.abgabeTable.tabulator.setData(mappedData) - this.$refs.abgabeTable.tabulator.redraw(true) }).finally(()=>{ this.saving = false + this.selectedData = preserveSelected }) this.$refs.modalContainerAddSeries.hide() @@ -705,7 +722,9 @@ export const AbgabetoolAssistenz = { return str }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ @@ -1034,6 +1053,24 @@ export const AbgabetoolAssistenz = { if(this.notenOptionFilter !== null && this.selectedStudiengangOption !== null) { this.loadProjektarbeiten() } + }, + selectedData(newVal) { + const table = this.$refs.abgabeTable?.tabulator + if(!table) return + + const allRows = table.getRows(); + + newVal.forEach(selected => { + const row = allRows.find(r => { + const data = r.getData() + if (data.projektarbeit_id == selected.projektarbeit_id) return r + }) + + row.select() + const cb = row.getElement().children[0]?.children[0]?.children[0] + if(cb) cb.checked = true + }) + } }, created() { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index f33333ea3..1b8eff3e2 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -294,7 +294,9 @@ export const AbgabetoolMitarbeiter = { return str }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ this.loading=true diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index ff68b680f..d03ef3ffc 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,7 +155,9 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - return new Date(date) < new Date(Date.now()) + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); + return nowInBerlin > deadline; }, setDetailComponent(details){ this.loading = true From a6daa7bf0ca7444d1336ac6138a336d49cd6fa03 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Tue, 17 Feb 2026 17:32:11 +0100 Subject: [PATCH 02/14] all abgabetool datepickers use date format via format="dd.MM.yyyy" instead of :format="formatDate" to enable text-input + autoapply; backend deadline datetime check for endupload; --- .../controllers/api/frontend/v1/Abgabe.php | 29 +++++++++++++++++++ .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 10 +++++-- .../Cis/Abgabetool/AbgabeStudentDetail.js | 3 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 3 +- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 3 +- system/phrasesupdate.php | 20 +++++++++++++ 6 files changed, 62 insertions(+), 6 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index b37c64713..23e11c202 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -373,6 +373,8 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('global', 'wrongParameters'), 'general'); } + $this->checkPaabgabeDeadline($paabgabe_id); + $this->checkProjektarbeitForFinishedStatus($projektarbeit_id); $zugeordnet = $this->checkZuordnung($projektarbeit_id, getAuthUID()); @@ -444,6 +446,33 @@ class Abgabe extends FHCAPI_Controller } } + + // 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) { + $this->load->model('education/Paabgabe_model', 'PaabgabeModel'); + + $result = $this->PaabgabeModel->load($paabgabe_id); + $paabgabeArr = $this->getDataOrTerminateWithError($result, 'general'); + + if (count($paabgabeArr) > 0) { + $paabgabe = $paabgabeArr[0]; + } else { + $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); + } + + $tz = new DateTimeZone('Europe/Berlin'); + $now = new DateTimeImmutable('now', $tz); + $deadline = DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $paabgabe->datum . ' 23:59:59', + $tz + ); + + if($now >= $deadline) { + $this->terminateWithError($this->p->t('abgabetool', 'c4deadlineExceeded')); + } + } /** * tabulator tabledata fetch for abgabetool/mitarbeiter diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index 971783746..ad740e978 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -715,7 +715,8 @@ export const AbgabeMitarbeiterDetail = { v-model="newTermin.datum" :clearable="false" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> @@ -864,7 +865,8 @@ export const AbgabeMitarbeiterDetail = { :clearable="false" :disabled="!termin.allowedToSave" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> @@ -931,7 +933,9 @@ export const AbgabeMitarbeiterDetail = { v-model="termin.abgabedatum" :clearable="false" :disabled="true" - :format="formatDate"> + locale="de" + format="dd.MM.yyyy" + > diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index 55120e223..9c14c2948 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js @@ -423,7 +423,8 @@ export const AbgabeStudentDetail = { :clearable="false" :disabled="true" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index e09171c25..db2eebaa8 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -1189,8 +1189,9 @@ export const AbgabetoolAssistenz = { style="width: 95%;" v-model="serienTermin.datum" :clearable="false" + locale="de" + format="dd.MM.yyyy" :enable-time-picker="false" - :format="formatDate" :text-input="true" auto-apply> diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 1b8eff3e2..8ee12bf79 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -525,7 +525,8 @@ export const AbgabetoolMitarbeiter = { v-model="serienTermin.datum" :clearable="false" :enable-time-picker="false" - :format="formatDate" + locale="de" + format="dd.MM.yyyy" :text-input="true" auto-apply> diff --git a/system/phrasesupdate.php b/system/phrasesupdate.php index 45e977987..8860e2cf6 100644 --- a/system/phrasesupdate.php +++ b/system/phrasesupdate.php @@ -46373,6 +46373,26 @@ array( ) ) ), + array( + 'app' => 'core', + 'category' => 'abgabetool', + 'phrase' => 'c4deadlineExceeded', + 'insertvon' => 'system', + 'phrases' => array( + array( + 'sprache' => 'German', + 'text' => 'Nicht rechtzeitig abgegeben!', + 'description' => '', + 'insertvon' => 'system' + ), + array( + 'sprache' => 'English', + 'text' => 'Deadline exceeded!', + 'description' => '', + 'insertvon' => 'system' + ) + ) + ), // ABGABETOOL PHRASEN END array( 'app' => 'core', From 90c845899f7645861c6daa4e933654643b4a853f Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 11:15:59 +0100 Subject: [PATCH 03/14] =?UTF-8?q?explicitely=20set=20deadline=20to=20end?= =?UTF-8?q?=20of=20day=20to=20achieve=20the=20desired=20"valid=20until=202?= =?UTF-8?q?3:59"=20logic,=20instead=20of=20just=20moving=20the=20deadline?= =?UTF-8?q?=20by=20one=20day;=20endupload=20deadline=20is=20now=20optional?= =?UTF-8?q?=20by=20defining=20it=20as=20a=20"nachreichen=20m=C3=B6glich"?= =?UTF-8?q?=20aka=20non=20fixtermin;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/api/frontend/v1/Abgabe.php | 18 ++++++++++++++++++ .../Cis/Abgabetool/AbgabetoolStudent.js | 6 +++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index 23e11c202..f0744fb99 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -461,6 +461,9 @@ class Abgabe extends FHCAPI_Controller $this->terminateWithError($this->p->t('abgabetool', 'c4projektabgabeNichtGefunden'), 'general'); } + // in that case any submission date is fine + if($paabgabe->fixtermin === false) return; + $tz = new DateTimeZone('Europe/Berlin'); $now = new DateTimeImmutable('now', $tz); $deadline = DateTimeImmutable::createFromFormat( @@ -502,6 +505,15 @@ class Abgabe extends FHCAPI_Controller $projektarbeiten = $this->ProjektarbeitModel->getMitarbeiterProjektarbeiten(getAuthUID(), $showAllBool); + $mapFunc = function($projektarbeit) { + return $projektarbeit->projektarbeit_id; + }; + $projektarbeiten_ids = array_map($mapFunc, $projektarbeiten->retval); + + $ret = $this->ProjektarbeitModel->getProjektarbeitenAbgabetermine($projektarbeiten_ids); + $projektabgaben = $this->getDataOrTerminateWithError($ret, 'general'); + + forEach($projektarbeiten->retval as $pa) { $result = $this->ProjektarbeitModel->getProjektbetreuerAnrede($pa->betreuer_person_id); @@ -518,6 +530,12 @@ class Abgabe extends FHCAPI_Controller Events::trigger('projektbeurteilung_formular_link', $pa->betreuerart_kurzbz, APP_ROOT, $pa->projektarbeit_id, $pa->student_uid, $returnFunc); $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + + $filterFunc = function($projektabgabe) use ($pa) { + return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; + }; + + $pa->abgabetermine = array_values(array_filter($projektabgaben, $filterFunc)); } diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index d03ef3ffc..fd88cbe02 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,7 +155,7 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }).endOf('day'); const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); return nowInBerlin > deadline; }, @@ -175,8 +175,8 @@ export const AbgabetoolStudent = { // old assumed production logic when qgates are required // termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine) - // new larifari we want qgates but they are optional fhtw mode - termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesOptional(pa.abgabetermine) + const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true + termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine) // development purposes From 328affa35caa1ad174dcf05b78e416a846f9ca8c Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 11:53:24 +0100 Subject: [PATCH 04/14] actually set deadline calculation to IANA timezone 'Europe/Vienna', so the code still works once Berlin moves to another timezone away from Austria. You never know. --- public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js | 6 +++--- public/js/components/Cis/Abgabetool/AbgabetoolStudent.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index db2eebaa8..e4609d050 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -722,9 +722,9 @@ export const AbgabetoolAssistenz = { return str }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day'); + const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna'); + return nowInVienna > deadline; }, setDetailComponent(details){ diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index fd88cbe02..4baf5316f 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -155,9 +155,9 @@ export const AbgabetoolStudent = { return qgate1positiv && qgate2positiv }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }).endOf('day'); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day'); + const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna'); + return nowInVienna > deadline; }, setDetailComponent(details){ this.loading = true From 6f28696556881cf7eb9cc2c56683f6f6ba8c747e Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 13:00:19 +0100 Subject: [PATCH 05/14] getDateStyleClass evaluation also with precise luxon calculation on all pages; qgate12 status col, next/prev termin col on betreuer page; table persistence on mitarbeiter page; same rowheight on betreuer table as in assistenz to achieve similar UX; --- .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 51 +-- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 34 +- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 304 +++++++++++++++++- .../Cis/Abgabetool/AbgabetoolStudent.js | 29 +- 4 files changed, 320 insertions(+), 98 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index ad740e978..f86fa44d9 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -270,48 +270,13 @@ export const AbgabeMitarbeiterDetail = { 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)) }, - convertDateToIsoString(date) { - // 1. Check if it is a Date object AND if the date value is valid (not 'Invalid Date') - if (param instanceof Date && !isNaN(param.getTime())) { - const year = param.getFullYear(); - // getMonth() is 0-indexed, so we add 1. - const month = param.getMonth() + 1; - const day = param.getDate(); - - // Helper to pad single-digit numbers with a leading zero - const pad = (num) => String(num).padStart(2, '0'); - - // Return the formatted string: YYYY-MM-DD - return `${year}-${pad(month)}-${pad(day)}`; - } - - // If it's not a valid Date, return the original parameter - return param; - }, - dateDiffInDays(datumParam){ - let datum = datumParam - if(datumParam instanceof Date && !isNaN(datum.getTime())) - { - const year = datumParam.getFullYear(); - const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed - const day = datumParam.getDate(); - const pad = (num) => String(num).padStart(2, '0'); - datum = `${year}-${pad(month)}-${pad(day)}` - } - - const dateToday = luxon.DateTime.now().startOf('day'); - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); + const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; + termin.diffindays = datum.diff(today, 'days').days; + const isLate = abgabedatum && abgabedatum > datum; // GRADE STATUS if (termin.note) { @@ -396,6 +361,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'); @@ -476,7 +442,6 @@ export const AbgabeMitarbeiterDetail = { termin.kurzbz = '' } } - }, computed: { getAllowedToCreateNewTermin() { @@ -626,7 +591,6 @@ export const AbgabeMitarbeiterDetail = { return '' }, getProjektarbeitStudent(){ - if(this.projektarbeit?.student) return this.$capitalize(this.$p.t('person/student')) + ': ' + this.projektarbeit.student return '' @@ -671,7 +635,6 @@ export const AbgabeMitarbeiterDetail = { this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? '' this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? '' this.form.seitenanzahl = newVal.seitenanzahl ?? 1 - }, }, created() { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index e4609d050..8b0fe7ddb 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -9,15 +9,6 @@ import AbgabeterminStatusLegende from "./StatusLegende.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { splitMailsHelper } from "../../../helpers/EmailHelpers.js" -// spoofed date testing -// const todayISO = '2025-08-08' -// const today = new Date(todayISO) -// const now = luxon.DateTime.fromISO(todayISO) - -// prod code -const today = new Date() -const now = luxon.DateTime.now() - export const AbgabetoolAssistenz = { name: "AbgabetoolAssistenz", components: { @@ -386,6 +377,8 @@ export const AbgabetoolAssistenz = { }, checkAbgabetermineProjektarbeit(projekt) { + const now = luxon.DateTime.now() + // calculate Abgabetermin time diff to now and assign last and next to projekt projekt.abgabetermine.forEach(termin => { @@ -393,7 +386,7 @@ export const AbgabetoolAssistenz = { // while already looping through each termin, calculate datestyle beforehand termin.dateStyle = this.getDateStyleClass(termin) - const date = luxon.DateTime.fromISO(termin.datum) + const date = luxon.DateTime.fromISO(termin.datum).endOf('day') termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past if (termin.diffMs < 0) { @@ -770,22 +763,13 @@ export const AbgabetoolAssistenz = { this.$refs.modalContainerAbgabeDetail.show() }, - dateDiffInDays(datum){ - const dateToday = luxon.DateTime.now().startOf('day'); - - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); + const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; + termin.diffindays = datum.diff(today, 'days').days; + const isLate = abgabedatum && abgabedatum > datum; // GRADE STATUS if (termin.note) { diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 8ee12bf79..2ad29d20c 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -79,7 +79,7 @@ export const AbgabetoolMitarbeiter = { placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')), selectable: true, selectableCheck: this.selectionCheck, - rowHeight: 80, + rowHeight: 40, columns: [ { formatter: function (cell, formatterParams, onRendered) { @@ -144,9 +144,14 @@ export const AbgabetoolMitarbeiter = { {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1}, {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8}, - {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1} + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerart'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), headerFilter: true, field: 'prevTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), headerFilter: true, field: 'nextTermin', formatter: this.abgabterminFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))), headerFilter: true, field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}, + {title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))), headerFilter: true, field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false} ], persistence: false, + persistenceID: 'abgabeTableBetreuer2026-02-18' }, abgabeTableEventHandlers: [{ event: "tableBuilt", @@ -182,6 +187,290 @@ export const AbgabetoolMitarbeiter = { ]}; }, methods: { + loadState() { + return JSON.parse(localStorage.getItem(this.abgabeTableOptions.persistenceID) || "null"); + }, + saveState(table) { + // avoid storing state after first restore part happened + if(!this.stateRestored) return + const rawLayout = table.getColumnLayout(); + const state = { + columns: rawLayout.map(col => ({ + field: col.field, + visible: col.visible, + width: col.width, + })), + sort: table.getSorters().map(s => ({ + field: s.field, + dir: s.dir, + })), + filters: table.getFilters(), + headerFilters: table.getHeaderFilters() + }; + + localStorage.setItem(this.abgabeTableOptions.persistenceID, JSON.stringify(state)); + }, + handleTableBuilt() { + const table = this.$refs.abgabeTable.tabulator + + this.tableBuiltResolve() + + table.on("columnMoved", () => { + this.saveState(table); + }); + + table.on("columnResized", () => { + this.saveState(table); + }); + + table.on("columnVisibilityChanged", () => { + this.saveState(table); + }); + + table.on("filterChanged", () => { + this.saveState(table); + }); + + table.on("headerFilterChanged", () => { + this.saveState(table); + }); + + table.on("dataSorted", () => { + this.saveState(table); + }); + + table.on("columnSorted", () => { + this.saveState(table); + }); + + table.on("sortersChanged", () => { + this.saveState(table); + }); + + const saved = this.loadState(); + + table.on("renderComplete", () => { + if(!this.stateRestored) { + + if (saved?.columns && !this.colLayoutRestored) { + const layout = saved.columns.map(col => ({ + field: col.field, + width: col.width, + visible: col.visible, + // add more if needed, but keep it simple + })); + + table.setColumnLayout(layout); + + this.colLayoutRestored = true; + } + + if (saved?.filters && !this.filtersRestored) { + this.filtersRestored = true // instantly avoid retriggers + table.setFilter(saved.filters); + } + if (saved?.headerFilters && !this.headerFiltersRestored) { + this.headerFiltersRestored = true // instantly avoid retriggers + for (let hf of saved.headerFilters) { + table.setHeaderFilterValue(hf.field, hf.value); + } + } + + if (saved?.sort?.length && !this.sortRestored) { + this.sortRestored = true; + + setTimeout(() => { + const sortList = saved.sort.map(s => { + const col = table.columnManager.findColumn(s.field); + if (!col) { + return null; + } + return { column: col, dir: s.dir }; + }).filter(Boolean); + + table.setSort(sortList); + }, 100); + } + this.stateRestored = true + + } + + }); + }, + checkQualityGateStatus(projekt) { + // TODO: might refine the representation of these states and maybe refactor code a little + const qgate1Termine = [] + const qgate2Termine = [] + + projekt.qgate1Status = this.$p.t('abgabetool/c4keinTerminVorhanden')// 'Kein Termin vorhanden' + projekt.qgate1StatusRank = 0 + projekt.qgate2Status = this.$p.t('abgabetool/c4keinTerminVorhanden') + projekt.qgate2StatusRank = 0 + + projekt.abgabetermine.forEach(termin => { + if(termin.paabgabetyp_kurzbz == 'qualgate1') qgate1Termine.push(termin) + if(termin.paabgabetyp_kurzbz == 'qualgate2') qgate2Termine.push(termin) + }) + + // calculate qgateStatusRank and display the highest order status rank of all quality gate termine until one + // counts as passed, which is just a positive note no matter if anything has been uploaded + + // reuse luxon calculated diffMs (termin.datum in relation to today) from previous datestyle check + qgate1Termine.forEach(qgate => { + if(qgate.note != null && projekt.qgate1StatusRank <= 5) { + const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) + if(noteOpt.positiv) { + projekt.qgate1Status = this.$p.t('abgabetool/c4positivBenotet') + projekt.qgate1StatusRank = 5 + } else { + projekt.qgate1Status = this.$p.t('abgabetool/c4negativBenotet') + projekt.qgate1StatusRank = 4 + } + } else if (qgate.note == null && projekt.qgate1StatusRank <= 3) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notYetGraded') + projekt.qgate1StatusRank = 3 + } else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate1StatusRank <= 2) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notSubmitted') + projekt.qgate1StatusRank = 2 + } else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate1StatusRank <= 1) { + projekt.qgate1Status = this.$p.t('abgabetool/c4notHappenedYet') + projekt.qgate1StatusRank = 1 + } + }) + + qgate2Termine.forEach(qgate => { + if(qgate.note != null && projekt.qgate1StatusRank <= 5) { + const noteOpt = this.notenOptions.find(opt => opt.note == qgate.note) + if(noteOpt.positiv) { + projekt.qgate2Status = this.$p.t('abgabetool/c4positivBenotet') + projekt.qgate2StatusRank = 5 + } else { + projekt.qgate2Status = this.$p.t('abgabetool/c4negativBenotet') + projekt.qgate2StatusRank = 4 + } + } else if (qgate.note == null && projekt.qgate2StatusRank <= 3) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notYetGraded') + projekt.qgate2StatusRank = 3 + } else if(qgate.upload_allowed == true && qgate.abgabedatum == null && projekt.qgate2StatusRank <= 2) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notSubmitted') + projekt.qgate2StatusRank = 2 + } else if (qgate.upload_allowed == false && qgate.diffMs <= 0 && projekt.qgate2StatusRank <= 1) { + projekt.qgate2Status = this.$p.t('abgabetool/c4notHappenedYet') + projekt.qgate2StatusRank = 1 + } + }) + }, + checkAbgabetermineProjektarbeit(projekt) { + const now = luxon.DateTime.now() + // calculate Abgabetermin time diff to now and assign last and next to projekt + projekt.abgabetermine.forEach(termin => { + + // while already looping through each termin, calculate datestyle beforehand + termin.dateStyle = this.getDateStyleClass(termin) + + const date = luxon.DateTime.fromISO(termin.datum).endOf('day') + termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past + + if (termin.diffMs < 0) { + if (!projekt.prevTermin || + termin.diffMs > projekt.prevTermin.diffMs // larger (less negative) = closer to now + ) { + projekt.prevTermin = termin; + } + } else if (termin.diffMs > 0) { + if (!projekt.nextTermin || + termin.diffMs < projekt.nextTermin.diffMs // smaller positive = closer to now + ) { + projekt.nextTermin = termin; + } + } + }) + + // seperate check for quality gates + this.checkQualityGateStatus(projekt) + }, + getDateStyleClass(termin) { + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); + const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; + termin.diffindays = datum.diff(today, 'days').days; + const isLate = abgabedatum && abgabedatum > datum; + + // GRADE STATUS + if (termin.note) { + if (termin.note.positiv) return 'bestanden'; + return 'nichtbestanden'; + } + + // ACTION REQUIRED FOR GRADE + if (termin.bezeichnung?.benotbar && datum < today) { + return 'beurteilungerforderlich'; + } + + // SUBMISSION STATUS + if (termin.upload_allowed) { + if (termin.abgabedatum) { + return isLate ? 'verspaetet' : 'abgegeben'; + } + + // no submission yet + if (datum < today) return 'verpasst'; + if (termin.diffindays <= 12) return 'abzugeben'; + return 'standard'; + } + + // GENERIC STATUS + return datum < today ? 'verpasst' : 'standard'; + }, + abgabterminFormatter(cell) { + const val = cell.getValue() + + if(val) { + let icon = '' + switch(val.dateStyle) { + case 'verspaetet': + icon = '' + break + case 'verpasst': + icon = '' + break + case 'abzugeben': + icon = '' + break + case 'standard': + icon = '' + break + case 'abgegeben': + icon = '' + break + case 'beurteilungerfolderlich': + icon = '' + break + case 'bestanden': + icon = '' + break + case 'nichtbestanden': + icon = '' + break + } + + const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung + + return '
' + + '
' + + icon + + '
' + + '
' + + '

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

' + + '
'+ + '
' + + } else { + return '' + } + + }, selectHandler(e, cell) { const row = cell.getRow(); @@ -294,9 +583,9 @@ export const AbgabetoolMitarbeiter = { return str }, isPastDate(date) { - const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Berlin' }); - const nowInBerlin = luxon.DateTime.now().setZone('Europe/Berlin'); - return nowInBerlin > deadline; + const deadline = luxon.DateTime.fromISO(date, { zone: 'Europe/Vienna' }).endOf('day'); + const nowInVienna = luxon.DateTime.now().setZone('Europe/Vienna'); + return nowInVienna > deadline; }, setDetailComponent(details){ this.loading=true @@ -381,11 +670,13 @@ export const AbgabetoolMitarbeiter = { return (projekt.typ + projekt.kurzbz)?.toUpperCase() }, setupData(data){ + + this.projektarbeiten = data[0] this.domain = data[1] this.tableData = data[0]?.retval?.map(projekt => { - + this.checkAbgabetermineProjektarbeit(projekt) projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter' return { @@ -601,6 +892,7 @@ export const AbgabetoolMitarbeiter = { @click:new=openAddSeriesModal :tabulator-options="abgabeTableOptions" :tabulator-events="abgabeTableEventHandlers" + @tableBuilt="handleTableBuilt" tableOnly :sideMenu="false" :useSelectionSpan="false" diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js index 4baf5316f..a0df7a81d 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolStudent.js @@ -48,30 +48,13 @@ export const AbgabetoolStudent = { }; }, methods: { - dateDiffInDays(datumParam) { - let datum = datumParam - if(datumParam instanceof Date && !isNaN(datum.getTime())) - { - const year = datumParam.getFullYear(); - const month = datumParam.getMonth() + 1; // getMonth() is 0-indexed - const day = datumParam.getDate(); - const pad = (num) => String(num).padStart(2, '0'); - datum = `${year}-${pad(month)}-${pad(day)}` - } - - const dateToday = luxon.DateTime.now().startOf('day'); - const dateDatum = luxon.DateTime.fromISO(datum).startOf('day'); - const duration = dateDatum.diff(dateToday, 'days'); - - return duration.values.days; - }, getDateStyleClass(termin) { - const datum = new Date(termin.datum) - const abgabedatum = new Date(termin.abgabedatum) - - termin.diffindays = this.dateDiffInDays(termin.datum) - - const isLate = termin.abgabedatum && abgabedatum > datum; + const zone = 'Europe/Vienna'; + const today = luxon.DateTime.now().setZone(zone); + const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); + const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; + termin.diffindays = datum.diff(today, 'days').days; + const isLate = abgabedatum && abgabedatum > datum; // GRADE STATUS if (termin.note) { From 4724008c2df9231341e6ee3d64fcd9e060e5e0ac Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 14:32:57 +0100 Subject: [PATCH 06/14] betreuer page update table after adding serientermin qgate1/2 status prev/next; preserve scrollX/Y in betreuer/assistenz page --- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 18 +++++++++--------- .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 8b0fe7ddb..fb6a8cd64 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js @@ -643,21 +643,21 @@ export const AbgabetoolAssistenz = { // reset selection to empty // this.$refs.abgabeTable.tabulator.deselectRow() const table = this.$refs.abgabeTable.tabulator; - const scrollX = table.rowManager.element.scrollLeft; - const scrollY = table.rowManager.element.scrollTop; + const scrollX = table.rowManager.scrollLeft; + const scrollY = table.rowManager.scrollTop; const mappedData = this.mapProjekteToTableData(this.projektarbeiten) table.setData(mappedData) table.redraw(true) - - requestAnimationFrame(() => { - table.rowManager.element.scrollLeft = scrollX; - table.rowManager.element.scrollTop = scrollY; - }); - - + Vue.nextTick(()=> { + const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = scrollX; + table.scrollTop = scrollY; + } + }) }).finally(()=>{ this.saving = false diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 2ad29d20c..2453f33bf 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -563,6 +563,24 @@ export const AbgabetoolMitarbeiter = { )).then(res => { if (res.meta.status === "success" && res.data) { this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/serienTerminGespeichert')) + + const oldScrollLeft = this.$refs.abgabeTable?.tabulator.rowManager.scrollLeft + const oldScrollTop = this.$refs.abgabeTable?.tabulator.rowManager.scrollTop + this.loading = true + this.loadProjektarbeiten(this.showAll, () => { + this.$refs.abgabeTable?.tabulator.redraw(true) + this.$refs.abgabeTable?.tabulator.setSort([]); + this.loading = false + + Vue.nextTick(()=> { + const table = this.$refs.abgabeTable?.tabulator.element.querySelector('.tabulator-tableholder') + if(table) { + table.scrollLeft = oldScrollLeft; + table.scrollTop = oldScrollTop; + } + }) + + }) } else { this.$fhcAlert.alertError(this.$p.t('abgabetool/errorSerienterminSpeichern')) } From 4b1a9fe8928c7984bd3731217c9d1b4a25ade76e Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Thu, 19 Feb 2026 17:33:41 +0100 Subject: [PATCH 07/14] avoid loading paabgaben a 2nd time for mitarbeiter; extracted getDateStyleClass from components; --- .../controllers/api/frontend/v1/Abgabe.php | 8 ++ .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 67 ++++------ .../Cis/Abgabetool/AbgabeStudentDetail.js | 2 +- .../Cis/Abgabetool/AbgabetoolAssistenz.js | 48 +------ .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 118 ++++++++---------- .../Cis/Abgabetool/AbgabetoolStudent.js | 44 +------ .../Cis/Abgabetool/getDateStyleClass.js | 37 ++++++ 7 files changed, 126 insertions(+), 198 deletions(-) create mode 100644 public/js/components/Cis/Abgabetool/getDateStyleClass.js diff --git a/application/controllers/api/frontend/v1/Abgabe.php b/application/controllers/api/frontend/v1/Abgabe.php index f0744fb99..a6390e97d 100644 --- a/application/controllers/api/frontend/v1/Abgabe.php +++ b/application/controllers/api/frontend/v1/Abgabe.php @@ -531,6 +531,14 @@ class Abgabe extends FHCAPI_Controller $pa->beurteilungLinkNew = $newLink; $pa->beurteilungLinkOld = $oldLink; + // has previously been retrieved via getStudentProjektabgaben but is fetched in advance to avoid having to reload abgaben + $projektarbeitIsCurrent = false; + $returnFunc = function ($result) use (&$projektarbeitIsCurrent) { + $projektarbeitIsCurrent = $result; + }; + Events::trigger('projektarbeit_is_current', $pa->projektarbeit_id, $returnFunc); + $pa->isCurrent = $projektarbeitIsCurrent; + $filterFunc = function($projektabgabe) use ($pa) { return $projektabgabe->projektarbeit_id == $pa->projektarbeit_id; }; diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index f86fa44d9..b760f567d 100644 --- a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js +++ b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js @@ -1,8 +1,8 @@ import BsModal from '../../Bootstrap/Modal.js'; import VueDatePicker from '../../vueDatepicker.js.php'; import ApiAbgabe from '../../../api/factory/abgabe.js' +import { getDateStyleClass } from "./getDateStyleClass.js"; -const today = new Date() export const AbgabeMitarbeiterDetail = { name: "AbgabeMitarbeiterDetail", components: { @@ -125,10 +125,12 @@ export const AbgabeMitarbeiterDetail = { // only insert new abgabe if we actually created a new one, not when saving/editing existing if(!existingTerminRes){ + newTerminRes.dateStyle = getDateStyleClass(newTerminRes, this.notenOptions) this.projektarbeit.abgabetermine.push(newTerminRes) } else { const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note) existingTerminRes.note = noteOptExisting + termin.dateStyle = getDateStyleClass(termin, this.notenOptions) } this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum)) @@ -270,40 +272,6 @@ export const AbgabeMitarbeiterDetail = { 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)) }, - getDateStyleClass(termin) { - const zone = 'Europe/Vienna'; - const today = luxon.DateTime.now().setZone(zone); - const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); - const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; - termin.diffindays = datum.diff(today, 'days').days; - const isLate = abgabedatum && abgabedatum > datum; - - // GRADE STATUS - if (termin.note) { - if (termin.note.positiv) return 'bestanden'; - return 'nichtbestanden'; - } - - // ACTION REQUIRED FOR GRADE - if (termin.bezeichnung?.benotbar && datum < today) { - return 'beurteilungerforderlich'; - } - - // SUBMISSION STATUS - if (termin.upload_allowed) { - if (termin.abgabedatum) { - return isLate ? 'verspaetet' : 'abgegeben'; - } - - // no submission yet - if (datum < today) return 'verpasst'; - if (termin.diffindays <= 12) return 'abzugeben'; - return 'standard'; - } - - // GENERIC STATUS - return datum < today ? 'verpasst' : 'standard'; - }, openBeurteilungLink(link) { window.open(link, '_blank') }, @@ -769,20 +737,29 @@ export const AbgabeMitarbeiterDetail = { -