From d5ff2097bd5b4613a81c7de7837d147a300d9e8a Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sun, 5 Jul 2026 15:23:29 +0200 Subject: [PATCH] feat(web): currency becomes a closed select (ALL / EUR / USD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currency was free text in the tariff editor (composer page + lab draft modal — shared form) and the subscription plan editor; a typo could publish an unknown code onto immutable versions. Both now offer a closed select from lib/currencies.ts. An out-of-set code already stored on an old record is appended as an extra option so it displays + round-trips unchanged. Blank tariff form defaults to ALL (was EUR) — the site's actual currency. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V --- apps/web/src/SubscriptionPlansManager.tsx | 7 ++++++- apps/web/src/TariffEditorForm.tsx | 9 +++++++-- apps/web/src/lib/currencies.ts | 11 +++++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/lib/currencies.ts diff --git a/apps/web/src/SubscriptionPlansManager.tsx b/apps/web/src/SubscriptionPlansManager.tsx index 006882c..9cf36eb 100644 --- a/apps/web/src/SubscriptionPlansManager.tsx +++ b/apps/web/src/SubscriptionPlansManager.tsx @@ -14,6 +14,7 @@ import { type SubscriptionPlan, } from "./api.js"; import { Modal } from "./ui/Modal.js"; +import { currencyOptions } from "./lib/currencies.js"; // Admin-only subscription PLAN catalog. Plans are admin-composed, versioned config the // operator sells from (so the operator never types a price). Editing a plan PUBLISHES A @@ -348,7 +349,11 @@ export function SubscriptionPlansManager() { setForm((f) => f && { ...f, priceMajor: e.target.value })} placeholder="e.g. 800" /> - setForm((f) => f && { ...f, currency: e.target.value })} /> + / {t(PERIOD_KEY[form.period])} diff --git a/apps/web/src/TariffEditorForm.tsx b/apps/web/src/TariffEditorForm.tsx index 3407375..50cd04d 100644 --- a/apps/web/src/TariffEditorForm.tsx +++ b/apps/web/src/TariffEditorForm.tsx @@ -1,4 +1,5 @@ import { useTranslation } from "react-i18next"; +import { currencyOptions } from "./lib/currencies.js"; import { isTariffV2, type TariffBlock, @@ -101,7 +102,7 @@ function emptyTier(): TierForm { export function emptyForm(): FormState { return { - currency: "EUR", + currency: "ALL", gracePeriodEntryMin: "15", incrementMin: "60", lostTicket: "20.00", @@ -339,7 +340,11 @@ export function TariffEditorForm({
- set("currency", e.target.value)} maxLength={3} /> + set("gracePeriodEntryMin", e.target.value)} /> diff --git a/apps/web/src/lib/currencies.ts b/apps/web/src/lib/currencies.ts new file mode 100644 index 0000000..28416e1 --- /dev/null +++ b/apps/web/src/lib/currencies.ts @@ -0,0 +1,11 @@ +// The currencies the booth can price in (ISO 4217). Money is always stored as +// integer minor units + one of these codes; the UI offers a closed select rather +// than free text so a typo can never publish an unknown currency. +export const CURRENCIES = ["ALL", "EUR", "USD"] as const; + +/** The select options: the known set, plus the current value when it's some + * historical code outside it (so an old record still displays + round-trips). */ +export function currencyOptions(current: string): string[] { + const cur = current.trim().toUpperCase(); + return cur && !CURRENCIES.includes(cur as (typeof CURRENCIES)[number]) ? [...CURRENCIES, cur] : [...CURRENCIES]; +}