/* ============================================================
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.
setBizFilter(e.target.value)}>
All businesses
{businesses.map((b) => {b.name} )}
setStatusFilter(e.target.value)}>
All statuses
{Object.keys(STATUS).map((k) => {STATUS[k].label} )}
Doc # Type Business Amount Status
{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)}>
{docNo}
{hasCN ? "credit_note" : "debit_note"}
{biz.name}
{fmtINR(d.amount)}
);
})}
{!rows.length && No documents match this filter.
}
>
);
}
function Stat({ n, label, tone }) {
return (
);
}
/* ---- 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) => (
{edit === b.id ? (
setVal(e.target.value)} placeholder="finance@business.in" autoFocus />
{ setBizEmail(b.id, val.trim()); setEdit(null); }}>
setEdit(null)}>
) : b.email ? (
{b.email}
{ setEdit(b.id); setVal(b.email); }}>Edit
) : (
No approver set
{ setEdit(b.id); setVal(""); }}>Set email
)}
{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.
{events === null ? (
) : 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 });