// ================= Screens: Overview, Brands list =================
const OverviewScreen = ({ setView, tweaks, openContext }) => {
const [filter, setFilter] = useStateS({ platform: null, tl: null, paceStatus: null, q: "" });
const brands = window.BRANDS.filter(b => {
if (filter.platform && !b.platforms.includes(filter.platform)) return false;
if (filter.tl && b.tl !== filter.tl) return false;
if (filter.paceStatus && b.pace_status !== filter.paceStatus) return false;
if (filter.q && !(`${b.code} ${b.name}`.toLowerCase().includes(filter.q.toLowerCase()))) return false;
return true;
});
const kpis = useMemoS(() => {
const totalDaily = brands.reduce((s, b) => s + b.daily_budget_hkd, 0);
const mtd = brands.reduce((s, b) => s + b.mtd_spend, 0);
const plan = brands.reduce((s, b) => s + b.plan_mtd, 0);
const today = brands.reduce((s, b) => s + b.accounts.reduce((s2, a) => s2 + a.today_spend, 0), 0);
const gp = brands.reduce((s, b) => s + b.gp_mtd, 0);
const sparkDaily = Array.from({ length: 30 }).map((_, i) => brands.reduce((s, b) => s + (b.daily_series[i] || 0), 0));
return {
myBrands: brands.length,
totalDaily,
today,
mtd,
plan,
pace: mtd / Math.max(1, plan),
gp,
open: window.ALERTS.length,
sparkDaily,
};
}, [brands.length]);
const paceTone = (p) => p > 1.2 ? "danger" : p < 0.7 ? "warn" : "success";
const paceLabel = (p) => p > 1.2 ? "Over" : p < 0.7 ? "Under" : "On track";
const columns = [
{
key: "brand", header: "Brand", minWidth: 220,
sort: (a, b) => a.name.localeCompare(b.name),
render: (r) => (
{r.code}
{r.name}
),
},
{
key: "platforms", header: "Platforms", width: 120,
render: (r) => (
{r.platforms.slice(0, 5).map(p =>
)}
),
},
{
key: "daily_budget_hkd", header: "Daily budget", right: true, width: 120, num: true,
sort: (a, b) => a.daily_budget_hkd - b.daily_budget_hkd,
render: (r) => window.fmt.hkd(r.daily_budget_hkd),
},
{
key: "mtd_spend", header: "MTD spend", right: true, width: 130, num: true,
sort: (a, b) => a.mtd_spend - b.mtd_spend,
render: (r) => (
{window.fmt.hkd(r.mtd_spend)}
of {window.fmt.hkdK(r.plan_mtd)}
),
},
{
key: "pace", header: "Pace", width: 150,
sort: (a, b) => a.pace - b.pace,
render: (r) => (
{(r.pace * 100).toFixed(0)}%
),
},
{
key: "spend_7d", header: "7d", right: true, width: 120, num: true,
sort: (a, b) => a.spend_7d - b.spend_7d,
render: (r) => (
{window.fmt.hkdK(r.spend_7d)}
),
},
{
key: "spend_30d", header: "30d", right: true, width: 80, num: true,
sort: (a, b) => a.spend_30d - b.spend_30d,
render: (r) => window.fmt.hkdK(r.spend_30d),
},
{
key: "non_media_mtd", header: "Non-media MTD", right: true, width: 130, num: true,
sort: (a, b) => a.non_media_mtd - b.non_media_mtd,
render: (r) => window.fmt.hkdK(r.non_media_mtd),
},
{
key: "gp_mtd", header: "GP MTD", right: true, width: 110, num: true,
sort: (a, b) => a.gp_mtd - b.gp_mtd,
render: (r) => (
{window.fmt.hkdK(r.gp_mtd)} ({((r.gp_mtd / Math.max(1, r.mtd_spend)) * 100).toFixed(0)}%)
),
},
{
key: "last_change", header: "Last change", right: true, width: 100, num: true,
sort: (a, b) => a.last_change - b.last_change,
render: (r) => {window.fmt.timeAgo(r.last_change)},
},
{
key: "alerts", header: "Alerts", width: 72, right: true, num: true,
sort: (a, b) => a.alerts - b.alerts,
render: (r) => r.alerts > 0 ? {r.alerts} : —,
},
];
return (
Hi Vincent — here's your fleet
Wed, Apr 22, 2026 · data refreshed 18m ago via Fivetran
{/* KPIs */}
b.platforms)).size} platforms`}/>
a.severity === "danger").length} critical`}/>
{/* Filter bar */}
setFilter(f => ({ ...f, q: e.target.value }))} style={{ width: 200 }}/>
Platform
{window.PLATFORMS_LIST.map(p => (
setFilter(f => ({ ...f, platform: f.platform === p.id ? null : p.id }))}>
{p.name}
))}
Pace
{[["success", "On track"], ["warn", "Under"], ["danger", "Over"]].map(([id, lbl]) => (
setFilter(f => ({ ...f, paceStatus: f.paceStatus === id ? null : id }))}>
{lbl}
))}
Team lead
{window.PEOPLE.TLS.map(tl => (
setFilter(f => ({ ...f, tl: f.tl === tl ? null : tl }))}>
{tl}
))}
{brands.length} of {window.BRANDS.length} brands
r.code}
onRowClick={r => setView({ name: "brand", brand: r.code })}
onRowContext={(r, e) => openContext(e, r, "brand")}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
);
};
window.OverviewScreen = OverviewScreen;