diff --git a/TRANSLATIONS.md b/TRANSLATIONS.md index 6b60ea4d..2fa4fd6e 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** diff --git a/backend/src/controllers/invoices.ts b/backend/src/controllers/invoices.ts index 9306dd86..b590d3ce 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 afbed9cd..7c6d4382 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 13cd9192..d20725ca 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 056bb965..c8d882ba 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 85a2e3e5..9bc0dbea 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}`; } diff --git a/frontend/src/lib/components/InvoiceEditor.svelte b/frontend/src/lib/components/InvoiceEditor.svelte index 3528718f..cd7c2ee5 100644 --- a/frontend/src/lib/components/InvoiceEditor.svelte +++ b/frontend/src/lib/components/InvoiceEditor.svelte @@ -18,12 +18,16 @@ // an explicit override — see the customerId-aware preview effect below). let invoiceNumberTouched = $state(false); + // Today's date in the user's timezone (toISOString() would give the UTC date). + const now = new Date(); + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; + let form = $state({ customerId: initInvoice?.customerId || "", invoiceNumber: initInvoice?.invoiceNumber ?? initNextInvoiceNumber, currency: initInvoice?.currency || "EUR", status: initInvoice?.status || "draft", - issueDate: initInvoice?.issueDate ? new Date(initInvoice.issueDate).toISOString().slice(0, 10) : new Date().toISOString().slice(0, 10), + issueDate: initInvoice?.issueDate ? new Date(initInvoice.issueDate).toISOString().slice(0, 10) : today, dueDate: initInvoice?.dueDate ? new Date(initInvoice.dueDate).toISOString().slice(0, 10) : "", taxMode: initInvoice?.taxMode || "invoice", taxRate: initInvoice?.taxRate || 0, diff --git a/frontend/src/lib/utils/date.test.ts b/frontend/src/lib/utils/date.test.ts new file mode 100644 index 00000000..dc62b9fa --- /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 00000000..2d8ffe6e --- /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/dashboard/+page.svelte b/frontend/src/routes/dashboard/+page.svelte index 21e69198..b75140e6 100644 --- a/frontend/src/routes/dashboard/+page.svelte +++ b/frontend/src/routes/dashboard/+page.svelte @@ -1,13 +1,14 @@