// Nexa — dashboard shell. // Calls TC_API.init() on mount to resolve identity from the session cookie. // Redirects to /customer/login if the session is missing or expired. const { NAV } = window.TC_HELPERS; const { SideIcon, LoadingBlock, ErrorBlock } = window.TC_UI; const { useFetch, normAgent } = window.TC_HELPERS; const { OverviewPage, TransactionsPage, BeneficiariesPage, DocumentsPage } = window.TC_PAGES_A; const { PoliciesPage, AgentsPage } = window.TC_PAGES_B; function NotificationBell({ principalId }) { const [open, setOpen] = useState(false); const [notifs, setNotifs] = useState([]); const [unread, setUnread] = useState(0); const [loading, setLoading] = useState(false); function load() { if (!principalId) return; setLoading(true); TC_API.getNotifications() .then(r => { setNotifs(r.notifications || []); setUnread(r.unread_count || 0); }) .catch(() => {}) .finally(() => setLoading(false)); } useEffect(() => { load(); const t = setInterval(load, 30000); return () => clearInterval(t); }, [principalId]); function toggle() { if (!open) load(); setOpen(v => !v); } function markRead(id) { TC_API.markNotificationRead(id) .then(() => { setNotifs(ns => ns.map(n => n.notification_id === id ? { ...n, read_at: new Date().toISOString() } : n)); setUnread(u => Math.max(0, u - 1)); }) .catch(() => {}); } function markAllRead() { TC_API.markAllNotificationsRead() .then(() => { setNotifs(ns => ns.map(n => ({ ...n, read_at: n.read_at || new Date().toISOString() }))); setUnread(0); }) .catch(() => {}); } const TYPE_LABELS = { approval_needed: "Approval needed", beneficiary_review: "Beneficiary review", rfi_created: "Information request", agent_suspended: "Agent suspended", activation_reminder:"Activation reminder", }; return (
{open && (
Notifications {unread > 0 && ( )}
{loading && notifs.length === 0 && (
Loading…
)} {!loading && notifs.length === 0 && (
No notifications yet.
)} {notifs.map(n => (
{ if (!n.read_at) markRead(n.notification_id); }} >
{TYPE_LABELS[n.type] || n.type} {!n.read_at && }
{n.title}
{n.message}
{new Date(n.created_at).toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" })}
))}
)}
); } function Dashboard() { const [session, setSession] = useState(null); // null = loading, false = unauthed, obj = ready const [page, setPage] = useState("overview"); const [pendingCount, setPendingCount] = useState(0); useEffect(() => { TC_API.init() .then(me => { setSession(me); }) .catch(err => { if (err.status === 401) { window.location.href = "/customer/login"; } else { setSession(false); } }); }, []); if (session === null) { return (
Loading…
); } if (session === false) { return (

Unable to load session. Sign in again

); } // Shared: agent list (used by Overview agent tags, Policies dropdown, Agents page). const agentsQ = useFetch(() => TC_API.getAgents(), [], { polling: 60000 }); const agents = useMemo(() => (agentsQ.data || []).map(normAgent), [agentsQ.data]); const today = useMemo( () => new Date().toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric" }), [] ); const displayName = session.full_name || session.email || "Account"; const initials = displayName.split(" ").map(w => w[0]).join("").slice(0, 2).toUpperCase(); const pageTitle = { overview: { eyebrow: "Treasury overview", h: "Overview" }, transactions: { eyebrow: "All instructions", h: "Transactions" }, beneficiaries: { eyebrow: "Approved counterparties", h: "Beneficiaries" }, documents: { eyebrow: "Compliance documents", h: "Documents" }, policies: { eyebrow: "Policy guardrails per agent", h: "Policies" }, agents: { eyebrow: `${agents.length} registered · ${agents.filter(a => a.status === "active").length} active`, h: "Agents" }, }[page] || { eyebrow: "", h: "" }; return (
{pageTitle.eyebrow} {pageTitle.h}
Live · {today}
{page === "overview" && } {page === "transactions" && } {page === "beneficiaries" && } {page === "documents" && } {page === "policies" && } {page === "agents" && }
); } ReactDOM.createRoot(document.getElementById("root")).render();