// 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 (
);
}
if (session === false) {
return (
);
}
// 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 (
{page === "overview" &&
}
{page === "transactions" &&
}
{page === "beneficiaries" &&
}
{page === "documents" &&
}
{page === "policies" &&
}
{page === "agents" &&
}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();