import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { fetchOccupancy, fetchSiteConfig, fetchValidationPrograms, saveSiteConfig, saveValidationProgram, type Occupancy, type SiteConfig, type ValidationProgramView, } from "./api.js"; import { STATIONS, ValidationStationsPanel, defaultProgram, type StationId } from "./ValidationSetup.js"; // Live occupancy + capacity + park metadata. Occupancy is shown to everyone (it's a // fold over the signed ledger); capacity and the metadata fields are admin-editable. // The FULL gate (refuse transient entry at capacity) is enforced server-side in the // entry flow. Metadata (name, NIUS, address, contact) feeds the ticket header. // See wiki/concepts/capacity-occupancy.md and wiki/concepts/site-metadata.md. // The optional text fields, in display order. `labelKey`/`phKey` are i18n keys // (resolved at render); only `address` is multiline. const META_FIELDS: ReadonlyArray<{ key: keyof SiteConfig; labelKey: string; phKey?: string; multiline?: boolean }> = [ { key: "parkName", labelKey: "site.fieldParkName", phKey: "site.fieldParkNamePh" }, { key: "operatorName", labelKey: "site.fieldOperator", phKey: "site.fieldOperatorPh" }, { key: "nius", labelKey: "site.fieldNius", phKey: "site.fieldNiusPh" }, { key: "address", labelKey: "site.fieldAddress", multiline: true }, { key: "phone", labelKey: "site.fieldPhone" }, { key: "email", labelKey: "site.fieldEmail" }, ]; export function SiteSettings({ canEdit }: { canEdit: boolean }) { const { t } = useTranslation(); const [occ, setOcc] = useState(null); const [capInput, setCapInput] = useState(""); const [meta, setMeta] = useState>({}); const [exitVoucherDefault, setExitVoucherDefault] = useState(false); const [reserveSubs, setReserveSubs] = useState(false); const [anprEntry, setAnprEntry] = useState(true); const [msg, setMsg] = useState(null); // Merchant-validation programs (bar / lavazh). The checkboxes below toggle a // station's `active` (persisted at once — each flip signs a config_change); the // right-column panel edits the enabled stations. See validation-discounts.md. const [programs, setPrograms] = useState([]); function reload() { fetchOccupancy().then(setOcc).catch(() => {}); } useEffect(() => { reload(); if (canEdit) { fetchValidationPrograms() .then((r) => setPrograms(r.programs)) .catch(() => {}); } fetchSiteConfig() .then((c) => { setCapInput(c.capacity == null ? "" : String(c.capacity)); setExitVoucherDefault(c.exitVoucherDefault); setReserveSubs(c.reserveSubscriberSpots); setAnprEntry(c.anprEntryEnabled); const m: Record = {}; for (const { key } of META_FIELDS) m[key] = c[key] == null ? "" : String(c[key]); setMeta(m); }) .catch(() => {}); }, [canEdit]); /** Flip a merchant station's checkbox: persist `active` at once (a signed * config_change server-side), creating the well-known row with comp defaults on * the first enable. Config details are edited in the right-column panel. */ async function toggleStation(id: StationId, active: boolean) { const existing = programs.find((p) => p.id === id); const body = existing ? { ...existing, active } : { ...defaultProgram(id, t(id === "bar" ? "val.enableBar" : "val.enableLavazh")), active }; try { const saved = await saveValidationProgram(id, body); setPrograms((ps) => [...ps.filter((p) => p.id !== id), saved]); } catch (e) { setMsg((e as Error).message); } } async function save() { setMsg(null); const raw = capInput.trim(); const patch: Partial = { capacity: raw === "" ? null : Math.round(Number(raw)), exitVoucherDefault, reserveSubscriberSpots: reserveSubs, anprEntryEnabled: anprEntry, }; // Send each metadata field; "" → null is applied server-side. for (const { key } of META_FIELDS) (patch as Record)[key] = meta[key] ?? ""; try { await saveSiteConfig(patch); reload(); setMsg(t("site.saved")); } catch (e) { setMsg((e as Error).message); } } return (
{t("site.occupancy")} {occ == null ? ( … ) : ( <> {occ.count} {occ.capacity != null ? `/ ${occ.capacity}` : t("site.noCapacitySet")} {occ.capacity != null && ( · {occ.free} {t("site.free")} )} {occ.full && {t("site.full")}} )}
{canEdit && (
{t("site.capacityLabel")} setCapInput(e.target.value)} placeholder={t("site.capacityPlaceholder")} />
{t("val.sectionTitle")}
{t("val.sectionHint")}
{STATIONS.map((id) => ( ))}
{t("site.parkDetails")}
{META_FIELDS.map(({ key, labelKey, phKey, multiline }) => (
{t(labelKey)} {multiline ? (