/* =========================================================================== 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" }}>
{c.label}
{moneyK(c.currentCents)} / {moneyK(c.targetCents)}
); return (
go("finance", "receivables") : undefined} /> go("finance", "payables") : undefined} /> = 0 ? "Surplus to date" : "Deficit to date"} deltaDir={revIn - costOut >= 0 ? "up" : "down"} glyph="shield" />
{rev.map((c) => )}
{cost.map((c) => )}
setEdit(null)} onSubmit={save} submitLabel="Save"> {edit && (<> setEdit({ ...edit, label: e.target.value })} />
setEdit({ ...edit, _cur: e.target.value })} /> setEdit({ ...edit, _tgt: e.target.value })} />
)}
); } 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} />
}>
{rows.map((r) => (
openEdit(r)} title="Click to edit invoice">
{r.counterparty}
{r.category || "Unassigned"}
{r.dueDate} {money(r.amountCents)} 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, _amt: e.target.value })} /> setEdit({ ...edit, dueDate: e.target.value })} />
)}
); } 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 ( }> go("finance", t)} /> {tab === "overview" && } {tab === "receivables" && } {tab === "payables" && } setShowAdd(false)} onSubmit={add} submitLabel="Add line item">
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
{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}
On stage
{s.current}
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, current: e.target.value })} />
setEdit({ ...edit, next: e.target.value })} /> setEdit({ ...edit, nextAt: e.target.value })} /> setEdit({ ...edit, headcount: e.target.value })} />
{!edit.isNew && } )}
); } 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
{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, callTime: e.target.value })} /> setEdit({ ...edit, location: e.target.value })} />
setEdit({ ...edit, contact: e.target.value })} /> {!edit.isNew && } )}
); } 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}
{i.notes}
{i.at}
delIncident(i.id)}>×
))} {shown.length === 0 &&
No open incidents — all clear.
}
setShowAdd(false)} onSubmit={log} submitLabel="Log">