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
+38 -6
View File
@@ -215,13 +215,43 @@ function duration(fromIso: string, toIso: string): string {
return h > 0 ? `${h}h ${m}m` : `${m}m`;
}
/** Local date+time "YYYY-MM-DD HH:MM" for a receipt row. The host clock is the
* site's local time (the appliance runs in the site's zone). */
function stamp(iso: string): string {
/** Albanian month names (customer-facing receipts are always Albanian — see
* i18n.md). Indexed by Date.getMonth() (0 = Janar). */
const SQ_MONTHS = [
"Janar",
"Shkurt",
"Mars",
"Prill",
"Maj",
"Qershor",
"Korrik",
"Gusht",
"Shtator",
"Tetor",
"Nëntor",
"Dhjetor",
] as const;
/** Human local date+time for a receipt row, e.g. "19 Qershor 2026 10:48:25". The
* host clock is the site's local time (the appliance runs in the site's zone);
* 24-hour with seconds (Albania uses 24h). Falls back to the raw ISO on a bad date.
* Exported (as formatStampSq) so other server-side printed output — e.g. the shift
* Z-report — shares one Albanian date format. */
export function stamp(iso: string): string {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
const p = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
const date = `${d.getDate()} ${SQ_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
const time = `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
return `${date} ${time}`;
}
/** Date-only Albanian format "19 Qershor 2026" (for subscription validity dates,
* which are date strings with no time). Passes through a non-date value unchanged. */
function dateOnly(value: string): string {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
return `${d.getDate()} ${SQ_MONTHS[d.getMonth()]} ${d.getFullYear()}`;
}
/** Build the ESC/POS byte stream for a free-form text report (e.g. shift Z-report). */
@@ -284,7 +314,7 @@ export function renderTicket(data: TicketData): Buffer {
DOUBLE_OFF,
BOLD_OFF,
line(),
line(STR.issuedAt(data.issuedAt)),
line(STR.issuedAt(stamp(data.issuedAt))),
FEED_AND_CUT,
]);
}
@@ -312,7 +342,9 @@ export function renderSubscriptionCard(data: SubscriptionCardData): Buffer {
];
if (data.holderName) parts.push(line(STR.holder(data.holderName)));
if (data.validFrom || data.validTo) {
parts.push(line(STR.validity(data.validFrom ?? "—", data.validTo ?? "—")));
parts.push(
line(STR.validity(data.validFrom ? dateOnly(data.validFrom) : "—", data.validTo ? dateOnly(data.validTo) : "—")),
);
}
parts.push(FEED_AND_CUT);
return Buffer.concat(parts);
+3
View File
@@ -21,6 +21,9 @@ export {
cashinoDriver,
} from "./drivers/index.js";
export { isPrinter, type PrinterRole } from "./drivers/printer-rongta.js";
// Albanian human date/time for printed slips (receipts, tickets, shift Z-report),
// kept in one place so all printed output formats dates identically.
export { stamp as formatStampSq } from "./drivers/printer-escpos.js";
export {
orderForRole,
printWithFailover,