// ================= Brand detail screen =================
const BrandDetailScreen = ({ brandCode, setView, tweaks, openContext, onMounted }) => {
const brand = window.BRANDS.find(b => b.code === brandCode);
const [tab, setTab] = useStateS("overview");
const [, setTick] = useStateS(0);
const [loadingDetail, setLoadingDetail] = useStateS(false);
useEffectS(() => {
if (!brand) return;
if ((brand.accounts || []).length > 0) return;
if (!window.__fabcomLoadBrandDetail) return;
setLoadingDetail(true);
window.__fabcomLoadBrandDetail(brandCode)
.then(() => { setLoadingDetail(false); setTick(x => x + 1); onMounted && onMounted(); })
.catch(() => setLoadingDetail(false));
}, [brandCode]);
if (!brand) return
Brand not found or no access.
;
const accounts = brand.accounts || [];
const allCampaigns = accounts.flatMap(a => a.campaigns || []);
const brandNonMediaAll = window.NON_MEDIA.filter(r => r.brand_code === brand.code);
const nmClientTotal = brandNonMediaAll.reduce((s, r) => s + r.client_cost_with_ac, 0);
const platSplit = useMemoS(() => {
const map = {};
for (const a of accounts) {
const spend = a.campaigns.reduce((s, c) => s + c.mtd_spend, 0);
map[a.platform] = (map[a.platform] || 0) + spend;
}
const palette = { meta: "#1877f2", google: "#ea4335", dv360: "#34a853", tiktok: "#b5b8c0", ttd: "#ff6e00", youtube: "#ff0000" };
return Object.entries(map).map(([k, v]) => ({ label: k, value: v, color: palette[k] }));
}, [brand.code]);
const paceTone = brand.pace_status;
return (
{/* Sticky brand header */}
{brand.code}
{brand.name}
{brand.industry}
{brand.code}
AM {brand.am}
Team Lead {brand.tl}
Platforms
{brand.platforms.map(p =>
)}
Media MTD
{window.fmt.hkdK(brand.mtd_spend)}
of {window.fmt.hkdK(brand.plan_mtd)}
Non-media MTD
{window.fmt.hkdK(nmClientTotal)}
{brandNonMediaAll.length} records
MTD pace
{(brand.pace * 100).toFixed(0)}%
Total {window.fmt.hkdK(brand.mtd_spend + nmClientTotal)}
{tab === "overview" && (() => {
const brandNonMedia = window.NON_MEDIA.filter(r => r.brand_code === brand.code);
const nmClient = brandNonMedia.reduce((s, r) => s + r.client_cost_with_ac, 0);
const nmGP = brandNonMedia.reduce((s, r) => s + r.gross_profit, 0);
const nmOverdue = brandNonMedia.filter(r => r.invoice_status === "Overdue").length;
const totalInvestment = brand.mtd_spend + nmClient;
return (
Spend by platform · MTD
{platSplit.map(it => (
{it.label}
{window.fmt.hkdK(it.value)}
))}
Daily spend · last 30 days
brand.plan_mtd / 30) },
]}
/>
{/* Media vs Non-media split */}
Media vs non-media · MTD
{window.fmt.hkdK(totalInvestment)}
total client investment
Media {window.fmt.hkdK(brand.mtd_spend)}
Non-media {window.fmt.hkdK(nmClient)}
Non-media GP{window.fmt.hkdK(nmGP)}
Records{brandNonMedia.length}
Overdue 0 ? "var(--danger)" : "var(--text)" }}>{nmOverdue}
{/* Recent non-media records */}
Recent non-media records
retainers · production · agency fees · vendor PO
{r.vendor_name} },
{ key: "service_type", header: "Service", width: 130, render: r => {r.service_type} },
{ key: "period", header: "Period", width: 160, render: r => {r.start_date} → {r.end_date} },
{ key: "client_cost_with_ac", header: "Client cost", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.client_cost_with_ac) },
{ key: "gross_profit", header: "GP", right: true, num: true, width: 90, render: r => {window.fmt.hkdK(r.gross_profit)} },
{ key: "invoice_status", header: "Invoice", width: 100, render: r => {r.invoice_status} },
{ key: "workflow_state", header: "Workflow", width: 130, render: r => {r.workflow_state} },
]}
rows={brandNonMedia.slice(0, 5)}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
Top campaigns by MTD spend
click a row to drill
},
{ key: "name", header: "Campaign", render: r => {r.name} },
{ key: "status", header: "", width: 80, render: r => },
{ key: "mtd_spend", header: "MTD spend", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.mtd_spend) },
{ key: "impr", header: "Impr.", right: true, num: true, width: 100, render: r => window.fmt.int(r.impressions) },
{ key: "ctr", header: "CTR", right: true, num: true, width: 70, render: r => window.fmt.pct(r.ctr, 2) },
{ key: "cpc", header: "CPC", right: true, num: true, width: 70, render: r => window.fmt.money2(r.cpc) },
{ key: "spark", header: "Trend", width: 100, render: r => },
]}
rows={allCampaigns.slice().sort((a,b) => b.mtd_spend - a.mtd_spend).slice(0, 8)}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
onRowClick={(r) => setView({ name: "campaign", brand: brand.code, account: r.account_id, campaign: r.id })}
onRowContext={(r, e) => openContext(e, r, "campaign")}
/>
);
})()}
{tab === "accounts" && (
},
{ key: "name", header: "Account", render: r => },
{ key: "daily_budget_hkd", header: "Daily budget", right: true, num: true, width: 110, render: r => window.fmt.hkd(r.daily_budget_hkd) },
{ key: "today_spend", header: "Today", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.today_spend) },
{ key: "spend_7d", header: "7d", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.spend_7d) },
{ key: "spend_30d", header: "30d", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.spend_30d) },
{ key: "ctr", header: "CTR", right: true, num: true, width: 70, render: r => window.fmt.pct(r.ctr, 2) },
{ key: "cpc", header: "CPC", right: true, num: true, width: 70, render: r => window.fmt.money2(r.cpc) },
{ key: "cpa", header: "CPA", right: true, num: true, width: 80, render: r => window.fmt.money2(r.cpa) },
{ key: "roas", header: "ROAS", right: true, num: true, width: 70, render: r => r.roas.toFixed(2) + "x" },
]}
rows={accounts}
onRowClick={(r) => setView({ name: "account", brand: brand.code, account: r.id })}
onRowContext={(r, e) => openContext(e, r, "account")}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
)}
{tab === "campaigns" && (
},
{ key: "name", header: "Campaign", render: r => {r.name} },
{ key: "status", header: "Status", width: 90, render: r => },
{ key: "daily_budget", header: "Daily", right: true, num: true, width: 90, render: r => window.fmt.hkd(r.daily_budget) },
{ key: "mtd_spend", header: "MTD", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.mtd_spend) },
{ key: "impressions", header: "Impr.", right: true, num: true, width: 100, render: r => window.fmt.int(r.impressions) },
{ key: "clicks", header: "Clicks", right: true, num: true, width: 90, render: r => window.fmt.int(r.clicks) },
{ key: "ctr", header: "CTR", right: true, num: true, width: 70, render: r => window.fmt.pct(r.ctr, 2) },
{ key: "cpm", header: "CPM", right: true, num: true, width: 80, render: r => window.fmt.money2(r.cpm) },
{ key: "cpc", header: "CPC", right: true, num: true, width: 70, render: r => window.fmt.money2(r.cpc) },
{ key: "conv", header: "Conv.", right: true, num: true, width: 70, render: r => window.fmt.int(r.conv) },
{ key: "cpa", header: "CPA", right: true, num: true, width: 80, render: r => window.fmt.money2(r.cpa) },
]}
rows={allCampaigns}
onRowClick={(r) => setView({ name: "campaign", brand: brand.code, account: r.account_id, campaign: r.id })}
onRowContext={(r, e) => openContext(e, r, "campaign")}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
)}
{tab === "pacing" && (
},
{ key: "name", header: "Campaign", render: r => {r.name} },
{ key: "flight", header: "Flight", width: 110, render: r => {r.days_elapsed}/{r.flight_days}d },
{ key: "plan", header: "Plan", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.plan_budget) },
{ key: "spend", header: "Spent", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.spend) },
{ key: "pace", header: "Pace", width: 170, render: r => {
const p = r.spend / Math.max(1, r.plan_budget * (r.days_elapsed / r.flight_days));
const t = p > 1.2 ? "danger" : p < 0.7 ? "warn" : "success";
return ;
} },
{ key: "forecast", header: "EOM forecast", right: true, num: true, width: 120, render: r => window.fmt.hkdK(r.spend / Math.max(1, r.days_elapsed) * r.flight_days) },
{ key: "var", header: "Variance", right: true, num: true, width: 90, render: r => {
const f = r.spend / Math.max(1, r.days_elapsed) * r.flight_days;
const v = (f - r.plan_budget) / Math.max(1, r.plan_budget);
return 0.2 ? "var(--danger)" : v < -0.2 ? "var(--warn)" : "var(--text-2)" }}>{window.fmt.pctDelta(v)};
} },
]}
rows={allCampaigns}
onRowClick={(r) => setView({ name: "campaign", brand: brand.code, account: r.account_id, campaign: r.id })}
onRowContext={(r, e) => openContext(e, r, "campaign")}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
)}
{tab === "nonmedia" && (
r.vendor_name },
{ key: "service_type", header: "Service", render: r => {r.service_type} },
{ key: "campaign_name", header: "Campaign", render: r => {r.campaign_name} },
{ key: "period", header: "Period", width: 170, render: r => {r.start_date} → {r.end_date} },
{ key: "net_vendor_cost", header: "Net vendor", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.net_vendor_cost) },
{ key: "client_cost_with_ac", header: "Client cost", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.client_cost_with_ac) },
{ key: "gross_profit", header: "GP", right: true, num: true, width: 100, render: r => {window.fmt.hkdK(r.gross_profit)} },
{ key: "invoice_status", header: "Invoice", width: 110, render: r => {r.invoice_status} },
{ key: "workflow_state", header: "Workflow", width: 130, render: r => {r.workflow_state} },
]}
rows={window.NON_MEDIA.filter(r => r.brand_code === brand.code)}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
)}
{tab === "changes" && (
{window.CHANGES.filter(c => c.brand_code === brand.code).slice(0, 24).map(c => (
{c.actor.slice(0, 2).toUpperCase()}
{c.actor} {c.action} {c.campaign}
{c.prev} → {c.next} · {c.platform.toUpperCase()} · {c.account}
{window.fmt.timeAgo(c.hours_ago)}
))}
)}
{tab === "creatives" && (
{Array.from({ length: 18 }).map((_, i) => {
const c = allCampaigns[i % allCampaigns.length];
const palette = ["#1a2340", "#0ea5a0", "#e5e8ee", "#ff6e00", "#8b5cf6", "#ec4899"];
const bg = palette[i % palette.length];
return (
{brand.code}_AD_{String(i+1).padStart(3, "0")}
Spend{window.fmt.hkdK(randCr(i))}
CTR{(0.8 + (i%10)*0.15).toFixed(2)}%
ROAS{(1.2 + (i%9)*0.3).toFixed(2)}x
);
})}
)}
);
};
const randCr = (i) => 3000 + (i * 2213) % 48000;
window.BrandDetailScreen = BrandDetailScreen;