/* ===========================================================================
launch-hubs-b.jsx — Finance + On-Site hubs.
On-Site is mobile-first (phones at the festival): big targets, stacked cards.
=========================================================================== */
const { LccPage: LccPageB, LccTabs: LccTabsB, LCC_TABS: TABS_B, Section: SectionB, money, moneyK } = window;
/* ---------------------------------------------------------------------------
FINANCE — budget vs actuals, money in, money out.
--------------------------------------------------------------------------- */
function FinanceOverview({ go }) {
const [cats, setCats] = window.useDB("budgetCategories");
const [edit, setEdit] = React.useState(null);
const rev = cats.filter((c) => c.kind === "revenue");
const cost = cats.filter((c) => c.kind === "cost");
const revIn = rev.reduce((s, c) => s + c.currentCents, 0);
const revTgt = rev.reduce((s, c) => s + c.targetCents, 0);
const costOut = cost.reduce((s, c) => s + c.currentCents, 0);
const costTgt = cost.reduce((s, c) => s + c.targetCents, 0);
const save = () => { setCats((xs) => xs.map((c) => c.id === edit.id ? { ...c, label: edit.label, currentCents: Math.round((+edit._cur || 0) * 100), targetCents: Math.round((+edit._tgt || 0) * 100) } : c)); if (window.argtPing) window.argtPing(edit.label + " updated"); setEdit(null); };
const del = () => { setCats((xs) => xs.filter((c) => c.id !== edit.id)); setEdit(null); };
const Line = ({ c, tone }) => (
setEdit({ ...c, _cur: c.currentCents / 100, _tgt: c.targetCents / 100 })} title="Click to edit line" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, alignItems: "center", padding: "10px 12px", background: "var(--pvt-surface-mid)", borderRadius: "var(--r-md)", cursor: "pointer" }}>
{moneyK(c.currentCents)} / {moneyK(c.targetCents)}
);
return (
);
}
function Invoices({ kind, title }) {
const [inv, setInv] = window.useDB("budgetInvoices");
const [show, setShow] = React.useState("all");
const base = inv.filter((i) => i.kind === kind);
const rows = show === "outstanding" ? base.filter((r) => r.status !== "paid") : base;
const tone = { paid: "approved", pending: "pending", draft: "draft" };
const total = base.reduce((s, r) => s + r.amountCents, 0);
const outstanding = base.filter((r) => r.status !== "paid").reduce((s, r) => s + r.amountCents, 0);
const outCount = base.filter((r) => r.status !== "paid").length;
const catOptions = (window.ARGT_DB.get("budgetCategories") || []).filter((c) => c.kind === (kind === "receivable" ? "revenue" : "cost")).map((c) => c.label);
const cycle = (id) => setInv((xs) => xs.map((r) => r.id === id ? { ...r, status: ({ draft: "pending", pending: "paid", paid: "draft" })[r.status], dueDate: r.status === "pending" ? "Paid" : (r.dueDate === "Paid" ? "TBD" : r.dueDate) } : r));
const [edit, setEdit] = React.useState(null);
const openEdit = (r) => setEdit({ ...r, _amt: r.amountCents / 100 });
const openAdd = () => setEdit({ id: "inv" + Date.now(), kind, counterparty: "", category: catOptions[0] || "", _amt: 0, dueDate: "TBD", status: "draft", isNew: true });
const save = () => {
const rec = { id: edit.id, kind: edit.kind || kind, counterparty: (edit.counterparty || "").trim() || "Untitled", category: edit.category || "", amountCents: Math.round((+edit._amt || 0) * 100), dueDate: edit.dueDate || "TBD", status: edit.status };
setInv((xs) => edit.isNew ? [...xs, rec] : xs.map((r) => r.id === edit.id ? rec : r));
if (window.argtPing) window.argtPing(edit.isNew ? "Invoice added" : "Invoice updated");
setEdit(null);
};
const del = (id) => setInv((xs) => xs.filter((r) => r.id !== id));
return (
setShow("all")} />
setShow("outstanding") : undefined} />
setShow("all")}>All setShow("outstanding")}>Outstanding{outCount ? " · " + outCount : ""} Add invoice }>
{rows.map((r) => (
openEdit(r)} title="Click to edit invoice">
{r.counterparty}
{r.category || "Unassigned"}
{r.dueDate}
{money(r.amountCents)}
{ e.stopPropagation(); cycle(r.id); }} title="Click to change status" style={{ background: "none", border: 0, padding: 0, cursor: "pointer" }}>{r.status}
del(r.id)}>×
))}
{rows.length === 0 &&
{show === "outstanding" ? "Nothing outstanding — all settled." : "No invoices yet. Add one above."}
}
setEdit(null)} onSubmit={save} submitLabel={edit && edit.isNew ? "Add" : "Save"}>
{edit && (<>
setEdit({ ...edit, counterparty: e.target.value })} />
setEdit({ ...edit, category: e.target.value })}>Unassigned {catOptions.map((c) => {c} )}
setEdit({ ...edit, _amt: e.target.value })} />
setEdit({ ...edit, dueDate: e.target.value })} />
setEdit({ ...edit, status: e.target.value })}>Draft Pending Paid
>)}
);
}
function FinanceHub({ tab, go }) {
const [, setCats] = window.useDB("budgetCategories");
const [showAdd, setShowAdd] = React.useState(false);
const [d, setD] = React.useState({ kind: "revenue", label: "", target: "" });
const add = () => {
if (!d.label.trim()) return;
setCats((xs) => [...xs, { id: "b" + Date.now(), kind: d.kind, label: d.label.trim(), targetCents: Math.round((+d.target || 0) * 100), currentCents: 0 }]);
if (window.argtPing) window.argtPing("Line item added to budget");
setShowAdd(false); setD({ kind: "revenue", label: "", target: "" });
};
return (
Export setShowAdd(true)}>Add line item >}>
go("finance", t)} />
{tab === "overview" && }
{tab === "receivables" && }
{tab === "payables" && }
setShowAdd(false)} onSubmit={add} submitLabel="Add line item">
setD({ ...d, kind: e.target.value })}>Revenue Cost
setD({ ...d, target: e.target.value })} />
setD({ ...d, label: e.target.value })} />
);
}
/* ---------------------------------------------------------------------------
ON-SITE — mobile-first festival weekend control.
--------------------------------------------------------------------------- */
function DayOfCommand() {
const [stages, setStages] = window.useDB("dayOfStages");
const tone = { live: ["#10B5C7", "Live"], changeover: ["#E0B638", "Changeover"], hold: ["#F25BA7", "Hold"] };
const cycle = (id) => setStages((xs) => xs.map((s) => s.id === id ? { ...s, status: ({ live: "changeover", changeover: "hold", hold: "live" })[s.status] } : s));
const [edit, setEdit] = React.useState(null);
const save = () => { setStages((xs) => edit.isNew ? [...xs, { ...edit, headcount: +edit.headcount || 0 }] : xs.map((s) => s.id === edit.id ? { ...edit, headcount: +edit.headcount || 0 } : s)); if (window.argtPing) window.argtPing(edit.stage + " updated"); setEdit(null); };
const del = (id) => setStages((xs) => xs.filter((s) => s.id !== id));
const addStage = () => setEdit({ id: "st" + Date.now(), stage: "New stage", status: "hold", current: "—", next: "TBD", nextAt: "TBD", headcount: 0, isNew: true });
return (
TAP A CARD TO EDIT · TAP THE STATUS TO ADVANCE
Add stage
{stages.map((s) => {
const [c, l] = tone[s.status];
return (
setEdit({ ...s })} title="Tap to edit this stage" style={{ borderColor: s.status === "hold" ? "rgba(242,91,167,0.35)" : "var(--pvt-border-light)", display: "flex", flexDirection: "column", gap: 12, cursor: "pointer" }}>
{s.stage}
{ e.stopPropagation(); cycle(s.id); }} title="Advance stage status" style={{ background: "none", border: 0, cursor: "pointer", display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11, fontWeight: 800, textTransform: "uppercase", letterSpacing: "0.05em", color: c, fontFamily: "var(--pvt-font-body)" }}> {l}
Next: {s.next} · {s.nextAt}
{s.headcount.toLocaleString()} here
);
})}
setEdit(null)} onSubmit={save} submitLabel={edit && edit.isNew ? "Add" : "Save"}>
{edit && (<>
setEdit({ ...edit, stage: e.target.value })} />
setEdit({ ...edit, status: e.target.value })}>Live Changeover Hold
setEdit({ ...edit, current: e.target.value })} />
setEdit({ ...edit, next: e.target.value })} />
setEdit({ ...edit, nextAt: e.target.value })} />
setEdit({ ...edit, headcount: e.target.value })} />
{!edit.isNew && { del(edit.id); setEdit(null); }} style={{ alignSelf: "flex-start", color: "#F25BA7", borderColor: "rgba(216,40,132,0.4)" }}>Remove stage }
>)}
);
}
function CrewCall() {
const [sheets, setSheets] = window.useDB("crewCallSheets");
const [edit, setEdit] = React.useState(null);
const save = () => { setSheets((xs) => edit.isNew ? [...xs, edit] : xs.map((s) => s.id === edit.id ? edit : s)); if (window.argtPing) window.argtPing(edit.isNew ? "Call sheet added" : "Call sheet updated"); setEdit(null); };
const del = (id) => setSheets((xs) => xs.filter((s) => s.id !== id));
const add = () => setEdit({ id: "cs" + Date.now(), shift: "New shift", callTime: "TBD", location: "TBD", contact: "TBD", day: "Sat", isNew: true });
return (
{sheets.length} CALL SHEETS · TAP TO EDIT
Add call sheet
{sheets.map((s) => (
setEdit({ ...s })} title="Tap to edit" style={{ display: "flex", flexDirection: "column", gap: 10, cursor: "pointer" }}>
{s.shift}
{s.day}
Call time {s.callTime}
Location {s.location}
Lead {s.contact}
))}
setEdit(null)} onSubmit={save} submitLabel={edit && edit.isNew ? "Add" : "Save"}>
{edit && (<>
setEdit({ ...edit, shift: e.target.value })} />
setEdit({ ...edit, day: e.target.value })}>Fri Sat Sun
setEdit({ ...edit, callTime: e.target.value })} />
setEdit({ ...edit, location: e.target.value })} />
setEdit({ ...edit, contact: e.target.value })} />
{!edit.isNew && { del(edit.id); setEdit(null); }} style={{ alignSelf: "flex-start", color: "#F25BA7", borderColor: "rgba(216,40,132,0.4)" }}>Remove call sheet }
>)}
);
}
function VenueMap() {
const [zones] = window.useDB("venueZones");
const tone = { stage: "#D8288C", vendor: "#E0B638", wellness: "#10B5C7", command: "#7A4FBF" };
return (
{zones.map((z) => (
{z.label}
))}
);
}
function IncidentLog() {
const [incidents, setIncidents] = window.useDB("incidents");
const sev = { P1: "#F25BA7", P2: "#E0B638", P3: "#10B5C7" };
const open = incidents.filter((i) => i.status === "open").length;
const resolve = (id) => setIncidents((xs) => xs.map((x) => x.id === id ? { ...x, status: x.status === "open" ? "resolved" : "open" } : x));
const delIncident = (id) => setIncidents((xs) => xs.filter((x) => x.id !== id));
const [showAdd, setShowAdd] = React.useState(false);
const [openOnly, setOpenOnly] = React.useState(false);
const shown = openOnly ? incidents.filter((i) => i.status === "open") : incidents;
const [d, setD] = React.useState({ kind: "Medical", stage: "Main", severity: "P3", notes: "" });
const log = () => {
if (!d.notes.trim()) return;
const t = new Date();
const at = ((t.getHours() % 12) || 12) + ":" + String(t.getMinutes()).padStart(2, "0") + " " + (t.getHours() >= 12 ? "PM" : "AM");
setIncidents((xs) => [{ id: "inc" + Date.now(), kind: d.kind, stage: d.stage, severity: d.severity, status: "open", at, notes: d.notes.trim() }, ...xs]);
if (window.argtPing) window.argtPing(d.severity + " incident logged");
setShowAdd(false); setD({ kind: "Medical", stage: "Main", severity: "P3", notes: "" });
};
return (
setOpenOnly((v) => !v) : undefined} />
setShowAdd(true)}>Log incident}>
{shown.map((i) => (
{i.severity}
{i.kind} · {i.stage}
resolve(i.id)}>{i.status === "open" ? "Resolve" : "Resolved"}
delIncident(i.id)}>×
))}
{shown.length === 0 &&
No open incidents — all clear.
}
setShowAdd(false)} onSubmit={log} submitLabel="Log">
setD({ ...d, kind: e.target.value })}>Medical Crowd Vendor Weather Lost Security
setD({ ...d, stage: e.target.value })}>Main Frequency Runway Lounge Block Village Gate
setD({ ...d, severity: e.target.value })}>P1 P2 P3
);
}
function Weather() {
const [days, setDays] = window.useDB("weather");
const ARMED = "Lightning hold plan armed";
const toggle = (id) => setDays((xs) => xs.map((d) => d.id === id ? { ...d, trigger: d.trigger === "none" ? ARMED : "none" } : d));
return (
{days.map((d) => {
const armed = d.trigger !== "none";
return (
{d.day}
{d.temp}
{d.forecast}
Wind {d.wind}
Precip {d.precip}
{armed ? "Lightning hold armed" : "Contingency disarmed"}
toggle(d.id)} style={armed ? { background: "#E0B638", color: "#0c0a18", borderColor: "#E0B638" } : {}}>{armed ? "Disarm" : "Arm"}
);
})}
);
}
function TicketPhases() {
const [phases, setPhases] = window.useDB("ticketPhases");
const tone = { live: ["#10B5C7", "Live"], ready: ["#E0B638", "Ready"], pending: ["var(--pvt-text-dim)", "Pending"] };
const goLive = (id) => setPhases((xs) => xs.map((p) => p.id === id ? { ...p, status: p.status === "ready" ? "live" : p.status } : p));
return (
FOUNDERS → GATE CASCADE}>
{phases.map((p) => {
const [c, l] = tone[p.status];
return (
{p.phase}
{p.sold}% of {p.allocation.toLocaleString()} allocation
{l}
goLive(p.id)} style={p.status !== "ready" ? { opacity: 0.5, cursor: "default" } : {}}>{p.status === "live" ? "Live" : "Go live " + p.goLive}
);
})}
);
}
function Recap() {
return (
{[
{ l: "Attendance", v: "—", n: "Captured at gates" },
{ l: "Ticket revenue", v: "—", n: "Final settlement" },
{ l: "Sponsor recaps", v: "0 sent", n: "Auto-generated decks" },
{ l: "Clean Earth raised", v: "—", n: "10% of every ticket" },
].map((m, i) => (
))}
Recap unlocks after the festival weekend. Metrics populate from gate scans, settlement, and sponsor deliverables, then export to per-sponsor recap decks.
);
}
function OnSiteHub({ tab, go }) {
return (
Radio dispatch}>
go("onsite", t)} />
{tab === "command" && }
{tab === "crew" && }
{tab === "map" && }
{tab === "incidents" && }
{tab === "weather" && }
{tab === "phases" && }
{tab === "recap" && }
);
}
window.LCC_PAGES = Object.assign(window.LCC_PAGES || {}, {
finance: (tab, go) => ,
onsite: (tab, go) => ,
});