feat(web): one date standard across the UI — "25 Qer 14:30"

Dates were a mix: catalog-formatted "25 Qershor 20:01" where screens used
formatRelativeDateTime, and browser-locale "7/6/2026, 9:34 AM" in ~20
places that called raw toLocaleString/-Date-/-Time-String. Unified:

- common.monthsShort in both catalogs (Jan/Shk/…/Qer/Korr/…/Dhj);
  formatDate ("25 Qer", year only when not current), formatDateTime
  ("25 Qer 14:30", optional seconds), formatClock ("HH:mm", 24h) in
  lib/format.ts. formatRelativeDateTime keeps Sot/Dje and switches its
  older-dates branch to the same short months.
- Every raw toLocale* DATE call swept: shifts X-report line, plan
  effective dates, sub version labels, drawer today feed, snapshot
  tooltips, device footer checkedAt, event-detail timestamp (keeps
  seconds — chain evidence), tariff composer active-since + version
  sidebar. Number toLocaleString (thousand separators) untouched.

The catalogs in this commit also carry the keys for the two follow-up
commits (fee breakdown, composer increment labels).

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-07-06 15:41:40 +02:00
parent 6cf3492bff
commit 5e1a885dcb
11 changed files with 96 additions and 22 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useQuery } from "@tanstack/react-query";
import { fetchDeviceStatus, type DeviceStatus } from "../api.js";
import { formatClock } from "../lib/format.js";
import { qk } from "../lib/query.js";
import { useLiveStore } from "../lib/live-store.js";
@@ -184,7 +185,7 @@ export function DeviceFooter() {
</div>
{d.detail && <div className="mt-0.5 break-words text-[0.6875rem] text-term-muted">{d.detail}</div>}
<div className="mt-0.5 text-[0.625rem] tabular-nums text-term-muted/70">
{t("devices.checkedAt", { time: new Date(d.checkedAt).toLocaleTimeString() })}
{t("devices.checkedAt", { time: formatClock(d.checkedAt) })}
</div>
</div>
</li>
+4 -3
View File
@@ -1,5 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { formatDateTime } from "../lib/format.js";
import { useQuery } from "@tanstack/react-query";
import { fetchSnapshots, snapshotImageUrl, type PlateRead } from "../api.js";
@@ -52,7 +53,7 @@ export function SnapshotStrip({ identity }: { identity: string }) {
key={`${p.plate}-${p.direction}-${i}`}
className="inline-flex items-center gap-1.5 rounded-term border border-term-cyan/40 bg-term-cyan/10 px-2 py-0.5 text-[0.6875rem]"
title={`${dirLabel(p.direction)}${p.region ? ` · ${p.region}` : ""}${
p.at ? ` · ${new Date(p.at).toLocaleString()}` : ""
p.at ? ` · ${formatDateTime(p.at, t)}` : ""
}`}
>
<span className="text-[0.5625rem] uppercase tracking-wider text-term-muted">{t("pay.plate")}</span>
@@ -72,7 +73,7 @@ export function SnapshotStrip({ identity }: { identity: string }) {
type="button"
onClick={() => setZoom(s.id)}
className="group flex flex-col items-center gap-1 rounded-term border border-term-border bg-term-panel-2 p-1 hover:border-term-amber"
title={`${dirLabel(s.direction)} · ${new Date(s.capturedAt).toLocaleString()}`}
title={`${dirLabel(s.direction)} · ${formatDateTime(s.capturedAt, t)}`}
>
<img
src={snapshotImageUrl(s.id)}
@@ -97,7 +98,7 @@ export function SnapshotStrip({ identity }: { identity: string }) {
<div
key={`fail-${f.direction ?? "both"}-${i}`}
className="flex h-[6.75rem] w-28 flex-col items-center justify-center gap-1 rounded-term border border-dashed border-term-amber/60 bg-term-amber/5 p-1 text-center"
title={`${dirLabel(f.direction)} · ${f.error}${f.occurredAt ? ` · ${new Date(f.occurredAt).toLocaleString()}` : ""}`}
title={`${dirLabel(f.direction)} · ${f.error}${f.occurredAt ? ` · ${formatDateTime(f.occurredAt, t)}` : ""}`}
>
<span className="text-lg leading-none text-term-amber">⚠</span>
<span className="text-[0.5625rem] uppercase tracking-wider text-term-amber">{dirLabel(f.direction)}</span>
+2 -2
View File
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next";
import { type ReactNode } from "react";
import { type LedgerEvent } from "../api.js";
import { formatMoney } from "../lib/format.js";
import { formatMoney, formatDateTime } from "../lib/format.js";
import { renderReason } from "../lib/reason.js";
import { Modal } from "./Modal.js";
import { SnapshotStrip } from "./SnapshotStrip.js";
@@ -226,7 +226,7 @@ export function EventDetailModal({ e, onClose }: { e: LedgerEvent; onClose: () =
{/* Humanized fields — labelled rows, not raw JSON. Only what applies renders. */}
<div>
<DetailRow label={t("booth.edTime")}>{new Date(e.occurredAt).toLocaleString()}</DetailRow>
<DetailRow label={t("booth.edTime")}>{formatDateTime(e.occurredAt, t, { seconds: true })}</DetailRow>
<DetailRow label={t("booth.edIndex")}>#{e.index}</DetailRow>
{e.direction && <DetailRow label={t("booth.edDirection")}>{e.direction}</DetailRow>}
{e.source && <DetailRow label={t("booth.edSource")}>{e.source}</DetailRow>}