// VendorIntelligencePage — window.VendorIntelligencePage
//
// Deep per-vendor / payables intelligence: spend ranking, spend analysis,
// payment-terms optimisation and a full per-vendor detail panel.
//
// HONEST DATA SOURCING: gl_transactions carry no payee, so vendor-level spend is
// built from the AP subledger — data.dimensions.apBills (bill_no, issue_date,
// due_date, vendor_name, category, terms, amount, open_balance, paid_date) — with
// data.dimensions.vendors (vendors_master: terms, category, status) for
// supplemental terms. When the AP subledger is absent the page renders an honest
// empty state, never a fabricated vendor. DPO is measured from paid_date −
// issue_date; cash-flow optimisation is derived from real terms, not invented.

(function () {
  const h = React.createElement;
  const { useState } = React;

  const num = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0; };
  const parseD = (d) => { const t = d ? +new Date(d) : 0; return Number.isFinite(t) ? t : 0; };
  const DAY = 86400000;
  const fmtMonYr = (d) => { try { return new Date(d).toLocaleDateString("en-US", { month: "short", year: "numeric" }); } catch (e) { return d || "—"; } };
  const fmtDay = (d) => { try { return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); } catch (e) { return d || "—"; } };
  const fmtYM = (ym) => { try { const [y, m] = ym.split("-"); return new Date(+y, +m - 1, 1).toLocaleDateString("en-US", { month: "short", year: "2-digit" }); } catch (e) { return ym; } };

  function monthsInRange(range) {
    const out = [];
    if (!range || !range.start || !range.end) return out;
    let y = range.start.getFullYear(), m = range.start.getMonth();
    const ey = range.end.getFullYear(), em = range.end.getMonth();
    while (y < ey || (y === ey && m <= em)) { out.push(y + "-" + String(m + 1).padStart(2, "0")); m++; if (m > 11) { m = 0; y++; } if (out.length > 60) break; }
    return out;
  }

  // Parse a payment-terms string → { net, discPct, discDays }. "Net 30" → net 30;
  // "2/10 Net 30" → net 30 + a 2% discount if paid within 10 days.
  function parseTerms(t) {
    if (!t) return { raw: null, net: null, discPct: null, discDays: null };
    const s = String(t);
    const disc = s.match(/(\d+(?:\.\d+)?)\s*\/\s*(\d+)/);
    const net = s.match(/net\s*(\d+)/i);
    let netDays = net ? +net[1] : null;
    if (netDays == null) { const any = s.match(/(\d+)/); if (any && !disc) netDays = +any[1]; }
    return { raw: s, net: netDays, discPct: disc ? +disc[1] : null, discDays: disc ? +disc[2] : null };
  }

  // Reusable stacked monthly bars (top 5 rows + Other). rows: [{name, byMonth}].
  function StackedBars(props) {
    const { months, rows, colors, onSeg } = props;
    const K = window.PerduraPageKit;
    const top = rows.slice(0, 5);
    const topNames = new Set(top.map((c) => c.name));
    const series = top.map((c) => ({ name: c.name, data: months.map((ym) => c.byMonth[ym] || 0) }));
    const otherData = months.map((ym) => rows.filter((c) => !topNames.has(c.name)).reduce((s, c) => s + (c.byMonth[ym] || 0), 0));
    const hasOther = otherData.some((v) => v > 0);
    const allSeries = series.concat(hasOther ? [{ name: "Other", data: otherData, other: true }] : []);
    const monthTotals = months.map((_, mi) => allSeries.reduce((s, se) => s + se.data[mi], 0));
    const maxV = Math.max.apply(null, monthTotals.concat([1]));
    const W = 820, H = 240, padL = 48, padR = 12, padT = 14, padB = 34;
    const chartH = H - padT - padB;
    const colW = (W - padL - padR) / Math.max(months.length, 1);
    const barW = Math.min(colW * 0.62, 46);
    const toY = (v) => padT + chartH * (1 - v / maxV);
    const fmtN = (v) => Math.abs(v) >= 1e6 ? (v / 1e6).toFixed(1) + "M" : Math.abs(v) >= 1e3 ? (v / 1e3).toFixed(0) + "K" : "" + Math.round(v);
    const els = [];
    [0, .25, .5, .75, 1].forEach((p, i) => { const y = padT + chartH * p; els.push(h("line", { key: "g" + i, x1: padL, x2: W - padR, y1: y, y2: y, stroke: "rgba(13,32,64,.07)" }), h("text", { key: "gl" + i, x: padL - 6, y: y + 3, textAnchor: "end", fontSize: 9, fill: "#94a3b8" }, fmtN(maxV * (1 - p)))); });
    months.forEach((ym, mi) => {
      const x = padL + mi * colW + (colW - barW) / 2;
      let acc = 0;
      allSeries.forEach((se, si) => {
        const v = se.data[mi]; if (!v) return;
        const yTop = toY(acc + v), yBot = toY(acc);
        const color = se.other ? "#c3ccdb" : colors[si % colors.length];
        els.push(h("rect", { key: "s" + mi + "-" + si, x: x, y: yTop, width: barW, height: Math.max(yBot - yTop, 1), fill: color, rx: 1.5, style: { cursor: se.other ? "default" : "pointer" }, onClick: se.other ? undefined : () => onSeg && onSeg(se.name, ym) }, h("title", null, se.name + " · " + fmtYM(ym) + " · " + (K ? K.moneyStr(v, { compact: true }) : Math.round(v)))));
        acc += v;
      });
      if (mi % Math.ceil(months.length / 12) === 0) els.push(h("text", { key: "xl" + mi, x: padL + mi * colW + colW / 2, y: H - 6, textAnchor: "middle", fontSize: 9, fill: "#94a3b8" }, fmtYM(ym)));
    });
    return h("div", null,
      h("svg", { viewBox: "0 0 " + W + " " + H, style: { width: "100%", height: "auto", display: "block" } }, els),
      h("div", { style: { display: "flex", flexWrap: "wrap", gap: 12, marginTop: 8, fontSize: 11, color: "#6475a0" } },
        allSeries.map((se, si) => h("span", { key: si, style: { display: "inline-flex", alignItems: "center", gap: 5 } },
          h("span", { style: { width: 10, height: 10, borderRadius: 2, background: se.other ? "#c3ccdb" : colors[si % colors.length], display: "inline-block" } }), se.name))));
  }

  function exportVendorCsv(v) {
    const headers = ["Bill #", "Issue Date", "Due Date", "Amount", "Open Balance", "Status", "Days to Pay"];
    const rows = v.bills.slice().sort((a, b) => (b.issue_date || "").localeCompare(a.issue_date || "")).map((b) => {
      const dtp = (b.paid_date && b.issue_date) ? Math.round((parseD(b.paid_date) - parseD(b.issue_date)) / DAY) : "";
      return [b.bill_no || "", (b.issue_date || "").slice(0, 10), (b.due_date || "").slice(0, 10), num(b.amount), num(b.open_balance), num(b.open_balance) > 0 ? "Open" : "Paid", dtp];
    });
    const meta = [["PerduraCFO — Vendor Report: " + v.name], ["Spend (period): " + Math.round(v.spend)], ["Bills: " + v.billCount], ["Avg DPO: " + (v.avgDPO != null ? Math.round(v.avgDPO) : "n/a")], ["Exported: " + new Date().toLocaleDateString()], [], headers, ...rows];
    const csv = meta.map((row) => row.map((cell) => { const s = String(cell == null ? "" : cell); return (s.includes(",") || s.includes("\"") || s.includes("\n")) ? "\"" + s.replace(/"/g, "\"\"") + "\"" : s; }).join(",")).join("\n");
    const blob = new Blob(["﻿" + csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = v.name.replace(/[^a-z0-9]/gi, "_") + "_vendor_report.csv";
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // ── vendor model from the AP subledger ──────────────────────────────────────
  function buildModel(bills, vendorMaster, range, cmpRange) {
    const start = +range.start, end = +range.end;
    const inWin = bills.filter((b) => { const d = parseD(b.issue_date); return d >= start && d <= end; });
    const firstSeen = {};
    bills.forEach((b) => { const k = b.vendor_name || "Unknown"; const d = b.issue_date || ""; if (!firstSeen[k] || d < firstSeen[k]) firstSeen[k] = d; });
    const priorByV = {};
    if (cmpRange) { const ps = +cmpRange.start, pe = +cmpRange.end; bills.forEach((b) => { const d = parseD(b.issue_date); if (d >= ps && d <= pe) { const k = b.vendor_name || "Unknown"; priorByV[k] = (priorByV[k] || 0) + Math.abs(num(b.amount)); } }); }

    const by = {};
    inWin.forEach((b) => {
      const k = b.vendor_name || "Unknown";
      const amt = Math.abs(num(b.amount));
      const o = by[k] || (by[k] = { name: k, spend: 0, billCount: 0, byMonth: {}, lastBill: null, openBalance: 0, dpoSum: 0, dpoN: 0, cats: {}, termsSeen: {}, bills: [] });
      o.spend += amt; o.billCount += 1;
      const ym = (b.issue_date || "").slice(0, 7); if (ym) o.byMonth[ym] = (o.byMonth[ym] || 0) + amt;
      if (!o.lastBill || (b.issue_date || "") > o.lastBill) o.lastBill = b.issue_date;
      o.openBalance += num(b.open_balance);
      if (b.paid_date && b.issue_date) { const dtp = (parseD(b.paid_date) - parseD(b.issue_date)) / DAY; if (dtp >= 0 && dtp < 3650) { o.dpoSum += dtp; o.dpoN += 1; } }
      if (b.category) o.cats[b.category] = (o.cats[b.category] || 0) + amt;
      if (b.terms) o.termsSeen[b.terms] = (o.termsSeen[b.terms] || 0) + 1;
      o.bills.push(b);
    });

    const total = Object.values(by).reduce((s, v) => s + v.spend, 0);
    const months = monthsInRange(range);
    const list = Object.values(by).map((v) => {
      const vm = vendorMaster[v.name] || null;
      const topCat = Object.keys(v.cats).sort((a, b) => v.cats[b] - v.cats[a])[0] || (vm && vm.category) || "Uncategorised";
      const terms = Object.keys(v.termsSeen).sort((a, b) => v.termsSeen[b] - v.termsSeen[a])[0] || (vm && vm.terms) || null;
      return Object.assign({}, v, {
        pct: total > 0 ? v.spend / total : 0,
        avgBill: v.billCount ? v.spend / v.billCount : 0,
        avgDPO: v.dpoN ? v.dpoSum / v.dpoN : null,
        prior: priorByV[v.name] || 0,
        trendPct: (priorByV[v.name] > 0) ? (v.spend - priorByV[v.name]) / priorByV[v.name] * 100 : null,
        isNew: !!firstSeen[v.name] && parseD(firstSeen[v.name]) >= start,
        category: topCat,
        terms: terms,
        spark: months.map((ym) => v.byMonth[ym] || 0),
        status: v.openBalance > 0 ? "Open" : "Paid",
      });
    }).sort((a, b) => b.spend - a.spend);

    const hhi = list.reduce((s, v) => s + (v.pct * 100) * (v.pct * 100), 0);
    const top1 = list[0] ? list[0].pct : 0;
    const top3 = list.slice(0, 3).reduce((s, v) => s + v.pct, 0);
    const newCount = list.filter((v) => v.isNew).length;
    const dpos = list.map((v) => v.avgDPO).filter((x) => x != null).sort((a, b) => a - b);
    const medianDPO = dpos.length ? dpos[Math.floor(dpos.length / 2)] : null;

    // Category rollup for donut / stacked bars.
    const catTotals = {};
    list.forEach((v) => { catTotals[v.category] = (catTotals[v.category] || 0) + v.spend; });
    const catRows = Object.keys(catTotals).map((k) => ({ name: k, spend: catTotals[k], byMonth: {} })).sort((a, b) => b.spend - a.spend);
    inWin.forEach((b) => { const cat = (by[b.vendor_name || "Unknown"] && by[b.vendor_name || "Unknown"].category) || b.category || "Uncategorised"; const r = catRows.find((x) => x.name === cat); if (r) { const ym = (b.issue_date || "").slice(0, 7); if (ym) r.byMonth[ym] = (r.byMonth[ym] || 0) + Math.abs(num(b.amount)); } });

    return { list, total, count: list.length, months, hhi, top1, top3, newCount, medianDPO, catRows };
  }

  function Page(props) {
    const K = window.PerduraPageKit;
    if (!K) return h("div", { className: "pc-page" }, "Loading…");
    const { data } = props;
    const MONO = K.MONO, INK = K.INK, MUTE = K.MUTE, POS = K.POS, NEG = K.NEG;
    const M = (v) => K.moneyStr(v, { compact: true });
    const Mf = (v) => K.moneyStr(v);

    const dims = (data && data.dimensions) || {};
    const bills = (dims.apBills || data.apBills || []).filter((b) => b && b.issue_date);
    const vmRows = dims.vendors || data.vendors || [];
    const vendorMaster = {};
    vmRows.forEach((vm) => { const nm = vm.name || vm.vendor_name; if (nm) vendorMaster[nm] = vm; });

    const plH = (data && (data.plHistory || data.pl)) || { labels: [], revenue: [] };
    const anchor = K.anchorFromPlH(plH);
    const ps = K.usePeriodState("vendor_intel", "ltm");
    const range = K.resolvePeriod(ps.mode, anchor, ps.custom);
    const cmpRange = K.comparePeriod(range, ps.cmp);

    const [tab, setTab] = useState("overview");
    const [selected, setSelected] = useState(null);
    const [rowLimit, setRowLimit] = useState(25);
    const [catFilter, setCatFilter] = useState(null);

    if (!bills.length) {
      return h(K.Shell, { hero: { eyebrow: "ANALYTICS", title: "Vendor Intelligence", subtitle: "Per-vendor spend, payment terms and cash-flow optimisation" } },
        h(K.Card, { title: "No payables subledger connected" },
          h("div", { style: { padding: 24, color: MUTE, fontSize: 13, lineHeight: 1.7 } },
            "Vendor-level spend, payment terms and DPO are built from the ", h("b", null, "AP bills subledger"), " (ap_bills) — not the general ledger, whose expense postings carry no payee. ",
            "Once bills are synced (Wave / QuickBooks / CSV), this page lights up with per-vendor ranking, concentration, days-payable and payment-terms optimisation.")));
    }

    const model = buildModel(bills, vendorMaster, range, cmpRange);
    const { list, total, count, months, hhi, top1, top3, newCount, medianDPO, catRows } = model;
    const avgPerVendor = count ? total / count : 0;
    const openVendor = (name) => { setSelected(name); setTab("detail"); };

    const hero = {
      eyebrow: "ANALYTICS",
      title: "Vendor Intelligence",
      subtitle: count + " active vendor" + (count === 1 ? "" : "s") + " · " + Mf(total) + " AP spend · " + Mf(avgPerVendor) + " avg per vendor · " + range.label,
      controls: h(K.PeriodControls, ps),
    };

    const TABS = [["overview", "📊 Overview"], ["analysis", "📈 Spend Analysis"], ["terms", "⏱ Payment Terms"], ["detail", "🏢 Vendor Detail"]];
    const tabBar = h("div", { style: { display: "flex", gap: 4, borderBottom: "2px solid rgba(13,32,64,.08)", marginBottom: 22, overflowX: "auto" } },
      TABS.map(([id, label]) => h("button", { key: id, onClick: () => setTab(id), style: { padding: "10px 18px", fontSize: 12, fontWeight: 700, cursor: "pointer", border: "none", borderBottom: tab === id ? "3px solid #0d2040" : "3px solid transparent", background: "transparent", color: tab === id ? "#0d2040" : "#6475a0", whiteSpace: "nowrap" } }, label)));

    const kpiRow = h("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))", gap: 14, marginBottom: 22 } },
      h(K.Kpi, { label: "Total AP Spend", value: M(total), sub: range.label, accent: "#DC2626" }),
      h(K.Kpi, { label: "Active Vendors", value: String(count), sub: newCount + " new this period", accent: "#0891B2" }),
      h(K.Kpi, { label: "Avg Spend / Vendor", value: M(avgPerVendor), accent: "#7C3AED" }),
      h(K.Kpi, { label: "Top Vendor", value: (top1 * 100).toFixed(0) + "%", sub: list[0] ? list[0].name : "—", accent: top1 >= 0.3 ? "#DC2626" : "#059669", onClick: list[0] ? () => openVendor(list[0].name) : undefined }),
      h(K.Kpi, { label: "Top 3 Concentration", value: (top3 * 100).toFixed(0) + "%", sub: "HHI " + Math.round(hhi).toLocaleString(), accent: top3 >= 0.6 ? "#d97706" : "#059669" }),
      h(K.Kpi, { label: "New Vendors", value: String(newCount), sub: range.label, accent: "#18a867" }));

    // ── Tab 1 — Overview ──────────────────────────────────────────────────────
    function overviewTab() {
      const filtered = catFilter ? list.filter((v) => v.category === catFilter) : list;
      const shown = filtered.slice(0, rowLimit);
      const trendCell = (v) => {
        if (v.trendPct == null) return h("span", { style: { color: MUTE, fontSize: 11 } }, v.isNew ? "new" : "—");
        const up = v.trendPct >= 0; // for spend, up is cost increase → red
        return h("span", { style: { color: up ? NEG : POS, fontWeight: 700, fontFamily: MONO, fontSize: 11 } }, (up ? "▲ " : "▼ ") + Math.abs(v.trendPct).toFixed(0) + "%");
      };
      const catPill = (v) => h("span", { onClick: (e) => { e.stopPropagation(); setCatFilter(catFilter === v.category ? null : v.category); setRowLimit(25); }, title: "Filter to " + v.category, style: { fontSize: 10.5, fontWeight: 600, color: "#3730a3", background: "#eef2ff", border: "1px solid #c7d2fe", borderRadius: 20, padding: "2px 9px", cursor: "pointer", whiteSpace: "nowrap" } }, v.category);
      const rankTable = h("table", { className: "pa-table", style: { width: "100%" } },
        h("thead", null, h("tr", null,
          h("th", null, "#"), h("th", null, "Vendor"), h("th", { className: "num" }, "Spend"), h("th", { className: "num" }, "% of Total"),
          h("th", null, "Category"), h("th", { className: "num" }, "Trend"), h("th", { className: "num" }, "Avg Bill"), h("th", null, "Last Bill"), h("th", null, "Spark"))),
        h("tbody", null, shown.map((v, i) => h("tr", { key: v.name, style: { cursor: "pointer" }, onClick: () => openVendor(v.name), title: "Open " + v.name + " detail" },
          h("td", { style: { color: MUTE, fontFamily: MONO } }, i + 1),
          h("td", { style: { fontWeight: 700, color: INK } }, v.name, v.isNew ? h("span", { style: { marginLeft: 6, fontSize: 9, fontWeight: 700, color: "#065f46", background: "#d1fae5", padding: "1px 6px", borderRadius: 10 } }, "NEW") : null),
          h("td", { className: "num", style: { fontFamily: MONO, fontWeight: 700 } }, M(v.spend)),
          h("td", { className: "num", style: { fontFamily: MONO, color: v.pct >= 0.2 ? "#d97706" : MUTE } }, (v.pct * 100).toFixed(1) + "%"),
          h("td", null, catPill(v)),
          h("td", { className: "num" }, trendCell(v)),
          h("td", { className: "num", style: { fontFamily: MONO } }, M(v.avgBill)),
          h("td", { style: { color: MUTE, fontSize: 11 } }, v.lastBill ? fmtMonYr(v.lastBill) : "—"),
          h("td", null, h(K.Spark, { values: v.spark, color: "#DC2626", width: 68, height: 22 }))))));
      const moreBtn = filtered.length > rowLimit ? h("div", { style: { padding: 12, textAlign: "center" } },
        h("button", { onClick: () => setRowLimit((n) => n + 25), style: { padding: "7px 16px", background: "#f0f4ff", color: "#1C4ED8", border: "1px solid #dbeafe", borderRadius: 8, cursor: "pointer", fontSize: 12, fontWeight: 600 } }, "Show 25 more (" + (filtered.length - rowLimit) + " left)")) : null;
      const filterChip = catFilter ? h("div", { style: { marginBottom: 12, display: "flex", alignItems: "center", gap: 8 } },
        h("span", { style: { fontSize: 12, color: MUTE } }, "Filtered to category:"),
        h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, background: "#eef2ff", border: "1px solid #c7d2fe", borderRadius: 20, padding: "3px 10px", fontSize: 12, color: "#3730a3", fontWeight: 700 } }, catFilter,
          h("button", { onClick: () => setCatFilter(null), style: { background: "none", border: "none", cursor: "pointer", color: "#3730a3", fontSize: 14, padding: 0, lineHeight: 1 } }, "×"))) : null;
      return h("div", null, kpiRow, filterChip,
        h(K.Card, { title: "Vendor Spend Ranking", sub: "Sorted by spend · click a vendor for full detail · click a category pill to filter", padding: 0 },
          h("div", { style: { overflowX: "auto" } }, rankTable), moreBtn));
    }

    // ── Tab 2 — Spend Analysis ────────────────────────────────────────────────
    function analysisTab() {
      const colors = K.RANK_COLORS;
      const donutItems = catRows.slice(0, 8).map((c, i) => ({ label: c.name, value: c.spend, color: colors[i % colors.length] }));
      // Top spend increases vs prior period.
      const increases = list.filter((v) => (v.spend - v.prior) > 0).map((v) => ({ name: v.name, prior: v.prior, cur: v.spend, change: v.spend - v.prior, spike: v.prior > 0 ? v.spend / v.prior : null, isNew: v.prior === 0 }))
        .sort((a, b) => b.change - a.change).slice(0, 8);
      const catList = h("div", { style: { display: "flex", flexDirection: "column", gap: 8 } },
        catRows.slice(0, 10).map((c, i) => { const pct = total > 0 ? c.spend / total * 100 : 0; return h("div", { key: c.name },
          h("div", { style: { display: "flex", justifyContent: "space-between", fontSize: 12, marginBottom: 3 } },
            h("span", { style: { color: INK, fontWeight: 600 } }, c.name),
            h("span", { style: { color: INK, fontFamily: MONO, fontWeight: 700 } }, M(c.spend) + " · " + pct.toFixed(0) + "%")),
          h("div", { style: { height: 6, background: "#f0f4ff", borderRadius: 3 } }, h("div", { style: { width: pct + "%", height: "100%", background: colors[i % colors.length], borderRadius: 3 } }))); }));
      const incRows = increases.map((r) => {
        const flag = r.isNew
          ? h("span", { style: { fontSize: 10.5, fontWeight: 700, color: "#065f46", background: "#d1fae5", padding: "1px 7px", borderRadius: 10 } }, "NEW VENDOR")
          : (r.spike && r.spike >= 2)
            ? h("span", { style: { fontSize: 10.5, fontWeight: 700, color: "#991b1b", background: "#fee2e2", padding: "1px 7px", borderRadius: 10 } }, "Spike (" + r.spike.toFixed(1) + "×)")
            : h("span", { style: { fontSize: 11, color: MUTE } }, "increase");
        return h("tr", { key: r.name, style: { cursor: "pointer" }, onClick: () => openVendor(r.name) },
          h("td", { style: { fontWeight: 600, color: INK } }, r.name),
          h("td", { className: "num", style: { fontFamily: MONO, color: MUTE } }, r.prior ? M(r.prior) : "—"),
          h("td", { className: "num", style: { fontFamily: MONO } }, M(r.cur)),
          h("td", { className: "num", style: { fontFamily: MONO, fontWeight: 700, color: NEG } }, "+" + M(r.change)),
          h("td", null, flag));
      });
      const incTable = increases.length ? h("table", { className: "pa-table", style: { width: "100%" } },
        h("thead", null, h("tr", null, h("th", null, "Vendor"), h("th", { className: "num" }, "Prior"), h("th", { className: "num" }, "Current"), h("th", { className: "num" }, "Change"), h("th", null, "Flag"))),
        h("tbody", null, incRows)) : h("div", { style: { padding: 16, color: MUTE, fontSize: 12.5 } }, "No vendor spend increases vs the prior period.");
      return h("div", null,
        h("div", { style: { display: "grid", gridTemplateColumns: "1fr 1.3fr", gap: 16, alignItems: "start", marginBottom: 16 } },
          h(K.Card, { title: "Spend by Category" }, K.Donut ? h(K.Donut, { items: donutItems }) : catList),
          h(K.Card, { title: "Category Breakdown", sub: "Share of AP spend" }, catList)),
        h(K.Card, { title: "Monthly Spend by Category", sub: "Top 5 categories + Other · click a segment to filter that category" },
          months.length ? h(StackedBars, { months, rows: catRows, colors, onSeg: (name) => { setCatFilter(name); setTab("overview"); } }) : h("div", { style: { padding: 16, color: MUTE } }, "No monthly AP activity in this window.")),
        h("div", { style: { marginTop: 16 } },
          h(K.Card, { title: "Top Spend Increases", sub: "vs the prior period", padding: 0 }, h("div", { style: { overflowX: "auto" } }, incTable))));
    }

    // ── Tab 3 — Payment Terms & Optimisation ──────────────────────────────────
    function termsTab() {
      const optOf = (v) => {
        const pt = parseTerms(v.terms);
        if (v.avgDPO == null) return { text: pt.net ? ("Net " + pt.net + " on file · no paid bills yet to measure DPO") : "No terms / paid history on file", cash: 0, tone: MUTE };
        if (pt.net == null) return { text: "No standard terms on file — currently paying ~" + Math.round(v.avgDPO) + "d; set terms to benchmark", cash: 0, tone: "#d97706" };
        if (v.avgDPO < pt.net - 3) { const extra = pt.net - v.avgDPO; const cash = v.spend / 365 * extra; return { text: "Paying ~" + Math.round(v.avgDPO) + "d vs Net " + pt.net + " — extend to terms to free ~" + M(cash) + " of working capital", cash: cash, tone: POS }; }
        if (v.avgDPO > pt.net + 5) return { text: "Paying ~" + Math.round(v.avgDPO) + "d, beyond Net " + pt.net + " — late-payment / relationship risk", cash: 0, tone: NEG };
        return { text: "On terms (~" + Math.round(v.avgDPO) + "d vs Net " + pt.net + ") — no action needed", cash: 0, tone: MUTE };
      };
      const rows = list.slice(0, 40).map((v) => Object.assign({}, v, { pt: parseTerms(v.terms), opt: optOf(v) }));
      const totalCash = rows.reduce((s, r) => s + r.opt.cash, 0);
      const discounts = rows.filter((r) => r.pt.discPct && r.avgDPO != null && r.avgDPO > (r.pt.discDays || 0));
      const termsTable = h("table", { className: "pa-table", style: { width: "100%" } },
        h("thead", null, h("tr", null, h("th", null, "Vendor"), h("th", null, "Terms"), h("th", { className: "num" }, "Avg DPO"), h("th", null, "Optimisation"))),
        h("tbody", null, rows.map((r) => h("tr", { key: r.name, style: { cursor: "pointer" }, onClick: () => openVendor(r.name) },
          h("td", { style: { fontWeight: 600, color: INK } }, r.name),
          h("td", { style: { color: MUTE } }, r.terms || "—"),
          h("td", { className: "num", style: { fontFamily: MONO } }, r.avgDPO != null ? Math.round(r.avgDPO) + "d" : "—"),
          h("td", { style: { fontSize: 12, color: r.opt.tone } }, r.opt.text)))));
      const discCard = discounts.length
        ? h("table", { className: "pa-table", style: { width: "100%" } },
            h("thead", null, h("tr", null, h("th", null, "Vendor"), h("th", null, "Terms"), h("th", { className: "num" }, "Currently pays"), h("th", null, "Opportunity"))),
            h("tbody", null, discounts.map((r) => { const ann = r.pt.discPct / (100 - r.pt.discPct) * 365 / Math.max(1, (r.pt.net - r.pt.discDays)); return h("tr", { key: r.name },
              h("td", { style: { fontWeight: 600, color: INK } }, r.name),
              h("td", { style: { color: MUTE } }, r.terms),
              h("td", { className: "num", style: { fontFamily: MONO } }, Math.round(r.avgDPO) + "d"),
              h("td", { style: { fontSize: 12, color: POS } }, "Take the " + r.pt.discPct + "% / " + r.pt.discDays + "d discount → ~" + (ann * 100).toFixed(0) + "% annualised on " + M(r.spend) + " of spend")); })))
        : h("div", { style: { padding: 16, color: MUTE, fontSize: 12.5 } }, "No early-payment discount terms detected on file (e.g. “2/10 Net 30”). If a vendor offers one, add it to their terms to surface the opportunity.");
      return h("div", null,
        h("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px,1fr))", gap: 14, marginBottom: 18 } },
          h(K.Kpi, { label: "Cash-Flow Improvement Potential", value: M(totalCash), sub: "from extending early payers to terms", accent: totalCash > 0 ? "#059669" : "#6475a0" }),
          h(K.Kpi, { label: "Median DPO", value: medianDPO != null ? Math.round(medianDPO) + "d" : "—", sub: "across vendors with paid history", accent: "#1C4ED8" }),
          h(K.Kpi, { label: "Discount Opportunities", value: String(discounts.length), sub: "vendors offering early-pay discounts", accent: discounts.length ? "#18a867" : "#6475a0" })),
        h(K.Card, { title: "Payment Terms Analysis", sub: "DPO measured from bill issue → payment date · optimisation from real terms", padding: 0 }, h("div", { style: { overflowX: "auto" } }, termsTable)),
        h("div", { style: { marginTop: 16 } }, h(K.Card, { title: "Early-Payment Discount Opportunities", sub: "Discounts worth taking when the annualised return beats your cost of capital", padding: 0 }, h("div", { style: { overflowX: "auto" } }, discCard))));
    }

    // ── Tab 4 — Vendor Detail ─────────────────────────────────────────────────
    function detailTab() {
      const v = list.find((x) => x.name === selected) || list[0];
      if (!v) return h(K.Card, { title: "Vendor Detail" }, h("div", { style: { padding: 20, color: MUTE } }, "No vendor selected."));
      const bs = v.bills.slice().sort((a, b) => (b.issue_date || "").localeCompare(a.issue_date || ""));
      const monthBars = months.map((ym) => ({ ym, val: v.byMonth[ym] || 0 }));
      const maxBar = Math.max.apply(null, monthBars.map((m) => m.val).concat([1]));
      const catTotal = catRows.find((c) => c.name === v.category);
      const catShare = catTotal && catTotal.spend > 0 ? v.spend / catTotal.spend * 100 : null;
      const pt = parseTerms(v.terms);
      const note = v.name + " accounts for " + Mf(v.spend) + " of AP spend this period (" + (v.pct * 100).toFixed(1) + "% of the total)"
        + (catShare != null ? " and " + catShare.toFixed(0) + "% of " + v.category + " spend" : "") + ". "
        + (v.avgDPO != null && pt.net != null ? ("You pay in ~" + Math.round(v.avgDPO) + " days against " + v.terms + (v.avgDPO < pt.net - 3 ? " — you could hold cash ~" + Math.round(pt.net - v.avgDPO) + " days longer without breaching terms." : v.avgDPO > pt.net + 5 ? " — that is beyond terms and a relationship risk." : " — squarely on terms.")) : (v.avgDPO != null ? ("Average days-to-pay is ~" + Math.round(v.avgDPO) + " days.") : "No paid-bill history yet to measure days-to-pay."))
        + (v.pct >= 0.15 ? " Given the concentration, benchmark against alternatives annually." : "");

      const picker = list.length > 1
        ? h("select", { value: v.name, onChange: (e) => setSelected(e.target.value), style: { padding: "6px 10px", border: "1px solid " + K.LINE, borderRadius: 7, fontSize: 12, fontWeight: 600, color: INK, cursor: "pointer" } }, list.slice(0, 200).map((x) => h("option", { key: x.name, value: x.name }, x.name)))
        : null;
      const header = h("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 14, gap: 12, flexWrap: "wrap" } },
        h("div", { style: { display: "flex", alignItems: "center", gap: 12 } },
          h(K.BackButton, { label: "all vendors", onClick: () => setTab("overview") }), picker,
          h("div", { style: { fontSize: 20, fontWeight: 800, color: INK } }, v.name)),
        h("button", { onClick: () => exportVendorCsv(v), style: { padding: "7px 14px", background: "#f0fdf4", color: "#065f46", border: "1px solid #bbf7d0", borderRadius: 8, cursor: "pointer", fontSize: 12, fontWeight: 600 } }, "⬇️ Export vendor report"));

      const stat = (label, value, sub) => h("div", { style: { padding: "10px 4px" } },
        h("div", { style: { fontSize: 9, fontWeight: 700, letterSpacing: "1px", textTransform: "uppercase", color: "#6475a0", fontFamily: MONO, marginBottom: 4 } }, label),
        h("div", { style: { fontSize: 17, fontWeight: 800, color: INK, fontFamily: MONO } }, value),
        sub ? h("div", { style: { fontSize: 10.5, color: MUTE, marginTop: 2 } }, sub) : null);
      const statsGrid = h("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(150px,1fr))", gap: 8 } },
        stat("Spend (" + range.label + ")", M(v.spend), v.trendPct != null ? ((v.trendPct >= 0 ? "▲ " : "▼ ") + Math.abs(v.trendPct).toFixed(0) + "% vs prior") : (v.isNew ? "new this period" : null)),
        stat("Bills", String(v.billCount), "avg " + M(v.avgBill)),
        stat("Category", v.category, catShare != null ? catShare.toFixed(0) + "% of category" : null),
        stat("Avg DPO", v.avgDPO != null ? Math.round(v.avgDPO) + " days" : "—", v.terms || null),
        stat("Last bill", v.lastBill ? fmtDay(v.lastBill) : "—"),
        stat("Open balance", v.openBalance > 0 ? Mf(v.openBalance) : "—", v.status));
      const statsCard = h(K.Card, { padding: "6px 18px" }, statsGrid);

      const trendBars = h("div", { style: { display: "flex", alignItems: "flex-end", gap: 4, height: 90 } },
        monthBars.map((mb) => h("div", { key: mb.ym, title: fmtYM(mb.ym) + ": " + Mf(mb.val), style: { flex: 1, display: "flex", flexDirection: "column", alignItems: "center", gap: 3 } },
          h("div", { style: { width: "100%", background: mb.val ? "#DC2626" : "#f4e9e9", height: Math.max((mb.val / maxBar) * 70, 2), borderRadius: "3px 3px 0 0" } }),
          h("div", { style: { fontSize: 8, color: MUTE, whiteSpace: "nowrap" } }, fmtYM(mb.ym)))));
      const trendCard = h("div", { style: { marginTop: 16 } }, h(K.Card, { title: "Monthly Spend Trend", sub: "AP bills by issue month" }, trendBars));

      const billRows = bs.slice(0, 200).map((b, i) => {
        const dtp = (b.paid_date && b.issue_date) ? Math.round((parseD(b.paid_date) - parseD(b.issue_date)) / DAY) : null;
        const open = num(b.open_balance) > 0;
        const overdue = open && b.due_date && parseD(b.due_date) < Date.now();
        const st = overdue ? "Overdue" : open ? "Open" : "Paid";
        const stc = overdue ? NEG : open ? "#d97706" : POS;
        return h("tr", { key: (b.bill_no || i) + "-" + i },
          h("td", { style: { fontFamily: MONO, fontWeight: 600 } }, b.bill_no || "—"),
          h("td", { style: { color: MUTE } }, b.issue_date ? fmtDay(b.issue_date) : "—"),
          h("td", { className: "num", style: { fontFamily: MONO, fontWeight: 700 } }, Mf(Math.abs(num(b.amount)))),
          h("td", { style: { color: MUTE } }, b.due_date ? fmtDay(b.due_date) : "—"),
          h("td", null, h("span", { style: { fontSize: 11, fontWeight: 700, color: stc } }, st)),
          h("td", { className: "num", style: { fontFamily: MONO } }, dtp != null ? dtp + " days" : "—"));
      });
      const billTable = h("table", { className: "pa-table", style: { width: "100%" } },
        h("thead", null, h("tr", null, h("th", null, "Bill #"), h("th", null, "Issue Date"), h("th", { className: "num" }, "Amount"), h("th", null, "Due Date"), h("th", null, "Status"), h("th", { className: "num" }, "Days to Pay"))),
        h("tbody", null, billRows));
      const billsCard = h("div", { style: { marginTop: 16 } },
        h(K.Card, { title: "Bill History", sub: v.billCount + " bills in " + range.label, padding: 0 }, h("div", { style: { overflowX: "auto", maxHeight: 420 } }, billTable)),
        bs.length > 200 ? h("div", { style: { padding: 8, fontSize: 11, color: MUTE, textAlign: "center" } }, "Showing first 200 of " + bs.length + " bills.") : null);

      const notePanel = h(K.CFOCommentaryPanel ? K.CFOCommentaryPanel : "div", { title: v.name, insights: [{ type: v.pct >= 0.2 ? "warning" : "neutral", text: note }] });
      return h("div", null, header, statsCard, trendCard, billsCard, notePanel);
    }

    const body = tab === "overview" ? overviewTab()
      : tab === "analysis" ? analysisTab()
      : tab === "terms" ? termsTab()
      : detailTab();
    return h(K.Shell, { hero }, tabBar, body);
  }

  window.VendorIntelligencePage = Page;
})();
