fix(web): booth UI/UX pass — readable font scaling + booth layout/report clarity
Build desktop / desktop (push) Successful in 4m12s
Build & push images / images (push) Successful in 2m42s
CI / check (push) Successful in 37s

A round of operator-facing fixes on the booth screen, shift views, and the
font-scale control. (Follows the font-scale feature in f706726, which used CSS
`zoom` — reverted here for the rem approach below.)

Font scaling (the A−/A+ control now actually works without breaking layout):
- The control scaled via CSS `zoom`, which also scaled viewport-locked containers
  (h-screen frame, max-h-[90vh] modals) so at 130% modal headers/footers were
  pushed off-screen. Reworked to scale TEXT only: converted every `text-[Npx]`
  font utility to rem across the web app (~230 sites in 25 files + the
  .label/.hint/.btn component classes + body in index.css; 16px root, so 100% is
  visually identical), and applyFontScale now sets the ROOT font-size. vh/h-screen
  layout stays put, so chrome never clips; tall content scrolls its own container.
  Verified at 130%: text 12px→15.6px while the frame stayed viewport-height.

Live feed (event rows):
- Plate, badges and reason now flow inline after the identity and wrap only when
  the row runs out of width — no more forced second line when there's empty space.
- Dropped the redundant TARGË via-badge (the plate chip already conveys it).
- Removed the Direction filter group (Hyrje/Dalje) — it duplicated the entry/exit
  options already in the Type filter.

Active sessions:
- Rebuilt as a real table (Ticket/subscriber · Plate · Entry · Elapsed) so columns
  align and long values (subscriber names, ticket ids) no longer truncate.
- Dropped the status column (an unpaid transient is normal; a subscriber shows ★ +
  name; overstay keeps a row tint). Removed the now-redundant status filter; only
  the Transient/Subscriber filter remains. Plate is now searchable (uses s.plate).

Shift report (close-shift modal + Shift History + printed Z-report slip):
- Removed the confusing `shitje` (subscription-sales) sub-line — Abonime is the
  total; only the out-of-window part is broken out. subscriptionSalesMinor stays in
  the signed payload (audit data), just not displayed/printed.
- Show the inherited opening cash ("Arka fillestare") above the expected drawer, so
  opening + cash-taken = expected reads clearly. Money values no longer line-wrap.

Subscription edit modal:
- Fixed the 2-col grid alignment: a lone "only one version" cell was shifting every
  following row by one column — it now emits a full label+value pair.

