/* ============================================================
   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 <span className="badge blue"><span className="b-dot" />Preview</span>;
  if (acct.simulated) return <span className="badge gray"><span className="b-dot" />Simulated (test)</span>;
  const st = acct.state, id = acct.master_id;
  if (st === "ALREADY_IN_TALLY") return <span className="badge teal"><span className="b-dot" />Already in Tally{id ? " · ID " + id : ""}</span>;
  if (st === "VERIFIED") return <span className="badge green"><span className="b-dot" />Posted · ID {id || "—"}</span>;
  if (st === "FLAGGED") return <span className="badge red"><span className="b-dot" />Failed</span>;
  return <span className="badge amber"><span className="b-dot" />Posting…</span>;   // SENT/IMPORTED/POSTED/COMPOSED
}

/* ---- icons (inline, no deps) -------------------------------------------- */
const Ic = {
  chevron: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><path d="M6 4l4 4-4 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  folder: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M1.5 4.5a1 1 0 0 1 1-1H6l1.5 1.5h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7.5z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/></svg>,
  check: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><path d="M3.5 8.5l3 3 6-6.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  x: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"/></svg>,
  refresh: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><path d="M13.5 8a5.5 5.5 0 1 1-1.6-3.9M13.5 2.5V5H11" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  search: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><circle cx="7" cy="7" r="4.5" stroke="currentColor" strokeWidth="1.6"/><path d="M10.5 10.5L14 14" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
  bolt: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><path d="M9 1.5L3.5 9H8l-1 5.5L12.5 7H8l1-5.5z" fill="currentColor"/></svg>,
  arrow: (p) => <svg width="16" height="16" viewBox="0 0 16 16" fill="none" {...p}><path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  shield: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M8 1.5l5 2v4c0 3.2-2.1 5.4-5 6.5-2.9-1.1-5-3.3-5-6.5v-4l5-2z" stroke="currentColor" strokeWidth="1.4" fill="none"/><path d="M5.8 8l1.6 1.6L10.4 6.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  mail: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><rect x="2" y="3.5" width="12" height="9" rx="1.5" stroke="currentColor" strokeWidth="1.4"/><path d="M2.5 4.5L8 8.5l5.5-4" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  doc: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M4 1.5h5l3 3v9.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-12a.5.5 0 0 1 .5-.5z" stroke="currentColor" strokeWidth="1.3"/><path d="M9 1.5V4.5h3M6 8h4M6 10.5h4" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round"/></svg>,
  building: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><rect x="3" y="2" width="10" height="12" rx="1" stroke="currentColor" strokeWidth="1.3"/><path d="M6 5h1M9 5h1M6 7.5h1M9 7.5h1M6 10h4v4" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round"/></svg>,
  clock: (p) => <svg width="14" height="14" viewBox="0 0 16 16" fill="none" {...p}><circle cx="8" cy="8" r="6" stroke="currentColor" strokeWidth="1.4"/><path d="M8 5v3.2l2 1.3" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/></svg>,
  tally: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M3 13V6M6.3 13V3M9.6 13V7.5M13 13V4.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
  layers: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M8 2l6 3-6 3-6-3 6-3z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/><path d="M2 8l6 3 6-3M2 11l6 3 6-3" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/></svg>,
  inbox: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M2 9l2-6h8l2 6v3.5a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5V9z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/><path d="M2 9h3.5l1 2h3l1-2H14" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round"/></svg>,
  download: (p) => <svg width="15" height="15" viewBox="0 0 16 16" fill="none" {...p}><path d="M8 2v7.5M5 7l3 3 3-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><path d="M2.5 12.5h11" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>,
};

