From 12d45660c6e67eecef809c2b03d11de605ac35b9 Mon Sep 17 00:00:00 2001 From: Bouni <948965+Bouni@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:28:47 +0000 Subject: [PATCH 1/5] Update missing translations, sort translations --- TRANSLATIONS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md index 6b60ea4..2fa4fd6 100644 --- a/TRANSLATIONS.md +++ b/TRANSLATIONS.md @@ -1,6 +1,6 @@ # Translation Status -Last updated: 2026-10-07 09:57:47 +Last updated: 2026-10-09 09:28:47 Reference locale (en): **346 keys** From 64e72820c3a07721d0dcbae725d84eacea3a5c7d Mon Sep 17 00:00:00 2001 From: Bouni Date: Fri, 9 Oct 2026 15:32:30 +0200 Subject: [PATCH 2/5] Read invoice dates in UTC on the backend (#149) Issue and due dates are stored as UTC midnight. Formatting them with local getters shifts them by a day when the server runs in a timezone west of UTC. Use UTC getters for the PDF, the UBL/Factur-X/FatturaPA exports and the overdue check. Co-Authored-By: Claude Opus 5.5 --- backend/src/controllers/invoices.ts | 6 +++--- backend/src/utils/facturx.ts | 6 +++--- backend/src/utils/fatturapa.ts | 6 +++--- backend/src/utils/pdf.ts | 8 +++++--- backend/src/utils/ubl.ts | 6 +++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/backend/src/controllers/invoices.ts b/backend/src/controllers/invoices.ts index 9306dd8..b590d3c 100644 --- a/backend/src/controllers/invoices.ts +++ b/backend/src/controllers/invoices.ts @@ -1470,9 +1470,9 @@ function applyDerivedOverdue< if (!inv.dueDate) return inv; const today = new Date(); const dd = new Date( - inv.dueDate.getFullYear(), - inv.dueDate.getMonth(), - inv.dueDate.getDate(), + inv.dueDate.getUTCFullYear(), + inv.dueDate.getUTCMonth(), + inv.dueDate.getUTCDate(), ); const td = new Date(today.getFullYear(), today.getMonth(), today.getDate()); if (dd < td) { diff --git a/backend/src/utils/facturx.ts b/backend/src/utils/facturx.ts index afbed9c..7c6d438 100644 --- a/backend/src/utils/facturx.ts +++ b/backend/src/utils/facturx.ts @@ -16,9 +16,9 @@ function xmlEscape(v: Maybe): string { function fmtDateYYYYMMDD(d?: Date): string | undefined { if (!d) return undefined; - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const dd = String(d.getDate()).padStart(2, "0"); + const yyyy = d.getUTCFullYear(); + const mm = String(d.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(d.getUTCDate()).padStart(2, "0"); return `${yyyy}${mm}${dd}`; } diff --git a/backend/src/utils/fatturapa.ts b/backend/src/utils/fatturapa.ts index 13cd919..d20725c 100644 --- a/backend/src/utils/fatturapa.ts +++ b/backend/src/utils/fatturapa.ts @@ -16,9 +16,9 @@ function xmlEscape(v: Maybe): string { function fmtDate(d?: Date): string | undefined { if (!d) return undefined; - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const dd = String(d.getDate()).padStart(2, "0"); + const yyyy = d.getUTCFullYear(); + const mm = String(d.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(d.getUTCDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } diff --git a/backend/src/utils/pdf.ts b/backend/src/utils/pdf.ts index 056bb96..c8d882b 100644 --- a/backend/src/utils/pdf.ts +++ b/backend/src/utils/pdf.ts @@ -153,9 +153,11 @@ function lighten(hex: string, amount = 0.85): string { function formatDate(d?: Date, format: string = "YYYY-MM-DD") { if (!d) return undefined; const date = new Date(d); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); + // Issue/due dates are stored as UTC midnight; read UTC parts so the + // server's timezone can't shift them by a day. + const year = date.getUTCFullYear(); + const month = String(date.getUTCMonth() + 1).padStart(2, "0"); + const day = String(date.getUTCDate()).padStart(2, "0"); if (format === "DD.MM.YYYY") { return `${day}.${month}.${year}`; diff --git a/backend/src/utils/ubl.ts b/backend/src/utils/ubl.ts index 85a2e3e..9bc0dbe 100644 --- a/backend/src/utils/ubl.ts +++ b/backend/src/utils/ubl.ts @@ -24,9 +24,9 @@ function xmlEscape(v: Maybe): string { function fmtDate(d?: Date): string | undefined { if (!d) return undefined; - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, "0"); - const dd = String(d.getDate()).padStart(2, "0"); + const yyyy = d.getUTCFullYear(); + const mm = String(d.getUTCMonth() + 1).padStart(2, "0"); + const dd = String(d.getUTCDate()).padStart(2, "0"); return `${yyyy}-${mm}-${dd}`; } From 7908e4fa67d2561df3680ad4b5a88d7e56d01126 Mon Sep 17 00:00:00 2001 From: Bouni Date: Fri, 9 Oct 2026 15:32:30 +0200 Subject: [PATCH 3/5] Show the correct issue date on the invoice page (#149) The invoice page read the UTC-midnight issue/due dates in the browser's local time, showing the previous day west of UTC. Add a shared formatDate() helper that reads date-only values in UTC and honours the Date Format setting, and use it on the invoice page. The overdue badge gets the same UTC treatment; history timestamps stay in local time. Co-Authored-By: Claude Opus 5.5 --- frontend/src/lib/utils/date.test.ts | 24 +++++++++++++++++++ frontend/src/lib/utils/date.ts | 23 ++++++++++++++++++ .../src/routes/invoices/[id]/+page.svelte | 18 ++++---------- 3 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 frontend/src/lib/utils/date.test.ts create mode 100644 frontend/src/lib/utils/date.ts diff --git a/frontend/src/lib/utils/date.test.ts b/frontend/src/lib/utils/date.test.ts new file mode 100644 index 0000000..dc62b9f --- /dev/null +++ b/frontend/src/lib/utils/date.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { formatDate } from "./date"; + +describe("formatDate", () => { + const utcMidnight = "2026-10-09T00:00:00.000Z"; + + test("YYYY-MM-DD", () => { + expect(formatDate(utcMidnight, "YYYY-MM-DD")).toBe("2026-10-09"); + }); + + test("DD.MM.YYYY", () => { + expect(formatDate(utcMidnight, "DD.MM.YYYY")).toBe("09.10.2026"); + }); + + test("defaults to YYYY-MM-DD", () => { + expect(formatDate(new Date(utcMidnight))).toBe("2026-10-09"); + }); + + test("empty and invalid input", () => { + expect(formatDate(undefined)).toBe(""); + expect(formatDate(null)).toBe(""); + expect(formatDate("not a date")).toBe(""); + }); +}); diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts new file mode 100644 index 0000000..2d8ffe6 --- /dev/null +++ b/frontend/src/lib/utils/date.ts @@ -0,0 +1,23 @@ +/** + * Format a date using the "Date Format" setting (Settings → Localization). + * + * Invoice issue/due dates are stored as UTC midnight, so by default the UTC + * parts are used; otherwise browsers west of UTC would show the previous day. + * Pass `utc = false` for real timestamps (e.g. updatedAt) to show local time. + */ +export function formatDate( + d: string | Date | null | undefined, + format: string = "YYYY-MM-DD", + utc = true, +): string { + if (!d) return ""; + const dt = typeof d === "string" ? new Date(d) : d; + if (Number.isNaN(dt.getTime())) return ""; + const year = utc ? dt.getUTCFullYear() : dt.getFullYear(); + const month = String((utc ? dt.getUTCMonth() : dt.getMonth()) + 1).padStart(2, "0"); + const day = String(utc ? dt.getUTCDate() : dt.getDate()).padStart(2, "0"); + if (format === "DD.MM.YYYY") { + return `${day}.${month}.${year}`; + } + return `${year}-${month}-${day}`; +} diff --git a/frontend/src/routes/invoices/[id]/+page.svelte b/frontend/src/routes/invoices/[id]/+page.svelte index 7539983..091fed7 100644 --- a/frontend/src/routes/invoices/[id]/+page.svelte +++ b/frontend/src/routes/invoices/[id]/+page.svelte @@ -8,6 +8,7 @@ import { hasPermission } from "$lib/types"; import { formatPostalCityLine } from "$lib/address"; + import { formatDate } from "$lib/utils/date"; let { data, form } = $props(); let t = getContext("i18n") as (key: string) => string; @@ -23,7 +24,7 @@ const due = invoice.dueDate ? new Date(invoice.dueDate as string) : null; if (!due) return false; const today = new Date(); - const dueDateObj = new Date(due.getFullYear(), due.getMonth(), due.getDate()); + const dueDateObj = new Date(due.getUTCFullYear(), due.getUTCMonth(), due.getUTCDate()); const todayObj = new Date(today.getFullYear(), today.getMonth(), today.getDate()); return dueDateObj.valueOf() < todayObj.valueOf(); }); @@ -61,17 +62,8 @@ emailDialog?.showModal(); } - function fmtDate(d?: string | Date) { - if (!d) return ""; - const dt = typeof d === "string" ? new Date(d) : d; - if (Number.isNaN(dt.getTime())) return ""; - const year = dt.getFullYear(); - const month = String(dt.getMonth() + 1).padStart(2, "0"); - const day = String(dt.getDate()).padStart(2, "0"); - if (getLoc()?.dateFormat === "DD.MM.YYYY") { - return `${day}.${month}.${year}`; - } - return `${year}-${month}-${day}`; + function fmtDate(d?: string | Date, utc = true) { + return formatDate(d, getLoc()?.dateFormat, utc); } function fmtMoney(v?: number) { @@ -80,7 +72,7 @@ function fmtDateTime(d: Date) { if (!d || Number.isNaN(d.getTime())) return ""; - const date = fmtDate(d); + const date = fmtDate(d, false); const h = String(d.getHours()).padStart(2, "0"); const m = String(d.getMinutes()).padStart(2, "0"); return `${date} ${h}:${m}`; From 80739e488851565f83e036491190b693e00085d8 Mon Sep 17 00:00:00 2001 From: Bouni Date: Fri, 9 Oct 2026 15:32:30 +0200 Subject: [PATCH 4/5] Use the Date Format setting in invoice list and dashboard (#149) Both views hard-coded toLocaleDateString() with a short month name, ignoring Settings > Localization > Date Format, and were affected by the same off-by-one. Use the shared formatDate() helper instead. Co-Authored-By: Claude Opus 5.5 --- frontend/src/routes/dashboard/+page.svelte | 5 +++-- frontend/src/routes/invoices/+page.svelte | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frontend/src/routes/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 21e6919..b75140e 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -1,13 +1,14 @@