diff --git a/apps/web/src/BoothPayModal.tsx b/apps/web/src/BoothPayModal.tsx index a7d6737..4a27a95 100644 --- a/apps/web/src/BoothPayModal.tsx +++ b/apps/web/src/BoothPayModal.tsx @@ -18,7 +18,7 @@ import { import { rootRoute } from "./router.js"; import { qk } from "./lib/query.js"; import { useShift } from "./lib/use-shift.js"; -import { formatDuration, formatMoney, formatTime, formatRelativeDateTime } from "./lib/format.js"; +import { formatDuration, formatMoney, formatRelativeDateTime } from "./lib/format.js"; import { CARD_PAYMENTS_ENABLED } from "./lib/features.js"; import { SnapshotStrip } from "./ui/SnapshotStrip.js"; import { Spinner } from "./ui/Spinner.js"; @@ -310,12 +310,12 @@ export function BoothPayModal({ identity, onClose }: { identity: string; onClose // figures, and the snapshot strip read-only. No tender / voucher / open here. <>
- {t("pay.alreadyClosed", { time: formatTime(s.exitedAt) })} + {t("pay.alreadyClosed", { time: formatRelativeDateTime(s.exitedAt, t, { seconds: true }) })}
- - + + {/* Session figures */}
- + {/* Closed-within-grace shows the recorded EXIT; an open session shows now. */} { }); }); -describe("formatTime", () => { - it("returns an em dash for null/invalid", () => { - expect(formatTime(null)).toBe("—"); - expect(formatTime("not-a-date")).toBe("—"); - }); - it("renders HH:MM:SS local time", () => { - expect(formatTime("2026-06-21T10:48:25.000Z")).toMatch(/^\d{2}:\d{2}:\d{2}$/); - }); -}); - describe("formatRelativeDateTime", () => { // A tiny fake t(): today/yesterday words + the month-name array. const months = ["Jan","Shkurt","Mars","Prill","Maj","Qershor","Korrik","Gusht","Sht","Tet","Nën","Dhj"]; @@ -61,6 +51,12 @@ describe("formatRelativeDateTime", () => { expect(formatRelativeDateTime(now.toISOString(), t)).toMatch(/^Sot \d{2}:\d{2}$/); }); + it("appends :ss with the seconds option (entry/exit rows read alike)", () => { + const now = new Date(); + now.setHours(19, 25, 44, 0); + expect(formatRelativeDateTime(now.toISOString(), t, { seconds: true })).toMatch(/^Sot \d{2}:\d{2}:44$/); + }); + it("labels yesterday with the localized word", () => { const y = new Date(); y.setDate(y.getDate() - 1); diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 1968160..8a01325 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -46,13 +46,6 @@ export function formatMinutes(mins: number): string { return h > 0 ? `${h}h ${m % 60}m` : `${m}m`; } -/** Local time-of-day HH:MM:SS from an ISO string. */ -export function formatTime(iso: string | null): string { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "—" : d.toTimeString().slice(0, 8); -} - /** Calendar-day difference (local) between two dates: 0 = same day, 1 = d is one day * before ref, etc. Compares date parts only (ignores time-of-day). */ function dayDiff(d: Date, ref: Date): number { @@ -61,10 +54,11 @@ function dayDiff(d: Date, ref: Date): number { return Math.round((b.getTime() - a.getTime()) / 86_400_000); } -/** HH:MM (local, 24h) for the relative-day labels. */ -function hhmm(d: Date): string { +/** HH:MM (local, 24h) for the relative-day labels; ":ss" appended when `seconds`. */ +function hhmm(d: Date, seconds = false): string { const p = (n: number) => String(n).padStart(2, "0"); - return `${p(d.getHours())}:${p(d.getMinutes())}`; + const base = `${p(d.getHours())}:${p(d.getMinutes())}`; + return seconds ? `${base}:${p(d.getSeconds())}` : base; } /** Minimal shape of i18next's `t` that we rely on: a string lookup, plus the @@ -118,8 +112,7 @@ export function formatDateTime(iso: string | null, t: TFn, opts?: { seconds?: bo if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "—"; - const sec = opts?.seconds ? `:${String(d.getSeconds()).padStart(2, "0")}` : ""; - return `${formatDate(iso, t)} ${hhmm(d)}${sec}`; + return `${formatDate(iso, t)} ${hhmm(d, opts?.seconds)}`; } /** @@ -130,14 +123,18 @@ export function formatDateTime(iso: string | null, t: TFn, opts?: { seconds?: bo * * `t` supplies the today/yesterday words AND the month names (the appliance browser * may lack Albanian Intl data, so month names come from the catalog, not Intl). + * + * `seconds` appends ":ss" — use it where a timestamp sits next to another that shows + * seconds (e.g. the booth pay modal's entry vs. exit rows), so the two read alike. */ -export function formatRelativeDateTime(iso: string | null, t: TFn): string { +export function formatRelativeDateTime(iso: string | null, t: TFn, opts?: { seconds?: boolean }): string { if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "—"; + const time = hhmm(d, opts?.seconds); const diff = dayDiff(d, new Date()); - if (diff === 0) return `${t("common.today")} ${hhmm(d)}`; - if (diff === 1) return `${t("common.yesterday")} ${hhmm(d)}`; + if (diff === 0) return `${t("common.today")} ${time}`; + if (diff === 1) return `${t("common.yesterday")} ${time}`; // Older (or future): "17 Qer 10:48" — the short-month standard, year only if it differs. - return `${formatDate(iso, t)} ${hhmm(d)}`; + return `${formatDate(iso, t)} ${time}`; }