/* ================================================================== Operations → Finance ------------------------------------------------------------------ The school's finance system (the Ledger app) surfaced in the admin panel. It is a SEPARATE Postgres database on port 5432, read through a read-only pool — see backend db/ledger.ts. Because it is a separate deployment it may simply not be running. That is an ordinary state, not an error: the whole screen becomes one "not connected" card that says which knob to turn, and no tile ever prints a zero that could be mistaken for "collected nothing". READ-ONLY BY DESIGN. Nothing here writes to the finance system; the book of record stays in the Ledger's own app. ================================================================== */ const FinanceScreen = ({ setScreen }) => { const CX = window.CX; const state = CX.useCockpitData("/admin/cockpit/finance", { pollMs: 300000 }); const d = state.data; const COMPONENT_COLOR = { TUITION: "var(--green-solid)", TRANSPORT: "var(--blue-solid)", HOSTEL: "var(--pink-solid)", READMISSION: "var(--amber-solid)", SECURITY: "var(--violet-solid)", }; const exportCsv = () => { if (!d?.connected) return; CX.exportCsv("fee-collection-by-month.csv", [ ["Month", "Receipts", "Amount (INR)"], ...d.collection.byMonth.map((m) => [m.month, m.n, m.amount]), ]); }; return (
Export collection )} /> }> {d && !d.connected && ( )} {d?.connected && ( <> {/* ---- Headline tiles ---- */}
= 0 ? "↑" : "↓"} ${Math.abs(d.collection.deltaPct)}%` : null} deltaTone={d.collection.deltaPct >= 0 ? "up" : "down"} footNote={`${CX.money(d.collection.lastMonth)} last month · ${CX.money(d.collection.today)} today`} action={Live} /> {d.students.active} active students} /> Register} />
{/* ---- Trend · mix ---- */}
Collection Trend
Last 8 months
{CX.money(d.collection.byMonth.reduce((a, m) => a + m.amount, 0))}
{CX.num(d.collection.byMonth.reduce((a, m) => a + m.n, 0))} receipts in the window
{d.collection.byMonth.map((m) => (
))}
{d.collection.byMonth.map((m) => (
{new Date(m.month + "-01T00:00:00").toLocaleDateString("en-IN", { month: "short" }).toUpperCase()}
))}
Read-only view of the Ledger · the finance app remains the book of record
Collection Mix
By head
{d.collection.byComponent.map((c) => { const share = d.collection.allTime ? Math.round((c.amount / d.collection.allTime) * 100) : 0; return (
{CX.title(c.component || "Other")} {CX.money(c.amount)} · {c.n}
); })}
PAYMENT MODE
{d.collection.byMode.map((m) => ( {CX.title(m.mode || "Other")} · {CX.num(m.n)} ))}
{/* ---- Entities · payables · payroll ---- */}
By Institution
{d.entities.length} entities
{d.entities.length === 0 ? ( ) : d.entities.map((e) => (
{(e.shortName || e.name || "?").slice(0, 3).toUpperCase()}
{e.name}
{CX.num(e.students)} active student(s)
{CX.money(e.collected)}
))}
{CX.num(d.students.withUid)} of {CX.num(d.students.total)} Ledger students carry a portal UID
Payables & Payroll
{d.payables.unpaid > 0 ? {d.payables.unpaid} unpaid : Settled}
{CX.money(d.payables.unpaidAmount)}
Unpaid vendor bills
{CX.money(d.payroll.amount)}
Salary paid this year
{d.payables.bills} bill(s) on record · {d.payroll.payments} salary payment(s) ·{" "} {d.payroll.employees} active employee(s)
Account Balances
{d.accounts.length} accounts
{d.accounts.length === 0 ? ( ) : ( // Capped rather than scrolled: a scroll container inside a // card clips its last row mid-height and reads as broken.
{d.accounts.slice(0, 6).map((a, i) => (
{a.type}
{a.name}
{CX.money(a.balance)}
))} {d.accounts.length > 6 && (
+{d.accounts.length - 6} more account(s) in the Ledger
)}
)}
)}
); }; window.CockpitFinance = FinanceScreen;