/* ---- 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 (
    <span className={"badge " + s.tone + (spin ? " spin" : "")}>
      <span className="b-dot" />{s.label}
    </span>
  );
}

/* ---- time-range filter (7d / 30d / all) --------------------------------- */
function TimeFilter({ value, onChange }) {
  return (
    <select className="select" value={value} onChange={(e) => onChange(e.target.value)}>
      <option value="7d">Last 7 days</option>
      <option value="30d">Last 30 days</option>
      <option value="all">All time</option>
    </select>
  );
}
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 (
    <div className={"pipe" + (busy ? " busy" : "")}>
      {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 ? <Ic.x /> : done ? <Ic.check /> : node.wide ? "✦" : node.lo;
        return (
          <div className={cls} key={i}>
            <span className="bar" />
            <span className="ring">{glyph}</span>
            {!compact && <span className={"lbl" + (node.wide ? " lbl-wide" : "")}>{label}</span>}
          </div>
        );
      })}
    </div>
  );
}

/* ---- 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 (
    <table className="li-tbl">
      <thead>
        <tr>
          <th>SKU</th><th>Description</th><th>Tally item</th><th className="num">Qty</th>
          <th className="num">Rate</th><th className="num">GST</th><th className="num">Amount</th>
        </tr>
      </thead>
      <tbody>
        {doc.lines.map((l, i) => (
          <tr key={i}>
            <td className="mono">{l.sku}</td>
            <td>{l.desc}</td>
            <td className="mono">{l.tallyItem || <span style={{ color: "var(--text-3)" }}>unmapped</span>}</td>
            <td className="num">{l.qty} {l.unit}</td>
            <td className="num">{fmtINR(l.rate)}</td>
            <td className="num">{l.gst}%</td>
            <td className="num">{fmtINR(l.amount != null ? l.amount : l.qty * l.rate)}</td>
          </tr>
        ))}
      </tbody>
      <tfoot>
        <tr><td colSpan="6">Taxable value</td><td className="num">{fmtINR(doc.taxable)}</td></tr>
        <tr><td colSpan="6">GST</td><td className="num">{fmtINR(doc.tax)}</td></tr>
        <tr><td colSpan="6" style={{ fontSize: "15px" }}>Total credit</td><td className="num" style={{ fontSize: "15px" }}>{fmtINR(doc.amount)}</td></tr>
      </tfoot>
    </table>
  );
}

/* ---- 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
    ? <textarea rows="2" {...common} />
    : <input {...common} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); e.target.blur(); } }} />;
}

function CNPreview({ doc, acct, editable, docId, onSaved }) {
  const legs = (acct && acct.legs) || [];
  // ---- inline editing (Lekha admin / finance): reassign ledgers, edit narration
  // + CN number. Each save PUTs draft-edit; the persisted change re-renders BOTH
  // this card and the raw XML below (they read the same composed rows).
  const [opts, setOpts] = React.useState(null);
  const [saving, setSaving] = React.useState(false);
  const [editErr, setEditErr] = React.useState("");
  const canEdit = !!(editable && docId && onSaved && acct && !acct.posted
    && ["DRAFT", "PENDING_APPROVAL", "IN_EXCEPTION"].includes(String(acct.state || "").toUpperCase()));
  React.useEffect(() => {
    if (!canEdit) { setOpts(null); return; }
    let live = true;
    api.get(`/api/credit-notes/${docId}/ledger-options`).then((o) => live && setOpts(o)).catch(() => {});
    return () => { live = false; };
  }, [canEdit, docId]);
  const save = (patch) => {
    setSaving(true); setEditErr("");
    return api.put(`/api/credit-notes/${docId}/draft-edit`, patch)
      .then((d) => onSaved(d))
      .catch((e) => setEditErr(String((e && e.message) || "Couldn't save this change.").replace(/^.*->\s*\d+\s*/, "") || "Couldn't save this change."))
      .finally(() => setSaving(false));
  };
  const ROLE_FIELD = { party: "party_ledger", sales_return: "sales_ledger", igst: "igst_ledger", cgst: "cgst_ledger", sgst: "sgst_ledger" };
  const ROLE_POOL = { party: "party", sales_return: "sales", igst: "igst", cgst: "cgst", sgst: "sgst" };
  const poolFor = (role) => (opts && opts[ROLE_POOL[role]]) || [];
  const party = legs.find((l) => l.is_party);
  const partyName = (party && party.ledger) || doc.vendor || "—";
  const consignee = (doc && doc.consigneeAddress) || "";   // ship-to (distinct from bill-to)
  const origBill = (acct && acct.against_invoice) || doc.invoice || "";   // original supplier bill
  const costCentre = ((legs.find((l) => l.cost_centre) || {}).cost_centre) || "";
  const vno = (acct && (acct.voucher_number || (acct.numbering && acct.numbering.number)))
    || (doc.cnNo && doc.cnNo !== "—" ? doc.cnNo : null);
  // SINGLE SOURCE OF TRUTH: every credit-note figure below comes from the COMPOSED
  // credit note (acct — exactly what render_cn_xml posts), NOT the debit note (doc).
  // doc.* is a fallback ONLY before a CN is composed (no acct yet).
  const items = (acct && acct.lines && acct.lines.length) ? acct.lines
    : (acct && acct.inventory && acct.inventory.length)
      ? acct.inventory.map((it) => ({ name: it.stock_item, qty: it.qty, unit: it.unit, rate: it.rate, amount: it.amount }))
      : (doc.lines || []).map((l) => ({ name: l.tallyItem || l.desc, qty: l.qty, unit: l.unit, rate: l.rate,
          amount: l.amount != null ? l.amount : l.qty * l.rate }));
  const taxes = (acct && acct.taxes && acct.taxes.length) ? acct.taxes.map((t) => ({ label: t.label, value: t.value }))
    : (doc.taxes && doc.taxes.length) ? doc.taxes.map((t) => ({ label: t.label, value: t.value }))
      : (doc.tax ? [{ label: "GST", value: doc.tax }] : []);
  const total = (acct && acct.total != null) ? acct.total : doc.amount;
  const taxable = (acct && acct.taxable != null) ? acct.taxable : doc.taxable;
  const taxTotal = (acct && acct.tax_total != null) ? acct.tax_total : doc.tax;
  const partyGstin = (acct && acct.party_gstin) || doc.gstin || "";
  const totalQty = items.reduce((s, it) => s + (Number(it.qty) || 0), 0);
  const bal = legs.reduce((s, l) => s + (l.side === "Dr" ? l.amount : -l.amount), 0);
  const blocked = acct && acct.outcome && acct.outcome !== "VOUCHER";

  return (
    <div className="tally-vch">
      {blocked && (
        <div className="tv-block"><Ic.shield style={{ flex: "0 0 14px", marginTop: 1 }} />
          <div><b>Not postable yet — {acct.outcome}.</b> {acct.detail || "Ledger names are assigned once Tally is connected and the binding is confirmed."}</div>
        </div>
      )}
      <div className="tv-head">
        <div className="row gap12" style={{ alignItems: "baseline", flexWrap: "wrap" }}>
          <span className="tv-badge">Credit Note</span>
          {canEdit
            ? <span className="tv-no mono">No. <InlineEdit value={vno || ""} placeholder="CN number"
                disabled={saving} onSave={(v) => save({ voucher_number: v })} className="tv-no-inp" /></span>
            : (vno && <span className="tv-no mono">No. {vno}</span>)}
          {origBill && <span className="tv-k">against original bill <span className="mono" style={{ color: "var(--text-2)" }}>{origBill}</span></span>}
        </div>
        <div className="tv-date">{doc.docDate || doc.fetched || ""}</div>
      </div>
      {canEdit && (
        <div className="tv-edit-bar">
          <span>{saving ? "Saving…" : "Editable — ledgers, narration & number update the Tally XML below."}</span>
          {editErr && <span className="tv-edit-err">{editErr}</span>}
        </div>
      )}
      <div className="tv-party">
        <div>
          <span className="tv-k">Party A/c name</span>
          <span className="tv-party-name">{partyName}</span>
          {partyGstin ? <div className="mono" style={{ marginTop: 3, fontSize: 11.5, color: "var(--text-3)", letterSpacing: ".02em" }}>GSTIN {partyGstin}</div> : null}
        </div>
        <div style={{ textAlign: "right" }}>
          {costCentre ? <div style={{ marginBottom: 8 }}><span className="tv-k">Cost Centre / Classes</span><span className="tv-party-name">{costCentre}</span></div> : null}
          <span className="tv-k">Debit note</span>
          <span className="tv-party-name mono" style={{ fontSize: 12.5 }}>{doc.dnNo}</span>
        </div>
      </div>
      {(canEdit || consignee) && (
        <div className="tv-party" style={{ borderTop: 0, paddingTop: 0 }}>
          <div style={{ width: "100%" }}>
            <span className="tv-k">Consignee (ship-to) address</span>
            {canEdit
              ? <InlineEdit value={consignee} multiline disabled={saving}
                  placeholder="Ship-to address — leave blank to use the party's billing address"
                  onSave={(v) => save({ consignee_address: v })} />
              : <span className="tv-party-name" style={{ fontWeight: 400, whiteSpace: "pre-wrap" }}>{consignee || "—"}</span>}
          </div>
        </div>
      )}

      <div className="tv-grid">
        <div className="tv-col">
          <table className="tv-tbl">
            <thead><tr><th>Name of Item</th><th className="num">Qty</th><th className="num">Rate</th><th>per</th><th className="num">Amount</th></tr></thead>
            <tbody>
              {items.map((it, i) => (
                <tr key={i}>
                  <td className="mono tv-item-name">{it.name}</td>
                  <td className="num">{it.qty}</td>
                  <td className="num">{fmtINR(it.rate)}</td>
                  <td>{it.unit || ""}</td>
                  <td className="num">{fmtINR(it.amount)}</td>
                </tr>
              ))}
              {taxes.map((t, i) => (
                <tr key={"t" + i} className="tv-tax">
                  <td colSpan="4">{t.label}</td>
                  <td className="num">{fmtINR(t.value)}</td>
                </tr>
              ))}
              <tr className="tv-fill"><td colSpan="5" /></tr>
            </tbody>
            <tfoot>
              <tr>
                <td className="tv-total-k">{totalQty ? totalQty + (items[0] && items[0].unit ? " " + items[0].unit : "") : ""}</td>
                <td colSpan="3" style={{ textAlign: "right", fontWeight: 700 }}>Total</td>
                <td className="num tv-total-v">{fmtINR(total)}</td>
              </tr>
            </tfoot>
          </table>
        </div>

        <div className="tv-col">
          {/* right column leads straight with the ledger entry — the CN number, GSTIN,
              debit-note ref and original bill now live in the header/party block, and
              taxable/tax are already in the items table on the left (no duplication). */}
          {legs.length > 0 && (
            <table className="tv-tbl tv-ledger">
              <thead><tr><th>Ledger entry</th><th className="num">Dr</th><th className="num">Cr</th></tr></thead>
              <tbody>
                {legs.map((l, i) => {
                  const pool = poolFor(l.role);
                  const asDropdown = canEdit && ROLE_FIELD[l.role] && pool.length > 0;
                  return (
                    <tr key={i}>
                      <td>
                        {asDropdown ? (
                          <select className="tv-edit-sel" value={l.ledger} disabled={saving}
                            onChange={(e) => save(l.role === "party"
                              ? { party_ledger: e.target.value }
                              : { leg_line_no: l.line_no, leg_ledger: e.target.value })}>
                            {pool.some((o) => o.name === l.ledger) ? null : <option value={l.ledger}>{l.ledger}</option>}
                            {pool.map((o) => <option key={o.guid} value={o.name}>{o.name}</option>)}
                          </select>
                        ) : l.ledger}
                        {l.cost_centre ? <div className="tv-leg-cc">↳ {l.cost_centre}</div> : null}
                      </td>
                      <td className="num">{l.side === "Dr" ? fmtINR(l.amount) : ""}</td>
                      <td className="num">{l.side === "Cr" ? fmtINR(l.amount) : ""}</td>
                    </tr>
                  );
                })}
                <tr className="tv-fill"><td colSpan="3" /></tr>
              </tbody>
              <tfoot><tr><td className="tv-total-k">{Math.abs(bal) < 0.01 ? "Balanced" : "Out of balance"}</td><td className="num" colSpan="2" style={{ fontWeight: 700 }}>{Math.abs(bal) < 0.01 ? "✓" : fmtINR(bal)}</td></tr></tfoot>
            </table>
          )}
        </div>
      </div>

      {canEdit ? (
        <div className="tv-narr"><span className="tv-k">Narration</span>
          <InlineEdit multiline disabled={saving}
            value={(acct.narration || "").replace(/\s*\[LEKHA:[^\]]*\]/g, "")}
            placeholder="Narration" onSave={(v) => save({ narration: v })} />
          <div className="tv-narr-note">The [LEKHA:…] idempotency stamp is kept automatically.</div>
        </div>
      ) : (acct && acct.narration && (
        <div className="tv-narr"><span className="tv-k">Narration</span>{acct.narration}</div>
      ))}

      {acct && acct.draft_xml && (
        <details className="tv-xml">
          <summary>View raw Tally XML — the exact import payload that posts</summary>
          <pre className="tv-xml-pre">{acct.draft_xml}</pre>
        </details>
      )}
    </div>
  );
}

