// ================= Account detail, Campaign detail =================
const AccountDetailScreen = ({ brandCode, accountId, setView, tweaks, openContext }) => {
const brand = window.BRANDS.find(b => b.code === brandCode);
const account = brand?.accounts.find(a => a.id === accountId);
const [tab, setTab] = useStateS("campaigns");
const [attribution, setAttribution] = useStateS(7);
const [expanded, setExpanded] = useStateS({});
if (!account) return null;
const daily = Array.from({ length: 30 }).map((_, i) => account.campaigns.reduce((s, c) => s + (c.daily_series[i] || 0), 0));
return (
{account.name}
{account.id}
·
{brand.name}
{tab === "campaigns" && (
|
Campaign / Ad set / Ad |
Status |
Daily |
MTD spend |
Impr. |
CTR |
CPC |
Conv |
Trend |
{account.campaigns.map(c => (
setView({ name: "campaign", brand: brandCode, account: accountId, campaign: c.id })}
onContextMenu={(e) => { e.preventDefault(); openContext(e, c, "campaign"); }}>
| { e.stopPropagation(); setExpanded(x => ({ ...x, [c.id]: !x[c.id] })); }}>
|
{c.name} |
|
{window.fmt.hkd(c.daily_budget)} |
{window.fmt.hkdK(c.mtd_spend)} |
{window.fmt.int(c.impressions)} |
{window.fmt.pct(c.ctr, 2)} |
{window.fmt.money2(c.cpc)} |
{window.fmt.int(c.conv)} |
|
{expanded[c.id] && Array.from({ length: 3 }).map((_, i) => (
|
├ {c.name.split("_")[1] || "SET"}_{String(i+1).padStart(2,"0")} |
|
{window.fmt.hkd(c.daily_budget / 3)} |
{window.fmt.hkdK(c.mtd_spend / 3)} |
{window.fmt.int(c.impressions / 3)} |
{window.fmt.pct(c.ctr * (0.8 + i*0.1), 2)} |
{window.fmt.money2(c.cpc * (0.9 + i*0.05))} |
{window.fmt.int(c.conv / 3)} |
|
))}
))}
)}
{tab === "performance" && (
Daily spend · 30d
markers show budget / bid changes
v * 0.02 * 50) },
]}
/>
)}
{tab === "conversions" && (
Attribution:
{[7, 14, 28].map(d => )}
{r.event} },
{ key: "count", header: "Count", right: true, num: true, render: r => window.fmt.int(r.count) },
{ key: "cpa", header: "CPA", right: true, num: true, render: r => window.fmt.money2(r.cpa) },
{ key: "roas", header: "ROAS", right: true, num: true, render: r => `${r.roas.toFixed(2)}x` },
{ key: "rate", header: "Rate", right: true, num: true, render: r => window.fmt.pct(r.rate, 2) },
]}
rows={[
{ id: 1, event: "purchase", count: Math.round(account.conv * 0.35 * (attribution/7)), cpa: account.cpa * 1.2, roas: account.roas * 1.1, rate: 0.021 * (attribution/7) },
{ id: 2, event: "add_to_cart", count: Math.round(account.conv * 1.6 * (attribution/7)), cpa: account.cpa * 0.3, roas: 0, rate: 0.085 * (attribution/7) },
{ id: 3, event: "lead", count: Math.round(account.conv * 0.8 * (attribution/7)), cpa: account.cpa * 0.6, roas: 0, rate: 0.055 * (attribution/7) },
{ id: 4, event: "view_content", count: Math.round(account.conv * 4.2 * (attribution/7)), cpa: account.cpa * 0.1, roas: 0, rate: 0.210 * (attribution/7) },
{ id: 5, event: "sign_up", count: Math.round(account.conv * 0.25 * (attribution/7)), cpa: account.cpa * 1.8, roas: 0, rate: 0.012 * (attribution/7) },
]}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
)}
{tab === "creatives" && (
{Array.from({ length: 12 }).map((_, i) => (
{brand.code}_AD_{String(i+1).padStart(3, "0")}
Spend{window.fmt.hkdK(2500 + i*1800)}
CTR{(0.8 + i*0.15).toFixed(2)}%
))}
)}
{tab === "changes" && (
{window.CHANGES.filter(c => c.brand_code === brandCode).slice(0, 12).map(c => (
{c.actor.slice(0, 2).toUpperCase()}
{c.actor} {c.action}
{c.prev} → {c.next}
{window.fmt.timeAgo(c.hours_ago)}
))}
)}
{tab === "targeting" && (
Audiences
{["Lookalike 1%", "Website visitors 30d", "Purchasers 180d", "Interest: Parents", "Interest: Pet owners"].map((a, i) => (
{a}
{(120000 + i*45000).toLocaleString()} reach
))}
Geo · demo
Top regions
{[["HK", 62], ["MO", 14], ["SG", 12], ["TW", 8], ["MY", 4]].map(([g, pct]) => (
))}
Gender · Age
F 55% M 42%O 3%
)}
);
};
// ---------- Campaign drill ----------
const CampaignDetailScreen = ({ brandCode, accountId, campaignId, setView, tweaks }) => {
const brand = window.BRANDS.find(b => b.code === brandCode);
const account = brand?.accounts.find(a => a.id === accountId) || brand?.accounts.find(a => a.campaigns.some(c => c.id === campaignId));
const c = account?.campaigns.find(x => x.id === campaignId);
if (!c) return Campaign not found
;
const adsetLabel = c.platform === "google" ? "Ad group" : c.platform === "dv360" || c.platform === "ttd" ? "Insertion order" : "Ad set";
const pace = c.spend / Math.max(1, c.plan_budget * (c.days_elapsed / c.flight_days));
const paceTone = pace > 1.2 ? "danger" : pace < 0.7 ? "warn" : "success";
return (
{c.name}
Bid {c.bid_strategy}
Budget {c.budget_type} {window.fmt.hkd(c.daily_budget)}/d
Flight {c.start_date} → {c.end_date}
Objective {c.objective}
Performance · 30d
spend + impressions + clicks + conversions
v * 0.02 * 80) },
{ label: "Conv", color: "#f59e0b", data: c.daily_series.map(v => v * 0.001 * 200) },
]}
/>
Pacing
{(pace * 100).toFixed(0)}%
{window.fmt.hkdK(c.spend)} of {window.fmt.hkdK(c.plan_budget)}
Days remaining
{c.days_remaining} / {c.flight_days}d
EOM forecast
{window.fmt.hkdK(c.spend / Math.max(1, c.days_elapsed) * c.flight_days)}
{adsetLabel}s breakdown
{r.name} },
{ key: "status", header: "Status", width: 90, render: r => },
{ key: "spend", header: "MTD spend", right: true, num: true, width: 110, render: r => window.fmt.hkdK(r.spend) },
{ key: "impr", header: "Impr.", right: true, num: true, width: 100, render: r => window.fmt.int(r.impr) },
{ key: "ctr", header: "CTR", right: true, num: true, width: 80, render: r => window.fmt.pct(r.ctr, 2) },
{ key: "cpa", header: "CPA", right: true, num: true, width: 80, render: r => window.fmt.money2(r.cpa) },
{ key: "spark", header: "Trend", width: 90, render: r => v * r.mul)}/> },
]}
rows={Array.from({ length: 4 }).map((_, i) => ({
id: i,
name: `${c.name.split("_")[1] || "SET"}_${String(i+1).padStart(2, "0")}`,
status: i === 3 ? { label: "Paused", tone: "info" } : c.status,
spend: c.mtd_spend * [0.38, 0.30, 0.22, 0.10][i],
impr: Math.round(c.impressions * [0.38, 0.30, 0.22, 0.10][i]),
ctr: c.ctr * [1.1, 0.95, 1.2, 0.7][i],
cpa: c.cpa * [0.9, 1.1, 0.8, 1.6][i],
mul: [1.1, 0.95, 1.2, 0.6][i],
}))}
striped={tweaks.tableStyle === "striped"}
lined={tweaks.tableStyle === "lined"}
/>
);
};
window.AccountDetailScreen = AccountDetailScreen;
window.CampaignDetailScreen = CampaignDetailScreen;