/* ============================================================ ui.jsx — shared primitives & connective components ============================================================ */ /* ---- Tally posting badge — distinct, honest states ---------------------- */ // Driven by the accounting card's `acct` ({posted, simulated, state, master_id}). // "simulated" = the test connector only; real-but-unconfirmed posts show their // actual state (Posting…/Failed) — never a misleading "Simulated". function PostBadge({ acct }) { if (!acct || !acct.posted) return Preview; if (acct.simulated) return Simulated (test); const st = acct.state, id = acct.master_id; if (st === "ALREADY_IN_TALLY") return Already in Tally{id ? " · ID " + id : ""}; if (st === "VERIFIED") return Posted · ID {id || "—"}; if (st === "FLAGGED") return Failed; return Posting…; // SENT/IMPORTED/POSTED/COMPOSED } /* ---- icons (inline, no deps) -------------------------------------------- */ const Ic = { chevron: (p) => , folder: (p) => , check: (p) => , x: (p) => , refresh: (p) => , search: (p) => , bolt: (p) => , arrow: (p) => , shield: (p) => , mail: (p) => , doc: (p) => , building: (p) => , clock: (p) => , tally: (p) => , layers: (p) => , inbox: (p) => , download: (p) => , }; /* ---- customer-facing status vocabulary ---------------------------------- Brand Ops & Finance never see Lekha's internal gate or the Tally queue — statuses read as a fully automated flow. Nakad/Lekha keep the real STATUS. */ const CUSTOMER_STATUS = { AVAILABLE: { label: "Available", tone: "gray" }, PENDING_ADMIN: { label: "Processing", tone: "gray" }, FETCHED: { label: "Processing", tone: "gray" }, CREATED: { label: "Processing", tone: "blue" }, PENDING_LEKHA_REVIEW: { label: "Processing", tone: "blue" }, PENDING_REVIEW: { label: "Awaiting Approval", tone: "amber" }, APPROVED: { label: "Approved", tone: "teal" }, POSTING: { label: "Approved", tone: "teal" }, POSTED: { label: "Posted to Tally", tone: "teal" }, CONFIRMED: { label: "Posted to Tally", tone: "green" }, REJECTED: { label: "Rejected", tone: "red" }, }; /* ---- StatusBadge (customer mode hides internal stages) ------------------ */ /* Ops-view status: the whole pipeline collapsed to 3 states (Brand Ops + Nakad console). Everything between generate and the final Tally round-trip reads "Processing" — the detailed statuses still drive the real pipeline underneath. */ function opsBadge(status) { if (status === "verified_in_tally" || status === "closed") return { label: "Verified in Tally", tone: "green" }; if (status === "received") return { label: "Generate credit note", tone: "gray" }; return { label: "Processing", tone: "blue" }; } function StatusBadge({ status, customer, ops }) { const s = ops ? opsBadge(status) : (customer && CUSTOMER_STATUS[status]) || STATUS[status]; if (!s) return null; const spin = s.spin && (ops || !customer); return ( {s.label} ); } /* ---- time-range filter (7d / 30d / all) --------------------------------- */ function TimeFilter({ value, onChange }) { return ( ); } function withinRange(iso, range) { if (range === "all" || !iso) return true; const days = range === "7d" ? 7 : 30; return (Date.now() - new Date(iso).getTime()) <= days * 86400000; } /* ---- Pipeline tracker ---------------------------------------------------- Visual node grouping. The canonical 7-step model (STEPS / STATUS.step) is unchanged; here steps 2 (CN Created) + 3 (Portal Entry) are merged into one wide "Lekha AI is creating credit note …" box that carries the brand+amount. `lo`/`hi` are the inclusive canonical-step range a node represents. */ const PIPE_NODES = [ { label: "Fetch", lo: 1, hi: 1 }, { wide: true, lo: 2, hi: 3 }, // CN Created + Portal Entry { label: "Notify Finance", lo: 4, hi: 4 }, { label: "Finance Review", lo: 5, hi: 5, failAt: 5 }, { label: "Post to Tally", lo: 6, hi: 6 }, { label: "Confirm", lo: 7, hi: 7 }, ]; function Pipeline({ status, compact, doc, biz }) { const s = STATUS[status] || {}; const cur = s.step || 1; const failed = status === "cancelled"; const busy = status === "posting"; return (
{PIPE_NODES.map((node, i) => { const isFail = failed && node.failAt === 5; const done = failed ? node.hi < 5 // everything before Finance Review stays done : (cur > node.hi || (status === "verified_in_tally" && cur >= node.lo)); const active = !failed && !done && cur >= node.lo && cur <= node.hi; let cls = "node" + (node.wide ? " wide" : ""); if (isFail) cls += " fail"; else if (done) cls += " done"; else if (active) cls += " active"; // wide-box label: kept SHORT (brand + amount are shown on the voucher itself) so // nodes stay equal width and the connecting bar never breaks. const wideLbl = done ? "Credit note created" : "Creating credit note…"; const label = node.wide ? wideLbl : node.label; const glyph = isFail ? : done ? : node.wide ? "✦" : node.lo; return (
{glyph} {!compact && {label}}
); })}
); } /* ---- line-item table (SKU mapping) -------------------------------------- */ function LineItems({ doc }) { // `tallyItem` = the CONFIRMED Tally stock-item name (the name that posts); // the platform's sku/description stays visible only as the document source. return ( {doc.lines.map((l, i) => ( ))}
SKUDescriptionTally itemQty RateGSTAmount
{l.sku} {l.desc} {l.tallyItem || unmapped} {l.qty} {l.unit} {fmtINR(l.rate)} {l.gst}% {fmtINR(l.amount != null ? l.amount : l.qty * l.rate)}
Taxable value{fmtINR(doc.taxable)}
GST{fmtINR(doc.tax)}
Total credit{fmtINR(doc.amount)}
); } /* ---- Tally-like credit-note voucher preview (shared: admin + finance) ---- Renders the composed credit note the way Tally shows a Credit Note voucher: party + cost centre band, an items box (Tally ACTUAL names only), a details table + the Dr/Cr ledger entry on the right, and the narration below. Read- only projection of the Step-5 draft (doc + acct) — no recompute, no posting. */ /* Inline edit control (input/textarea) — commits on blur or Enter, only if changed. */ function InlineEdit({ value, onSave, placeholder, multiline, className, disabled }) { const [v, setV] = React.useState(value || ""); React.useEffect(() => { setV(value || ""); }, [value]); const commit = () => { if ((v || "") !== (value || "")) onSave(v.trim()); }; const common = { className: "tv-edit-inp " + (className || ""), value: v, placeholder, disabled, onChange: (e) => setV(e.target.value), onBlur: commit, }; return multiline ?