// ================= Pacing, Non-media, Changes, Login screens ================= const PacingScreen = ({ setView, tweaks, openContext }) => { const [filter, setFilter] = useStateS({ platform: null, tl: null, severity: null, q: "" }); const rows = useMemoS(() => { const base = window.PACING_ROWS || []; return base.filter(r => { if (filter.platform && r.platform !== filter.platform) return false; if (filter.tl && r.tl !== filter.tl) return false; if (filter.severity && r.sev !== filter.severity) return false; if (filter.q && !(`${r.brand_code || ""} ${r.name || ""}`.toLowerCase().includes(filter.q.toLowerCase()))) return false; return true; }); }, [filter]); return (

Budget pacing

All active campaigns across all brands · plan vs actual vs end-of-month forecast
setFilter(f => ({ ...f, q: e.target.value }))}/>
{window.PLATFORMS_LIST.map(p => (
setFilter(f => ({ ...f, platform: f.platform === p.id ? null : p.id }))}> {p.name}
))}
{[["danger", "Over"], ["warn", "Off-track"], ["success", "On track"]].map(([id, lbl]) => (
setFilter(f => ({ ...f, severity: f.severity === id ? null : id }))}> {lbl}
))}
{rows.length} campaigns
{r.brand_code} }, { key: "plat", header: "", width: 28, render: r => }, { key: "name", header: "Campaign", render: r => {r.name} }, { key: "flight", header: "Flight", width: 160, render: r => {r.start_date} → {r.end_date} }, { key: "days", header: "Days", width: 90, render: r => {r.days_elapsed}/{r.flight_days}d }, { key: "plan", header: "Plan", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.plan_budget) }, { key: "spend", header: "Spent", right: true, num: true, width: 100, render: r => window.fmt.hkdK(r.spend) }, { key: "pace", header: "Pace", width: 170, sort: (a, b) => a.pace - b.pace, render: r =>
{(r.pace * 100).toFixed(0)}%
}, { key: "forecast", header: "EOM forecast", right: true, num: true, width: 120, render: r => window.fmt.hkdK(r.forecast) }, { key: "var", header: "Var %", right: true, num: true, width: 90, render: r => 0.2 ? "var(--danger)" : r.variance < -0.2 ? "var(--warn)" : "var(--text-2)" }}>{window.fmt.pctDelta(r.variance)} }, ]} rows={rows} onRowClick={(r) => setView({ name: "campaign", brand: r.brand_code, account: r.account_id, campaign: r.id })} onRowContext={(r, e) => openContext(e, r, "campaign")} striped={tweaks.tableStyle === "striped"} lined={tweaks.tableStyle === "lined"} /> ); }; const NonMediaScreen = ({ tweaks, openContext }) => { const [filter, setFilter] = useStateS({ state: null, q: "" }); const rows = window.NON_MEDIA.filter(r => { if (filter.state && r.workflow_state !== filter.state) return false; if (filter.q && !(`${r.brand_code} ${r.vendor_name} ${r.campaign_name}`.toLowerCase().includes(filter.q.toLowerCase()))) return false; return true; }); const states = ["Draft", "Pending approval", "Approved", "Invoiced", "Paid"]; const totalGP = rows.reduce((s, r) => s + r.gross_profit, 0); const totalCost = rows.reduce((s, r) => s + r.client_cost_with_ac, 0); const overdue = rows.filter(r => r.invoice_status === "Overdue").length; return (

Non-media spending

Direct / non-biddable costs — retainers, creative production, agency fees, vendor PO
setFilter(f => ({ ...f, q: e.target.value }))}/>
Workflow {states.map(s => (
setFilter(f => ({ ...f, state: f.state === s ? null : s }))}>{s}
))}
{r.brand_code} }, { key: "vendor_name", header: "Vendor", width: 200, render: r => r.vendor_name }, { key: "service_type", header: "Service", width: 140, 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: 110, render: r => {window.fmt.hkdK(r.gross_profit)} }, { key: "invoice_status", header: "Invoice", width: 110, render: r => {r.invoice_status} }, { key: "vendor_po_status", header: "PO", width: 110, render: r => {r.vendor_po_status} }, { key: "workflow_state", header: "Workflow", width: 140, render: r => {r.workflow_state} }, ]} rows={rows} onRowContext={(r, e) => openContext(e, r, "nonmedia")} striped={tweaks.tableStyle === "striped"} lined={tweaks.tableStyle === "lined"} /> ); }; const ChangesScreen = ({ setView }) => { return (

Changes

Cross-platform audit feed · who edited what, when
{window.CHANGES.map(c => (
{c.actor.slice(0, 2).toUpperCase()}
{c.actor} {c.action} {c.campaign}
{c.prev}{c.next} · {c.brand_name} · {c.account}
{window.fmt.timeAgo(c.hours_ago)}
))}
); }; const SettingsScreen = ({ platform }) => (

{platform === "meta" ? "Meta" : platform === "google" ? "Google Ads" : "DV360"} settings

Audit trail from fabcom-etl-core.{platform}_ads_settings.*_history
This view is wired to the existing *_history tables. Placeholder shown in this prototype.
); // ---------- Login ---------- const LoginScreen = ({ onCredential, theme, errorMsg }) => { const btnRef = useRefS(); const [gisReady, setGisReady] = useStateS(!!(window.google && window.google.accounts && window.google.accounts.id)); const clientId = (window.FABCOM_CONFIG && window.FABCOM_CONFIG.googleClientId) || ""; useEffectS(() => { if (gisReady) return; const iv = setInterval(() => { if (window.google && window.google.accounts && window.google.accounts.id) { setGisReady(true); clearInterval(iv); } }, 150); return () => clearInterval(iv); }, [gisReady]); useEffectS(() => { if (!gisReady || !clientId) return; window.google.accounts.id.initialize({ client_id: clientId, callback: (resp) => onCredential && onCredential(resp.credential), ux_mode: "popup", hd: "fabcom.hk", auto_select: false, }); if (btnRef.current) { btnRef.current.innerHTML = ""; window.google.accounts.id.renderButton(btnRef.current, { type: "standard", theme: "filled_blue", size: "large", shape: "pill", text: "signin_with", logo_alignment: "left", width: 320, }); } }, [gisReady, clientId, onCredential]); return (
Fabcom
One portal for every platform, every brand, every dollar.
Settings, spend, performance, budget pacing and non-media cost across Meta, Google, DV360, TikTok, TTD and YouTube — all in one place.
{["Meta", "Google Ads", "DV360", "TikTok", "TTD", "YouTube"].map(p => ( {p} ))}
© Fabcom 2026 · ops.fabcom.app

Sign in

Access restricted to Fabcom team members (@fabcom.hk).
{!clientId && (
Google client not configured on server. Set GOOGLE_CLIENT_ID.
)} {errorMsg && (
{errorMsg}
)}
{!gisReady && clientId && (
Loading Google Sign-In…
)}
Row-level access is applied from main_am_contact and team_lead.
); }; window.PacingScreen = PacingScreen; window.NonMediaScreen = NonMediaScreen; window.ChangesScreen = ChangesScreen; window.SettingsScreen = SettingsScreen; window.LoginScreen = LoginScreen;