/* ==================================================================
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
{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}
{CX.money(a.balance)}
))}
{d.accounts.length > 6 && (
+{d.accounts.length - 6} more account(s) in the Ledger
)}
)}
>
)}
);
};
window.CockpitFinance = FinanceScreen;