// Nexa — page components (Overview, Transactions, Beneficiaries) // All data fetched live from window.TC_API. const { fmtN, fmtMoney, fmtDate, fmtDateLong, fmtTime, fmtRelative, fmtUntil, labelOp, symbolFor, normTxn, normApproval, normBeneficiary, normAccount, useFetch, } = window.TC_HELPERS; const { StatusBadge, DirectionGlyph, Flag, AgentTag, Spinner, LoadingBlock, ErrorBlock, PlusIcon, CheckIcon, XIcon, RefreshIcon, } = window.TC_UI; // ──────────────────────────────────────────────────────────── // OVERVIEW // ──────────────────────────────────────────────────────────── function OverviewPage({ agents, onNavigate, onPendingCount }) { const accountsQ = useFetch(() => TC_API.getAccounts(), [], { polling: 30000 }); const summaryQ = useFetch(() => TC_API.getSummary(), [], { polling: 15000 }); const approvalsQ = useFetch(() => TC_API.getApprovals(), [], { polling: 5000 }); const txnsQ = useFetch(() => TC_API.getTransactions({ limit: 7 }), [], { polling: 10000 }); const accounts = useMemo(() => (accountsQ.data || []).map(normAccount), [accountsQ.data]); const approvals = useMemo(() => (approvalsQ.data || []).map(normApproval) .filter(a => a.status === "pending_approval" || a.status === "pending"), [approvalsQ.data]); const recent = useMemo(() => (txnsQ.data || []).map(normTxn), [txnsQ.data]); const summary = summaryQ.data || {}; const today = summary.today || {}; // Notify shell so the side-nav badge can show pending count useEffect(() => { onPendingCount && onPendingCount(approvals.length); }, [approvals.length, onPendingCount]); const onApprove = async (id) => { try { await TC_API.approveApproval(id); approvalsQ.refetch(); txnsQ.refetch(); accountsQ.refetch(); summaryQ.refetch(); } catch (e) { alert(`Approve failed: ${e.message}`); } }; const onReject = async (id) => { try { await TC_API.rejectApproval(id); approvalsQ.refetch(); txnsQ.refetch(); accountsQ.refetch(); summaryQ.refetch(); } catch (e) { alert(`Reject failed: ${e.message}`); } }; const paymentsEnabled = summary.payments_enabled !== false ? true : (summaryQ.loading ? null : false); const [activating, setActivating] = React.useState(false); const onActivatePayments = async () => { setActivating(true); try { await TC_API.activatePayments(); summaryQ.refetch(); } catch (e) { alert(`Activation failed: ${e.message}`); } finally { setActivating(false); } }; return (
{/* Payments not active banner */} {paymentsEnabled === false && (
Payments not yet active.{" "} Review your policy rules and activate payments to allow your agent to start operating.
)} {/* Account cards */}

Accounts

{accountsQ.loading ? "loading…" : `${accounts.length} ${accounts.length === 1 ? "account" : "accounts"}`}
{accountsQ.loading && !accounts.length ? : accountsQ.error ? : accounts.length === 0 ?
No accounts yet.
:
{accounts.map(a => (
{symbolFor(a.currency) || a.currency?.[0] || "•"} {a.currency} {a.label || "—"}
{symbolFor(a.currency)} {fmtN(a.balance)}
{a.iban || "—"}
AVAILABLE {fmtMoney(a.available, a.currency)}
HELD 0 ? "is-held" : "is-zero"}`}> {fmtMoney(a.held, a.currency)}
))}
}
{/* Summary metrics */}
{summaryQ.error ? :
Pending approvals 0 ? "is-pending" : ""}`}> {summary.pending_approval_count ?? (summaryQ.loading ? : 0)} {(summary.pending_approval_count || 0) > 0 ? "awaiting your sign-off" : "all clear"}
Active agents {summary.active_agent_count ?? (summaryQ.loading ? : 0)} registered & operating
Today · transactions {today.total_transactions ?? (summaryQ.loading ? : 0)} {today.approved != null ? `${today.approved} approved` : ""} {today.rejected != null ? ` · ${today.rejected} rejected` : ""}
Open info requests {summary.open_information_requests_count ?? (summaryQ.loading ? : 0)} agent-initiated
}
{/* Pending approvals */}

Pending approvals

held by policy
{approvalsQ.loading && !approvals.length ?
: approvalsQ.error ? :
{approvals.length === 0 && (
No instructions held. Agents are operating within policy.
)} {approvals.map(t => (
{t.currency || ""} {t.operation ? `· ${labelOp(t.operation)}` : ""} {fmtMoney(Math.abs(t.amount), t.currency)}
{t.destination && } {t.counterparty || "—"}
id · {t.id} {t.createdAt && · submitted {fmtRelative(t.createdAt)}}
{t.reason &&
{t.reason}
}
{t.expiresAt && expires {fmtUntil(t.expiresAt)}}
))}
}
{/* Recent transactions */}

Recent transactions

{txnsQ.loading && !recent.length ?
: txnsQ.error ? : recent.length === 0 ?
No transactions yet.
:
{recent.map(t => (
{fmtTime(t.timestamp)}
{fmtDate(t.timestamp)}
{t.counterparty || "—"} {t.country || "—"} · {labelOp(t.operation)}
{t.direction === "in" ? "+" : "−"}{fmtMoney(t.amount, t.currency)}
))}
}
); } // ──────────────────────────────────────────────────────────── // TRANSACTIONS // ──────────────────────────────────────────────────────────── function TransactionsPage({ agents }) { const [filter, setFilter] = useState("all"); const txnsQ = useFetch( () => TC_API.getTransactions({ status: filter, limit: 200 }), [filter], { polling: 15000 } ); const txns = useMemo(() => (txnsQ.data || []).map(normTxn), [txnsQ.data]); return (

