/* ===========================================================================
launch-hubs-a.jsx — Pipelines + Marketing hubs.
Registers into window.LCC_PAGES. Uses helpers from launch-app.jsx.
=========================================================================== */
const { LccPage, LccTabs, LCC_TABS, Section, Avatar, Bar, EngineChip } = window;
/* ---------------------------------------------------------------------------
PIPELINES — one stage board, audience switcher, + Talent Advance + Analytics.
Absorbs People + Artist/Sponsor/Vendor/Crew/Partner + BKK + Pipeline Analytics.
--------------------------------------------------------------------------- */
const AUDIENCES = [
{ id: "artist", label: "Artists" }, { id: "sponsor", label: "Sponsors" }, { id: "vendor", label: "Vendors" },
{ id: "crew", label: "Crew" }, { id: "partner", label: "Partners" },
];
const STAGE_TONE = { Inquiry: "var(--pvt-text-dim)", Review: "#E0B638", Confirmed: "#10B5C7", Advanced: "#D8288C" };
function StageBoard() {
const [aud, setAud] = React.useState("artist");
const [items, setItems] = window.useDB("pipelineItems");
const [stages] = window.useDB("pipelineStages");
const team = window.ARGT_DB.get("launchTeam") || [];
const here = items.filter((i) => i.audience === aud);
const [drag, setDrag] = React.useState(null);
const [over, setOver] = React.useState(null);
const [edit, setEdit] = React.useState(null);
const moveTo = (id, st) => setItems((xs) => xs.map((x) => x.id === id ? { ...x, stage: st, updatedAt: "now" } : x));
const saveEdit = () => {
setItems((xs) => xs.map((x) => x.id === edit.id ? { ...edit, updatedAt: "now" } : x));
if (window.argtPing) window.argtPing(edit.name + " updated");
setEdit(null);
};
const removeItem = () => {
setItems((xs) => xs.filter((x) => x.id !== edit.id));
if (window.argtPing) window.argtPing(edit.name + " removed from pipeline");
setEdit(null);
};
return (
{AUDIENCES.map((a) => (
))}
Drag a card to move it between stages
{stages.map((st) => {
const col = here.filter((i) => i.stage === st);
return (
{ e.preventDefault(); if (over !== st) setOver(st); }}
onDragLeave={() => setOver((o) => o === st ? null : o)}
onDrop={() => { if (drag) moveTo(drag, st); setDrag(null); setOver(null); }}
style={{ background: over === st ? "var(--pvt-surface-mid)" : "var(--pvt-surface)", border: "1px solid " + (over === st ? "var(--role-accent)" : "var(--pvt-border-light)"), borderRadius: "var(--r-lg)", padding: 12, display: "flex", flexDirection: "column", gap: 10, minHeight: 220, transition: "border-color 120ms, background 120ms" }}>
{st}
{col.length}
{col.map((it) => {
const owner = team.find((m) => m.name.split(" ")[0] === it.owner) || { name: it.owner };
return (
{ setDrag(it.id); e.dataTransfer.effectAllowed = "move"; }}
onDragEnd={() => { setDrag(null); setOver(null); }}
onClick={() => setEdit({ ...it })}
title="Drag to move · click to edit"
style={{ background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", padding: 12, cursor: "grab", opacity: drag === it.id ? 0.4 : 1, display: "flex", flexDirection: "column", gap: 8 }}>
{it.name}
{it.meta}
{it.value}
{it.updatedAt}
);
})}
{col.length === 0 &&
Empty
}
);
})}
setEdit(null)} onSubmit={saveEdit} submitLabel="Save">
{edit && (<>
setEdit({ ...edit, name: e.target.value })} />
setEdit({ ...edit, value: e.target.value })} />
setEdit({ ...edit, meta: e.target.value })} />
>)}
);
}
function TalentAdvance() {
const [rows, setRows] = window.useDB("talentAdvance");
const tone = { ready: "approved", "in-progress": "review", "at-risk": "rejected" };
const CYCLE = { travel: ["pending", "booked", "n/a"], hospitality: ["pending", "done"], techNeeds: ["pending", "received"] };
const cycle = (id, field) => setRows((xs) => xs.map((r) => {
if (r.id !== id) return r;
const opts = CYCLE[field]; const i = opts.indexOf(r[field]);
return { ...r, [field]: opts[(i + 1) % opts.length] };
}));
const cycleStatus = (id) => setRows((xs) => xs.map((r) => r.id === id ? { ...r, status: ({ ready: "in-progress", "in-progress": "at-risk", "at-risk": "ready" })[r.status] } : r));
const cell = (r, field) => {
const v = r[field];
const ok = v === "done" || v === "signed" || v === "received" || v === "booked";
const na = v === "n/a";
return ;
};
return (
{rows.length} ARTISTS · TAP A CELL}>
ArtistSetFeeTravelHosp.TechStatus
{rows.map((r) => (
{r.artist}
{r.stage} · {r.setTime}
{r.setTime.split(" ")[0]}
{r.fee}
{cell(r, "travel")}{cell(r, "hospitality")}{cell(r, "techNeeds")}
))}
);
}
function PipelineAnalytics() {
const [items] = window.useDB("pipelineItems");
const [stages] = window.useDB("pipelineStages");
const byStage = stages.map((s) => ({ stage: s, n: items.filter((i) => i.stage === s).length }));
const max = Math.max(...byStage.map((b) => b.n), 1);
const total = items.length;
const advanced = items.filter((i) => i.stage === "Advanced").length;
return (
{byStage.map((b) => (
))}
);
}
function PipelinesHub({ tab, go }) {
const [items, setItems] = window.useDB("pipelineItems");
const [showAdd, setShowAdd] = React.useState(false);
const [d, setD] = React.useState({ audience: "artist", name: "", meta: "", value: "" });
const add = () => {
if (!d.name.trim()) return;
setItems((xs) => [...xs, { id: d.audience + "-" + Date.now(), audience: d.audience, name: d.name.trim(), stage: "Inquiry", owner: "Kam", value: d.value || "\u2014", meta: d.meta, updatedAt: "now" }]);
if (window.argtPing) window.argtPing(d.name.trim() + " added to pipeline");
setShowAdd(false); setD({ audience: "artist", name: "", meta: "", value: "" });
};
return (
>}>
go("pipelines", t)} />
{tab === "board" && }
{tab === "talent" && }
{tab === "analytics" && }
setShowAdd(false)} onSubmit={add} submitLabel="Add">
setD({ ...d, value: e.target.value })} />
setD({ ...d, name: e.target.value })} />
setD({ ...d, meta: e.target.value })} />
);
}
/* ---------------------------------------------------------------------------
MARKETING — Comms calendar · Content studio · Planners · Public surfaces.
--------------------------------------------------------------------------- */
const CHANNEL_TONE = { Instagram: "#D8288C", Email: "#10B5C7", TikTok: "#7A4FBF", Press: "#E0B638" };
function CommsCalendar() {
const [posts, setPosts] = window.useDB("commsCalendar");
const tone = { approved: "approved", scheduled: "review", draft: "draft" };
const cycle = (id) => setPosts((xs) => xs.map((p) => p.id === id ? { ...p, status: ({ draft: "scheduled", scheduled: "approved", approved: "draft" })[p.status] } : p));
const [edit, setEdit] = React.useState(null);
const team = window.ARGT_DB.get("launchTeam") || [];
const save = () => { setPosts((xs) => xs.map((p) => p.id === edit.id ? { ...edit } : p)); if (window.argtPing) window.argtPing("Post updated"); setEdit(null); };
const del = (id) => setPosts((xs) => xs.filter((p) => p.id !== id));
return (
);
}
const STUDIO_STAGES = [ { id: "brief", label: "Brief" }, { id: "in-progress", label: "In progress" }, { id: "approved", label: "Approved" }, { id: "published", label: "Published" } ];
function ContentStudio() {
const [assets, setAssets] = window.useDB("contentStudio");
const [drag, setDrag] = React.useState(null);
const [over, setOver] = React.useState(null);
const [edit, setEdit] = React.useState(null);
const moveTo = (id, st) => setAssets((xs) => xs.map((a) => a.id === id ? { ...a, stage: st } : a));
const saveEdit = () => {
setAssets((xs) => xs.map((a) => a.id === edit.id ? { ...edit } : a));
if (window.argtPing) window.argtPing(edit.title + " updated");
setEdit(null);
};
return (
{STUDIO_STAGES.map((st) => {
const col = assets.filter((a) => a.stage === st.id);
return (
{ e.preventDefault(); if (over !== st.id) setOver(st.id); }}
onDragLeave={() => setOver((o) => o === st.id ? null : o)}
onDrop={() => { if (drag) moveTo(drag, st.id); setDrag(null); setOver(null); }}
style={{ background: over === st.id ? "var(--pvt-surface-mid)" : "var(--pvt-surface)", border: "1px solid " + (over === st.id ? "var(--role-accent)" : "var(--pvt-border-light)"), borderRadius: "var(--r-lg)", padding: 12, display: "flex", flexDirection: "column", gap: 10, minHeight: 200, transition: "border-color 120ms, background 120ms" }}>
{st.label}{col.length}
{col.map((a) => (
{ setDrag(a.id); e.dataTransfer.effectAllowed = "move"; }}
onDragEnd={() => { setDrag(null); setOver(null); }}
onClick={() => setEdit({ ...a })}
title="Drag to move · click to edit"
style={{ background: "var(--pvt-surface-mid)", border: "1px solid var(--pvt-border-light)", borderRadius: "var(--r-md)", padding: 12, cursor: "grab", opacity: drag === a.id ? 0.4 : 1, display: "flex", flexDirection: "column", gap: 8 }}>
{a.title}
{a.type}{a.due}
))}
);
})}
setEdit(null)} onSubmit={saveEdit} submitLabel="Save">
{edit && (<>
setEdit({ ...edit, title: e.target.value })} />
setEdit({ ...edit, due: e.target.value })} />
>)}
);
}
function Planners() {
const [seqs, setSeqs] = window.useDB("emailSequences");
const [out, setOut] = window.useDB("outreach");
const stone = { active: "approved", scheduled: "review", draft: "draft", paused: "pending" };
const otone = { confirmed: "approved", replied: "review", sent: "draft" };
const pauseSeq = (id) => setSeqs((xs) => xs.map((s) => s.id === id ? { ...s, status: s.status === "paused" ? "active" : "paused" } : s));
const cycleOut = (id) => setOut((xs) => xs.map((o) => o.id === id ? { ...o, status: ({ sent: "replied", replied: "confirmed", confirmed: "sent" })[o.status] } : o));
return (
{seqs.map((s) => (
{s.name}
{s.audience} · {s.sends} sends
{s.nextSend}
{s.status}
))}
{out.map((o) => (
{o.kind}
))}
);
}
function PublicSurfaces() {
const [rows, setRows] = window.useDB("publicSurfaces");
const tone = { live: "approved", qa: "review", draft: "draft" };
const cycle = (id) => setRows((xs) => xs.map((r) => r.id === id ? { ...r, status: ({ draft: "qa", qa: "live", live: "draft" })[r.status] } : r));
const toggleVerified = (id) => setRows((xs) => xs.map((r) => r.id === id ? { ...r, verified: !r.verified } : r));
const verifiedCount = rows.filter((r) => r.verified).length;
return (
{verifiedCount}/{rows.length} VERIFIED}>
OKPagePublic claimMaps toStatus
{rows.map((r) => (
toggleVerified(r.id)} title={r.verified ? "Verified" : "Mark verified"} style={{ width: 22, height: 22, borderRadius: 7, border: "2px solid " + (r.verified ? "var(--role-accent)" : "var(--pvt-border-light)"), background: r.verified ? "var(--role-accent)" : "transparent", display: "grid", placeItems: "center", color: "#fff", fontSize: 13, fontWeight: 800, cursor: "pointer" }}>{r.verified ? "✓" : ""}
{r.claim}
{r.mapsTo}
))}
);
}
function MarketingHub({ tab, go }) {
const [posts, setPosts] = window.useDB("commsCalendar");
const [showAdd, setShowAdd] = React.useState(false);
const [d, setD] = React.useState({ scheduledAt: "", channel: "Instagram", title: "" });
const add = () => {
if (!d.title.trim()) return;
setPosts((xs) => [...xs, { id: "cc" + Date.now(), scheduledAt: d.scheduledAt || "TBD", channel: d.channel, title: d.title.trim(), status: "draft", owner: "Jordan" }]);
if (window.argtPing) window.argtPing("Post added to calendar");
setShowAdd(false); setD({ scheduledAt: "", channel: "Instagram", title: "" });
};
return (
>}>
go("marketing", t)} />
{tab === "comms" && }
{tab === "studio" && }
{tab === "planners" && }
{tab === "public" && }
setShowAdd(false)} onSubmit={add} submitLabel="Schedule">
setD({ ...d, scheduledAt: e.target.value })} />
setD({ ...d, title: e.target.value })} />
);
}
window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, {
pipelines: (tab, go) => ,
marketing: (tab, go) => ,
});