/* ===========================================================================
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 (
{on ? "On" : "Off"}
);
}
function SRow({ label, sub, children }) {
return (
);
}
/* 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 (
{ setOpen((o) => !o); setQ(""); }} style={{ ...sInput(), padding: "6px 10px", display: "flex", alignItems: "center", gap: 9, cursor: "pointer", textAlign: "left" }}>
{current ? : }
{current ? current.name : (value || "Unassigned")}
{current && {current.role} }
▾
{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 (
{ onChange(m.name.split(" ")[0]); setOpen(false); }}
style={{ display: "flex", alignItems: "center", gap: 10, width: "100%", padding: "8px 10px", background: on ? "var(--pvt-surface-mid)" : "transparent", border: 0, borderRadius: "var(--r-sm)", cursor: "pointer", textAlign: "left", color: "var(--pvt-text)", fontFamily: "var(--pvt-font-body)" }}
onMouseEnter={(e) => e.currentTarget.style.background = "var(--pvt-surface-mid)"}
onMouseLeave={(e) => e.currentTarget.style.background = on ? "var(--pvt-surface-mid)" : "transparent"}>
{m.name}
{m.role}
{on && ✓ }
);
})}
{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 (
onMove(i, -1)} style={{ ...btn, opacity: i === 0 ? 0.3 : 1, cursor: i === 0 ? "default" : "pointer" }}>▲
onMove(i, 1)} style={{ ...btn, opacity: i === n - 1 ? 0.3 : 1, cursor: i === n - 1 ? "default" : "pointer" }}>▼
);
}
/* 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" && (
Add stage}>
{F.stageNames.map((n, i) => (
{i + 1}
setStage(i, e.target.value)} />
removeStage(i)} style={delBtnStyle()}>×
))}
{F.stageNames.length === 0 &&
No stages yet. Add one above.
}
)}
{/* ---------------- TIMELINE & PHASES ---------------- */}
{tab === "timeline" && (
Add phase}>
Order Phase Status Go-live Allocation Price
{phases.map((p, i) => (
setPhase(p.id, "phase", e.target.value)} />
setPhase(p.id, "status", e.target.value)}>Pending Ready Live
setPhase(p.id, "goLive", e.target.value)} />
setPhase(p.id, "allocation", +e.target.value || 0)} />
setPhase(p.id, "price", e.target.value)} />
removePhase(p.id)} style={delBtnStyle()}>×
))}
{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, "engine", e.target.value)}>{Object.keys(window.ENGINE_LABELS).map((k) => {window.ENGINE_LABELS[k]} )}
setWeek(i, "status", e.target.value)}>To do In progress Done
removeWeek(i)} style={delBtnStyle()}>×
setWeek(i, "label", e.target.value)} />
Milestones (comma-separated)
setWeek(i, "milestones", e.target.value.split(",").map((m) => m.trim()).filter(Boolean))} />
))}
{timeline.length === 0 &&
No timeline blocks. Add the first one above.
}
)}
{/* ---------------- ENGINES ---------------- */}
{tab === "engines" && (
{engines.filter((e) => e.enabled).length}/{engines.length} ENABLED}>
Engine Owner Target Enabled
{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 && removeRole(r)} style={{ color: "#F25BA7", borderColor: "rgba(216,40,132,0.4)" }}>Remove }
{DESTS.map((d) => {
const on = acc.includes(d.id);
return toggleDest(r, d.id)} className="pill" style={{ background: on ? "var(--role-accent)" : "transparent", color: on ? "#fff" : "var(--pvt-muted)", borderColor: on ? "var(--role-accent)" : "var(--pvt-border-light)", cursor: owner ? "default" : "pointer", opacity: owner && !on ? 0.4 : 1 }}>{d.label} ;
})}
);
})}
setNewRole(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addRole(); }} />
Add role
setInvEmail(e.target.value)} />
setInvRole(e.target.value)}>Admin Editor Viewer
Send invite
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 : (
setPersonAccess(r.id, e.target.value)}>
{Array.from(new Set([...access.roles.filter((x) => x !== "Owner"), r.access])).map((opt) => {opt} )}
)}
))}
{(access.invites || []).length > 0 && (
{access.invites.length} OUTSTANDING}>
{access.invites.map((iv) => (
{iv.email}
{iv.role}
sent {iv.sent}
setAccess((a) => ({ ...a, invites: a.invites.filter((x) => x.id !== iv.id) }))}>Revoke
))}
)}
)}
{/* ---------------- INTEGRATIONS ---------------- */}
{tab === "integrations" && (
{window.PMIntegrations ?
: null}
{s.integrations.map((ig) => {
const on = ig.status === "connected";
return (
{ig.name}
setS((x) => ({ ...x, integrations: x.integrations.map((q) => q.id === ig.id ? { ...q, status: on ? "off" : "connected", detail: on ? "Not connected" : "Connected just now" } : q) }))}>{on ? "Connected" : ig.status === "action" ? "Reauthorize" : "Connect"}
);
})}
)}
{/* ---------------- 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]) => (
setEsc(k, e.target.value)}>Call + SMS SMS Slack In-app
))}
setS((x) => ({ ...x, alerting: { ...x.alerting, digestTime: e.target.value } }))} />
)}
{/* ---------------- BRANDING ---------------- */}
{tab === "branding" && (
{Object.keys(SACC).map((k) => (
setBrand("accent", k)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 16px", borderRadius: "var(--r-md)", background: B.accent === k ? "var(--pvt-surface-light)" : "var(--pvt-surface-mid)", border: "1px solid " + (B.accent === k ? SACC[k].bright : "var(--pvt-border-light)"), cursor: "pointer", color: "var(--pvt-text)" }}>
{k}
))}
setBrand("theme", e.target.value)}>Obsidian Midnight Ink
Upload logo (SVG or PNG)
)}
{/* ---------------- PLAN & DATA ---------------- */}
{tab === "data" && (
Used {ST.usedGb} GB of {ST.totalGb} GB
setS((x) => ({ ...x, storage: { ...x.storage, retentionDays: RET[e.target.value] } }))}>180 days 365 days 3 years Forever
Export all data
Create backup
Reset the command center back to seed data. This clears every edit, task, incident, and invite you have made in this demo.
{ window.ARGT_DB.reset(); if (window.argtPing) window.argtPing("Command center reset to seed data"); }}>Reset to seed data
)}
);
}
window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, {
settings: (tab, go) => ,
});