Removed orphaned i18n keys (fStatus*, fDir*, srcSubSales) from sq+en (parity kept).
Full workspace build/lint/test green.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-28 15:15:09 +02:00
parent f706726eeb
commit cce99aadfd
31 changed files with 410 additions and 401 deletions
+24 -16
View File
@@ -412,7 +412,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
<h2 className="mb-3 text-h4 font-semibold text-term-text">{t("subs.title")}</h2>
<ul className="mb-3 list-none p-0">
{subs.map((s) => (
<li key={s.id} className="flex flex-wrap items-center gap-2 border-b border-term-border/60 py-2 text-[12px]">
<li key={s.id} className="flex flex-wrap items-center gap-2 border-b border-term-border/60 py-2 text-[0.75rem]">
<strong className="text-term-text">{s.holderName ?? t("subs.unnamed")}</strong>
<span className={s.status === "active" ? "text-term-green" : "text-term-amber"}>{t(STATUS_KEY[s.status])}</span>
<span className="tabular-nums text-term-cyan">{priceLabel(s, t)}</span>
@@ -464,13 +464,13 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
</option>
))}
</select>
{plans.length === 0 && <span className="text-[12px] text-term-amber">{t("subs.planNoneAvail")}</span>}
{plans.length === 0 && <span className="text-[0.75rem] text-term-amber">{t("subs.planNoneAvail")}</span>}
</span>
</>
) : (
<>
<label className="label">{t("subs.plan")}</label>
<span className="text-[13px] text-term-text">{form.planId || t("subs.noPrice")}</span>
<span className="text-[0.8125rem] text-term-text">{form.planId || t("subs.noPrice")}</span>
{/* VERSION CORRECTION (admins). The plan itself is frozen, but an admin may
move the sub to a different VERSION of that same plan (e.g. one with
different timeframes). Price stays as billed. Only shown when the sub has
@@ -486,7 +486,15 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
if (cur) versions.unshift(cur);
}
if (versions.length < 2 && versions.some((v) => v.id === form.planVersionId)) {
return <span className="text-[12px] text-term-muted">{t("subs.versionOnlyOne")}</span>;
// Keep the 2-col grid flow intact: a lone cell here would shift every
// following row by one column (label↔input swap). Emit a full row —
// the version label + the "only one version" hint as its control.
return (
<>
<label className="label">{t("subs.version")}</label>
<span className="text-[0.75rem] text-term-muted">{t("subs.versionOnlyOne")}</span>
</>
);
}
return (
<>
@@ -504,7 +512,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
</option>
))}
</select>
<span className="text-[12px] text-term-muted">{t("subs.versionHint")}</span>
<span className="text-[0.75rem] text-term-muted">{t("subs.versionHint")}</span>
</span>
</>
);
@@ -523,7 +531,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
inputMode="numeric"
onChange={(e) => setForm((f) => ({ ...f, quantity: e.target.value, maxConcurrent: e.target.value }))}
/>
<span className="text-[12px] text-term-muted">{t("subs.quantityHint")}</span>
<span className="text-[0.75rem] text-term-muted">{t("subs.quantityHint")}</span>
</span>
</>
)}
@@ -533,7 +541,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
<>
<label className="label">{t("subs.tender")}</label>
<span className="flex items-center gap-3">
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-text">
<label className="inline-flex items-center gap-1.5 text-[0.75rem] text-term-text">
<input
type="radio"
name="tender"
@@ -543,7 +551,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
/>
{t("subs.tenderCash")}
</label>
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-text">
<label className="inline-flex items-center gap-1.5 text-[0.75rem] text-term-text">
<input
type="radio"
name="tender"
@@ -553,13 +561,13 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
/>
{t("subs.tenderCard")}
</label>
<span className="text-[12px] text-term-muted">{t("subs.tenderHint")}</span>
<span className="text-[0.75rem] text-term-muted">{t("subs.tenderHint")}</span>
</span>
</>
)}
<label className="label">{t("subs.carLimit")}</label>
<span className="flex items-center gap-3">
<label className="inline-flex items-center gap-1.5 text-[12px] text-term-text">
<label className="inline-flex items-center gap-1.5 text-[0.75rem] text-term-text">
<input type="checkbox" className="accent-term-amber" checked={form.carBound} onChange={(e) => setForm((f) => ({ ...f, carBound: e.target.checked }))} /> {t("subs.limitCarsInAtOnce")}
</label>
{form.carBound && (
@@ -580,7 +588,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
inputMode="numeric"
onChange={(e) => setForm((f) => ({ ...f, count: e.target.value }))}
/>
<span className="text-[12px] text-term-muted">
<span className="text-[0.75rem] text-term-muted">
× {t(PERIOD_KEY[selectedPlan.period])}
</span>
</span>
@@ -592,7 +600,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
{/* Live SERVER quote: ceil(periods) × per-period price. The operator can't
override it — this is exactly what will be charged + signed. */}
{editing === "new" && form.planId.trim() !== "" && (
<span className="text-[12px] text-term-cyan">
<span className="text-[0.75rem] text-term-cyan">
{quoting
? t("subs.quoting")
: quote
@@ -610,7 +618,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
<input className="input" value={form.platesText} onChange={(e) => setForm((f) => ({ ...f, platesText: e.target.value }))} placeholder={t("subs.commaSeparatedOptional")} />
</div>
<h4 className="mt-4 mb-1 text-[12px] font-semibold uppercase tracking-wider text-term-muted">{t("subs.credentials")}</h4>
<h4 className="mt-4 mb-1 text-[0.75rem] font-semibold uppercase tracking-wider text-term-muted">{t("subs.credentials")}</h4>
{form.credentials.map((c, i) => (
<div key={i} className="mb-1.5 flex items-center gap-2">
{/* Operator chooses the credential type: QR (auto-generated) or RFID
@@ -625,7 +633,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
c.value.trim() ? (
<input className="input input-sm flex-1 opacity-70" value={c.value} readOnly />
) : (
<span className="flex-1 self-center text-[12px] italic text-term-muted">{t("subs.qrAutoGen")}</span>
<span className="flex-1 self-center text-[0.75rem] italic text-term-muted">{t("subs.qrAutoGen")}</span>
)
) : (
// RFID: the value is read off a physical card (or typed). "Read card"
@@ -643,7 +651,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
{/* Capture panel: pick a reader, present the card; the captured value fills
the credential. The OTHER reader keeps serving the live flow. */}
{capture && (
<div className="mt-3 rounded-term border border-term-cyan/50 bg-term-cyan/5 p-3 text-[12px]">
<div className="mt-3 rounded-term border border-term-cyan/50 bg-term-cyan/5 p-3 text-[0.75rem]">
{capture.phase === "pick" ? (
<>
<div className="mb-1.5 text-term-text">{t("subs.captureChooseReader")}</div>
@@ -673,7 +681,7 @@ export function SubscriptionManager({ user }: { user: SessionUser | null }) {
<button type="button" className="btn btn-sm" onClick={() => setEditing(null)}>{t("subs.cancel")}</button>
</div>
</Modal>
{msg && <p className={msg.kind === "ok" ? "mt-3 text-[12px] text-term-green" : "mt-3 text-[12px] text-term-red"}>{msg.text}</p>}
{msg && <p className={msg.kind === "ok" ? "mt-3 text-[0.75rem] text-term-green" : "mt-3 text-[0.75rem] text-term-red"}>{msg.text}</p>}
</section>
);
}