/* ---- confidence meter ---------------------------------------------------- */
function Confidence({ value }) {
  const pct = Math.round(value * 100);
  const col = value >= 0.9 ? "var(--green-fg)" : value >= 0.8 ? "var(--amber-fg)" : "var(--red-fg)";
  return (
    <div className="row gap8">
      <span className="conf-bar"><i style={{ width: pct + "%", background: col }} /></span>
      <span className="tiny" style={{ color: col, fontWeight: 700 }}>{pct}%</span>
    </div>
  );
}

/* ---- Drawer -------------------------------------------------------------- */
function Drawer({ title, sub, onClose, children, footer, center }) {
  React.useEffect(() => {
    const h = (e) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  return (
    <>
      <div className="scrim" onClick={onClose} />
      <aside className={"drawer" + (center ? " drawer-center" : "")} role="dialog">
        <div className="dr-head row" style={{ justifyContent: "space-between", alignItems: "flex-start" }}>
          <div>
            <div style={{ fontFamily: "var(--serif)", fontSize: 20, fontWeight: 700 }}>{title}</div>
            {sub && <div className="tiny mt8" style={{ marginTop: 4 }}>{sub}</div>}
          </div>
          <button className="icon-btn" onClick={onClose}><Ic.x /></button>
        </div>
        <div className="dr-body">{children}</div>
        {footer && <div className="dr-foot">{footer}</div>}
      </aside>
    </>
  );
}

/* ---- Toasts -------------------------------------------------------------- */
function Toasts({ toasts }) {
  return (
    <div className="toast-wrap">
      {toasts.map((t) => (
        <div className="toast" key={t.id}>
          <span className={"t-ic " + (t.kind === "bad" ? "bad" : t.kind === "info" ? "info" : "ok")}>
            {t.kind === "bad" ? <Ic.x /> : t.kind === "info" ? <Ic.bolt /> : <Ic.check />}
          </span>
          {t.msg}
        </div>
      ))}
    </div>
  );
}

/* ---- business avatar ----------------------------------------------------- */
function BizAvatar({ biz, size = 30 }) {
  return (
    <span style={{
      width: size, height: size, borderRadius: 8, flex: `0 0 ${size}px`,
      background: biz.color + "22", color: biz.color, display: "grid", placeItems: "center",
      fontWeight: 700, fontSize: size * 0.42,
    }}>{biz.short}</span>
  );
}

/* ---- product header + tabs ---------------------------------------------- */
function AppHeader({ sub, who, right, store }) {
  // the account menu (was in the removed top proto-bar) now lives here on the right
  const acct = store && store.user;
  return (
    <div className="appHeader">
      <div className="brand">
        <span className="logo">Lekha AI</span>
        {sub && <span className="sub">{sub}</span>}
      </div>
      <div className="spacer" />
      {right}
      {acct
        ? <AccountMenu user={store.user} logout={store.logout} store={store} />
        : who && (
          <div className="who">
            <span className="avatar">{who.av}</span>
            <span className="who-meta"><b>{who.name}</b><span>{who.role}</span></span>
          </div>
        )}
    </div>
  );
}
function Tabs({ items, active, onChange }) {
  return (
    <div className="tabs">
      {items.map((t) => (
        <button key={t.id} className={"tab" + (active === t.id ? " on" : "")} onClick={() => onChange(t.id)}>
          {t.label}
          {t.count != null && <span className="tcount">{t.count}</span>}
        </button>
      ))}
    </div>
  );
}

/* ---- per-user notification settings (Finance & Brand Ops) --------------- */
function NotificationSettings() {
  const [items, setItems] = React.useState(null);
  React.useEffect(() => { api.get("/api/notifications/me").then((d) => setItems(d.items)); }, []);
  const toggle = (t, enabled) =>
    api.put("/api/notifications/me", { notif_type: t, enabled }).then((d) => setItems(d.items));

  if (!items) return <div className="empty"><div className="e-s">Loading…</div></div>;
  return (
    <>
      <div className="page-head"><div><div className="ph-title">Notifications</div><div className="ph-sub">Choose which emails you receive. Some may be turned off by your Lekha administrator.</div></div></div>
      <div className="card">
        {items.map((i) => (
          <div className="biz-card" key={i.notif_type}>
            <span className="k-ic" style={{ background: "var(--blue-bg)", color: "var(--blue-fg)", flex: "0 0 36px", width: 36, height: 36 }}><Ic.mail /></span>
            <div style={{ flex: "1 1 auto", minWidth: 0 }}>
              <div className="biz-name">{i.label}</div>
              <div className="biz-meta">{i.locked ? "Turned off by your Lekha administrator" : "Email me when this happens"}</div>
            </div>
            <label className="switch" style={{ flex: "0 0 46px", opacity: i.locked ? 0.4 : 1 }}>
              <input type="checkbox" checked={i.enabled && i.allowed} disabled={i.locked} onChange={(e) => toggle(i.notif_type, e.target.checked)} />
              <span className="track"><span className="knob" /></span>
            </label>
          </div>
        ))}
        {!items.length && <div className="biz-card" style={{ justifyContent: "center", color: "var(--text-3)" }}>No notification settings for your role.</div>}
      </div>
    </>
  );
}

/* ---- append-only event history (the real state trail, not the synthetic one) --- */
function fmtEventTime(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (isNaN(d)) return "";
  return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
}

// keys that are bookkeeping context, not a user-facing field change
const _DIFF_HIDE = ["via", "provider", "finance_try", "kind", "attempt", "exception_id", "resolved", "voucher"];

function DiffView({ diff }) {
  if (!diff || typeof diff !== "object" || Array.isArray(diff)) return null;
  const entries = Object.entries(diff).filter(([k]) => _DIFF_HIDE.indexOf(k) < 0);
  if (!entries.length) return null;
  const fmt = (v) => (v === null || v === undefined || v === "" ? "∅"
    : typeof v === "object" ? JSON.stringify(v) : String(v));
  return (
    <div style={{ marginTop: 6, display: "flex", flexDirection: "column", gap: 3 }}>
      {entries.map(([k, v]) => {
        const ft = v && typeof v === "object" && !Array.isArray(v) && ("from" in v || "to" in v);
        return (
          <div key={k} style={{ fontSize: 12, lineHeight: 1.5 }}>
            <span style={{ color: "var(--muted)", textTransform: "capitalize" }}>{k.replace(/_/g, " ")}: </span>
            {ft ? (
              <span>
                <span style={{ textDecoration: "line-through", color: "var(--muted)" }}>{fmt(v.from)}</span>
                <span style={{ margin: "0 5px" }}>→</span>
                <span style={{ fontWeight: 600 }}>{fmt(v.to)}</span>
              </span>
            ) : (<span style={{ fontWeight: 600 }}>{fmt(v)}</span>)}
          </div>
        );
      })}
    </div>
  );
}

// events: the `history` array from /api/credit-notes/{id} — oldest first; we show newest first.
function EventHistory({ events, title, max }) {
  if (!events) return null;
  const rows = events.slice().reverse();
  const shown = max ? rows.slice(0, max) : rows;
  return (
    <div className="card card-pad">
      <div className="section-title">{title || "History"}</div>
      {shown.length ? (
        <div className="timeline">
          {shown.map((e) => (
            <div className="tl-item" key={e.id}>
              <div className="tl-rail"><span className="tl-dot" /><span className="tl-line" /></div>
              <div className="tl-body">
                <div className="tl-t">
                  {e.label}
                  {e.from && e.to && e.from !== e.to ? (
                    <span style={{ marginLeft: 8, fontSize: 11, fontWeight: 600, color: "var(--muted)", fontFamily: "var(--mono, monospace)" }}>
                      {e.from} → {e.to}
                    </span>
                  ) : null}
                </div>
                <div className="tl-meta">{e.actor}{e.roleLabel ? " · " + e.roleLabel : ""} · {fmtEventTime(e.ts)}</div>
                <DiffView diff={e.diff} />
                {e.comment && !(e.from && e.to) ? (
                  <div className="tiny" style={{ marginTop: 4, color: "var(--muted)" }}>{e.comment}</div>
                ) : null}
              </div>
            </div>
          ))}
        </div>
      ) : <div className="tiny" style={{ color: "var(--muted)" }}>No recorded events yet.</div>}
    </div>
  );
}

/* ---- Confidence as a mini ring gauge (green ≥90%, amber ≥70%, red below) ---- */
function ConfidenceRing({ value, size = 46 }) {
  const pct = Math.max(0, Math.min(1, Number(value) || 0));
  const r = (size - 6) / 2;
  const c = 2 * Math.PI * r;
  const col = pct >= 0.9 ? "var(--green-fg)" : pct >= 0.7 ? "#c9970a" : "var(--red-fg)";
  return (
    <span className="conf-ring" title={`Extraction confidence ${Math.round(pct * 100)}%`}>
      <span className="conf-ring-svg" style={{ width: size, height: size }}>
        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ transform: "rotate(-90deg)" }}>
          <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke="var(--line)" strokeWidth="4" />
          <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={col} strokeWidth="4"
            strokeLinecap="round" strokeDasharray={c} strokeDashoffset={c * (1 - pct)}
            style={{ transition: "stroke-dashoffset .9s cubic-bezier(.22,1,.36,1)" }} />
        </svg>
        <b className="conf-ring-num" style={{ color: col }}>{Math.round(pct * 100)}</b>
      </span>
      <span className="tiny" style={{ color: "var(--text-3)", lineHeight: 1.15 }}>%<br />confidence</span>
    </span>
  );
}