All transactions

{[ { k: "all", label: "All" }, { k: "approved", label: "Approved" }, { k: "pending_approval", label: "Pending" }, { k: "rejected", label: "Rejected" }, ].map(f => ( ))}
{txnsQ.loading && !txns.length ?
: txnsQ.error ? :
{txns.map(t => ( ))} {txns.length === 0 && ( )}
Time Date Dir. Counterparty Agent Currency Amount Status
{fmtTime(t.timestamp)} {fmtDate(t.timestamp)}
{t.counterparty || "—"} {t.country || "—"} · {labelOp(t.operation)} · {t.id} {t.paymentReason && ( ↳ {t.paymentReason} )}
{t.currency} {t.direction === "in" ? "+" : "−"}{fmtMoney(t.amount, t.currency)}
No transactions match this filter.
}
); } // ──────────────────────────────────────────────────────────── // BENEFICIARIES // ──────────────────────────────────────────────────────────── function BeneficiariesPage() { const benQ = useFetch(() => TC_API.getBeneficiaries(), [], { polling: 30000 }); const beneficiaries = useMemo(() => (benQ.data || []).map(normBeneficiary), [benQ.data]); const [showForm, setShowForm] = useState(false); const [busyId, setBusyId] = useState(null); const onApprove = async (id) => { setBusyId(id); try { await TC_API.approveBeneficiary(id); benQ.refetch(); } catch (e) { alert(`Approve failed: ${e.message}`); } finally { setBusyId(null); } }; const onBlock = async (id) => { const reason = prompt("Reason for blocking this beneficiary?", ""); if (reason == null) return; setBusyId(id); try { await TC_API.blockBeneficiary(id, reason); benQ.refetch(); } catch (e) { alert(`Block failed: ${e.message}`); } finally { setBusyId(null); } }; const pendingCount = beneficiaries.filter(b => b.status === "pending_review" || b.status === "pending").length; return (

Beneficiaries

{benQ.loading ? "loading…" : `${beneficiaries.length} total · ${pendingCount} pending review`}
{showForm && ( setShowForm(false)} onSubmit={async (b) => { try { await TC_API.addBeneficiary(b); setShowForm(false); benQ.refetch(); } catch (e) { alert(`Add failed: ${e.message}`); } }} /> )} {benQ.loading && !beneficiaries.length ?
: benQ.error ? :
{beneficiaries.map(b => { const isPending = b.status === "pending_review" || b.status === "pending"; const isBlocked = b.status === "blocked"; return ( ); })} {beneficiaries.length === 0 && ( )}
Name Country Currency IBAN Added Last paid Total paid Status
{b.name || "—"} {b.id}
{b.currency} {b.iban || "—"} {fmtDate(b.addedAt)} {b.lastPaidAt ? fmtDate(b.lastPaidAt) : "—"} {b.totalPaid > 0 ? fmtMoney(b.totalPaid, b.currency) : "—"}
{isPending && ( )} {!isBlocked && ( )}
No beneficiaries yet. Add one to enable agent payments.
}

