/* =========================================================================== launch-settings.jsx — comprehensive "manage the command center" surface. Eight tabs of real, editable configuration. Everything writes through the mock-db fold seam (launchSettings, launchAccess, launchEngines, ticketPhases, timeline) and persists. Registers window.LCC_PAGES.settings. =========================================================================== */ const { LccPage: SPage, LccTabs: STabs, LCC_TABS: STABS, Section: SSection, Avatar: SAvatar, Bar: SBar, lccInput: sInput, LccField: SField, act: sAct, LCC_ACCENTS: SACC, ENGINE_COLORS: SEC } = window; /* small reusable pill toggle */ function SToggle({ on, onClick }) { return ( ); } function SRow({ label, sub, children }) { return (
{label}
{sub &&
{sub}
}
{children}
); } /* Searchable owner assignment — click to open a popover, filter the team roster by name OR role, pick to assign. Reuses the team roster + Avatar. */ function OwnerPicker({ value, team, onChange }) { const [open, setOpen] = React.useState(false); const [q, setQ] = React.useState(""); const ref = React.useRef(null); React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; const onKey = (e) => { if (e.key === "Escape") setOpen(false); }; document.addEventListener("mousedown", onDoc); document.addEventListener("keydown", onKey); return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; }, [open]); const current = team.find((m) => m.name.split(" ")[0] === value); const filtered = team.filter((m) => (m.name + " " + m.role).toLowerCase().includes(q.trim().toLowerCase())); return (
{open && (
setQ(e.target.value)} placeholder="Search name or role…" style={{ ...sInput(), padding: "8px 11px" }} />
{filtered.map((m) => { const on = m.name.split(" ")[0] === value; return ( ); })} {filtered.length === 0 &&
No teammate matches “{q}”.
}
)}
); } /* Small reorder control — up/down, reused by phases, timeline, stages. */ function MoveBtns({ i, n, onMove }) { const btn = { width: 24, height: 17, display: "grid", placeItems: "center", background: "var(--pvt-surface-light)", border: "1px solid var(--pvt-border-light)", borderRadius: 5, color: "var(--pvt-text-dim)", fontSize: 8, lineHeight: 1, padding: 0, fontFamily: "var(--pvt-font-body)" }; return ( ); } /* generic immutable swap */ function swap(arr, i, dir) { const j = i + dir; if (j < 0 || j >= arr.length) return arr; const c = arr.slice(); const t = c[i]; c[i] = c[j]; c[j] = t; return c; } /* tiny round delete button */ function delBtnStyle() { return { width: 28, height: 28, borderRadius: 8, display: "grid", placeItems: "center", background: "var(--pvt-surface-light)", border: "1px solid var(--pvt-border-light)", color: "var(--pvt-text-dim)", cursor: "pointer", fontSize: 16, lineHeight: 1, flexShrink: 0 }; } function LccSettings({ tab, go }) { const [s, setS] = window.useDB("launchSettings"); const [access, setAccess] = window.useDB("launchAccess"); const [engines, setEngines] = window.useDB("launchEngines"); const [phases, setPhases] = window.useDB("ticketPhases"); const [timeline, setTimeline] = window.useDB("timeline"); const DESTS = window.LCC_NAV || []; const setFest = (k, v) => setS((x) => ({ ...x, festival: { ...x.festival, [k]: v } })); const setStage = (i, v) => setS((x) => ({ ...x, festival: { ...x.festival, stageNames: x.festival.stageNames.map((n, j) => j === i ? v : n) } })); const setBrand = (k, v) => setS((x) => ({ ...x, branding: { ...x.branding, [k]: v } })); const toggleChan = (k) => setS((x) => ({ ...x, alerting: { ...x.alerting, channels: { ...x.alerting.channels, [k]: !x.alerting.channels[k] } } })); const toggleEvent = (k) => setS((x) => ({ ...x, alerting: { ...x.alerting, events: { ...x.alerting.events, [k]: !x.alerting.events[k] } } })); const setEsc = (k, v) => setS((x) => ({ ...x, alerting: { ...x.alerting, escalation: { ...x.alerting.escalation, [k]: v } } })); // ----- invites / roles ----- const [invEmail, setInvEmail] = React.useState(""); const [invRole, setInvRole] = React.useState("Editor"); const sendInvite = () => { if (!invEmail.trim()) return; setAccess((a) => ({ ...a, invites: [...(a.invites || []), { id: "inv" + Date.now(), email: invEmail.trim(), role: invRole, status: "pending", sent: "now" }] })); if (window.argtPing) window.argtPing("Invite sent to " + invEmail.trim()); setInvEmail(""); }; const setPersonAccess = (id, val) => setS((x) => ({ ...x, roles: x.roles.map((r) => r.id === id ? { ...r, access: val } : r) })); // ----- role & permission editor (launchAccess.roles + access matrix) ----- const [newRole, setNewRole] = React.useState(""); const addRole = () => { const name = newRole.trim(); if (!name || access.roles.includes(name)) return; setAccess((a) => ({ ...a, roles: [...a.roles, name], access: { ...a.access, [name]: ["today"] } })); if (window.argtPing) window.argtPing("Role “" + name + "” added"); setNewRole(""); }; const removeRole = (r) => setAccess((a) => { const acc = { ...a.access }; delete acc[r]; return { ...a, roles: a.roles.filter((x) => x !== r), access: acc, currentRole: a.currentRole === r ? "Owner" : a.currentRole }; }); const toggleDest = (r, d) => setAccess((a) => { const cur = a.access[r] || []; return { ...a, access: { ...a.access, [r]: cur.includes(d) ? cur.filter((x) => x !== d) : [...cur, d] } }; }); // ----- ticket phase editor ----- const setPhase = (id, k, v) => setPhases((xs) => xs.map((p) => p.id === id ? { ...p, [k]: v } : p)); const movePhase = (i, dir) => setPhases((xs) => swap(xs, i, dir)); const removePhase = (id) => setPhases((xs) => xs.filter((p) => p.id !== id)); const addPhase = () => { setPhases((xs) => [...xs, { id: "tp" + Date.now(), phase: "New phase", status: "pending", sold: 0, allocation: 1000, goLive: "TBD", price: "$0" }]); if (window.argtPing) window.argtPing("Phase added"); }; // ----- production timeline editor ----- const setWeek = (i, k, v) => setTimeline((xs) => xs.map((w, j) => j === i ? { ...w, [k]: v } : w)); const moveWeek = (i, dir) => setTimeline((xs) => swap(xs, i, dir)); const removeWeek = (i) => setTimeline((xs) => xs.filter((_, j) => j !== i)); const addWeek = () => { setTimeline((xs) => [...xs, { week: "T-0", label: "New milestone block", engine: "content", status: "todo", milestones: [] }]); if (window.argtPing) window.argtPing("Timeline block added"); }; // ----- festival stage editor ----- const moveStage = (i, dir) => setS((x) => ({ ...x, festival: { ...x.festival, stageNames: swap(x.festival.stageNames, i, dir) } })); const removeStage = (i) => setS((x) => ({ ...x, festival: { ...x.festival, stageNames: x.festival.stageNames.filter((_, j) => j !== i) } })); const addStage = () => setS((x) => ({ ...x, festival: { ...x.festival, stageNames: [...x.festival.stageNames, "New stage"] } })); const F = s.festival, B = s.branding, A = s.alerting, ST = s.storage; const team = window.ARGT_DB.get("launchTeam") || []; const RET = { "180 days": 180, "365 days": 365, "3 years": 1095, "Forever": 0 }; const retLabel = (d) => Object.keys(RET).find((k) => RET[k] === d) || "365 days"; const records = ["launchTasks", "pipelineItems", "incidents", "commsCalendar", "contentStudio", "checklists", "launchFiles", "budgetInvoices"].reduce((n, k) => n + ((window.ARGT_DB.get(k) || []).length), 0); return ( Save changes}> go("settings", t)} /> {/* ---------------- FESTIVAL ---------------- */} {tab === "festival" && (
setFest("name", e.target.value)} /> setFest("presentedBy", e.target.value)} />
setFest("tagline", e.target.value)} />
setFest("venue", e.target.value)} /> setFest("address", e.target.value)} />
setFest("startDate", e.target.value)} /> setFest("endDate", e.target.value)} /> setFest("gatesOpen", e.target.value)} /> setFest("capacity", e.target.value)} /> setFest("artists", e.target.value)} />
setFest("pledgePct", e.target.value)} /> setFest("beneficiary", e.target.value)} />
Add stage}>
{F.stageNames.map((n, i) => (
{i + 1} setStage(i, e.target.value)} />
))} {F.stageNames.length === 0 &&
No stages yet. Add one above.
}
)} {/* ---------------- TIMELINE & PHASES ---------------- */} {tab === "timeline" && (
Add phase}>
OrderPhaseStatusGo-liveAllocationPrice
{phases.map((p, i) => (
setPhase(p.id, "phase", e.target.value)} /> setPhase(p.id, "goLive", e.target.value)} /> setPhase(p.id, "allocation", +e.target.value || 0)} /> setPhase(p.id, "price", e.target.value)} />
))} {phases.length === 0 &&
No phases. Add the first one above.
}
Add block}> setS((x) => ({ ...x, timelineWeeks: e.target.value }))} />
{timeline.map((w, i) => (
setWeek(i, "week", e.target.value)} />
setWeek(i, "label", e.target.value)} />
))} {timeline.length === 0 &&
No timeline blocks. Add the first one above.
}
)} {/* ---------------- ENGINES ---------------- */} {tab === "engines" && ( {engines.filter((e) => e.enabled).length}/{engines.length} ENABLED}>
EngineOwnerTargetEnabled
{engines.map((e) => (
{e.label} setEngines((xs) => xs.map((x) => x.key === e.key ? { ...x, owner: v } : x))} /> setEngines((xs) => xs.map((x) => x.key === e.key ? { ...x, target: ev.target.value } : x))} /> setEngines((xs) => xs.map((x) => x.key === e.key ? { ...x, enabled: !x.enabled, status: !x.enabled ? "attention" : "paused" } : x))} />
))}
)} {/* ---------------- TEAM & ROLES ---------------- */} {tab === "roles" && (
{access.roles.length} ROLES}>

Define each role and exactly which destinations it can open. Owner always keeps full access. The role you’re viewing as is gated live by this matrix.

{access.roles.map((r) => { const owner = r === "Owner"; const acc = owner ? DESTS.map((d) => d.id) : (access.access[r] || []); return (
{r} {owner && LOCKED} {r === access.currentRole && VIEWING AS} {!owner && }
{DESTS.map((d) => { const on = acc.includes(d.id); return ; })}
); })}
setNewRole(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addRole(); }} />
setInvEmail(e.target.value)} />

