import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { fetchUsers, saveValidationProgram, type ManagedUser, type ValidationMode, type ValidationProgramView, } from "./api.js"; // The /setup/site RIGHT panel: per-station merchant-validation config (Bar / Lavazh). // The checkboxes on the left card toggle a station's `active`; this panel edits the // enabled stations' programs — one panel, tabs when both are on. Storage is generic // (validation_programs rows keyed "bar"/"lavazh"); the UI is deliberately these two // fixed stations. Amounts are entered in MAJOR units and stored in integer minor // units (the tariff-composer convention). See wiki/concepts/validation-discounts.md. /** The two well-known stations the checkboxes toggle. */ export const STATIONS = ["bar", "lavazh"] as const; export type StationId = (typeof STATIONS)[number]; /** A blank program draft for a station enabled for the first time. */ export function defaultProgram(id: StationId, label: string): Omit { return { name: label, mode: "comp", minutes: null, percent: null, maxAmountMinor: null, maxPerDay: null, active: true, userIds: [], }; } const toMinor = (s: string): number | null => { const v = s.trim(); if (v === "") return null; const n = Number(v); return Number.isFinite(n) && n > 0 ? Math.round(n * 100) : null; }; const fromMinor = (m: number | null): string => (m == null ? "" : String(m / 100)); const toInt = (s: string): number | null => { const v = s.trim(); if (v === "") return null; const n = Number(v); return Number.isInteger(n) && n > 0 ? n : null; }; function StationForm({ program, onSaved, }: { program: ValidationProgramView; onSaved: (p: ValidationProgramView) => void; }) { const { t } = useTranslation(); const [name, setName] = useState(program.name); const [mode, setMode] = useState(program.mode); const [minutes, setMinutes] = useState(program.minutes == null ? "" : String(program.minutes)); const [percent, setPercent] = useState(program.percent == null ? "" : String(program.percent)); const [maxAmount, setMaxAmount] = useState(fromMinor(program.maxAmountMinor)); const [maxPerDay, setMaxPerDay] = useState(program.maxPerDay == null ? "" : String(program.maxPerDay)); const [userIds, setUserIds] = useState>(new Set(program.userIds)); const [users, setUsers] = useState(null); const [msg, setMsg] = useState(null); // Reset the form when the tab switches to another station. useEffect(() => { setName(program.name); setMode(program.mode); setMinutes(program.minutes == null ? "" : String(program.minutes)); setPercent(program.percent == null ? "" : String(program.percent)); setMaxAmount(fromMinor(program.maxAmountMinor)); setMaxPerDay(program.maxPerDay == null ? "" : String(program.maxPerDay)); setUserIds(new Set(program.userIds)); setMsg(null); }, [program.id]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { fetchUsers() .then((r) => setUsers(r.users)) .catch(() => setUsers([])); }, []); const valid = useMemo(() => { if (!name.trim()) return false; if (mode === "timeCredit") return toInt(minutes) != null; if (mode === "percent") { const p = toInt(percent); return p != null && p <= 100; } if (mode === "fixed") return toMinor(maxAmount) != null; return true; }, [name, mode, minutes, percent, maxAmount]); async function save() { setMsg(null); try { const saved = await saveValidationProgram(program.id, { name: name.trim(), mode, minutes: mode === "timeCredit" ? toInt(minutes) : null, percent: mode === "percent" ? toInt(percent) : null, maxAmountMinor: mode === "fixed" ? toMinor(maxAmount) : null, maxPerDay: toInt(maxPerDay), active: program.active, userIds: [...userIds], }); onSaved(saved); setMsg(t("val.saved")); } catch (e) { setMsg((e as Error).message); } } const toggleUser = (id: string) => setUserIds((prev) => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); return (
{t("val.labelName")} setName(e.target.value)} placeholder={t("val.labelNamePh")} />
{t("val.mode")}
{mode === "timeCredit" && (
{t("val.minutes")} setMinutes(e.target.value)} placeholder="60" />
)} {mode === "percent" && (
{t("val.percent")} setPercent(e.target.value)} placeholder="100" />
)} {mode === "fixed" && (
{t("val.maxAmount")} setMaxAmount(e.target.value)} placeholder="1000" />
)}
{t("val.maxPerDay")} setMaxPerDay(e.target.value)} />
{t("val.users")}
{t("val.usersHint")}
{users == null ? ( … ) : users.length === 0 ? ( {t("val.noUsers")} ) : ( users.map((u) => ( )) )}
{msg && {msg}}
); } /** The right-column panel: tabs across the ENABLED stations, one form each. */ export function ValidationStationsPanel({ programs, onSaved, }: { programs: ValidationProgramView[]; onSaved: (p: ValidationProgramView) => void; }) { const { t } = useTranslation(); const enabled = STATIONS.map((id) => programs.find((p) => p.id === id)).filter( (p): p is ValidationProgramView => p != null && p.active, ); const [tab, setTab] = useState(null); const current = enabled.find((p) => p.id === tab) ?? enabled[0]; if (!current) return null; return (
{t("val.sectionTitle")}
{enabled.length > 1 && (
{enabled.map((p) => ( ))}
)}
); }