From 4f902d869efb61c6d074e18eec328fcfebbfa94b Mon Sep 17 00:00:00 2001
From: Julian Cuni
Date: Sun, 5 Jul 2026 16:37:53 +0200
Subject: [PATCH] feat(web): published-versions sidebar on the composer page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The lab redesign gave only the lab tab the published-history sidebar;
the composer page was expected to have it too. /setup/tariff now lists
every published version (name or effective date, active badge, currency)
on the right; clicking one loads it into the editor as the SEED for the
next publish — which always creates a new immutable version (the sidebar
hint states this), making "roll back to last month's prices" a two-click
republish while the history stays append-only.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
---
apps/web/src/TariffComposer.tsx | 94 ++++++++++++++++++++++++++-------
apps/web/src/lib/i18n/en.ts | 3 ++
apps/web/src/lib/i18n/sq.ts | 3 ++
wiki/concepts/tariff.md | 6 ++-
4 files changed, 87 insertions(+), 19 deletions(-)
diff --git a/apps/web/src/TariffComposer.tsx b/apps/web/src/TariffComposer.tsx
index 7b24dce..fa6efe8 100644
--- a/apps/web/src/TariffComposer.tsx
+++ b/apps/web/src/TariffComposer.tsx
@@ -1,19 +1,23 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
-import { ApiError, fetchTariff, publishTariffVersion, type TariffState } from "./api.js";
-import { TariffEditorForm, emptyForm, formFromActive, toStructure, type FormState } from "./TariffEditorForm.js";
+import { ApiError, fetchTariff, publishTariffVersion, type TariffState, type TariffVersion } from "./api.js";
+import { TariffEditorForm, emptyForm, formFromActive, formFromVersion, toStructure, type FormState } from "./TariffEditorForm.js";
// Tariff composer — the admin edits + publishes the LIVE rate card. Publishing
// creates a new IMMUTABLE version (the active card); old versions are kept so past
-// sessions reprice correctly. The form machinery is shared with the Tariff Lab's
-// draft modal — see TariffEditorForm.tsx. To experiment without publishing, use the
-// lab (a draft only becomes real through this same publish path). See
+// sessions reprice correctly. A right sidebar lists the published history (named
+// since 2026-07-05); clicking a version loads it into the editor as the STARTING
+// POINT — publishing always creates a new version effective now, it never edits the
+// clicked one. The form machinery is shared with the Tariff Lab's draft modal — see
+// TariffEditorForm.tsx. To experiment without publishing, use the lab. See
// wiki/concepts/tariff.md.
export function TariffComposer() {
const { t } = useTranslation();
const [state, setState] = useState(null);
const [form, setForm] = useState(emptyForm);
+ // Which published version the editor was last loaded from (sidebar highlight).
+ const [loadedId, setLoadedId] = useState(null);
// Optional label for the version about to be published. Deliberately NOT prefilled
// from the active version — a tweaked card republished under last season's name
// would mislabel the history.
@@ -26,10 +30,17 @@ export function TariffComposer() {
.then((s) => {
setState(s);
setForm(formFromActive(s));
+ setLoadedId(s.active?.id ?? null);
})
.catch((e) => setMsg({ kind: "err", text: (e as Error).message }));
}, []);
+ function loadVersion(v: TariffVersion) {
+ setForm(formFromVersion(v.currency, v.structure));
+ setLoadedId(v.id);
+ setMsg(null);
+ }
+
async function publish() {
setSaving(true);
setMsg(null);
@@ -41,6 +52,7 @@ export function TariffComposer() {
});
const fresh = await fetchTariff();
setState(fresh);
+ setLoadedId(fresh.active?.id ?? null);
setVersionName("");
setMsg({ kind: "ok", text: t("tariff.publishedOk") });
} catch (e) {
@@ -71,20 +83,66 @@ export function TariffComposer() {
+
+ {/* Published history — click a version to load it into the editor. Same list
+ the lab's sidebar shows; here it seeds the next publish. */}
+ {state && state.versions.length > 0 && (
+
)}
diff --git a/apps/web/src/lib/i18n/en.ts b/apps/web/src/lib/i18n/en.ts
index 14f5314..9690e57 100644
--- a/apps/web/src/lib/i18n/en.ts
+++ b/apps/web/src/lib/i18n/en.ts
@@ -336,6 +336,9 @@ export const en: Catalog = {
publishNewVersion: "Publish new version",
publishing: "Publishing…",
versionNamePh: "Version name (optional), e.g. Summer 2026",
+ versionsTitle: "Published versions",
+ versionsHint: "Click one to load it into the editor. Publishing always creates a new version — past versions never change.",
+ activeBadge: "active",
publishedOk: "New tariff version published — it's now the active rate.",
defaultCard: "Base rate (always active)",
defaultCardHint: "The base rate applied when no time/seasonal tier matches. This alone is enough for most car parks.",
diff --git a/apps/web/src/lib/i18n/sq.ts b/apps/web/src/lib/i18n/sq.ts
index a1bfef2..52a0aa9 100644
--- a/apps/web/src/lib/i18n/sq.ts
+++ b/apps/web/src/lib/i18n/sq.ts
@@ -339,6 +339,9 @@ export const sq = {
publishNewVersion: "Publiko version të ri",
publishing: "Duke publikuar…",
versionNamePh: "Emri i versionit (opsional), p.sh. Vera 2026",
+ versionsTitle: "Versione të publikuara",
+ versionsHint: "Kliko një për ta ngarkuar në editor. Publikimi krijon gjithmonë version të ri — versionet e kaluara nuk ndryshojnë kurrë.",
+ activeBadge: "aktive",
publishedOk: "U publikua versioni i ri i tarifës — tani është tarifa aktive.",
defaultCard: "Tarifa bazë (gjithmonë aktive)",
defaultCardHint: "Çmimi bazë i zbatuar kur asnjë nivel kohor/sezonal nuk vlen. Kjo e vetme është mjaftueshëm për shumicën e parkimeve.",
diff --git a/wiki/concepts/tariff.md b/wiki/concepts/tariff.md
index 00461ae..d353005 100644
--- a/wiki/concepts/tariff.md
+++ b/wiki/concepts/tariff.md
@@ -193,7 +193,11 @@ The admin authors the rate card at runtime — no hand-seeding:
composer accumulates per-band hours into the engine's cumulative `uptoMin` (minutes) on submit. The
**last band is always the open-ended "thereafter"** row (not removable, no hours field), so a
published card always satisfies the open-ended-last rule. Shows the active version + history;
- "Publish" creates a new version (past sessions keep their pricing).
+ "Publish" creates a new version (past sessions keep their pricing). Since 2026-07-05 a **right
+ sidebar lists the published history** (name or effective date, active badge — mirrors the lab's
+ sidebar); clicking a version **loads it into the editor as the starting point** for the next
+ publish (currency select ALL/EUR/USD; optional version-name field). Publishing never edits the
+ clicked version — the sidebar hint says so explicitly.
- Ships **blank** — until a version is published, `GET /api/tariff` returns `active: null` and the
pay station returns `409 no active tariff`. Verified end to end (publish → pay station prices).