feat: human + relative dates; fix language/theme toggle stale-context

Dates were raw ISO on printed slips and time-only in the UI (a session from
two days ago showed just "10:48"). Make them human and day-relative. Also fix
a latent toggle bug surfaced while testing.

Dates:
- Printed tickets/receipts/subscription cards now show "19 Qershor 2026
  10:48:25" (Albanian month, 24h with seconds) instead of YYYY-MM-DD HH:MM.
  stamp() exported as formatStampSq so the shift Z-report shares it.
- Shift Z-report is now Albanian (Operatori/Nga/Deri/Para në dorë/Arka…),
  was English-only with ISO dates.
- Web sessions/logs/history show relative days: "Sot 10:48" / "Dje 17:33" /
  "17 Qershor 10:48" via formatRelativeDateTime(). Month names come from the
  i18n catalog (common.months), NOT Intl — the appliance browser's ICU lacks
  Albanian locale data and Intl silently falls back to English month names.

Toggle fix:
- The language + theme toggles read the active value from the TanStack Router
  context `user`, which is captured at route-resolution time and does not
  re-render on setUser. After one switch the highlight froze and the equality
  guard blocked switching back until a page refresh. Drive them off live state
  instead: language from i18n.language (useTranslation subscribes to
  languageChanged), theme from local useState. (Bug dated to 040c0ff.)

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-19 11:14:06 +02:00
parent f31e57b4ae
commit 00f3d141b6
10 changed files with 175 additions and 51 deletions
+4 -16
View File
@@ -2,7 +2,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { fetchShifts, type ShiftSummary, type SessionUser } from "./api.js";
import { formatMoney, formatDuration } from "./lib/format.js";
import { formatMoney, formatDuration, formatRelativeDateTime } from "./lib/format.js";
// Completed shift history. Scope is enforced SERVER-SIDE by permission: an operator
// gets only their own shifts; an admin (shift:cash) gets all + a date/operator
@@ -10,19 +10,6 @@ import { formatMoney, formatDuration } from "./lib/format.js";
// reports scope:"all". Each row is one signed shift_z_report; expanding it shows the
// drawer reconciliation. See wiki/concepts/shift.md.
/** Local date + time (history spans days, so not just time-of-day). */
function fmtDateTime(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return "—";
return d.toLocaleString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
function money(minor: number, currency: string | null): string {
return currency ? formatMoney(minor, currency) : (minor / 100).toFixed(2);
}
@@ -138,6 +125,7 @@ function ShiftRow({ s, showOperator, colSpan }: { s: ShiftSummary; showOperator:
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const cur = s.currency;
const when = (iso: string) => formatRelativeDateTime(iso, t);
return (
<>
@@ -146,9 +134,9 @@ function ShiftRow({ s, showOperator, colSpan }: { s: ShiftSummary; showOperator:
onClick={() => setOpen((o) => !o)}
>
{showOperator && <td className="px-3 py-1.5 text-term-text">{s.operator}</td>}
<td className="px-3 py-1.5">{fmtDateTime(s.startedAt)}</td>
<td className="px-3 py-1.5">{when(s.startedAt)}</td>
<td className="px-3 py-1.5">
{fmtDateTime(s.endedAt)}
{when(s.endedAt)}
<span className="ml-2 text-term-muted">{formatDuration(s.startedAt, s.endedAt)}</span>
</td>
<td className="px-3 py-1.5 text-right">{s.paymentCount}</td>