Admins can open Settings and edit the festival profile. Editors work the operational hubs. Viewers are read-only. Only Owner and Admin can reach this configuration surface.

{s.roles.map((r) => (
{r.name} {r.scope} {r.access === "Owner" ? Owner : ( )}
))}
{(access.invites || []).length > 0 && ( {access.invites.length} OUTSTANDING}>
{access.invites.map((iv) => (
{iv.email} {iv.role} sent {iv.sent}
))}
)}
)} {/* ---------------- INTEGRATIONS ---------------- */} {tab === "integrations" && (
{window.PMIntegrations ? : null}
{s.integrations.map((ig) => { const on = ig.status === "connected"; return (
{ig.name}
{ig.purpose}
{ig.detail}
); })}
)} {/* ---------------- ALERTS ---------------- */} {tab === "alerts" && (
{Object.entries(A.channels).map(([k, v]) => toggleChan(k)} />)}
{Object.entries(A.events).map(([k, v]) => toggleEvent(k)} />)}
{Object.entries(A.escalation).map(([k, v]) => ( ))} setS((x) => ({ ...x, alerting: { ...x.alerting, digestTime: e.target.value } }))} />
)} {/* ---------------- BRANDING ---------------- */} {tab === "branding" && (
{Object.keys(SACC).map((k) => ( ))}
)} {/* ---------------- PLAN & DATA ---------------- */} {tab === "data" && (
Used{ST.usedGb} GB of {ST.totalGb} GB

Reset the command center back to seed data. This clears every edit, task, incident, and invite you have made in this demo.

)} ); } window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, { settings: (tab, go) => , });