/* =========================================================================== 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) => (
{b.stage}
{b.n}
))}
); } 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 (
{posts.length} SCHEDULED}>
{posts.map((p) => (
setEdit({ ...p })} title="Click to edit post"> {p.scheduledAt} {p.channel} {p.title} {p.owner} del(p.id)}>×
))}
setEdit(null)} onSubmit={save} submitLabel="Save"> {edit && (<> setEdit({ ...edit, title: e.target.value })} />
setEdit({ ...edit, scheduledAt: e.target.value })} />
)}
); } 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}
{o.contact}
{o.note}
))}
); } 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.page}
{r.url}
{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) => , });