From 557e43e19c499debb5976bc4d519685e354933d0 Mon Sep 17 00:00:00 2001 From: Johann Hoffmann Date: Fri, 22 May 2026 11:17:37 +0200 Subject: [PATCH] extracted date handling with luxon into dateUtils file and only work with those functions to completely avoid anymore timezone bugs due to js Dates. formatDate was still affected by timezone issues and was showing off by one dates in rare cases. --- .../Cis/Abgabetool/AbgabeMitarbeiterDetail.js | 22 +++++--------- .../Cis/Abgabetool/AbgabeStudentDetail.js | 14 +++------ .../Cis/Abgabetool/AbgabetoolAssistenz.js | 30 +++++++------------ .../Cis/Abgabetool/AbgabetoolMitarbeiter.js | 17 ++++------- .../js/components/Cis/Abgabetool/dateUtils.js | 26 ++++++++++++++++ .../Cis/Abgabetool/getDateStyleClass.js | 4 +-- 6 files changed, 55 insertions(+), 58 deletions(-) create mode 100644 public/js/components/Cis/Abgabetool/dateUtils.js diff --git a/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js b/public/js/components/Cis/Abgabetool/AbgabeMitarbeiterDetail.js index c278b43ea..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", @@ -138,7 +139,7 @@ export const AbgabeMitarbeiterDetail = { 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, @@ -597,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, diff --git a/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js b/public/js/components/Cis/Abgabetool/AbgabeStudentDetail.js index 18962f5ae..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", @@ -166,14 +167,7 @@ export const AbgabeStudentDetail = { window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url) }, formatDate(dateParam) { - const date = new Date(dateParam) - const padZero = (num) => String(num).padStart(2, '0'); - - const month = padZero(date.getUTCMonth() + 1); - const day = padZero(date.getUTCDate()); - const year = date.getUTCFullYear(); - - return `${day}.${month}.${year}` + return formatISODate(dateParam) }, async upload(termin) { @@ -210,7 +204,7 @@ export const AbgabeStudentDetail = { if(res.meta.status == "success") { this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3'))) - termin.abgabedatum = new Date().toISOString().split('T')[0]; + termin.abgabedatum = getViennaTodayISO(); if(res?.data?.signatur !== undefined) { termin.signatur = res.data.signatur } @@ -684,4 +678,4 @@ export const AbgabeStudentDetail = { `, }; -export default AbgabeStudentDetail; \ No newline at end of file +export default AbgabeStudentDetail; diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js b/public/js/components/Cis/Abgabetool/AbgabetoolAssistenz.js index 108aa33b8..fee321299 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", @@ -118,7 +119,7 @@ export const AbgabetoolAssistenz = { fixtermin: false, }, serienTermin: Vue.reactive({ - datum: new Date().toISOString().split('T')[0], + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -404,7 +405,7 @@ export const AbgabetoolAssistenz = { field: 'datum', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminColISO, - sorter: (a, b) => new Date(a) - new Date(b), + sorter: compareISODateValues, formatter: (cell) => this.formatDate(cell.getValue()), minWidth: 100 }, @@ -413,7 +414,7 @@ export const AbgabetoolAssistenz = { field: 'abgabedatum', headerFilter: dateFilter, headerFilterFunc: this.headerFilterTerminColISO, - sorter: (a, b) => new Date(a) - new Date(b), + sorter: compareISODateValues, formatter: (cell) => this.formatDate(cell.getValue()), minWidth: 100 }, @@ -633,7 +634,7 @@ export const AbgabetoolAssistenz = { upload_allowed: false, fixtermin: false, } - this.serienEdit.datum = new Date().toISOString().split('T')[0] + this.serienEdit.datum = getViennaTodayISO() this.serienEdit.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === 'zwischen') this.serienEdit.kurzbz = '' this.serienEdit.upload_allowed = false @@ -1087,7 +1088,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)); } @@ -1124,7 +1125,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)); } @@ -1372,7 +1373,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 @@ -1640,16 +1641,7 @@ 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'); @@ -1758,7 +1750,7 @@ 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)) }) this.projektarbeiten = this.mapProjekteToTableData(this.projektarbeiten) @@ -1822,7 +1814,7 @@ export const AbgabetoolAssistenz = { findLatestTerminWithUpload(projekt) { const withAbgabedatumSorted = projekt?.abgabetermine ?.filter(t => t.abgabedatum != null) - ?.sort((a, b) => new Date(b.abgabedatum) - new Date(a.abgabedatum)); + ?.sort((a, b) => compareISODateValues(b.abgabedatum, a.abgabedatum)); if(withAbgabedatumSorted.length) { return withAbgabedatumSorted[0] diff --git a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js index 9d8db2d8f..547218fce 100644 --- a/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js +++ b/public/js/components/Cis/Abgabetool/AbgabetoolMitarbeiter.js @@ -7,6 +7,7 @@ import FhcOverlay from "../../Overlay/FhcOverlay.js"; import { getDateStyleClass } from "./getDateStyleClass.js"; import { dateFilter } from '../../../tabulator/filters/Dates.js'; import {splitMailsHelper} from "../../../helpers/EmailHelpers.js"; +import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js"; export const AbgabetoolMitarbeiter = { name: "AbgabetoolMitarbeiter", @@ -63,7 +64,7 @@ export const AbgabetoolMitarbeiter = { allowedNotenOptions: null, notenOptionsNonFinal: null, serienTermin: Vue.reactive({ - datum: new Date().toISOString().split('T')[0], + datum: getViennaTodayISO(), bezeichnung: { paabgabetyp_kurzbz: 'zwischen', bezeichnung: 'Zwischenabgabe' @@ -571,7 +572,7 @@ export const AbgabetoolMitarbeiter = { if (val instanceof Date) { dt = luxon.DateTime.fromJSDate(val); } else if (typeof val === "string") { - dt = luxon.DateTime.fromISO(val); + dt = toViennaDate(val); } else { // fallback dt = luxon.DateTime.fromMillis(Number(val)); } @@ -803,7 +804,7 @@ export const AbgabetoolMitarbeiter = { // while already looping through each termin, calculate datestyle beforehand termin.dateStyle = getDateStyleClass(termin, this.notenOptions) - const date = luxon.DateTime.fromISO(termin.datum).endOf('day') + const date = toViennaDate(termin.datum).endOf('day') termin.luxonDate = date termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past @@ -914,15 +915,7 @@ export const AbgabetoolMitarbeiter = { return option.bezeichnung }, formatDate(dateParam) { - const date = new Date(dateParam) - // handle missing leading 0 - const padZero = (num) => String(num).padStart(2, '0'); - - const month = padZero(date.getMonth() + 1); // Months are zero-based - const day = padZero(date.getDate()); - const year = date.getFullYear(); - - return `${day}.${month}.${year}`; + return formatISODate(dateParam); }, undoSelection(cell) { // checks if cells row is selected and unselects -> imitates columns which dont trigger row selection diff --git a/public/js/components/Cis/Abgabetool/dateUtils.js b/public/js/components/Cis/Abgabetool/dateUtils.js new file mode 100644 index 000000000..c9cb080a9 --- /dev/null +++ b/public/js/components/Cis/Abgabetool/dateUtils.js @@ -0,0 +1,26 @@ +const zone = 'Europe/Vienna'; + +export function getViennaTodayISO() { + return luxon.DateTime.now().setZone(zone).toISODate(); +} + +export function formatISODate(dateParam) { + if (!dateParam) return ''; + + const date = luxon.DateTime.fromISO(String(dateParam), { zone }); + return date.isValid ? date.toFormat('dd.MM.yyyy') : ''; +} + +export function toViennaDate(dateParam) { + if (!dateParam) return null; + + return luxon.DateTime.fromISO(String(dateParam), { zone }); +} + +export function compareISODateValues(a, b) { + if (!a && !b) return 0; + if (!a) return 1; + if (!b) return -1; + + return String(a).localeCompare(String(b)); +} diff --git a/public/js/components/Cis/Abgabetool/getDateStyleClass.js b/public/js/components/Cis/Abgabetool/getDateStyleClass.js index ba5224647..b6d8e3b78 100644 --- a/public/js/components/Cis/Abgabetool/getDateStyleClass.js +++ b/public/js/components/Cis/Abgabetool/getDateStyleClass.js @@ -1,8 +1,8 @@ const zone = 'Europe/Vienna'; -const today = luxon.DateTime.now().setZone(zone); export function getDateStyleClass(termin, notenOptions) { + const today = luxon.DateTime.now().setZone(zone); const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day'); const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null; termin.diffindays = datum.diff(today, 'days').days; @@ -35,4 +35,4 @@ export function getDateStyleClass(termin, notenOptions) { if (datum < today) return 'verpasst'; if (termin.diffindays <= 12) return 'abzugeben'; return 'standard'; -} \ No newline at end of file +}