/* ---- one-shot confetti burst (self-contained, no external lib) ---- */
function Confetti() {
  const cols = ["#c5e063", "#8bbf3f", "#e0b34a", "#5aa9e6", "#e06a5a", "#7d5ae0"];
  const bits = Array.from({ length: 40 }, (_, i) => ({
    left: Math.random() * 100, delay: Math.random() * 0.3,
    dur: 1.7 + Math.random() * 1.3, rot: Math.random() * 360,
    w: 6 + Math.random() * 6, col: cols[i % cols.length],
  }));
  return (
    <div className="confetti" aria-hidden="true">
      {bits.map((b, i) => (
        <span key={i} className="confetti-bit" style={{
          left: b.left + "%", background: b.col, width: b.w, height: b.w * 0.45,
          animationDelay: b.delay + "s", animationDuration: b.dur + "s",
          "--rot": b.rot + "deg",
        }} />
      ))}
    </div>
  );
}

Object.assign(window, {
  Ic, StatusBadge, Pipeline, LineItems, Confidence, ConfidenceRing, Confetti, Drawer, Toasts, BizAvatar, AppHeader, Tabs,
  CUSTOMER_STATUS, TimeFilter, withinRange, NotificationSettings,
  EventHistory, DiffView, fmtEventTime,
});
