/* global React, Icon, Avatar, Pill, Badge, Checkbox */ /* Data: mock data + Table, FeedItem, ListRow, MetricRow */ const { useState, useMemo } = React; /* ---------- Mock data ---------- */ const MOCK = { audits: [ { id: "AUD-0142", name: "Loop 14 — Northbeam media mix", client: "Northbeam", status: "active", phase: "Diagnose", lead: "Inés Vidal", office: "MIA", spend: 280000, days: 11, sent: "2 hours ago", tone: "info" }, { id: "AUD-0141", name: "BrandLens — Avenue9 reposition", client: "Avenue9", status: "active", phase: "Simulate", lead: "Marco Ferraro", office: "NYC", spend: 145000, days: 6, sent: "yesterday", tone: "info" }, { id: "AUD-0140", name: "HIVE deployment — Caja Andina", client: "Caja Andina", status: "blocked", phase: "Simulate", lead: "Daniela Reyes", office: "SCL", spend: 92000, days: 18, sent: "3 days ago", tone: "warn" }, { id: "AUD-0139", name: "Catalyst — Soltera retention", client: "Soltera", status: "active", phase: "Prescribe", lead: "Inés Vidal", office: "MIA", spend: 412000, days: 22, sent: "1 week ago", tone: "info" }, { id: "AUD-0138", name: "Milton.AI — Magnet ops index", client: "Magnet Global", status: "review", phase: "Prescribe", lead: "Tomás Otero", office: "MIA", spend: 60000, days: 4, sent: "this morning", tone: "warn" }, { id: "AUD-0137", name: "BIGSights — Vela cohort study", client: "Vela", status: "shipped", phase: "Shipped", lead: "Marco Ferraro", office: "NYC", spend: 188000, days: 9, sent: "Apr 28", tone: "pos" }, { id: "AUD-0136", name: "BrandLens — Quirón comms refresh", client: "Quirón", status: "shipped", phase: "Shipped", lead: "Daniela Reyes", office: "SCL", spend: 75000, days: 14, sent: "Apr 21", tone: "pos" }, { id: "AUD-0135", name: "Catalyst — Borealis spend reset", client: "Borealis", status: "archived", phase: "Archived", lead: "Tomás Otero", office: "MIA", spend: 524000, days: 28, sent: "Apr 03", tone: "neutral" }, ], inbox: [ { id: 1, from: "Milton.AI", title: "Loop 14 finished diagnose phase", body: "Northbeam diagnostic complete — 4 high-confidence issues, 2 worth simulating.", time: "12m", icon: , tone: "grad", unread: true, tag: "milton" }, { id: 2, from: "Marco Ferraro", title: "Avenue9 — pull rates updated", body: "Latest CPMs landed. Want me to rerun the simulator before the 3pm review?", time: "38m", unread: true, tag: "team" }, { id: 3, from: "HIVE", title: "Agent C-04 escalated — needs review", body: "Caja Andina ingestion paused; schema drift on `customers.json` since Friday.", time: "1h", icon: , tone: "warn", unread: true, tag: "agent" }, { id: 4, from: "Daniela Reyes", title: "Quirón handoff doc, v2", body: "Final read on the comms calendar. Ready for client review.", time: "2h", tag: "team" }, { id: 5, from: "Catalyst", title: "Weekly office hours · MIA", body: "Inés, Tomás, and 3 others added to Friday 11:00.", time: "3h", icon: , tone: "info", tag: "system" }, { id: 6, from: "Magnet Global", title: "Network sync — Q2 targets", body: "Reading list and 2024 brief enclosed.", time: "yesterday", tag: "external" }, { id: 7, from: "BrandLens", title: "Avenue9 sentiment shift detected", body: "Brand sentiment moved +3.4 in 7 days. Driven by earned coverage.", time: "yesterday", icon: , tone: "pos", tag: "agent" }, { id: 8, from: "Tomás Otero", title: "Borealis archive — confirmed", body: "Closed out the Borealis engagement. All artifacts in /archive.", time: "Apr 28", tag: "team" }, ], members: [ { name: "Inés Vidal", role: "Strategy lead", office: "MIA", email: "ines@bnmrglvz.com" }, { name: "Marco Ferraro", role: "Senior strategist", office: "NYC", email: "marco@bnmrglvz.com" }, { name: "Daniela Reyes", role: "Strategist", office: "SCL", email: "daniela@bnmrglvz.com" }, { name: "Tomás Otero", role: "Engineer · HIVE", office: "MIA", email: "tomas@bnmrglvz.com" }, ], // 30-day series s30: [124, 132, 128, 145, 152, 161, 158, 173, 180, 168, 176, 195, 210, 198, 206, 218, 224, 213, 232, 248, 240, 255, 271, 268, 282, 295, 305, 312, 326, 340], s30b: [98, 102, 110, 105, 112, 121, 118, 132, 138, 130, 140, 148, 158, 156, 162, 170, 174, 168, 180, 192, 188, 200, 212, 210, 220, 228, 240, 246, 256, 268], s12m: ["May","Jun","Jul","Aug","Sep","Oct","Nov","Dec","Jan","Feb","Mar","Apr"], bars12: [142, 168, 184, 156, 198, 220, 245, 278, 262, 308, 326, 384], }; /* ---------- Table ---------- */ function Table({ columns, rows, selectable, onRowClick, sticky = true }) { const [sel, setSel] = useState(new Set()); const [sort, setSort] = useState({ key: null, dir: "asc" }); const sorted = useMemo(() => { if (!sort.key) return rows; const r = [...rows].sort((a, b) => { const av = a[sort.key], bv = b[sort.key]; if (av == null) return 1; if (bv == null) return -1; if (typeof av === "number") return sort.dir === "asc" ? av - bv : bv - av; return sort.dir === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); }); return r; }, [rows, sort]); const allChecked = sel.size === rows.length && rows.length > 0; const someChecked = sel.size > 0 && !allChecked; const toggleAll = () => { if (allChecked || someChecked) setSel(new Set()); else setSel(new Set(rows.map(r => r.id))); }; const toggleOne = (id) => { const next = new Set(sel); next.has(id) ? next.delete(id) : next.add(id); setSel(next); }; const onSort = (key) => { setSort(s => s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: "asc" }); }; return (
{selectable && sel.size > 0 && (
{sel.size} selected
)}
{selectable && ( )} {columns.map((c) => ( ))} {sorted.map((r, ri) => ( onRowClick && onRowClick(r)} style={{ borderBottom: "1px solid var(--line)", cursor: onRowClick ? "pointer" : "default", background: sel.has(r.id) ? "var(--bg-2)" : "transparent", transition: "background 150ms ease", }} onMouseEnter={(e) => { if (!sel.has(r.id)) e.currentTarget.style.background = "var(--bg-2)"; }} onMouseLeave={(e) => { if (!sel.has(r.id)) e.currentTarget.style.background = "transparent"; }} > {selectable && ( )} {columns.map((c) => ( ))} ))}
c.sortable && onSort(c.key)}> {c.label} {c.sortable && ( {sort.key === c.key ? (sort.dir === "asc" ? : ) : } )}
{ e.stopPropagation(); toggleOne(r.id); }}> toggleOne(r.id)}/> {c.render ? c.render(r) : r[c.key]}
); } const thStyle = { padding: "10px var(--d-cell-px)", fontFamily: "var(--mono)", fontSize: 10, fontWeight: 700, letterSpacing: "0.08em", textTransform: "uppercase", color: "var(--ink-3)", whiteSpace: "nowrap", }; const tdStyle = { padding: "var(--d-cell-py) var(--d-cell-px)", fontSize: 13, color: "var(--ink)", verticalAlign: "middle", height: "var(--d-row-h)", }; const btnGhost = { border: "1px solid var(--line)", background: "var(--bg)", color: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12, fontWeight: 500, padding: "5px 10px", borderRadius: "var(--r-sharp)", cursor: "pointer", }; /* ---------- Feed item ---------- */ function FeedItem({ item, onClick, active }) { return (
{ if (!active) e.currentTarget.style.background = "var(--bg-2)"; }} onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = "transparent"; }}>
{item.icon ? ( {item.icon} ) : ( )}
{item.from} {item.time}
{item.title}
{item.body &&
{item.body}
}
{item.unread && }
); } /* ---------- Status pill (audit-specific colors) ---------- */ function StatusPill({ status }) { const map = { active: { tone: "info", label: "Active" }, blocked: { tone: "warn", label: "Blocked" }, review: { tone: "warn", label: "In review" }, shipped: { tone: "pos", label: "Shipped" }, archived: { tone: "neutral", label: "Archived" }, }; const m = map[status] || map.active; return {m.label}; } window.MOCK = MOCK; Object.assign(window, { Table, FeedItem, StatusPill });