fix(web): booth UI/UX pass — readable font scaling + booth layout/report clarity
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:
@@ -237,19 +237,19 @@ export function SubscriptionPlansManager() {
|
||||
return (
|
||||
<section className="px-4 py-6">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h3 className="text-[13px] font-semibold uppercase tracking-wider text-term-muted">{t("plans.title")}</h3>
|
||||
<h3 className="text-[0.8125rem] font-semibold uppercase tracking-wider text-term-muted">{t("plans.title")}</h3>
|
||||
<button type="button" className="btn btn-go btn-sm" onClick={() => setForm(emptyForm())}>
|
||||
{t("plans.add")}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-3 text-[12px] text-term-muted">{t("plans.intro")}</p>
|
||||
<p className="mb-3 text-[0.75rem] text-term-muted">{t("plans.intro")}</p>
|
||||
|
||||
{msg && (
|
||||
<div className={`mb-3 text-[12px] ${msg.kind === "ok" ? "text-term-green" : "text-term-red"}`}>{msg.text}</div>
|
||||
<div className={`mb-3 text-[0.75rem] ${msg.kind === "ok" ? "text-term-green" : "text-term-red"}`}>{msg.text}</div>
|
||||
)}
|
||||
|
||||
{groups.length === 0 ? (
|
||||
<p className="text-[13px] text-term-muted">{t("plans.noneYet")}</p>
|
||||
<p className="text-[0.8125rem] text-term-muted">{t("plans.noneYet")}</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{groups.map(({ planId, head: p, active, versions }) => {
|
||||
@@ -263,15 +263,15 @@ export function SubscriptionPlansManager() {
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span className="font-semibold text-term-text">{p.name}</span>
|
||||
{active ? (
|
||||
<span className="rounded border border-term-green px-1 text-[10px] text-term-green">{t("plans.inForce")}</span>
|
||||
<span className="rounded border border-term-green px-1 text-[0.625rem] text-term-green">{t("plans.inForce")}</span>
|
||||
) : (
|
||||
<span className="rounded border border-term-border px-1 text-[10px] text-term-muted">{t("plans.retired")}</span>
|
||||
<span className="rounded border border-term-border px-1 text-[0.625rem] text-term-muted">{t("plans.retired")}</span>
|
||||
)}
|
||||
{versions > 1 && <span className="text-[10px] text-term-muted">{t("plans.versionCount", { count: versions })}</span>}
|
||||
{versions > 1 && <span className="text-[0.625rem] text-term-muted">{t("plans.versionCount", { count: versions })}</span>}
|
||||
</div>
|
||||
|
||||
{/* Details: price · hours · effective */}
|
||||
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-[12px] text-term-muted">
|
||||
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-[0.75rem] text-term-muted">
|
||||
<span className="tabular-nums text-term-text">
|
||||
{(p.pricePerPeriodMinor / 100).toLocaleString()} {p.currency} / {t(PERIOD_KEY[p.period])}
|
||||
</span>
|
||||
@@ -280,7 +280,7 @@ export function SubscriptionPlansManager() {
|
||||
</div>
|
||||
|
||||
{/* Used by */}
|
||||
<div className="mt-1 text-[12px]">
|
||||
<div className="mt-1 text-[0.75rem]">
|
||||
{users.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -295,12 +295,12 @@ export function SubscriptionPlansManager() {
|
||||
)}
|
||||
</div>
|
||||
{isOpen && users.length > 0 && (
|
||||
<ul className="mt-1 flex flex-wrap gap-x-4 gap-y-1 rounded-term bg-term-bg px-3 py-2 text-[12px]">
|
||||
<ul className="mt-1 flex flex-wrap gap-x-4 gap-y-1 rounded-term bg-term-bg px-3 py-2 text-[0.75rem]">
|
||||
{users.map((s) => (
|
||||
<li key={s.id} className={s.status === "active" ? "text-term-text" : "text-term-muted"}>
|
||||
{s.holderName || t("subs.unnamed")}
|
||||
{s.quantity > 1 && <span className="text-term-muted"> ×{s.quantity}</span>}
|
||||
{s.status !== "active" && <span className="ml-1 text-[10px]">({t(STATUS_KEY[s.status])})</span>}
|
||||
{s.status !== "active" && <span className="ml-1 text-[0.625rem]">({t(STATUS_KEY[s.status])})</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -349,14 +349,14 @@ export function SubscriptionPlansManager() {
|
||||
<span className="flex items-center gap-2">
|
||||
<input className="input w-28" value={form.priceMajor} inputMode="decimal" onChange={(e) => setForm((f) => f && { ...f, priceMajor: e.target.value })} placeholder="e.g. 800" />
|
||||
<input className="input w-16" value={form.currency} onChange={(e) => setForm((f) => f && { ...f, currency: e.target.value })} />
|
||||
<span className="text-[12px] text-term-muted">/ {t(PERIOD_KEY[form.period])}</span>
|
||||
<span className="text-[0.75rem] text-term-muted">/ {t(PERIOD_KEY[form.period])}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Timeframes (tariff bridge): restrict WHEN a subscriber may park. Outside the
|
||||
window they're charged the transient tariff for the gap. Off = 24/7. */}
|
||||
<div className="mt-3 border-t border-term-border pt-3">
|
||||
<label className="flex items-center gap-2 text-[12px] text-term-text">
|
||||
<label className="flex items-center gap-2 text-[0.75rem] text-term-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-term-amber"
|
||||
@@ -370,7 +370,7 @@ export function SubscriptionPlansManager() {
|
||||
<label className="label">{t("plans.days")}</label>
|
||||
<span className="flex flex-wrap gap-2">
|
||||
{DOW_ORDER.map((d) => (
|
||||
<label key={d} className="inline-flex items-center gap-1 text-[12px] text-term-text">
|
||||
<label key={d} className="inline-flex items-center gap-1 text-[0.75rem] text-term-text">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="accent-term-amber"
|
||||
@@ -386,7 +386,7 @@ export function SubscriptionPlansManager() {
|
||||
))}
|
||||
</span>
|
||||
<label className="label">{t("plans.window")}</label>
|
||||
<span className="flex flex-wrap items-center gap-2 text-[12px] text-term-muted">
|
||||
<span className="flex flex-wrap items-center gap-2 text-[0.75rem] text-term-muted">
|
||||
{t("plans.enterAfter")}
|
||||
<input type="time" className="input w-28" value={form.winFrom} onChange={(e) => setForm((f) => f && { ...f, winFrom: e.target.value })} />
|
||||
{t("plans.exitBefore")}
|
||||
@@ -395,14 +395,14 @@ export function SubscriptionPlansManager() {
|
||||
<label className="label">{t("plans.grace")}</label>
|
||||
<span className="flex items-center gap-2">
|
||||
<input className="input w-16" value={form.graceMin} inputMode="numeric" onChange={(e) => setForm((f) => f && { ...f, graceMin: e.target.value })} />
|
||||
<span className="text-[12px] text-term-muted">{t("plans.graceHint")}</span>
|
||||
<span className="text-[0.75rem] text-term-muted">{t("plans.graceHint")}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="mt-1.5 text-[11px] text-term-muted">{t("plans.timeframesHint")}</p>
|
||||
<p className="mt-1.5 text-[0.6875rem] text-term-muted">{t("plans.timeframesHint")}</p>
|
||||
</div>
|
||||
|
||||
{form.planId && <p className="mt-2 text-[11px] text-term-amber">{t("plans.newVersionHint")}</p>}
|
||||
{form.planId && <p className="mt-2 text-[0.6875rem] text-term-amber">{t("plans.newVersionHint")}</p>}
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button type="button" className="btn btn-sm" onClick={() => setForm(null)}>{t("subs.cancel")}</button>
|
||||
<button type="button" className="btn btn-go btn-sm" onClick={save}>{t("subs.save")}</button>
|
||||
|
||||
Reference in New Issue
Block a user