/* ============================================================ nakad.jsx — Nakad user (sender / status watcher) Monitors the pipeline + configures authorised finance emails across 40+ businesses. Read-only on approvals. ============================================================ */ function NakadApp({ store }) { const { docs, businesses, bizById } = store; const who = store.user || {}; const [tab, setTab] = React.useState("docs"); const [bizFilter, setBizFilter] = React.useState("all"); const [statusFilter, setStatusFilter] = React.useState("all"); const [openId, setOpenId] = React.useState(null); const open = openId && docs.find((d) => d.id === openId); const tabs = [ { id: "docs", label: "Documents", count: docs.length }, { id: "biz", label: "Businesses", count: businesses.length }, { id: "audit", label: "Audit Log" }, ]; return (
{tab === "docs" && ( )} {tab === "biz" && } {tab === "audit" && }
{open && setOpenId(null)} />}
); } function NakadDocuments({ docs, bizById, businesses, bizFilter, setBizFilter, statusFilter, setStatusFilter, onOpen }) { const [range, setRange] = React.useState("30d"); let rows = docs.filter((d) => withinRange(d.createdAt, range)); if (bizFilter !== "all") rows = rows.filter((d) => d.biz === bizFilter); if (statusFilter !== "all") rows = rows.filter((d) => d.status === statusFilter); const pending = docs.filter((d) => ["received", "validated", "draft", "pending_approval", "approved", "irn_pending", "irn_generated", "irn_failed", "posting", "posted", "post_failed"].includes(d.status)).length; const confirmed = docs.filter((d) => d.status === "verified_in_tally").length; return ( <>
Documents
Live status of every debit & credit note you've originated.
{rows.map((d) => { const biz = bizById(d.biz); const hasCN = d.cnNo && d.cnNo !== "—"; const docNo = hasCN ? d.cnNo : d.dnNo; return ( onOpen(d.id)}> ); })} {!rows.length && }
Doc #TypeBusinessAmountStatus
{docNo} {hasCN ? "credit_note" : "debit_note"} {biz.name} {fmtINR(d.amount)}
No documents match this filter.
); } function Stat({ n, label, tone }) { return (
{n}
{label}
); } /* ---- businesses + finance-email config ---------------------------------- */ function NakadBusinesses({ store }) { const { businesses, setBizEmail } = store; const [edit, setEdit] = React.useState(null); const [val, setVal] = React.useState(""); return ( <>
Businesses
Authorised finance email per business — only this address can approve their credit notes.
You manage {businesses.length} businesses. Each needs one authorised finance email. Approvals from any other address are blocked.
{businesses.map((b) => (
{b.name}
{b.gst}
{edit === b.id ? (
setVal(e.target.value)} placeholder="finance@business.in" autoFocus />
) : b.email ? (
{b.email}
) : (
No approver set
)}
{b.live ? "Live" : "Paused"}
))}
); } function fmtAuditTime(iso) { if (!iso) return ""; const d = new Date(iso); return d.toLocaleString("en-IN", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }); } function NakadAudit({ docs, bizById, businesses }) { const [bizFilter, setBizFilter] = React.useState("all"); const [q, setQ] = React.useState(""); const [events, setEvents] = React.useState(null); // Real append-only audit trail from audit_events (not reconstructed from status) React.useEffect(() => { api.get("/api/audit-log").then((d) => setEvents(d.items)).catch(() => setEvents([])); }, []); const term = q.trim().toLowerCase(); const rows = (events || []).filter((e) => (bizFilter === "all" || String(e.bizId) === String(bizFilter)) && (!term || (e.act || "").toLowerCase().includes(term) || (e.who || "").toLowerCase().includes(term) || JSON.stringify(e.meta || {}).toLowerCase().includes(term)) ); return ( <>
Audit Log
Every state change across your businesses — who did what, and when.
setQ(e.target.value)} />
{events === null ? (
Loading…
) : rows.length ? (
{rows.slice(0, 120).map((e) => (
{e.act} {e.from && e.to && e.from !== e.to ? ( {e.from} → {e.to} ) : null}
{e.who}{e.roleLabel ? " · " + e.roleLabel : ""}{e.biz ? " · " + e.biz : ""} · {fmtAuditTime(e.t)}
{e.comment && !(e.from && e.to) ?
{e.comment}
: null}
))}
) : (
No audit events match this filter.
)}
); } /* ---- read-only detail drawer (numbering is automatic, no manual box) ----- */ function NakadDrawer({ doc, biz, onClose, admin }) { const s = STATUS[doc.status]; const hasCN = doc.cnNo && doc.cnNo !== "—"; const [acct, setAcct] = React.useState(null); const [detail, setDetail] = React.useState(null); // enriched doc (lines carry tallyItem + GST) const reload = React.useCallback(() => { api.get(`/api/credit-notes/${doc.id}`).then((d) => { setAcct(d.accounting); setDetail(d); }).catch(() => {}); }, [doc.id]); React.useEffect(() => { if (!hasCN) return; reload(); }, [doc.id, hasCN, reload]); const num = acct && acct.numbering; const stages = [ { t: "Debit note fetched via API", k: "done", meta: doc.dnNo + " · " + doc.fetched }, { t: "Credit note generated", k: STATUS[doc.status].step >= 2 ? "done" : "pending", meta: hasCN ? doc.cnNo : "Awaiting Lekha admin gate" }, { t: "Portal entry created", k: STATUS[doc.status].step >= 3 ? "done" : "pending", meta: "Linked to " + doc.invoice }, { t: "Finance team notified", k: STATUS[doc.status].step >= 4 ? "done" : "pending", meta: biz.email || "No approver configured" }, { t: doc.status === "cancelled" ? "Rejected by finance" : "Finance review", k: doc.status === "cancelled" ? "fail" : STATUS[doc.status].step >= 5 ? "done" : (STATUS[doc.status].step === 4 ? "active" : "pending"), meta: doc.status === "cancelled" ? doc.rejectReason : STATUS[doc.status].step >= 5 ? "Approved" : "Pending" }, { t: "Posted to Tally", k: STATUS[doc.status].step >= 6 && doc.status !== "cancelled" ? "done" : "pending", meta: "" }, { t: "Confirmed in Tally", k: doc.status === "verified_in_tally" ? "done" : "pending", meta: "" }, ]; return ( {biz.name} · against {doc.invoice}} onClose={onClose} center >
{fmtINR(doc.amount)}
Pipeline progress
{s.blurb}
{doc.status === "cancelled" && (
Rejected. {doc.rejectReason}. No further action available from here.
)}
Credit note — Tally voucher
{acct && }
{ setAcct(d.accounting); setDetail(d); }} /> {acct && acct.ar_impact && (
Downstream impact: {acct.ar_impact.ledger} (Accounts Receivable) is reduced by {fmtINR(acct.ar_impact.amount)} — Tally applies this automatically when the voucher posts.
)}
{/* Tally write preview — fed from the Step-5 DRAFT voucher XML: Tally ACTUALS only (confirmed stock items, brand-series voucher number). Platform naming never appears here. */} {acct && (num || (acct.inventory || []).length > 0 || acct.draft_xml) && (
Tally write preview — actuals
{num && (num.missing ? (
No voucher number. The brand's Tally shows no credit-note series to continue, and numbering is strictly manual — this note will not post until Lekha assigns a number in the brand's own style.
) : (
Voucher number {num.number} — {num.source === "manual" ? "set manually by Lekha admin" : <>continues the brand's own series{num.last_number ? <> (last in Tally: {num.last_number}) : null}}.
))} {acct.draft_xml && (
Draft import XML — exactly what will be sent to Tally
{acct.draft_xml}
)}
)}
Timeline
{stages.map((st, i) => (
{st.t}
{st.meta &&
{st.meta}
}
))}
{/* Real append-only history: every state change + edit, who + what changed */} {detail && detail.history ? : null}
); } Object.assign(window, { NakadApp, NakadDrawer });