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
+15 -13
View File
@@ -1,5 +1,5 @@
import { eq, devices, ledgerEvents, type Db } from "@parking/db";
import { registry, type PrinterDevice } from "@parking/devices";
import { registry, formatStampSq as zStamp, type PrinterDevice } from "@parking/devices";
import type { LedgerPayload } from "@parking/shared";
import type { FastifyBaseLogger } from "fastify";
import type { EventLog } from "./event-log.js";
@@ -393,21 +393,23 @@ export class ShiftService {
}
const cur = r.currency ?? "";
const money = (m: number) => (m / 100).toFixed(2);
// Customer/operator-facing print is Albanian (see i18n.md — printed slips are not
// governed by the UI language), with human dates "19 Qershor 2026 10:48:25".
const lines = [
`Operator: ${r.operator}`,
`From: ${r.startedAt}`,
`To: ${r.endedAt}`,
`Operatori: ${r.operator}`,
`Nga: ${zStamp(r.startedAt)}`,
`Deri: ${zStamp(r.endedAt)}`,
"",
`Payments: ${r.paymentCount}`,
`Cash: ${money(r.cashTotalMinor)} ${cur}`,
`Card: ${money(r.cardTotalMinor)} ${cur}`,
`Pagesa: ${r.paymentCount}`,
`Para në dorë: ${money(r.cashTotalMinor)} ${cur}`,
`Kartë: ${money(r.cardTotalMinor)} ${cur}`,
"",
"-- Drawer --",
`Opening float: ${money(r.openingFloatMinor)} ${cur}`,
`Cash taken: ${money(r.cashTotalMinor)} ${cur}`,
`Cash added: ${money(r.cashAddedMinor)} ${cur}`,
`Cash removed: ${money(r.cashRemovedMinor)} ${cur}`,
`Expected drawer: ${money(r.expectedDrawerMinor)} ${cur}`,
"-- Arka --",
`Fillimi (kusur): ${money(r.openingFloatMinor)} ${cur}`,
`Para të marra: ${money(r.cashTotalMinor)} ${cur}`,
`Para të shtuara: ${money(r.cashAddedMinor)} ${cur}`,
`Para të hequra: ${money(r.cashRemovedMinor)} ${cur}`,
`Arka e pritur: ${money(r.expectedDrawerMinor)} ${cur}`,
];
try {
await printer.printReport({ title: "RAPORT TURNI", lines });