// Overview / Home screen
function StatCard({ label, value, sub, icon, tone = "default", spark }) {
const tones = {
default: "text-1",
red: "accent-text",
};
return (
{icon && }
{label}
{spark && (
)}
);
}
function FilterBar({ query, setQuery, business, setBusiness, currency, setCurrency, status, setStatus, businessOptions, onExport }) {
return (
);
}
function AccountsTable({ accounts, selectedId, onSelect, onContextMenu }) {
const [sortBy, setSortBy] = useState("daily_budget_hkd");
const [sortDir, setSortDir] = useState("desc");
const sorted = useMemo(() => {
const s = [...accounts];
s.sort((a,b) => {
const av = sortBy === "daily_budget_hkd" ? window.toHKD(a.daily_budget, a.ccy)
: sortBy === "ratio" ? a.ratio
: sortBy === "last" ? a.last_change_ts
: sortBy === "name" ? a.name.localeCompare(b.name) * -1
: 0;
const bv = sortBy === "daily_budget_hkd" ? window.toHKD(b.daily_budget, b.ccy)
: sortBy === "ratio" ? b.ratio
: sortBy === "last" ? b.last_change_ts
: sortBy === "name" ? 0
: 0;
return (sortDir === "desc" ? -1 : 1) * (av < bv ? -1 : av > bv ? 1 : 0);
});
return s;
}, [accounts, sortBy, sortDir]);
const toggleSort = (k) => {
if (sortBy === k) setSortDir(d => d === "desc" ? "asc" : "desc");
else { setSortBy(k); setSortDir("desc"); }
};
const caret = (k) => sortBy === k ? {sortDir==="desc"?"▾":"▴"} : null;
return (
| toggleSort("name")}>Account{caret("name")} |
Business |
Ccy |
Camps |
Adsets |
toggleSort("daily_budget_hkd")}>Daily budget (HKD){caret("daily_budget_hkd")} |
toggleSort("ratio")}>vs 30d avg{caret("ratio")} |
30d spend |
toggleSort("last")}>Last change{caret("last")} |
|
{sorted.map(a => {
const activeCamps = window.FABCOM_DATA.campaigns.filter(c => c.account_id === a.id && c.status === "ACTIVE").length;
const activeAdsets = window.FABCOM_DATA.adsets.filter(s => s.account_id === a.id && s.status === "ACTIVE").length;
const sev = a.ratio > 3 ? "red" : a.ratio > 2 ? "amber" : null;
return (
onSelect(a.id)}
onContextMenu={(e) => { e.preventDefault(); onContextMenu(e, a); }}>
|
|
{a.business} |
{a.ccy} |
{activeCamps}/{a.campaign_ids.length} |
{activeAdsets} |
{window.fmtHKD(window.toHKD(a.daily_budget, a.ccy))}
{a.ccy !== "HKD" && {window.fmtCCY(a.daily_budget, a.ccy)} }
|
|
|
{window.timeAgo(a.last_change_ts)}
{window.formatFullTs(a.last_change_ts)}
|
→
|
);
})}
Showing {sorted.length} of {window.FABCOM_DATA.meta.total_accounts} accounts
Right-click a row for BQ query, copy ID, or open in Ads Manager.
);
}
function AlertsRail({ alerts, onInvestigate }) {
const [tab, setTab] = useState("all");
const filtered = alerts.filter(a => tab === "all" || a.severity === tab).slice(0, 18);
const redCount = alerts.filter(a => a.severity === "red").length;
const amberCount = alerts.filter(a => a.severity === "amber").length;
return (
);
}
function Overview({ filters, setFilters, selectedId, onSelect, onContextMenu, onInvestigate }) {
const data = window.FABCOM_DATA;
const businesses = [...new Set(data.accounts.map(a => a.business))].sort();
const filtered = data.accounts.filter(a => {
if (filters.query && !(a.name.toLowerCase().includes(filters.query.toLowerCase()) || a.business.toLowerCase().includes(filters.query.toLowerCase()))) return false;
if (filters.business && a.business !== filters.business) return false;
if (filters.currency && a.ccy !== filters.currency) return false;
if (filters.status && a.status !== filters.status) return false;
return true;
});
const activeAccounts = data.accounts.filter(a => a.status === "ACTIVE").length;
const totalDailyHKD = data.accounts.filter(a => a.status === "ACTIVE")
.reduce((s,a) => s + window.toHKD(a.daily_budget, a.ccy), 0);
const liveCampaigns = data.campaigns.filter(c => c.status === "ACTIVE").length;
const changesToday = data.history.filter(h => Date.now() - h.ts < 24*3600e3).length;
const heroSpark = Array.from({length: 30}, (_, i) =>
data.accounts.reduce((s, a) => s + window.toHKD(a.series[i] || 0, a.ccy), 0)
);
const [toast, setToast] = useState(null);
return (
Overview · all accounts
Media Ops — Control Room
Last refreshed {window.timeAgo(data.meta.last_refreshed)} · next sync in 4h
setFilters({...filters, query:v})}
business={filters.business} setBusiness={v => setFilters({...filters, business:v})}
currency={filters.currency} setCurrency={v => setFilters({...filters, currency:v})}
status={filters.status} setStatus={v => setFilters({...filters, status:v})}
businessOptions={businesses}
onExport={() => setToast("Exporting 20 accounts to a new Google Sheet…")}
/>
{toast && setToast(null)}/>}
);
}
Object.assign(window, { Overview, StatCard, AlertsRail });