// Data — live row counts and last-updated for every Vault dataset.
// Reads real Supabase counts via the get_data_inventory RPC (no synthetic data).
(function () {
  const { useState, useEffect, useCallback, useMemo, useRef } = React;
  const BLUE = "#196EA7", INK = "#1A2733", INK2 = "#5A6B7B", MUTE = "#93A1B0", LINE = "#E4E9F0";

  // Curated datasets, grouped. table = Supabase table name; label = human name.
  // uploadable: shows an Upload/Undo control. Only enable once a table's load rule is
  // confirmed AND it is allow-listed in the replace_table_data RPC (server-side guard).
  const GROUPS = [
    { group: "Harvey imports (scraped \u2192 Supabase)", items: [
      { table: "user_activities",      label: "Activity Master", appendbulk: true },
      { table: "outreach_counts",      label: "Outreach", uploadable: true, windowreplace: true },
      { table: "activities",           label: "Activities", uploadable: true, windowreplace: true },
      { table: "project_universe",     label: "Project Universe", uploadable: true },
      { table: "pipeline",             label: "Pipeline", uploadable: true },
      { table: "project_lists",        label: "Project Lists", uploadable: true },
      { table: "harvey_offers",        label: "Offers & Better", uploadable: true },
      { table: "closed_deals",         label: "Closed Deals", appendable: true },
      { table: "crs",                  label: "CRs (Custom Re-approaches)", crimport: true },
      { table: "white_space_projects", label: "White Space" },  // curated in Vault — scrape upload retired 2026-07-08
    ]},
    { group: "Custom reports (manually prepared)", items: [
      { table: "projects",             label: "Projects / Engagements (retainers)" },
    ]},
    { group: "Vault-managed", items: [
      { table: "cr_calendar_feed",  label: "CR Calendar Feed" },
      { table: "people",            label: "People" },
      { table: "pipeline_picks",    label: "Pipeline Picks" },
      { table: "project_links",     label: "Project Links" },
      { table: "client_project_map",label: "Client \u2013 Project Map" },
      { table: "new_clients",       label: "New Clients" },
      { table: "research_goals",    label: "Research Goals" },
      { table: "research_plans",    label: "Research Plans" },
      { table: "progression_cards", label: "Progression Cards" },
    ]},
  ];
  // Page Impact is DERIVED from the freshness chip's view->tables map (shell.jsx),
  // so it always lists every page that needs a table, and stays in sync for free.
  function impactFor(table) {
    const vt = window.VAULT_VIEW_TABLES || {}, vl = window.VAULT_VIEW_LABELS || {};
    const out = [];
    Object.keys(vt).forEach(v => { if ((vt[v] || []).includes(table)) out.push({ view: v, label: vl[v] || v }); });
    return out;
  }
  const ALL_TABLES = GROUPS.flatMap(g => g.items.map(i => i.table));

  function fmtInt(n) {
    if (n == null) return "\u2014";
    return Number(n).toLocaleString("en-US");
  }
  function relTime(iso) {
    if (!iso) return { label: "no timestamp", stale: null };
    const then = new Date(iso).getTime();
    if (isNaN(then)) return { label: "\u2014", stale: null };
    const mins = Math.floor((Date.now() - then) / 60000);
    let label;
    if (mins < 1) label = "just now";
    else if (mins < 60) label = mins + "m ago";
    else if (mins < 1440) label = Math.floor(mins / 60) + "h ago";
    else label = Math.floor(mins / 1440) + "d ago";
    return { label, stale: mins > 14 * 1440 }; // >14d = stale flag
  }
  function fmtDate(iso) {
    if (!iso) return "\u2014";
    const d = new Date(iso);
    if (isNaN(d.getTime())) return "\u2014";
    return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) +
      " " + d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" });
  }

  function parseCSV(text) {
    const rows = []; let i = 0, field = "", row = [], inQ = false;
    const pushF = () => { row.push(field); field = ""; };
    const pushR = () => { pushF(); rows.push(row); row = []; };
    while (i < text.length) {
      const c = text[i];
      if (inQ) {
        if (c === '"') { if (text[i + 1] === '"') { field += '"'; i += 2; continue; } inQ = false; i++; continue; }
        field += c; i++; continue;
      }
      if (c === '"') { inQ = true; i++; continue; }
      if (c === ",") { pushF(); i++; continue; }
      if (c === "\r") { i++; continue; }
      if (c === "\n") { pushR(); i++; continue; }
      field += c; i++;
    }
    if (field.length || row.length) pushR();
    if (!rows.length) return [];
    const header = rows[0].map(h => String(h).trim());
    return rows.slice(1).filter(r => r.length && r.some(v => v !== ""))
      .map(r => { const o = {}; header.forEach((h, idx) => { o[h] = r[idx] != null ? r[idx] : ""; }); return o; });
  }

  function DataScreen({ user }) {
    // Tables the freshness chip flagged (missing or >7d stale) -- glow them on arrival.
    const [flaggedTables] = useState(() => {
      const f = window.__VAULT_FLAG_TABLES || [];
      window.__VAULT_FLAG_TABLES = null;
      return new Set(f);
    });
    const [byTable, setByTable] = useState({});
    const [loading, setLoading] = useState(true);
    const [err, setErr] = useState(null);
    const [loadedAt, setLoadedAt] = useState(null);
    const [showHelp, setShowHelp] = useState(false);
    const [uploads, setUploads] = useState([]);
    const [busy, setBusy] = useState(null);
    const [prog, setProg] = useState(null);
    const [confirmReq, setConfirmReq] = useState(null);
    const [toast, setToast] = useState(null);
    const toastTimer = useRef(null);
    const uploaderLabel = (user && (user.name || user.email || user.id)) || "unknown";

    function showToast(kind, msg) {
      if (toastTimer.current) clearTimeout(toastTimer.current);
      setToast({ kind, msg });
      toastTimer.current = setTimeout(() => setToast(null), kind === "error" ? 8000 : 5000);
    }
    function askConfirm(opts) { return new Promise(resolve => setConfirmReq(Object.assign({}, opts, { resolve }))); }

    const load = useCallback(() => {
      setLoading(true); setErr(null);
      window.VaultAPI.dataInventory(ALL_TABLES)
        .then(rows => {
          const m = {};
          (rows || []).forEach(r => { m[r.table] = r; });
          setByTable(m); setLoadedAt(Date.now());
        })
        .catch(e => setErr(e.message || String(e)))
        .finally(() => setLoading(false));
    }, []);
    useEffect(() => { load(); }, [load]);

    const loadUploads = useCallback(() => {
      window.VaultAPI.listUploads().then(rows => setUploads(rows || [])).catch(() => {});
    }, []);
    useEffect(() => { loadUploads(); }, [loadUploads]);

    // latest upload record per table (uploads come back newest-first)
    const uploadsByTable = useMemo(() => {
      const m = {};
      (uploads || []).forEach(u => { if (!m[u.table_name]) m[u.table_name] = u; });
      return m;
    }, [uploads]);

    // Append-only Closed Deals import: inserts new closes (dedup on Harvey Event ID + name/date),
    // never updates or deletes existing rows. Attribution: director->dm1, analyst->dm2, completed_by->dm3.
    async function handleClosedAppend(file) {
      if (!file) return;
      const F = window.VAULT_FIRM;
      const norm = (x) => { const v = String(x || "").trim(); if (!v) return null; const n = F.normalizePersonId ? F.normalizePersonId(v) : v; if (F.PEOPLE_BY_ID[n]) return n; if (F.PEOPLE_BY_ID[v]) return v; return null; };
      const d10 = (val) => { const t = String(val || "").trim(); const m = t.match(/^(\d{4})-(\d{2})-(\d{2})/); if (m) return m[0]; const us = t.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/); if (us) return us[3] + "-" + us[1].padStart(2,"0") + "-" + us[2].padStart(2,"0"); return t; };
      let rows;
      const fname = (file.name || "").toLowerCase();
      try {
        if (fname.endsWith(".csv")) rows = parseCSV(await file.text());
        else if (fname.endsWith(".xlsx") || fname.endsWith(".xls")) {
          if (!window.XLSX) { showToast("error", "The Excel reader is still loading \u2014 try again in a second."); return; }
          const wb = window.XLSX.read(await file.arrayBuffer(), { type: "array" });
          rows = window.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: "", raw: false });
        } else { showToast("error", "Please upload a .csv or .xlsx file."); return; }
      } catch (e) { showToast("error", "Couldn't read file: " + (e.message || e)); return; }
      if (!rows || !rows.length) { showToast("error", "No rows in that file."); return; }
      setBusy("closed_deals"); setProg({ pct: 0, label: "Preparing\u2026" });
      try {
        const keys = await window.VaultAPI.listClosedDealKeys();
        const existingEid = new Set(keys.filter(k => k.harveyEventId).map(k => k.harveyEventId));
        // eids whose rows are SOFT-DELETED: upserts land on them but stay hidden
        // (deleted_at survives merge-duplicates by design). Surface it in the toast
        // so a "didn't stay" mystery (Qualitiscape, 7/8) is diagnosed at import time.
        const hiddenEid = new Set(keys.filter(k => k.harveyEventId && k.deletedAt).map(k => k.harveyEventId));
        const ndKey = (nm, cd) => String(nm).trim().toLowerCase() + "|" + d10(cd);
        // Map name+date -> the eid-LESS seed row occupying that slot, so an incoming
        // scraped (eid-bearing) row can REPLACE its stale manual twin instead of
        // being skipped as a dup. (This was the "16 already present" bug: good
        // scraped rows were blocked by dateless C-### / C-002 seeds.)
        const seedByND = new Map();
        keys.forEach(k => { if (!k.harveyEventId) { const kk = ndKey(k.name, k.closeDate); if (!seedByND.has(kk)) seedByND.set(kk, k.id); } });
        const existingND = new Set(keys.map(k => ndKey(k.name, k.closeDate)));
        const projByEid = new Map(keys.filter(k => k.harveyEventId).map(k => [k.harveyEventId, k.harveyProjectId]));
        let added = 0, updated = 0, replaced = 0, dup = 0, linked = 0, noCredit = 0, errors = 0, hiddenUpd = 0, i = 0;
        for (const r of rows) {
          i++;
          const eid = String(r.harvey_event_id || "").trim();
          const nm = String(r.name || "").trim();
          const cdate = d10(r.close_date);
          const hpid = (r.project_id !== "" && r.project_id != null) ? Number(r.project_id) : null;
          const hpname = String(r.project_name || "").trim() || null;
          const eidMatch = !!(eid && existingEid.has(eid));
          const seedId = (!eidMatch && eid) ? seedByND.get(ndKey(nm, cdate)) : null;
          // Skip ONLY when the incoming row has no event id AND a name+date twin
          // already exists. An eid-bearing row is authoritative: if it matches an
          // eid-less seed, we insert it and delete the seed (replace, below).
          if (!eid && !eidMatch && existingND.has(ndKey(nm, cdate))) { dup++; setProg({ pct: Math.round(i/rows.length*100), label: "Importing " + i + " / " + rows.length }); continue; }
          const dm1 = norm(r.director), dm2 = norm(r.analyst), dm3 = norm(r.completed_by);
          if (!dm1) { noCredit++; setProg({ pct: Math.round(i/rows.length*100), label: "Importing " + i + " / " + rows.length }); continue; }
          try {
            // Upserts on id (= C-IMP-<eid>). eid-matched rows update in place (e.g. corrected fee); new rows insert.
            await window.VaultAPI.createClosedDeal({
              id: "C-IMP-" + (eid || (nm + "-" + cdate).replace(/\W+/g, "")),
              name: nm || "(untitled)", buyer: String(r.buyer || "").trim() || null,
              dm1, dm2: dm2 || null, dm3: dm3 || null,
              tev: (r.tev !== "" && r.tev != null) ? Number(r.tev) : null,
              fee: (r.fee !== "" && r.fee != null) ? Number(r.fee) : null,
              closeDate: cdate || null, type: "add-on", status: "closed",
              harveyEventId: eid || null, harveyTargetId: String(r.harvey_target_id || "").trim() || null,
              harveyProjectId: hpid, harveyProjectName: hpname,
            });
            // Replace: this eid-bearing row supersedes a stale eid-less seed at the
            // same name+date -> archive the seed so no duplicate remains.
            if (seedId && seedId !== ("C-IMP-" + eid)) {
              try { await window.VaultAPI.deleteClosedDeal(seedId); replaced++; seedByND.delete(ndKey(nm, cdate)); } catch (e) {}
            }
            if (eid) { existingEid.add(eid); if (hpid != null) projByEid.set(eid, hpid); }
            existingND.add(ndKey(nm, cdate));
            if (eidMatch) { updated++; if (hiddenEid.has(eid)) hiddenUpd++; } else added++;
          } catch (e) { errors++; }
          setProg({ pct: Math.round(i/rows.length*100), label: "Importing " + i + " / " + rows.length });
        }
        setBusy(null); setProg(null);
        await loadUploads(); load();
        // Refresh the in-memory closed set so every screen shows the import
        // immediately (HARVEY_CLOSED otherwise only loads at boot).
        try {
          const fresh = await window.VaultAPI.getAllClosedDeals();
          if (fresh && fresh.length) { window.HARVEY_CLOSED = fresh; window.dispatchEvent(new CustomEvent("vault:closed-updated")); }
        } catch (e) { console.warn("[data] post-import closed refresh failed (hard-refresh to see new rows):", e); }
        let msg = "Closed Deals \u2014 " + added + " new added";
        if (updated) msg += ", " + updated + " updated";
        if (replaced) msg += ", " + replaced + " manual rows replaced by Harvey";
        if (dup) msg += ", " + dup + " already present";
        if (linked) msg += ", " + linked + " project links added";
        if (noCredit) msg += ", " + noCredit + " skipped (no module match \u2014 add manually)";
        if (hiddenUpd) msg += ", \u26a0 " + hiddenUpd + " updated rows are SOFT-DELETED (hidden) \u2014 resurrect in SQL if unintended";
        if (errors) msg += ", " + errors + " error(s)";
        showToast((noCredit || errors) ? "info" : "success", msg + ".");
      } catch (e) { setBusy(null); setProg(null); showToast("error", "Import failed: " + (e.message || e)); }
    }

    async function handleCRImport(file) {
      if (!file) return;
      let rows;
      const name = (file.name || "").toLowerCase();
      try {
        if (name.endsWith(".csv")) rows = parseCSV(await file.text());
        else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
          if (!window.XLSX) { showToast("error", "The Excel reader is still loading \u2014 give it a second and try again."); return; }
          const wb = window.XLSX.read(await file.arrayBuffer(), { type: "array" });
          rows = window.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: "", raw: false });
        } else { showToast("error", "Please upload a .csv or .xlsx file."); return; }
      } catch (e) { showToast("error", "Couldn't read that file: " + (e.message || e)); return; }
      if (!rows || !rows.length) { showToast("error", "No rows found in that file."); return; }
      setBusy("crs"); setProg({ pct: 0, label: "Preparing\u2026" });
      try {
        // Upsert by harvey_event_id so Vault-owned lifecycle (status / assignee / due date) survives.
        const existing = await window.VaultAPI.listCRsForReconcile();
        const byHid = {}; existing.forEach(x => { byHid[x.harveyEventId] = x; });
        let created = 0, markedDone = 0, skipped = 0, errors = 0;
        const nowIso = new Date().toISOString();
        for (let i = 0; i < rows.length; i++) {
          const r = rows[i];
          const hid = String(r.harvey_event_id || "").trim();
          if (!hid) { errors++; continue; }
          const who = String(r.logged_by || "").trim();
          const ex = byHid[hid];
          const isDone = /^(true|1|yes|t)$/i.test(String(r.completed || "").trim());
          if (isDone) {
            if (ex && ex.status !== "done" && ex.status !== "declined") {
              try { await window.VaultAPI.updateCR(ex.id, { status: "done", completedAt: nowIso, completedBy: who || null }); markedDone++; }
              catch (e) { errors++; }
            } else skipped++;
          } else if (ex) {
            const tid = String(r.target_id || "").trim(); if (tid) window.VaultAPI.backfillCRTarget(hid, tid); skipped++;
          } else {
            try {
              await window.VaultAPI.createCR({
                target: r.target || "(untitled)", ownerId: who, requesterId: who,
                dueDate: String(r.cr_date || "").trim() || null, note: String(r.note || "").trim() || null,
                module: String(r.module || "").trim() || null, status: "requested",
                harveyEventId: hid, harveyTargetId: String(r.target_id || "").trim() || null,
              });
              byHid[hid] = { id: null, harveyEventId: hid, status: "requested" }; created++;
            } catch (e) { errors++; }
          }
          if (i % 10 === 0) setProg({ pct: Math.round(i / rows.length * 100), label: "Importing " + i + " / " + rows.length });
        }
        setBusy(null); setProg(null);
        await loadUploads(); load();
        showToast(errors ? "info" : "success", "CRs \u2014 " + created + " New \u00b7 " + markedDone + " Marked Done \u00b7 " + skipped + " Unchanged" + (errors ? ", " + errors + " error(s)" : "") + ".");
      } catch (e) { setBusy(null); setProg(null); showToast("error", "Import failed: " + (e.message || e)); }
    }

    async function handleUpload(table, file, append) {
      if (!file) return;
      let parsed;
      const name = (file.name || "").toLowerCase();
      try {
        if (name.endsWith(".csv")) {
          parsed = parseCSV(await file.text());
        } else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
          if (!window.XLSX) { showToast("error", "The Excel reader is still loading \u2014 give it a second and try again."); return; }
          const wb = window.XLSX.read(await file.arrayBuffer(), { type: "array" });
          const ws = wb.Sheets[wb.SheetNames[0]];
          parsed = window.XLSX.utils.sheet_to_json(ws, { defval: "", raw: false });
        } else { showToast("error", "Please upload a .csv or .xlsx file."); return; }
      } catch (e) { showToast("error", "Couldn't read that file: " + (e.message || e)); return; }
      if (!parsed || !parsed.length) { showToast("error", "No rows found in that file."); return; }
      // empty string -> null so numeric/date/bool columns accept blanks
      const rows = parsed.map(r => { const o = {}; for (const k in r) { const v = r[k]; o[k] = (v === "" || v == null) ? null : v; } return o; });
      const cur = byTable[table];
      const ok = append
        ? await askConfirm({
            title: "Append to " + table + "?",
            message: "This adds " + fmtInt(rows.length) + " rows from " + file.name + " to the existing " + (cur ? fmtInt(cur.rows) : "") + " rows in \"" + table + "\". Duplicates are skipped automatically \u2014 nothing is deleted.",
            confirmLabel: "Append " + fmtInt(rows.length) + " rows",
          })
        : await askConfirm({
            title: "Replace " + table + " data?",
            message: "This replaces all " + (cur ? fmtInt(cur.rows) : "") + " rows in \"" + table + "\" with " + fmtInt(rows.length) + " rows from " + file.name + ".\n\nThe current data is snapshotted first, so you can Undo it.",
            confirmLabel: "Replace " + fmtInt(rows.length) + " rows", danger: true,
          });
      if (!ok) return;
      setBusy(table); setProg({ pct: 0, label: "Preparing\u2026" });
      try {
        const BATCH = 2000;
        await window.VaultAPI.stageClear(table);
        for (let i = 0; i < rows.length; i += BATCH) {
          const chunk = rows.slice(i, i + BATCH);
          await window.VaultAPI.stageInsert(table, chunk);
          const done = Math.min(i + BATCH, rows.length);
          setProg({ pct: Math.round(done / rows.length * 100), label: "Uploading " + fmtInt(done) + " / " + fmtInt(rows.length) });
        }
        setProg({ pct: 100, label: "Finalizing\u2026" });
        const res = append
          ? await window.VaultAPI.commitTableAppend(table, file.name, uploaderLabel)
          : await window.VaultAPI.commitTableLoad(table, file.name, uploaderLabel);
        const r = Array.isArray(res) ? res[0] : res;
        await loadUploads(); load();
        if (append) {
          showToast("success", "Appended \u2014 \"" + table + "\" added " + fmtInt(r ? r.rows_added : "?") + " new rows (now " + fmtInt(r ? r.rows_after : "?") + ", was " + fmtInt(r ? r.rows_before : "?") + ").");
        } else {
          showToast("success", "Uploaded \u2014 \"" + table + "\" now has " + fmtInt(r ? r.rows_after : rows.length) + " rows (was " + fmtInt(r ? r.rows_before : "?") + ").");
        }
      } catch (e) { showToast("error", "Upload failed: " + (e.message || e)); }
      finally { setBusy(null); setProg(null); }
    }

    async function handleWindowUpload(table, file) {
      if (!file) return;
      let parsed;
      const name = (file.name || "").toLowerCase();
      try {
        if (name.endsWith(".csv")) parsed = parseCSV(await file.text());
        else if (name.endsWith(".xlsx") || name.endsWith(".xls")) {
          if (!window.XLSX) { showToast("error", "The Excel reader is still loading \u2014 give it a second and try again."); return; }
          const wb = window.XLSX.read(await file.arrayBuffer(), { type: "array" });
          parsed = window.XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: "", raw: false });
        } else { showToast("error", "Please upload a .csv or .xlsx file."); return; }
      } catch (e) { showToast("error", "Couldn't read that file: " + (e.message || e)); return; }
      if (!parsed || !parsed.length) { showToast("error", "No rows found in that file."); return; }
      const rows = parsed.map(r => { const o = {}; for (const k in r) { const v = r[k]; o[k] = (v === "" || v == null) ? null : v; } return o; });
      // The window IS the file: min(week_from)..max(week_to). Everything currently in
      // that range is deleted and replaced by these rows \u2014 outside it, untouched.
      let wFrom = null, wTo = null;
      for (const r of rows) {
        const f = String(r.week_from || "").slice(0, 10), t = String(r.week_to || r.week_from || "").slice(0, 10);
        if (!/^\d{4}-\d{2}-\d{2}$/.test(f)) { showToast("error", "Row missing a valid week_from \u2014 is this an Activities CSV?"); return; }
        if (wFrom == null || f < wFrom) wFrom = f;
        if (wTo == null || t > wTo) wTo = t;
      }
      const cur = byTable[table];
      const ok = await askConfirm({
        title: "Import window into " + table + "?",
        message: "Replaces all rows dated " + wFrom + " \u2192 " + wTo + " with " + fmtInt(rows.length) + " rows from " + file.name + ". The other " + (cur ? fmtInt(cur.rows) : "") + " rows outside that window are untouched.",
        confirmLabel: "Replace window",
      });
      if (!ok) return;
      setBusy(table); setProg({ pct: 0, label: "Preparing\u2026" });
      try {
        const BATCH = 2000;
        await window.VaultAPI.stageClear(table);
        for (let i = 0; i < rows.length; i += BATCH) {
          const chunk = rows.slice(i, i + BATCH);
          await window.VaultAPI.stageInsert(table, chunk);
          const done = Math.min(i + BATCH, rows.length);
          setProg({ pct: Math.round(done / rows.length * 100), label: "Uploading " + fmtInt(done) + " / " + fmtInt(rows.length) });
        }
        setProg({ pct: 100, label: "Finalizing\u2026" });
        const res = await window.VaultAPI.commitTableWindowReplace(table, wFrom, wTo, file.name, uploaderLabel);
        const r = Array.isArray(res) ? res[0] : res;
        await loadUploads(); load();
        showToast("success", "Window " + wFrom + " \u2192 " + wTo + " \u2014 replaced " + fmtInt(r ? r.rows_deleted : "?") + " rows with " + fmtInt(rows.length) + " (table now " + fmtInt(r ? r.rows_after : "?") + ").");
      } catch (e) { showToast("error", "Window import failed: " + (e.message || e)); }
      finally { setBusy(null); setProg(null); }
    }

    async function handleUndo(table) {
      const ok = await askConfirm({
        title: "Revert " + table + "?",
        message: "This replaces the current \"" + table + "\" contents with the snapshot taken before the last upload.",
        confirmLabel: "Revert", danger: true,
      });
      if (!ok) return;
      setBusy(table); setProg({ pct: 100, label: "Reverting\u2026" });
      try {
        const res = await window.VaultAPI.undoLastUpload(table, uploaderLabel);
        const r = Array.isArray(res) ? res[0] : res;
        await loadUploads(); load();
        showToast("success", "Reverted \u2014 \"" + table + "\" restored to " + fmtInt(r ? r.rows_after : "previous") + " rows.");
      } catch (e) { showToast("error", "Undo failed: " + (e.message || e)); }
      finally { setBusy(null); setProg(null); }
    }

    return (
      <div style={{ maxWidth: 1360, margin: "0 auto", padding: "24px 34px 64px" }}>
        <style>{`
          .du-row { transition: background .13s ease; }
          .du-row:hover { background: #FAFBFC; }
          .du-btn { font-size: 12px; font-weight: 600; padding: 5px 10px; width: 100%; max-width: 138px;
                    border-radius: 7px; border: 1px solid transparent; cursor: pointer;
                    transition: background .15s ease, border-color .15s ease, transform .06s ease; white-space: nowrap; }
          .du-btn:active:not(:disabled) { transform: translateY(1px); }
          .du-btn:disabled { opacity: .45; cursor: default; }
          .du-green { background: #EAF5EF; color: #1E7A48; border-color: #D3EADD; }
          .du-green:hover:not(:disabled) { background: #DFF0E7; border-color: #BFE2CF; }
          .du-red { background: #FCF0EF; color: #B3261E; border-color: #F4D7D3; }
          .du-red:hover:not(:disabled) { background: #F9E5E2; border-color: #EFC5BF; }
          .du-dash { color: #C4CDD6; text-align: center; }
          .du-link { color: #196EA7; cursor: pointer; }
          .du-link:hover { text-decoration: underline; }
        `}</style>
        {toast && (
          <div onClick={() => setToast(null)} style={{ position: "fixed", top: 18, left: "50%", transform: "translateX(-50%)", zIndex: 1001, cursor: "pointer",
            background: toast.kind === "error" ? "#FDECEC" : "#E8F5EC", border: "1px solid " + (toast.kind === "error" ? "#F5C2C2" : "#BFE3CC"),
            color: toast.kind === "error" ? "#9B2C2C" : "#216A3A", padding: "11px 16px", borderRadius: 10, fontSize: 13.5, fontWeight: 600, boxShadow: "0 6px 20px rgba(0,0,0,.12)", maxWidth: 540 }}>
            {toast.msg}
          </div>
        )}
        {confirmReq && (
          <div onClick={() => { confirmReq.resolve(false); setConfirmReq(null); }}
            style={{ position: "fixed", inset: 0, background: "rgba(20,30,45,.35)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 }}>
            <div onClick={e => e.stopPropagation()}
              style={{ background: "#fff", borderRadius: 14, border: `1px solid ${LINE}`, padding: 22, maxWidth: 440, margin: 16, boxShadow: "0 12px 44px rgba(0,0,0,.20)" }}>
              <div style={{ fontSize: 16, fontWeight: 600, color: INK, marginBottom: 8 }}>{confirmReq.title || "Confirm"}</div>
              <div style={{ fontSize: 13.5, color: INK2, lineHeight: 1.55, marginBottom: 18, whiteSpace: "pre-line" }}>{confirmReq.message}</div>
              <div style={{ display: "flex", justifyContent: "flex-end", gap: 8 }}>
                <button onClick={() => { confirmReq.resolve(false); setConfirmReq(null); }}
                  style={{ padding: "8px 16px", borderRadius: 9, border: `1px solid ${LINE}`, background: "#fff", color: INK2, fontSize: 13, fontWeight: 600, cursor: "pointer" }}>Cancel</button>
                <button onClick={() => { confirmReq.resolve(true); setConfirmReq(null); }}
                  style={{ padding: "8px 16px", borderRadius: 9, border: "none", background: confirmReq.danger ? "#C2410C" : BLUE, color: "#fff", fontSize: 13, fontWeight: 600, cursor: "pointer" }}>{confirmReq.confirmLabel || "Confirm"}</button>
              </div>
            </div>
          </div>
        )}
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end", marginBottom: 6 }}>
          <div>
            <div style={{ fontSize: 25, fontWeight: 600, letterSpacing: "-.3px", color: INK }}>Data</div>
            <div style={{ fontSize: 13, color: INK2, marginTop: 4 }}>
              Live row counts and last-updated for every Vault dataset.
              {loadedAt && <span style={{ color: MUTE }}> {"\u00b7"} counts as of {relTime(new Date(loadedAt).toISOString()).label}</span>}
            </div>
              <div style={{ marginTop: 8, padding: "10px 14px", borderRadius: 10, background: "#F2F7F4", border: "1px solid #D7E8DD", color: "#1F513A", fontSize: 12.5, lineHeight: 1.55 }}>
                Scrape in Harvey with the extension, then load the CSV here. <b>Upload Master</b> replaces the whole table (use for full-history scrapes; Activity Master appends + dedups, Closed Deals appends by event id, CRs upsert preserving your edits). <b>Upload Date Window</b> replaces only the weeks contained in the file (use for windowed scrapes) {"\u2014"} history outside the window is untouched. <b>{"\u21BB"} Revert</b> restores the table to just before the last Upload Master.
              </div>
          </div>
          <button onClick={load} disabled={loading} style={{
            padding: "8px 16px", borderRadius: 9, border: `1px solid ${LINE}`, background: "#fff",
            color: BLUE, fontSize: 13, fontWeight: 600, cursor: loading ? "default" : "pointer", opacity: loading ? .6 : 1,
          }} title="Re-reads row counts and last-updated times from Supabase. It does NOT pull from Harvey \u2014 see 'How to refresh this data' below.">{loading ? "Checking\u2026" : "Re-check counts"}</button>
        </div>

        <div style={{ marginTop: 14 }}>
          <button onClick={() => setShowHelp(h => !h)} style={{ background: "none", border: "none", padding: 0, cursor: "pointer", color: BLUE, fontSize: 13, fontWeight: 600 }}>
            {showHelp ? "\u25be" : "\u25b8"} How to refresh this data
          </button>
          {showHelp && (
            <div style={{ marginTop: 10, border: `1px solid ${LINE}`, borderRadius: 14, background: "#fff", padding: "18px 22px", fontSize: 13, color: INK2, lineHeight: 1.65 }}>
              <div style={{ marginBottom: 14 }}>
                Vault does not pull from Harvey on its own. You run the <b>Harvey Report Launcher</b> (a Chrome extension) to produce a CSV, then that CSV is loaded into Supabase. The numbers on this page reflect whatever was <i>last loaded</i> \u2014 so "Re-check counts" just re-reads them; it never fetches new data from Harvey.
              </div>

              <div style={{ color: INK, fontWeight: 600, marginBottom: 4 }}>1. Install the launcher (once)</div>
              <div style={{ marginBottom: 12 }}>Download the latest <code>harvey-report-launcher</code> zip, unzip it, then go to <code>chrome://extensions</code> \u2192 enable Developer mode \u2192 <b>Load unpacked</b> \u2192 pick the unzipped folder. To update later, replace the folder and click the reload icon on its card.</div>

              <div style={{ color: INK, fontWeight: 600, marginBottom: 4 }}>2. Run a report</div>
              <div style={{ marginBottom: 12 }}>Open <code>db.harveyllc.com</code> and make sure you are logged in. Click the launcher button in the toolbar, check the report(s) you want, and hit <b>Run selected</b>. Each finished report downloads a CSV to your Downloads folder.</div>

              <div style={{ color: INK, fontWeight: 600, marginBottom: 4 }}>3. Load it into Vault</div>
              <div style={{ marginBottom: 14 }}>Most datasets import here by replacing the table from a scraped CSV. <b>CRs</b> use <b>Import from Harvey CSV</b>, which upserts by Harvey event id so your assignee / status / due-date edits are preserved.</div>

              <div style={{ color: INK, fontWeight: 600, marginBottom: 4 }}>Rough run times <span style={{ color: MUTE, fontWeight: 400 }}>(ballpark \u2014 will tighten as we use it more)</span></div>
              <div style={{ marginBottom: 2 }}><b style={{ color: "#2F855A" }}>Seconds to ~1 min:</b> Pipeline, Offers &amp; Better, White Space, Project Lists, CRs (30-day window).</div>
              <div><b style={{ color: "#C2410C" }}>A few minutes:</b> Project Universe (~20k), Activity Master, Closed Deals \u2014 these loop company-by-company, so time scales with how much data is in range and how fast Harvey responds.</div>
            </div>
          )}
        </div>

        {err && (
          <div style={{ marginTop: 16, padding: "12px 16px", borderRadius: 10, background: "#FDECEC", border: "1px solid #F5C2C2", color: "#9B2C2C", fontSize: 13 }}>
            Couldn't load inventory: {err}. If this says the function is missing, the <code>get_data_inventory</code> RPC needs to be created in Supabase.
          </div>
        )}

        {GROUPS.map(g => (
          <div key={g.group} style={{ marginTop: 26 }}>
            <div style={{ fontSize: 11, fontWeight: 600, color: MUTE, textTransform: "uppercase", letterSpacing: ".04em", marginBottom: 8 }}>{g.group}</div>
            <div style={{ border: `1px solid ${LINE}`, borderRadius: 14, background: "#fff", overflow: "hidden" }}>
              <div style={{ display: "grid", gridTemplateColumns: "minmax(150px,1.15fr) minmax(140px,1fr) 128px 128px 150px 84px 106px 96px 84px", gap: 10, padding: "10px 18px", borderBottom: `1px solid ${LINE}`, fontSize: 11, fontWeight: 600, color: MUTE, textTransform: "uppercase", letterSpacing: ".03em", alignItems: "center" }}>
                <div>Dataset</div><div>Page Impact</div>
                <div style={{ textAlign: "center" }}>Upload Master</div>
                <div style={{ textAlign: "center" }}>Upload Window</div>
                <div style={{ textAlign: "center" }}>Revert</div>
                <div style={{ textAlign: "right" }}>Rows</div>
                <div style={{ textAlign: "right" }}>Last Updated</div>
                <div style={{ textAlign: "right" }}>By User</div>
                <div style={{ textAlign: "right" }}>Since Last</div>
              </div>
              {g.items.map((it, idx) => {
                const rec = byTable[it.table];
                const rel = relTime(rec && rec.lastUpdated);
                const up = uploadsByTable[it.table];
                const hasMaster = !!(it.uploadable || it.appendable || it.appendbulk || it.crimport);
                const canRevert = !!(up && up.snapshot_available && up.kind === "replace");
                const isBusy = busy === it.table;
                return (
                  <div key={it.table} className={"du-row" + (flaggedTables.has(it.table) ? " flag-glow" : "")} style={{ display: "grid", gridTemplateColumns: "minmax(150px,1.15fr) minmax(140px,1fr) 128px 128px 150px 84px 106px 96px 84px", gap: 10, padding: "12px 18px", borderTop: idx === 0 ? "none" : `1px solid ${LINE}`, alignItems: "center", fontSize: 13.5 }}>
                    <div>
                      <span style={{ color: INK, fontWeight: 600 }}>{it.label}</span>
                      {isBusy && prog && (
                        <span style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 6 }}>
                          <span style={{ width: 110, height: 5, borderRadius: 4, background: "#E9EEF3", overflow: "hidden", display: "inline-block" }}>
                            <span style={{ display: "block", height: "100%", width: prog.pct + "%", background: BLUE, borderRadius: 4, transition: "width .25s ease" }} />
                          </span>
                          <span style={{ color: BLUE, fontSize: 11.5, fontWeight: 600, whiteSpace: "nowrap" }}>{prog.label}</span>
                        </span>
                      )}
                    </div>
                    <div style={{ fontSize: 12, lineHeight: 1.55 }}>
                      {impactFor(it.table).length ? impactFor(it.table).map((p, i) => (
                        <React.Fragment key={p.view}>
                          {i > 0 && <span style={{ color: MUTE }}>{" \u00b7 "}</span>}
                          <a className="du-link" onClick={() => window.VaultNav && window.VaultNav(p.view)}>{p.label}</a>
                        </React.Fragment>
                      )) : <span style={{ color: MUTE }}>{"\u2014"}</span>}
                    </div>
                    <div style={{ textAlign: "center" }}>
                      {hasMaster ? (
                        <button className="du-btn du-green" disabled={isBusy}
                          onClick={() => { const el = document.getElementById("m_" + it.table); if (el) el.click(); }}>
                          {isBusy ? "Working\u2026" : "Upload Master"}
                        </button>
                      ) : <span className="du-dash">{"\u2014"}</span>}
                    </div>
                    <div style={{ textAlign: "center" }}>
                      {it.windowreplace ? (
                        <button className="du-btn du-green" disabled={isBusy}
                          onClick={() => { const el = document.getElementById("w_" + it.table); if (el) el.click(); }}>
                          {isBusy ? "Working\u2026" : "Upload Window"}
                        </button>
                      ) : <span className="du-dash">{"\u2014"}</span>}
                    </div>
                    <div style={{ textAlign: "center" }}>
                      {canRevert ? (
                        <button className="du-btn du-red" disabled={isBusy} onClick={() => handleUndo(it.table)}>
                          {"\u21BB"} Revert
                        </button>
                      ) : <span className="du-dash">{"\u2014"}</span>}
                    </div>
                    <div style={{ textAlign: "right", color: INK, fontWeight: 600, fontVariantNumeric: "tabular-nums" }}>
                      {loading && !rec ? "\u2026" : fmtInt(rec && rec.rows)}
                    </div>
                    <div style={{ textAlign: "right", color: INK2, fontVariantNumeric: "tabular-nums", fontSize: 12.5 }}>
                      {loading && !rec ? "" : fmtDate(rec && rec.lastUpdated)}
                    </div>
                    <div style={{ textAlign: "right", color: INK2, fontSize: 12.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                      {up && up.uploaded_by ? up.uploaded_by : "\u2014"}
                    </div>
                    <div style={{ textAlign: "right", fontWeight: 600, fontSize: 12.5, color: rel.stale ? "#C2410C" : (rel.stale === null ? MUTE : "#2F855A") }}>
                      {loading && !rec ? "" : rel.label}
                    </div>
                    {hasMaster && (
                      <input id={"m_" + it.table} type="file" accept=".csv,.xlsx,.xls" style={{ display: "none" }}
                        onChange={e => { const f = e.target.files && e.target.files[0]; e.target.value = "";
                          if (it.crimport) handleCRImport(f);
                          else if (it.appendable) handleClosedAppend(f);
                          else if (it.appendbulk) handleUpload(it.table, f, true);
                          else handleUpload(it.table, f); }} />
                    )}
                    {it.windowreplace && (
                      <input id={"w_" + it.table} type="file" accept=".csv,.xlsx,.xls" style={{ display: "none" }}
                        onChange={e => { const f = e.target.files && e.target.files[0]; e.target.value = ""; handleWindowUpload(it.table, f); }} />
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        ))}

        <div style={{ marginTop: 22, fontSize: 12, color: MUTE, lineHeight: 1.6 }}>
          "Last updated" reads the newest <code>created_at</code>/<code>updated_at</code> in each table, so it reflects the most recent CSV import. Amber means nothing new in 14+ days {"\u2014"} worth a re-pull if that dataset should be moving.
        </div>
      </div>
    );
  }

  window.DataScreen = DataScreen;
})();