New beneficiaries enter pending review by default. Agents cannot transact to a beneficiary until a principal approves it.

); } // ── Client-side IBAN validation (ISO 7064 mod-97, no external API) ────────── const IBAN_LENGTHS = { AL:28,AD:24,AT:20,AZ:28,BH:22,BE:16,BA:20,BR:29,BG:22,BY:28,CR:22,HR:21, CY:28,CZ:24,DK:18,DO:28,SV:28,EE:20,FO:18,FI:18,FR:27,GE:22,DE:22,GI:23, GR:27,GL:18,GT:28,HU:28,IS:26,IQ:23,IE:22,IL:23,IT:27,JO:30,KZ:20,XK:20, KW:30,LV:21,LB:28,LI:21,LT:20,LU:20,MK:19,MT:31,MR:27,MU:30,MC:27,MD:24, ME:22,NL:18,NO:15,PK:24,PS:29,PL:28,PT:25,QA:29,RO:24,SM:27,SA:24,RS:22, SC:31,SK:24,SI:19,ES:24,SE:24,CH:21,TN:24,TR:26,AE:23,GB:22,VA:22,VG:24, }; const IBAN_COUNTRY_NAMES = { AL:"Albania",AD:"Andorra",AT:"Austria",AZ:"Azerbaijan",BH:"Bahrain",BE:"Belgium", BA:"Bosnia",BR:"Brazil",BG:"Bulgaria",BY:"Belarus",CR:"Costa Rica",HR:"Croatia", CY:"Cyprus",CZ:"Czech Republic",DK:"Denmark",DO:"Dominican Republic",SV:"El Salvador", EE:"Estonia",FO:"Faroe Islands",FI:"Finland",FR:"France",GE:"Georgia",DE:"Germany", GI:"Gibraltar",GR:"Greece",GL:"Greenland",GT:"Guatemala",HU:"Hungary",IS:"Iceland", IQ:"Iraq",IE:"Ireland",IL:"Israel",IT:"Italy",JO:"Jordan",KZ:"Kazakhstan",XK:"Kosovo", KW:"Kuwait",LV:"Latvia",LB:"Lebanon",LI:"Liechtenstein",LT:"Lithuania",LU:"Luxembourg", MK:"North Macedonia",MT:"Malta",MR:"Mauritania",MU:"Mauritius",MC:"Monaco",MD:"Moldova", ME:"Montenegro",NL:"Netherlands",NO:"Norway",PK:"Pakistan",PS:"Palestine",PL:"Poland", PT:"Portugal",QA:"Qatar",RO:"Romania",SM:"San Marino",SA:"Saudi Arabia",RS:"Serbia", SC:"Seychelles",SK:"Slovakia",SI:"Slovenia",ES:"Spain",SE:"Sweden",CH:"Switzerland", TN:"Tunisia",TR:"Turkey",AE:"UAE",GB:"United Kingdom",VA:"Vatican City",VG:"British Virgin Islands", }; function validateIban(raw) { if (!raw || !raw.trim()) return { valid: false, error: null }; // empty = no error shown const norm = raw.replace(/[\s\-]/g, "").toUpperCase(); if (norm.length < 5) return { valid: false, error: "Too short" }; const cc = norm.slice(0, 2); if (!/^[A-Z]{2}$/.test(cc)) return { valid: false, error: "Must start with a 2-letter country code" }; if (!/^\d{2}/.test(norm.slice(2))) return { valid: false, error: "Positions 3–4 must be digits" }; const expected = IBAN_LENGTHS[cc]; if (expected && norm.length !== expected) return { valid: false, error: `${cc} IBANs must be ${expected} characters (got ${norm.length})` }; // Mod-97 const rearranged = norm.slice(4) + norm.slice(0, 4); const digits = rearranged.split("").map(c => /[A-Z]/.test(c) ? String(c.charCodeAt(0) - 55) : c).join(""); let remainder = 0; for (const chunk of digits.match(/.{1,9}/g) || []) { remainder = Number(String(remainder) + chunk) % 97; } if (remainder !== 1) return { valid: false, error: "Check digits are incorrect — please re-check the IBAN" }; return { valid: true, country: cc, countryName: IBAN_COUNTRY_NAMES[cc] || cc, formatted: norm.match(/.{1,4}/g).join(" ") }; } function AddBeneficiaryForm({ onCancel, onSubmit }) { const [name, setName] = useState(""); const [country, setCountry] = useState(""); const [currency, setCurrency] = useState("GBP"); const [iban, setIban] = useState(""); const [ibanTouched, setIbanTouched] = useState(false); const [submitting, setSubmitting] = useState(false); const ibanValidation = validateIban(iban); const submit = async (e) => { e.preventDefault(); if (!name.trim()) return; if (iban.trim() && !ibanValidation.valid) return; setSubmitting(true); await onSubmit({ name: name.trim(), country: country.trim().toUpperCase(), currency: currency.trim().toUpperCase(), iban: iban.trim().replace(/\s+/g, ""), }); setSubmitting(false); }; const ibanNorm = iban.replace(/[\s\-]/g, "").toUpperCase(); const showIbanError = ibanTouched && ibanNorm.length > 4 && !ibanValidation.valid; const showIbanOk = ibanNorm.length > 4 && ibanValidation.valid; return (

