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() {

)} - +
+
+ -
- setVersionName(e.target.value)} - placeholder={t("tariff.versionNamePh")} - /> - - {msg && ( - {msg.text} +
+ setVersionName(e.target.value)} + placeholder={t("tariff.versionNamePh")} + /> + + {msg && ( + {msg.text} + )} +
+
+ + {/* 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).