// Nexa — Policies & Agents pages (live API) const { fmtDate: fmtDate_b, ruleLabel, ruleDetail, normPolicyRule, normAgent, useFetch: useFetch_b, fmtMoney: fmtMoney_b, symbolFor: symbolFor_b, } = window.TC_HELPERS; const { AgentBadge: AgentBadge_b, Toggle: Toggle_b, PlusIcon: PlusIcon_b, ChevronIcon: ChevronIcon_b, XIcon: XIcon_b, CheckIcon: CheckIcon_b, Spinner: Spinner_b, RefreshIcon: RefreshIcon_b, LoadingBlock: LoadingBlock_b, ErrorBlock: ErrorBlock_b, } = window.TC_UI; // ── Constants ─────────────────────────────────────────────── const OPS = ["on_ramp", "off_ramp", "issue_card", "card_spend"]; const COMMON_CURRENCIES = ["GBP", "EUR", "USD", "CAD", "CHF", "AUD", "SGD"]; const RULE_TYPE_DEFS = [ { value: "transaction_limit", label: "Per-Transaction Limit", shape: "amount" }, { value: "daily_limit", label: "Daily Aggregate Limit", shape: "amount" }, { value: "weekly_limit", label: "Weekly Aggregate Limit", shape: "amount" }, { value: "monthly_limit", label: "Monthly Aggregate Limit", shape: "amount" }, { value: "approval_threshold", label: "Approval Threshold", shape: "amount", alwaysSoft: true }, { value: "min_balance", label: "Minimum Balance Floor", shape: "amount" }, { value: "counterparty_whitelist", label: "Counterparty Whitelist", shape: "namelist" }, { value: "counterparty_blacklist", label: "Counterparty Blacklist", shape: "namelist" }, { value: "currency_whitelist", label: "Currency Whitelist", shape: "currency" }, { value: "corridor_blacklist", label: "Corridor Blacklist", shape: "codelist" }, { value: "time_window", label: "Time Window", shape: "time" }, { value: "velocity_limit", label: "Velocity Limit", shape: "velocity" }, { value: "registered_beneficiary", label: "Registered Beneficiary Required", shape: "toggle" }, ]; // ── Rule form (create + edit) ──────────────────────────────── function RuleForm({ initial, onCancel, onSubmit, title = "New policy rule" }) { // Initialise from existing rule or empty const init = initial || {}; const [type, setType] = useState(init.type || RULE_TYPE_DEFS[0].value); const [mode, setMode] = useState(init.mode || "block"); const [amount, setAmount] = useState(init.amount != null ? String(init.amount) : ""); const [currency, setCurrency] = useState(init.currency || "GBP"); const [nameList, setNameList] = useState((init.values || []).join("\n")); const [codeList, setCodeList] = useState((init.values || []).join("\n")); const [curList, setCurList] = useState(init.values || []); const [customCur, setCustomCur] = useState(""); const [maxCount, setMaxCount] = useState(init.maxCount != null ? String(init.maxCount) : "50"); const [periodHours, setPH] = useState(init.periodHours != null ? String(init.periodHours) : "24"); const [startHour, setSH] = useState(init.startHour != null ? String(init.startHour) : "7"); const [endHour, setEH] = useState(init.endHour != null ? String(init.endHour) : "19"); const [togVal, setTogVal] = useState(init.enabled !== false); const [submitting, setSub] = useState(false); const meta = RULE_TYPE_DEFS.find(r => r.value === type); const isAlwaysSoft = meta && meta.alwaysSoft; function toggleCur(cur) { setCurList(prev => prev.includes(cur) ? prev.filter(c => c !== cur) : [...prev, cur]); } function addCustomCur() { const c = customCur.trim().toUpperCase(); if (c && !curList.includes(c)) { setCurList(prev => [...prev, c]); setCustomCur(""); } } const submit = async (e) => { e.preventDefault(); const rule = { policy_type: type, is_hard_block: isAlwaysSoft ? false : (mode === "block"), enabled: true, }; if (meta.shape === "amount") { if (!amount) return; rule.amount = Number(amount); if (currency) rule.currency = currency; } else if (meta.shape === "namelist") { const values = nameList.split("\n").map(s => s.trim()).filter(Boolean); if (!values.length) return; rule.values = values; } else if (meta.shape === "codelist") { const values = codeList.split("\n").map(s => s.trim().toUpperCase()).filter(Boolean); if (!values.length) return; rule.values = values; } else if (meta.shape === "currency") { if (!curList.length) return; rule.values = curList; } else if (meta.shape === "time") { rule.start_hour = Number(startHour); rule.end_hour = Number(endHour); } else if (meta.shape === "velocity") { rule.max_count = Number(maxCount); rule.period_hours = Number(periodHours); } else if (meta.shape === "toggle") { rule.enabled = togVal; } setSub(true); try { await onSubmit(rule); } finally { setSub(false); } }; const hours = Array.from({length: 24}, (_, i) => i); return (

{title}

Rule type
Enforcement mode {isAlwaysSoft ? (
Always "require approval"
) : (
)}
{meta.shape === "amount" && (
Amount setAmount(e.target.value)} required />
Currency (optional)
)} {meta.shape === "namelist" && (
Names · one per line