/* ============================================================ brandops.jsx — Brand Ops (Step 1 trigger persona) Master table of the brand's debit notes. Each AVAILABLE note has a "Generate Credit Note" button so the user picks the exact one to push through the pipeline; already-generated notes show their live status. ============================================================ */ function BrandOpsApp({ store }) { const { docs, businesses, bizById, generateCreditNote, fetchDebitNote, uploadDebitNote } = store; const [busyId, setBusyId] = React.useState(null); const [fetching, setFetching] = React.useState(false); const [uploading, setUploading] = React.useState(false); const fileRef = React.useRef(null); const [err, setErr] = React.useState(""); const [range, setRange] = React.useState("all"); const [filter, setFilter] = React.useState("all"); // all | available | done const [tab, setTab] = React.useState("create"); const who = store.user || {}; const brand = businesses[0] || bizById(who.business_id || ""); const noApprover = brand && !brand.email; let rows = docs.filter((d) => withinRange(d.createdAt, range)); if (filter === "available") rows = rows.filter((d) => d.status === "received"); if (filter === "done") rows = rows.filter((d) => d.status !== "received"); // available first, then most-recent rows = rows.slice().sort((a, b) => (a.status === "received" ? 0 : 1) - (b.status === "received" ? 0 : 1) || (b.createdAt || "").localeCompare(a.createdAt || "")); const availableCount = docs.filter((d) => d.status === "received").length; const doFetch = async () => { setErr(""); setFetching(true); const res = await fetchDebitNote(); setFetching(false); if (res && !res.ok) setErr(res.error === "no_finance_approver" ? "No authorised finance approver is configured for your brand yet. Contact Nakad to set it up." : res.error === "no_documents" ? "No debit notes available from the source right now." : "Couldn't fetch a debit note just now — please try again."); }; const doUpload = async (e) => { const file = e.target.files && e.target.files[0]; if (e.target) e.target.value = ""; // allow re-selecting the same file if (!file) return; setErr(""); setUploading(true); const res = await uploadDebitNote(file); setUploading(false); if (res && !res.ok) setErr(res.error === "no_finance_approver" ? "No authorised finance approver is configured for your brand yet." : /already been uploaded/i.test(res.error || "") ? "This exact PDF has already been uploaded." : "Couldn't upload that PDF — please try again."); }; const generate = async (id) => { setErr(""); if (noApprover) { setErr("No authorised finance approver is configured for your brand yet. Contact Nakad to set it up before generating a credit note."); return; } setBusyId(id); const res = await generateCreditNote(id); setBusyId(null); if (res && !res.ok) setErr(res.error === "no_finance_approver" ? "No authorised finance approver is configured for your brand yet. Contact Nakad to set it up." : "Couldn't generate the credit note just now — please try again."); }; return (
{tab === "notif" ? (
) : (
Debit notes
Pick a debit note and generate its credit note — it's taken through the pipeline automatically.
{err ? (
{err}
) : (
{availableCount} debit note{availableCount === 1 ? "" : "s"} ready to generate for {brand ? brand.name : "your brand"}. Generating moves a note to Processing until it's verified in Tally.
)}
{rows.map((d) => { const available = d.status === "received"; return ( ); })} {!rows.length && }
Debit noteInvoiceDateAmountStatus / Action
{d.dnNo} {d.invoice || "—"} {d.docDate || d.fetched} {fmtINR(d.amount)} {available ? ( ) : ( )}
No debit notes in this view.
)}
); } Object.assign(window, { BrandOpsApp });