/* ============================================================ admin.jsx — Lekha admin (internal operator) Cross-business console · manual-approval gate · global toggle. The gate lets the admin VIEW + EDIT the OCR-parsed debit-note fields, then Approve or Reject (per Q10). ============================================================ */ function AdminApp({ store }) { const { docs, businesses, bizById, setBizGate, setBizFinanceGate, adminApprove, adminReject } = store; const [tab, setTab] = React.useState("overview"); const [bizFilter, setBizFilter] = React.useState("all"); const [statusFilter, setStatusFilter] = React.useState("all"); const [openId, setOpenId] = React.useState(null); const [gateId, setGateId] = React.useState(null); // OCR gate = raw debit notes awaiting admin verify (pre-compose); Draft review // (G1) = composed credit notes in 'draft'. New-schema statuses (2026-07 remaster). const gateQ = docs.filter((d) => ["received", "validated", "validation_failed"].includes(d.status)); const draftQ = docs.filter((d) => d.status === "draft"); const open = openId && docs.find((d) => d.id === openId); const gateDoc = gateId && docs.find((d) => d.id === gateId); const who = store.user || {}; const gateOn = businesses.filter((b) => b.manual_approval).length; const tallyQueued = (store.bridges || []).reduce((n, b) => n + (b.queued || 0), 0); const [excCount, setExcCount] = React.useState(0); React.useEffect(() => { api.get("/api/exceptions?status=OPEN").then((d) => setExcCount(d.total)).catch(() => {}); }, [docs.length]); const tabs = [ { id: "overview", label: "Overview" }, { id: "queue", label: "Approval Queue", count: gateQ.length }, { id: "drafts", label: "Draft Review", count: draftQ.length || null }, { id: "all", label: "All Documents", count: docs.length }, { id: "exceptions", label: "Exceptions", count: excCount || null }, { id: "loops", label: "Feedback Loops" }, { id: "replica", label: "Tally Sync" }, { id: "settings", label: "Settings" }, ]; return (
Verify gate on for {gateOn}/{businesses.length}
} />
{tab === "overview" && setTab("queue")} />} {tab === "queue" && } {tab === "drafts" && } {tab === "all" && ( )} {tab === "exceptions" && } {tab === "loops" && setTab("exceptions")} />} {tab === "replica" && } {tab === "settings" && }
{open && setOpenId(null)} />} {gateDoc && setGateId(null)} />} ); } /* ---- overview ------------------------------------------------------------ */ function AdminOverview({ store, onGoQueue }) { const { docs, businesses } = store; const [conn, setConn] = React.useState(null); // real connector health React.useEffect(() => { api.get("/api/admin/overview").then((d) => setConn(d.connectors)).catch(() => {}); }, [docs.length]); const count = (fn) => docs.filter(fn).length; const gate = count((d) => ["received", "validated", "validation_failed"].includes(d.status)); const review = count((d) => ["draft", "pending_approval"].includes(d.status)); const inflight = count((d) => ["approved", "irn_pending", "irn_generated", "irn_failed", "posting", "posted", "post_failed"].includes(d.status)); const confirmed = count((d) => ["verified_in_tally", "closed"].includes(d.status)); const rejected = count((d) => d.status === "cancelled"); const gateOn = businesses.filter((b) => b.manual_approval).length; const tiles = [ { label: "At Lekha admin gate", val: gate, foot: gate ? "Awaiting your approval" : "Queue clear", ic: , bg: "var(--amber-bg)", fg: "var(--amber-fg)", action: gate ? onGoQueue : null }, { label: "In finance review", val: review, foot: "Awaiting finance teams", ic: , bg: "var(--blue-bg)", fg: "var(--blue-fg)" }, { label: "Posting / in-flight", val: inflight, foot: "Moving to Tally", ic: , bg: "var(--teal-bg)", fg: "var(--teal-fg)" }, { label: "Confirmed", val: confirmed, foot: rejected + " rejected", ic: , bg: "var(--green-bg)", fg: "var(--green-fg)" }, ]; return ( <>
Operations overview
Every credit note across all {businesses.length} businesses, end to end.
{tiles.map((t, i) => (
{t.label} {t.ic}
{t.val}
{t.foot}{t.action ? " →" : ""}
))}
By business
{businesses.map((b) => { const ds = docs.filter((d) => d.biz === b.id); const inP = ds.filter((d) => !["verified_in_tally", "closed", "cancelled"].includes(d.status)).length; const conf = ds.filter((d) => ["verified_in_tally", "closed"].includes(d.status)).length; return ( ); })}
BusinessIn pipelineConfirmedApprover
{b.name} {inP} {conf} {b.email ? Set : Missing}
Lekha admin gate
Verify gate on for {gateOn} of {businesses.length}
Set per company in Settings — on for new brands, off once their debit notes are reliably clean.
{gate > 0 && ( )}
Tally status
); } function ConnLine({ label, ok }) { const loading = ok == null; const color = loading ? "var(--text-3)" : ok ? "var(--green-fg)" : "var(--red-fg)"; return (
{label} {loading ? "Checking…" : ok ? "Healthy" : "Down"}
); } /* ---- approval queue ------------------------------------------------------ */ function AdminQueue({ docs, bizById, approve, reject, onOpen }) { if (!docs.length) return } />; return ( <>
Approval Queue
Verify the scanned details against the source PDF, edit if wrong, then approve to let Lekha generate the credit note.
Only debit notes from companies with the verify gate on wait here. Open one to check its scanned fields against the source document, correct anything wrong, then approve.
{(() => { // group the queue by company const groups = {}; for (const d of docs) (groups[d.biz] ||= []).push(d); const order = Object.keys(groups).sort((a, b) => (bizById(a).name || "").localeCompare(bizById(b).name || "")); return order.map((bid) => { const biz = bizById(bid); return (
{biz.name} {groups[bid].length}
{groups[bid].map((d) => (
onOpen(d.id)}>
{d.dnNo}
Against {d.invoice} · {d.lines.length} line items · fetched {d.fetched}
Confidence
{fmtINR(d.amount)}
))}
); }); })()} ); } /* ---- G1: Lekha review of composed drafts (flow inversion 2026-07-05) ------ The draft was composed on ACTUALS (real ledgers from the company's own chart) before anything became brand-visible. Open shows the same accounting impact the finance screen renders; approve releases it to brand finance. */ function DraftRow({ d, biz, approve, reject, onOpen }) { const [rejecting, setRejecting] = React.useState(false); const [reason, setReason] = React.useState(""); const [working, setWorking] = React.useState(false); return (
onOpen(d.id)}>
{d.cnNo} from {d.dnNo}
{biz.name} · against {d.invoice} · {d.lines.length} line items
{fmtINR(d.amount)}
{rejecting && (
Reason for rejection