Add beneficiary

Name setName(e.target.value)} placeholder="e.g. EMEA Logistics GmbH" required />
Country (ISO-2/3) setCountry(e.target.value)} placeholder="DE" />
Currency
IBAN / account number
setIban(e.target.value)} onBlur={() => setIbanTouched(true)} placeholder="DE89 3704 0044 0532 0130 00" style={{ paddingRight: 28, borderColor: showIbanError ? "var(--red,#ef4444)" : showIbanOk ? "var(--green,#22c55e)" : undefined, outline: showIbanError ? "1px solid var(--red,#ef4444)" : showIbanOk ? "1px solid var(--green,#22c55e)" : undefined, }} /> {showIbanOk && ( )}
{showIbanOk && (
{ibanValidation.countryName ? `${ibanValidation.countryName} · ` : ""} {ibanValidation.formatted}
)} {showIbanError && (
{ibanValidation.error}
)}
); } // ──────────────────────────────────────────────────────────── // DOCUMENTS PAGE // ──────────────────────────────────────────────────────────── const DOC_TYPES = [ "Certificate of Incorporation", "Articles of Association", "Proof of Address", "UBO Declaration", "Director ID", "Audited Financials", "Bank Statement", "Source of Funds Declaration", "KYC Document", "Other", ]; const DOC_CATEGORIES = ["corporate","financial","identity","regulatory","operational","compliance","correspondence"]; function DocumentUploadForm({ onCancel, onUploaded }) { const [file, setFile] = useState(null); const [docType, setType] = useState(DOC_TYPES[0]); const [category, setCat] = useState("corporate"); const [progress, setP] = useState(null); const [error, setError] = useState(""); const submit = async (e) => { e.preventDefault(); if (!file) { setError("Select a file first."); return; } setP("uploading"); setError(""); try { const result = await TC_API.uploadDocument(file, docType, category, null, ""); setP("done"); setTimeout(() => onUploaded(result), 1200); } catch (err) { setP("error"); setError(err.message || String(err)); } }; return (

Upload document

Document type
Category
File { setFile(e.target.files[0] || null); setError(""); }} accept=".pdf,.png,.jpg,.jpeg,.gif,.webp,.doc,.docx,.xls,.xlsx,.txt,.csv" /> {file && {file.name} · {(file.size/1024).toFixed(1)} KB}
{error &&

{error}

} {progress === "uploading" &&

Uploading…

} {progress === "done" &&

Document received and verified ✓

}
); } function DocumentsPage() { const docsQ = useFetch(() => TC_API.getDocuments(), []); const [showUpload, setShowUpload] = useState(false); const docs = useMemo(() => docsQ.data || [], [docsQ.data]); const fmtSize = (bytes) => { if (!bytes) return "—"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1048576) return `${(bytes/1024).toFixed(1)} KB`; return `${(bytes/1048576).toFixed(1)} MB`; }; return (

Documents on file

{docs.length} document{docs.length !== 1 ? "s" : ""}
{!showUpload && ( )} {showUpload && ( setShowUpload(false)} onUploaded={() => { setShowUpload(false); docsQ.refetch(); }} /> )} {docsQ.loading && !docs.length ? : docsQ.error ? : docs.length === 0 ?
No documents on file. Upload your first document above.
: (
{docs.map(d => ( ))}
Document Category Uploaded By Size
{d.doc_type || "Document"} {d.file_name}
{d.category} {fmtDate(d.uploaded_at)} {d.uploaded_by || "—"} {fmtSize(d.file_size)} {d.has_file && ( Download )}
) } {docs.some(d => d.extracted_data && Object.keys(d.extracted_data).length > 0) && (

Extracted data

structured data from uploaded documents
{docs.filter(d => d.extracted_data && Object.keys(d.extracted_data).length > 0).map(d => (
{d.doc_type} {d.file_name}
{Object.entries(d.extracted_data).slice(0,12).map(([k,v]) => (
{k.replace(/_/g," ")} {String(v).slice(0,80)}
))}
))}
)}
); } window.TC_PAGES_A = { OverviewPage, TransactionsPage, BeneficiariesPage, DocumentsPage };