From 6f28696556881cf7eb9cc2c56683f6f6ba8c747e Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Wed, 18 Feb 2026 13:00:19 +0100 Subject: [PATCH] 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) {