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:
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user