Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion TRANSLATIONS.md
Original file line number Diff line number Diff line change
@@ -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**

Expand Down
6 changes: 3 additions & 3 deletions backend/src/controllers/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions backend/src/utils/facturx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ function xmlEscape(v: Maybe<string | number | boolean>): 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}`;
}

Expand Down
6 changes: 3 additions & 3 deletions backend/src/utils/fatturapa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ function xmlEscape(v: Maybe<string | number | boolean>): 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}`;
}

Expand Down
8 changes: 5 additions & 3 deletions backend/src/utils/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand Down
6 changes: 3 additions & 3 deletions backend/src/utils/ubl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ function xmlEscape(v: Maybe<string | number | boolean>): 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}`;
}

Expand Down
6 changes: 5 additions & 1 deletion frontend/src/lib/components/InvoiceEditor.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/lib/utils/date.test.ts
Original file line number Diff line number Diff line change
@@ -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("");
});
});
23 changes: 23 additions & 0 deletions frontend/src/lib/utils/date.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
5 changes: 3 additions & 2 deletions frontend/src/routes/dashboard/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
<script lang="ts">
import { ShieldOff } from "lucide-svelte";
import { getContext } from "svelte";
import { formatDate } from "$lib/utils/date";
import { invoiceStatusLabel } from "$lib/i18n/mod";

let { data } = $props();

let t = getContext("i18n") as (key: string) => string;
let numberFormat = $derived(data.localization?.numberFormat || "comma");
let dateLocale = $derived(data.localization?.locale || "en");
let dateFormat = $derived(data.localization?.dateFormat || "YYYY-MM-DD");
let statusCounts = $derived((data.status || {}) as Record<string, number>);
let user = $derived(data.user);
let canViewInvoices = $derived(user?.isAdmin || user?.permissions?.some((p) => p.resource === "invoices" && p.action === "read"));
Expand Down Expand Up @@ -225,7 +226,7 @@
</td>
<td class="text-right text-sm tabular-nums">
{#if inv.issueDate}
{new Date(inv.issueDate).toLocaleDateString(dateLocale, { year: "numeric", month: "short", day: "numeric" })}
{formatDate(inv.issueDate, dateFormat)}
{/if}
</td>
</tr>
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/routes/invoices/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import { ShieldOff, SquarePen } from "lucide-svelte";
import { getContext } from "svelte";
import { formatDate } from "$lib/utils/date";

let { data } = $props();

let t = getContext("i18n") as (key: string) => string;
let numberFormat = $derived(data.localization?.numberFormat || "comma");
let dateLocale = $derived(data.localization?.locale || "en");
let dateFormat = $derived(data.localization?.dateFormat || "YYYY-MM-DD");
let user = $derived(data.user);
let canCreate = $derived(user?.isAdmin || user?.permissions?.some((p) => p.resource === "invoices" && p.action === "create"));
let canViewCustomers = $derived(user?.isAdmin || user?.permissions?.some((p) => p.resource === "customers" && p.action === "read"));
Expand Down Expand Up @@ -177,7 +178,7 @@
<a href={`/invoices/${inv.id}`}>{inv.invoiceNumber}</a>
<div class="text-xs opacity-70 sm:hidden">
{#if inv.issueDate}
{new Date(inv.issueDate).toLocaleDateString(dateLocale, { year: "numeric", month: "short", day: "numeric" })}
{formatDate(inv.issueDate, dateFormat)}
{/if}
</div>
</td>
Expand Down Expand Up @@ -206,7 +207,7 @@
</td>
<td class="hidden text-sm tabular-nums sm:table-cell">
{#if inv.issueDate}
{new Date(inv.issueDate).toLocaleDateString(dateLocale, { year: "numeric", month: "short", day: "numeric" })}
{formatDate(inv.issueDate, dateFormat)}
{/if}
</td>
{#if showPaidWith}
Expand All @@ -216,7 +217,7 @@
{/if}
<td class="hidden text-right text-sm tabular-nums opacity-70 md:table-cell">
{#if inv.updatedAt}
{new Date(inv.updatedAt).toLocaleDateString(dateLocale, { year: "numeric", month: "short", day: "numeric" })}
{formatDate(inv.updatedAt, dateFormat, false)}
{/if}
</td>
</tr>
Expand Down
18 changes: 5 additions & 13 deletions frontend/src/routes/invoices/[id]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
});
Expand Down Expand Up @@ -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) {
Expand All @@ -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}`;
Expand Down
Loading