// Bylt Capital CRM — Import (a CSV, an Excel sheet, or a zip of a lead list with
// a folder of documents per lead), Export, and the Settings cards that go with
// them: tags, saved column layouts, and the data tools.
//
// The file goes up in 10 MB pieces, three at a time, each retried on its own.
// Once the last piece is in, the tab can be closed: reading the file, the
// preview and the import itself run on the server, and the bell says when an
// import is done.

const IMPORT_STATUS = {
  uploading: "Uploading", scanning: "Reading the file", mapping: "Match the columns", previewing: "Checking every row",
  review: "Ready to import", applying: "Importing", done: "Done", failed: "Stopped", cancelled: "Cancelled",
};
const colLetter = (c) => { let s = ""; c++; while (c) { const m = (c - 1) % 26; s = String.fromCharCode(65 + m) + s; c = Math.floor((c - 1) / 26); } return s; };
const mb = (n) => `${(n / 1048576).toFixed(n > 10 * 1048576 ? 0 : 1)} MB`;

async function uploadInPieces(file, onProgress, cancelled) {
  const start = await API.startImport(file.name, file.size);
  const { import: imp, partSize, partCount } = start;
  let done = 0, next = 1, failed = null;
  const worker = async () => {
    while (next <= partCount && !failed && !cancelled()) {
      const n = next++;
      const piece = file.slice((n - 1) * partSize, Math.min(file.size, n * partSize));
      for (let attempt = 1; ; attempt++) {
        try { await API.importPart(imp.id, n, piece); break; }
        catch (err) {
          if (attempt >= 5 || (err.status && err.status < 500 && err.status !== 408 && err.status !== 429)) { failed = err; return; }
          await new Promise(r => setTimeout(r, 800 * attempt * attempt));
        }
      }
      done++; onProgress(done, partCount);
    }
  };
  await Promise.all([worker(), worker(), worker()]);
  if (cancelled()) { await API.cancelImport(imp.id).catch(() => {}); throw Object.assign(new Error("Upload cancelled"), { cancelled: true }); }
  if (failed) throw failed;
  await API.completeImport(imp.id);
  return imp.id;
}

function ImportView({ user, toast, confirm, selectedId, select, go }) {
  const [recent, setRecent] = useState(null);
  const [over, setOver] = useState(false);
  const [up, setUp] = useState(null);           // { name, size, done, total }
  const [err, setErr] = useState("");
  const stop = useRef(false);
  const input = useRef(null);
  const loadRecent = useCallback(() => API.imports().then(setRecent).catch(() => setRecent([])), []);
  useEffect(() => { if (!selectedId) loadRecent(); }, [selectedId]);
  useEffect(() => {
    if (!up) return;
    const warn = (e) => { e.preventDefault(); e.returnValue = ""; };
    window.addEventListener("beforeunload", warn);
    return () => window.removeEventListener("beforeunload", warn);
  }, [!!up]);

  const pick = async (files) => {
    const f = files && files[0];
    if (!f) return;
    setErr("");
    if (!/\.(zip|csv|xlsx|xlsm)$/i.test(f.name)) return setErr(/\.xls$/i.test(f.name) ? "This is an old Excel file (.xls). Open it in Excel and save it as .xlsx or .csv." : "Choose a .zip, .csv or .xlsx file.");
    stop.current = false;
    setUp({ name: f.name, size: f.size, done: 0, total: Math.max(1, Math.ceil(f.size / (10 * 1048576))) });
    try {
      const id = await uploadInPieces(f, (done, total) => setUp(u => ({ ...u, done, total })), () => stop.current);
      setUp(null); toast("Uploaded. Reading the file…"); select(id);
    } catch (e) { setUp(null); if (!e.cancelled) setErr(e.message); }
  };

  if (selectedId) return <ImportRun id={selectedId} user={user} toast={toast} confirm={confirm} onBack={() => select(null)} go={go} />;
  return (
    <main className="workspace single">
      <div className="page">
        <div className="page-head">
          <div><h1>Import</h1><p>A CSV or Excel list of leads, or a zip with the list and a folder of documents for each lead. Nothing is saved until you have checked the preview. An import left unfinished for 3 days is cancelled and its file deleted.</p></div>
          <div className="page-tools"><button className="btn" onClick={() => go("leads")}>Back to Leads</button></div>
        </div>
        {err ? <div className="banner" role="alert">{err}</div> : null}
        <div className="card card-body">
          {up ? (
            <div className="upload-progress">
              <div className="section-bar"><b>{up.name}</b><span className="muted tnum">{mb(Math.min(up.size, up.done * 10 * 1048576))} of {mb(up.size)}</span></div>
              <div className="progress" style={{ marginTop: 10 }}><i style={{ width: `${Math.round(up.done / up.total * 100)}%`, background: "var(--blue)" }} /></div>
              <p className="hint" style={{ marginTop: 10 }}>Keep this tab open until the upload finishes. After that you can close it: the rest runs on the server.</p>
              <button className="btn btn-sm" style={{ marginTop: 8 }} onClick={() => { stop.current = true; }}>Cancel upload</button>
            </div>
          ) : (
            <div className={`drop${over ? " over" : ""}`} onDragOver={e => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)}
                 onDrop={e => { e.preventDefault(); setOver(false); pick(e.dataTransfer.files); }}>
              <div><b>Drop a .zip, .csv or .xlsx file here</b><br />Zips up to 4 GB. The spreadsheet can have a header row or not.</div>
              <div style={{ marginTop: 12 }}><button type="button" className="btn btn-primary" onClick={() => input.current.click()}><Icon name="upload" size={14} />Choose file</button></div>
              <input ref={input} type="file" hidden accept=".zip,.csv,.xlsx,.xlsm" onChange={e => { pick(e.target.files); e.target.value = ""; }} />
            </div>
          )}
        </div>
        <div className="section-bar" style={{ marginTop: 18 }}><span className="label">Recent imports</span></div>
        <div className="card card-flush">
          {recent === null ? <div className="card-body dim">Loading…</div> : !recent.length ? <div className="card-body dim">No imports yet.</div>
            : recent.map(r => (
              <button className="row row-btn" key={r.id} onClick={() => select(r.id)}>
                <span className="doc-icon">{(r.fileName.split(".").pop() || "").toUpperCase()}</span>
                <div className="grow"><div className="title">{r.fileName}</div>
                  <div className="line">{r.summary || (r.rowCount ? `${r.rowCount} rows` : "")}{r.as === "deals" ? " · as deals" : ""}</div>
                  <div className="when">{fmt.when(r.createdAt)} · {(r.createdBy && r.createdBy.name) || ""}{r.expired ? " · left unfinished, file deleted" : ""}</div></div>
                <div className="right"><Pill status={r.status === "done" ? "funded" : r.status === "failed" ? "declined" : r.status === "cancelled" ? "off" : r.status === "review" || r.status === "mapping" ? "submitted" : "new"} label={IMPORT_STATUS[r.status] || r.status} /></div>
              </button>
            ))}
        </div>
      </div>
    </main>
  );
}

function ImportRun({ id, user, toast, confirm, onBack, go }) {
  const [info, setInfo] = useState(null);
  const [error, setError] = useState("");
  const load = useCallback(async () => { try { setInfo(await API.importInfo(id)); setError(""); } catch (err) { setError(err.message); } }, [id]);
  useEffect(() => { load(); }, [id]);
  const status = info && info.import.status;
  useEffect(() => {
    // Also while a finished import is still queuing its automatic reading (a few seconds a page of leads).
    const queuing = status === "done" && info.import.autoRead && !(info.import.autoReadProgress || {}).done;
    if (!["scanning", "previewing", "applying", "uploading"].includes(status) && !queuing) return;
    const t = setInterval(load, queuing ? 3000 : 1500);
    return () => clearInterval(t);
  }, [status, load, info && info.import.autoReadProgress && info.import.autoReadProgress.done]);
  const imp = info && info.import;
  const cancel = () => confirm({ title: "Cancel this import?", body: "The uploaded file is deleted. Nothing has been imported yet.", confirmLabel: "Cancel import",
    onConfirm: async () => { await API.cancelImport(id); toast("Import cancelled"); onBack(); } });
  return (
    <main className="workspace single">
      <div className="page">
        <div className="page-head">
          <div><button className="link-btn" onClick={onBack}><Icon name="back" size={13} /> All imports</button><h1>{imp ? imp.fileName : "Import"}</h1>
            {imp ? <p>{IMPORT_STATUS[imp.status] || imp.status}{imp.sheetFile && imp.kind === "zip" ? ` · list: ${imp.sheetFile}` : ""}{imp.rowCount ? ` · ${imp.rowCount} rows` : ""}{imp.zip ? ` · ${imp.zip.folders} folders, ${imp.zip.files} files` : ""}</p> : null}</div>
          {imp && ["mapping", "review"].includes(imp.status) ? <div className="page-tools"><button className="btn" onClick={cancel}>Cancel import</button></div> : null}
        </div>
        {error ? <div className="banner" role="alert">{error}</div> : null}
        {!imp ? <div className="card card-body dim">Loading…</div>
          : imp.status === "scanning" || imp.status === "uploading" ? <Working text="Reading the file: finding the list and the folders. You can leave this page; it carries on." />
          : imp.status === "previewing" ? <Working text="Checking every row and matching folders to leads. Nothing is saved yet." />
          : imp.status === "mapping" ? <ImportMapping info={info} onSaved={load} toast={toast} dealsOk={!!canOf(user).importDeals} />
          : imp.status === "review" ? <ImportReview info={info} onBack={load} onChanged={load} onApplied={load} toast={toast} />
          : imp.status === "applying" ? <Working text={imp.progress && imp.progress.phase === "files" ? `Storing the documents (${imp.progress.next || 0} of ${imp.progress.of || "…"} folders). You can leave this page; the bell tells you when it's done.` : "Saving the leads. You can leave this page; the bell tells you when it's done."} />
          : imp.status === "done" ? <ImportDone imp={imp} toast={toast} go={go} />
          : imp.status === "failed" ? <div className="card card-body"><div className="banner" role="alert">{imp.error || "The import stopped."}</div><p className="dim" style={{ marginTop: 10 }}>Fix the file and start a new import. Anything already saved stays (re-running the same file won't add it twice).{imp.expired ? " The uploaded file has been deleted." : ""}</p><button className="btn" style={{ marginTop: 10 }} onClick={onBack}>Start again</button></div>
          : <div className="card card-body dim">{imp.expired ? "This import was left unfinished for 3 days, so it was cancelled and the uploaded file deleted. Upload the file again to import it." : "This import was cancelled."}</div>}
      </div>
    </main>
  );
}
const Working = ({ text }) => <div className="card card-body working"><span className="spinner" aria-hidden="true" /><span>{text}</span></div>;

// "Read statements automatically" on the column screen: remembered for the next import in this browser.
const AUTO_READ_KEY = "ef.importAutoRead";
const AUTO_READ_DEFAULTS = { on: true, minRevenue: "100,000", newest: "4" };
const readAutoRead = () => { try { return { ...AUTO_READ_DEFAULTS, ...(JSON.parse(localStorage.getItem(AUTO_READ_KEY) || "{}") || {}) }; } catch (e) { return { ...AUTO_READ_DEFAULTS }; } };
const rememberAutoRead = (a) => { try { localStorage.setItem(AUTO_READ_KEY, JSON.stringify(a)); } catch (e) { /* private window */ } };

function ImportMapping({ info, onSaved, toast, dealsOk }) {
  const imp = info.import;
  const [map, setMap] = useState(imp.map || []);
  const [headerRow, setHeaderRow] = useState(!!imp.headerRow);
  const [as, setAs] = useState(imp.as || "leads");
  // A new import starts tagged with today's date (MM/DD/YY, New York time); it can be removed or added to (DEC-097).
  const [tags, setTags] = useState(() => Array.isArray(imp.tags) ? imp.tags
    : [new Date().toLocaleDateString("en-US", { timeZone: "America/New_York", month: "2-digit", day: "2-digit", year: "2-digit" })]);
  const [tagOptions, setTagOptions] = useState([]);
  const [offersOn, setOffersOn] = useState(false);
  const [offer, setOffer] = useState(savedOfferSettings);
  const [existing, setExisting] = useState("skip");
  const [auto, setAuto] = useState(() => (imp.autoRead ? { on: true, minRevenue: String(imp.autoRead.minRevenue), newest: String(imp.autoRead.newest || "") } : readAutoRead()));
  const [layoutName, setLayoutName] = useState(imp.layoutName || "");
  const save = useSave(onSaved);
  useEffect(() => { API.tags().then(setTagOptions).catch(() => {}); }, []);
  const isZip = imp.kind === "zip";
  const sample = (info.sample || []).slice(headerRow ? 1 : 0);
  const heads = headerRow && info.sample && info.sample[0] ? info.sample[0] : null;
  const fields = info.fields.filter(f => as === "deals" || !["dealStatus", "fundedAt", "funder"].includes(f.key));
  const used = new Map(); map.forEach((k, c) => { if (k && k !== "note" && k !== "skip" && k !== "keep") used.set(k, (used.get(k) || 0) + 1); });
  const dupes = [...used].filter(([, n]) => n > 1).map(([k]) => (fields.find(f => f.key === k) || {}).label || k);
  const submit = () => {
    if (dupes.length) return save.setError(`${dupes.join(", ")} is matched to more than one column.`);
    if (!map.some(k => k === "biz" || k === "name" || k === "firstName")) return save.setError("Match the business name (or the owner's name) to a column.");
    if (offersOn) { const e = offerSettingsError(offer); if (e) return save.setError(e); if (!map.includes("monthlyRevenue")) return save.setError("Offer links are sized from monthly revenue: match that column first."); }
    const o = offersOn ? { on: true, ...offer, pct: Number(offer.pct), round: Number(offer.round), min: fmt.parseMoney(offer.min), days: Number(offer.days), tmin: Number(offer.tmin), tmax: Number(offer.tmax), tstep: Number(offer.tstep), fmin: Number(offer.fmin), fmax: Number(offer.fmax), term: Number(offer.term), factor: Number(offer.factor), existing } : null;
    if (offersOn) rememberOfferSettings(offer);
    const autoOn = as === "leads" && isZip && auto.on;
    if (autoOn && !map.includes("monthlyRevenue")) return save.setError("Reading statements automatically goes by monthly revenue: match that column first, or turn it off.");
    if (autoOn && fmt.parseMoney(auto.minRevenue) < 1000) return save.setError("Set the monthly revenue for automatic reading ($1,000 or more).");
    if (as === "leads" && isZip) rememberAutoRead(auto);
    const autoRead = autoOn ? { on: true, minRevenue: fmt.parseMoney(auto.minRevenue), newest: auto.newest === "" ? 0 : Number(auto.newest) } : null;
    save.run(() => API.importMapping(imp.id, { map, headerRow, as, tags, offers: o, autoRead, saveLayout: layoutName.trim() ? { name: layoutName.trim() } : null }));
  };
  return (
    <>
      {imp.layoutName ? <div className="banner info">Columns matched from your saved layout <b>{imp.layoutName}</b>. Check them and change anything that's off.</div> : null}
      {imp.zip && imp.zip.skipped ? <div className="banner info">{imp.zip.skipped} {imp.zip.skipped === 1 ? "file isn't" : "files aren't"} a PDF or picture and will be skipped{imp.zip.skippedNames && imp.zip.skippedNames.length ? `: ${imp.zip.skippedNames.slice(0, 4).map(n => n.split("/").pop()).join(", ")}` : ""}.</div> : null}
      <div className="card card-body import-opts">
        <div className="form-grid">
          <div className="field"><span className="label">Import as</span>
            <Segmented value={as} onChange={setAs} label="Import as" options={dealsOk || as === "deals" ? [{ id: "leads", label: "Leads" }, { id: "deals", label: "Deals" }] : [{ id: "leads", label: "Leads" }]} />
            <div className="hint">{as === "leads" ? "Not sent to a lender yet. Each becomes a deal when its first submission is logged." : "Already submitted for offers. Rows marked funded become clients."}</div>
          </div>
          <div className="field"><span className="label">Tags for every row</span><TagPicker value={tags} onChange={setTags} options={tagOptions} allowCreate placeholder="Add tags (e.g. the list's name)" /></div>
          <label className="check" style={{ gridColumn: "1 / -1" }}><input type="checkbox" checked={headerRow} onChange={e => setHeaderRow(e.target.checked)} />The first row is column names, not a lead</label>
        </div>
      </div>
      <div className="section-bar" style={{ marginTop: 16 }}><span className="label">Columns</span><span className="muted" style={{ fontSize: 12.5 }}>SSNs show only their last 4 here.</span></div>
      <p className="hint" style={{ margin: "0 2px 8px" }}>A column with no field here is kept on each lead as it is, under the column's name ("Keep as it is"). Pick "Not imported" to leave one out.</p>
      <div className="card card-flush">
        <div className="table-wrap">
          <table className="list compact map-table">
            <thead><tr><th>Column</th><th>Values in the file</th><th>Goes to</th></tr></thead>
            <tbody>
              {map.map((k, c) => (
                <tr key={c} className={!k ? "unused" : undefined}>
                  <td><b>{colLetter(c)}</b>{heads && heads[c] ? <div className="muted" style={{ fontSize: 12 }}>{heads[c]}</div> : null}</td>
                  <td className="samples">{sample.slice(0, 3).map((r, i) => <div key={i} className="clip">{String(r[c] == null ? "" : r[c]) || <span className="muted">(blank)</span>}</div>)}</td>
                  <td><select className="select sm" value={k || ""} aria-label={`Column ${colLetter(c)}`} onChange={e => setMap(map.map((x, i) => i === c ? (e.target.value || null) : x))}>
                    <option value="">Not imported</option>
                    {fields.map(f => <option key={f.key} value={f.key}>{f.label}</option>)}
                  </select></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>
      <div className="card card-body" style={{ marginTop: 16 }}>
        <Field label="Save these columns for next time (optional)" hint="Files with the same columns from this vendor are matched the same way automatically."><TextInput value={layoutName} onChange={setLayoutName} placeholder="Vendor or list name" /></Field>
      </div>
      {as === "leads" ? (
        <div className="card card-body" style={{ marginTop: 16 }}>
          <label className="check"><input type="checkbox" checked={offersOn} onChange={e => setOffersOn(e.target.checked)} /><b>Make an offer link for every lead</b></label>
          <div className="hint" style={{ marginTop: 4 }}>Sized from monthly revenue. You get a CSV of the links when the import is done.</div>
          {offersOn ? <div style={{ marginTop: 14 }}><OfferSettingsForm s={offer} set={setOffer} />
            <div className="field" style={{ marginTop: 12 }}><span className="label">Leads that already have an open link</span><Segmented value={existing} onChange={setExisting} label="Existing links" options={[{ id: "skip", label: "Keep theirs" }, { id: "replace", label: "Replace" }]} /></div></div> : null}
        </div>
      ) : null}
      {as === "leads" && isZip ? (
        <div className="card card-body" style={{ marginTop: 16 }}>
          <label className="check"><input type="checkbox" checked={!!auto.on} onChange={e => setAuto({ ...auto, on: e.target.checked })} /><b>Read statements automatically for the bigger leads</b></label>
          <div className="hint" style={{ marginTop: 4 }}>Once the import is done, leads at or above this stated monthly revenue have their statements read in the background, biggest first. The rest show "Not analyzed" until someone clicks Analyze statements.</div>
          {auto.on ? (
            <div className="form-grid" style={{ marginTop: 12 }}>
              <Field label="Stated monthly revenue at least"><TextInput value={auto.minRevenue} onChange={v => setAuto({ ...auto, minRevenue: v })} inputMode="numeric" /></Field>
              <Field label="Newest statements per lead" hint="Months from the file names. Blank reads them all."><TextInput value={auto.newest} onChange={v => setAuto({ ...auto, newest: v.replace(/\D/g, "").slice(0, 2) })} inputMode="numeric" /></Field>
            </div>
          ) : null}
        </div>
      ) : null}
      <div className="import-foot">
        {save.error ? <span className="err" role="alert">{save.error}</span> : null}
        <button className="btn btn-primary btn-lg" disabled={save.busy} onClick={submit}>{save.busy ? "Starting…" : "Preview import"}</button>
      </div>
    </>
  );
}

const OUTCOME = { new: ["New", "new"], merge: ["Re-applied", "submitted"], existing: ["Already a deal or client", "review"], inFile: ["Twice in this file", "off"], reject: ["Rejected", "declined"] };
const FOLDER_HOW = { name: "Folder name", file: "File named after the business", close: "Close name", application: "EIN in the application", picked: "Picked by you", skipped: "Skipped" };
function ImportReview({ info, onChanged, onApplied, toast }) {
  const imp = info.import;
  const c = imp.counts || {};
  const fc = imp.folderCounts;
  const [tab, setTab] = useState(c.rejected ? "reject" : c.merge ? "merge" : "new");
  const [rows, setRows] = useState(null);
  const [allRows, setAllRows] = useState([]);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState("");
  useEffect(() => { setRows(null); API.importRows(imp.id, tab === "issues" ? { issues: "1" } : { outcome: tab }).then(r => setRows(r.rows)).catch(e => setErr(e.message)); }, [tab, imp.id]);
  useEffect(() => { API.importRows(imp.id, {}).then(r => setAllRows(r.rows.filter(x => x.outcome !== "reject"))).catch(() => {}); }, [imp.id]);
  const folders = info.folders || [];
  const needs = folders.filter(f => f.status === "review" || f.status === "unmatched");
  const bizOf = (i) => (allRows.find(r => r.i === i) || {}).biz || `Row ${i + 1}`;
  const setFolder = async (folder, v) => { try { await API.importFolders(imp.id, { [folder]: v === "skip" ? "skip" : Number(v) }); onChanged(); } catch (e) { toast(e.message, true); } };
  const apply = async () => {
    setBusy(true); setErr("");
    try { await API.applyImport(imp.id); toast("Importing…"); onApplied(); } catch (e) { setErr(e.message); setBusy(false); }
  };
  const back = async () => { try { await API.importMapping(imp.id, { map: imp.map, headerRow: imp.headerRow, as: imp.as, tags: imp.tags, offers: imp.offers ? { on: true, ...imp.offers } : null, autoRead: imp.autoRead ? { on: true, ...imp.autoRead } : null }); } catch (e) { /* stays */ } onChanged(); };
  const toWrite = (c.new || 0) + (c.merge || 0) + (c.existing || 0) + (c.inFile || 0);
  const tabs = [["new", `New (${c.new || 0})`], ["merge", `Re-applied (${c.merge || 0})`], ["existing", `Already deal or client (${c.existing || 0})`], ["inFile", `Twice in file (${c.inFile || 0})`], ["reject", `Rejected (${c.rejected || 0})`], ["issues", `Fixed fields (${c.withIssues || 0})`]].filter(([k]) => k === "new" || k === "issues" ? true : (c[k === "reject" ? "rejected" : k] || 0) > 0);
  return (
    <>
      <div className="card stats import-stats">
        <div className="stat"><div className="label">New {imp.as === "deals" ? "deals" : "leads"}</div><div className="v tnum">{c.new || 0}</div></div>
        <div className="stat"><div className="label">Re-applied (merged)</div><div className="v tnum">{c.merge || 0}</div></div>
        <div className="stat"><div className="label">Already a deal or client</div><div className="v tnum">{c.existing || 0}</div></div>
        <div className="stat"><div className="label">Rejected</div><div className="v tnum bad">{c.rejected || 0}</div></div>
        <div className="stat"><div className="label">{imp.offers ? "Offer links" : "Fields fixed or blanked"}</div><div className="v tnum">{imp.offers ? imp.offerCount || 0 : c.withIssues || 0}</div></div>
      </div>
      <p className="hint" style={{ margin: "8px 2px 0" }}>Re-applied leads are merged into the lead already on file: new details fill blanks, nothing existing is wiped, and the rep is told. A business already a deal or client gets a new lead, marked, and its rep is told.</p>

      {fc ? (
        <>
          <div className="section-bar" style={{ marginTop: 18 }}><span className="label">Folders</span><span className="muted" style={{ fontSize: 12.5 }}>{fc.matched + (fc.close || 0)} matched{fc.review ? ` · ${fc.review} need you` : ""}{fc.unmatched ? ` · ${fc.unmatched} unmatched` : ""}{fc.noStatements ? ` · ${fc.noStatements} with no statements` : ""}</span></div>
          <div className="card card-flush">
            {folders.map(f => (
              <div className="row folder-row" key={f.folder}>
                <span className={`hicon ${f.status === "matched" || f.status === "close" ? "t-good" : f.status === "skipped" ? "t-quiet" : "t-warn"}`}><Icon name={f.status === "matched" || f.status === "close" ? "check" : "file"} size={14} /></span>
                <div className="grow">
                  <div className="title">{f.folder}</div>
                  <div className="line">{f.files} {f.files === 1 ? "file" : "files"}{f.roles ? ` · ${f.roles.statement || 0} ${f.roles.statement === 1 ? "statement" : "statements"}${f.roles.application ? ", application" : ""}` : ""}{f.row !== null && f.row !== undefined ? ` → ${bizOf(f.row)}` : ""}{f.how ? ` · ${FOLDER_HOW[f.how] || f.how}` : ""}{f.shared ? " · shares a lead with another folder" : ""}{f.sameRowAs ? ` · its closest match already has folder "${f.sameRowAs}"` : ""}</div>
                </div>
                {f.status === "review" || f.status === "unmatched" || f.status === "close" || f.how === "picked" || f.status === "skipped" ? (
                  <select className="select sm" aria-label={`Lead for ${f.folder}`} value={f.status === "skipped" ? "skip" : f.row === null || f.row === undefined ? "" : String(f.row)} onChange={e => e.target.value !== "" && setFolder(f.folder, e.target.value)}>
                    <option value="">Pick the lead…</option>
                    {(f.candidates || []).map(x => <option key={"c" + x.row} value={x.row}>{bizOf(x.row)} ({Math.round(x.score * 100)}%)</option>)}
                    {allRows.filter(r => !(f.candidates || []).some(x => x.row === r.i)).map(r => <option key={r.i} value={r.i}>{r.biz}</option>)}
                    <option value="skip">Skip this folder</option>
                  </select>
                ) : null}
              </div>
            ))}
          </div>
        </>
      ) : null}

      <div className="section-bar" style={{ marginTop: 18 }}><span className="label">Rows</span></div>
      <div className="seg" role="group" aria-label="Rows" style={{ marginBottom: 10 }}>{tabs.map(([k, label]) => <button type="button" key={k} aria-pressed={tab === k} onClick={() => setTab(k)}>{label}</button>)}</div>
      <div className="card card-flush">
        {rows === null ? <div className="card-body dim">Loading…</div> : !rows.length ? <div className="card-body dim">None.</div> : rows.slice(0, 200).map(r => (
          <div className="row" key={r.i}>
            <span className="muted tnum" style={{ width: 44, flex: "none" }}>#{r.i + 1}</span>
            <div className="grow">
              <div className="title">{r.biz || <span className="muted">(no business name)</span>} {OUTCOME[r.outcome] ? <Pill status={OUTCOME[r.outcome][1]} label={OUTCOME[r.outcome][0]} /> : null}</div>
              <div className="line">{[r.name, r.state, r.revenue ? fmt.money(r.revenue) + "/mo" : "", r.ein4 ? `EIN ••${r.ein4}` : "", r.ssn4 ? `SSN ••${r.ssn4}` : "", r.folders ? `folder: ${r.folders.join(", ")}` : ""].filter(Boolean).join(" · ")}</div>
              {r.outcome === "merge" || r.outcome === "existing" ? <div className="when">Matched on {r.on} with {r.target && r.target.biz ? r.target.biz : "a private client"}</div> : null}
              {r.outcome === "inFile" ? <div className="when">Same business as row {r.row + 1} (matched on {r.on}); folded into it</div> : null}
              {r.reason ? <div className="when err">{r.reason}</div> : null}
              {r.issues && r.issues.length ? <div className="when">Blanked: {r.issues.join("; ")}</div> : null}
            </div>
          </div>
        ))}
      </div>
      <div className="import-foot">
        {err ? <span className="err" role="alert">{err}</span> : null}
        {needs.length ? <span className="muted">{needs.filter(f => f.status === "review").length ? `Pick the lead for ${needs.filter(f => f.status === "review").length} ${needs.filter(f => f.status === "review").length === 1 ? "folder" : "folders"} (or skip them) first.` : ""}</span> : null}
        <button className="btn" onClick={back}>Back to columns</button>
        <button className="btn btn-primary btn-lg" disabled={busy || needs.some(f => f.status === "review")} onClick={apply}>{busy ? "Starting…" : `Import ${toWrite} ${toWrite === 1 ? "row" : "rows"}`}</button>
      </div>
    </>
  );
}

// Files this import stored, sorted again by today's rules (statements with names like "K (1)" or "..._NSTMNT").
// It shows what would change first; nothing moves until it's confirmed.
function ResortFiles({ imp, toast }) {
  const [plan, setPlan] = useState(null);
  const [busy, setBusy] = useState(false);
  const check = async () => { setBusy(true); try { setPlan(await API.resortImportFiles(imp.id, true)); } catch (e) { toast(e.message, true); } setBusy(false); };
  const apply = async () => { setBusy(true); try { const r = await API.resortImportFiles(imp.id, false); toast(`${r.changed} ${r.changed === 1 ? "file" : "files"} sorted again`); setPlan({ ...r, done: true }); } catch (e) { toast(e.message, true); } setBusy(false); };
  if (imp.as === "deals") return null;
  return (
    <div className="card card-body" style={{ marginTop: 16 }}>
      <b>Sort the imported files again</b>
      <p className="dim" style={{ fontSize: 13, margin: "6px 0 10px" }}>Bank statements named like "K (1)", "9.pdf" or "…_NSTMNT" may have been filed as other documents or as the application. This files them as statements, so the leads show them and Analyze statements reads them. Only the file type changes.</p>
      {!plan ? <button className="btn" disabled={busy} onClick={check}>{busy ? "Checking…" : "Check the files"}</button> : plan.done ? (
        <p>Done: {plan.changed} {plan.changed === 1 ? "file" : "files"} sorted again. {plan.leadsWithStatements.after} of {plan.leadsWithStatements.of} leads now have statements.{plan.autoRead ? " Statements found on the bigger leads are being read too." : ""}</p>
      ) : plan.changed ? (
        <>
          <p><b>{plan.changed} of {plan.files} files</b> would change: {Object.entries(plan.moves).map(([k, n]) => `${n} ${k.replace(/-/g, " ")}`).join(", ")}. Leads with statements: {plan.leadsWithStatements.before} now, {plan.leadsWithStatements.after} after (of {plan.leadsWithStatements.of}).</p>
          <div className="offer-acts" style={{ marginTop: 8 }}><button className="btn btn-primary" disabled={busy} onClick={apply}>{busy ? "Sorting…" : `Sort ${plan.changed} files`}</button><button className="btn" disabled={busy} onClick={() => setPlan(null)}>Cancel</button></div>
        </>
      ) : <p>Every file is already sorted.</p>}
    </div>
  );
}

function AutoReadLine({ imp }) {
  const a = imp.autoRead, p = imp.autoReadProgress || {};
  const who = `leads with ${fmt.moneyShort(a.minRevenue)}+ stated monthly revenue${a.newest ? `, newest ${a.newest} each` : ""}`;
  let text;
  if (p.error) text = `Automatic reading stopped (${p.error}). Use Analyze statements on the leads.`;
  else if (!p.done) text = `Queuing statements to read on ${who}${p.of ? ` · ${p.next || 0} of ${p.of} leads` : "…"}`;
  else if (!p.queued) text = p.of ? `Automatic reading: nothing new to read on the ${p.of} ${who}.` : `Automatic reading: no ${who} in this import.`;
  else text = `Automatic reading: ${p.queued} ${p.queued === 1 ? "statement" : "statements"} on ${p.leads} ${who}, read in the background biggest first. Each lead shows "Reading…" until its figures are in. Opening one and clicking Analyze statements moves it to the front.`;
  return <p className="hint" style={{ marginTop: 8 }}><Icon name="file" size={13} /> {text}</p>;
}

function ImportDone({ imp, toast, go }) {
  const r = imp.result || {};
  const openLeads = () => { try { localStorage.setItem("ef.leads.filters", JSON.stringify({ tags: imp.tags || [], extra: "", show: "all" })); } catch (e) { /* private window */ } go(imp.as === "deals" ? "deals" : "leads"); };
  return (
    <>
      <div className="card stats import-stats">
        <div className="stat"><div className="label">{imp.as === "deals" ? "Deals" : "New leads"}</div><div className="v tnum">{imp.as === "deals" ? r.deals || 0 : r.created || 0}</div></div>
        <div className="stat"><div className="label">Re-applied</div><div className="v tnum">{r.merged || 0}</div></div>
        <div className="stat"><div className="label">Files stored</div><div className="v tnum">{r.files || 0}</div><div className="s muted" style={{ fontSize: 12 }}>{[r.duplicateFiles ? `${r.duplicateFiles} already there` : "", r.fileErrors ? `${r.fileErrors} couldn't be taken out (see each lead's log)` : ""].filter(Boolean).join(" · ")}</div></div>
        <div className="stat"><div className="label">Offer links</div><div className="v tnum">{r.offers || 0}</div></div>
        <div className="stat"><div className="label">Rejected</div><div className="v tnum bad">{r.rejected || 0}</div></div>
      </div>
      <div className="card card-body" style={{ marginTop: 16 }}>
        <p>Imported {fmt.when(imp.finishedAt)}. The uploaded file has been deleted; the leads, their files and the encrypted numbers are in the CRM.{r.rejected && !imp.filesGone ? " The rejected-rows file is kept for 30 days." : ""}</p>
        <div className="offer-acts" style={{ marginTop: 12 }}>
          <button className="btn btn-primary" onClick={openLeads}>Open these {imp.as === "deals" ? "deals" : "leads"}</button>
          {r.offers ? <button className="btn" onClick={() => API.importOffers(imp.id).catch(e => toast(e.message, true))}><Icon name="download" size={14} />Offer links CSV</button> : null}
          {r.rejected && !imp.filesGone ? <button className="btn" onClick={() => API.importRejects(imp.id).catch(e => toast(e.message, true))}><Icon name="download" size={14} />Rejected rows</button> : null}
        </div>
        {imp.as !== "deals" ? <p className="hint" style={{ marginTop: 12 }}>To hand them out: open them, tick "Select all", then Assign (to one rep, split evenly, or by revenue).</p> : null}
        {imp.autoRead ? <AutoReadLine imp={imp} /> : null}
      </div>
      <ResortFiles imp={imp} toast={toast} />
    </>
  );
}

// ── Export ──────────────────────────────────────────────────────────────────
function ExportSheet({ ids, what: initial, tagOptions, onClose, toast }) {
  const [what, setWhat] = useState(initial || "leads");
  const [tag, setTag] = useState([]);
  const [full, setFull] = useState(false);
  const [typed, setTyped] = useState("");
  const [log, setLog] = useState(null);
  const [opts, setOpts] = useState(tagOptions || []);
  const save = useSave(null);
  useEffect(() => { API.exportLog().then(setLog).catch(() => setLog([])); if (!tagOptions) API.tags().then(setOpts).catch(() => {}); }, []);
  const submit = () => {
    if (full && typed !== "EXPORT") return save.setError("Type EXPORT to include full Social Security numbers.");
    save.run(async () => {
      const r = await API.exportCsv({ what, tag: tag[0] || "", ids: ids || undefined, full, confirm: full ? "EXPORT" : undefined });
      toast(`Exported ${r.rows === null ? "" : r.rows + " "}${what}`);
      setLog(await API.exportLog().catch(() => log));
      save.setError("");
    });
  };
  return (
    <Sheet eyebrow={ids ? `${ids.length} selected` : "Admin"} title="Export" onClose={onClose} onSubmit={submit} submitLabel="Export CSV" busy={save.busy} error={save.error}>
      <div className="form-section">
        {ids ? null : <Segmented value={what} onChange={setWhat} label="What" options={[{ id: "leads", label: "Leads" }, { id: "deals", label: "Deals" }, { id: "clients", label: "Clients" }]} />}
        {!ids && what !== "clients" ? <div style={{ marginTop: 12 }}><span className="label">Tag (optional)</span><TagPicker value={tag} onChange={(x) => setTag(x.slice(-1))} options={opts} placeholder="Everything" /></div> : null}
        <p className="hint" style={{ marginTop: 10 }}>Columns include First Name, Last Name, ID and Extra, with the same names the importer reads, so a file can go to another system or come back in.</p>
      </div>
      <div className="form-section">
        <label className="check"><input type="checkbox" checked={full} onChange={e => { setFull(e.target.checked); setTyped(""); }} /><b>Include full SSN, EIN and date of birth</b></label>
        <div className="hint" style={{ marginTop: 4 }}>Off: SSN and EIN show their last 4, date of birth shows "On file".</div>
        {full ? (
          <div className="banner" style={{ marginTop: 10 }}>
            This file will hold full Social Security numbers. Keep it encrypted and delete it when you are done. Type EXPORT to continue.
            <input className="input" style={{ marginTop: 8 }} value={typed} onChange={e => setTyped(e.target.value)} autoComplete="off" aria-label="Type EXPORT" />
          </div>
        ) : null}
      </div>
      <div className="form-section">
        <span className="label">Export log</span>
        <div className="card card-flush" style={{ marginTop: 6 }}>
          {log === null ? <div className="card-body dim">Loading…</div> : !log.length ? <div className="card-body dim">No exports yet.</div> : log.slice(0, 12).map((l, i) => <div className="row" key={i}><div className="grow"><div className="line" style={{ color: "var(--ink)" }}>{l.text}</div><div className="when">{fmt.when(l.ts)}</div></div></div>)}
        </div>
      </div>
    </Sheet>
  );
}

// ── Settings cards ──────────────────────────────────────────────────────────
function TagsCard({ toast, confirm }) {
  const [tags, setTags] = useState(null);
  const [editing, setEditing] = useState(null);
  const [to, setTo] = useState("");
  const [q, setQ] = useState("");
  const load = () => API.tags().then(setTags).catch(() => setTags([]));
  useEffect(() => { load(); }, []);
  const rename = async () => {
    if (!to.trim()) return;
    try { const r = await API.changeTag({ action: "rename", from: editing, to: to.trim() }); toast(`Tag renamed on ${r.changed} ${r.changed === 1 ? "lead" : "leads"}`); setEditing(null); setTo(""); load(); }
    catch (err) { toast(err.message, true); }
  };
  const remove = (t) => confirm({ title: `Delete the tag “${t}”?`, body: "It is taken off every lead. The leads stay.", confirmLabel: "Delete tag",
    onConfirm: async () => { await API.changeTag({ action: "delete", from: t }); toast("Tag deleted"); load(); } });
  const list = (tags || []).filter(t => !q || t.tag.toLowerCase().includes(q.toLowerCase()));
  return (
    <div className="card">
      <div className="card-head"><span className="label">Tags</span>{tags && tags.length > 8 ? <input className="input sm" value={q} onChange={e => setQ(e.target.value)} placeholder="Find a tag" style={{ maxWidth: 180 }} /> : null}</div>
      <div className="card-body dim" style={{ fontSize: 12.5, paddingBottom: 0 }}>Rename a tag onto one that exists to merge the two.</div>
      {tags === null ? <div className="card-body dim">Loading…</div> : !list.length ? <div className="card-body dim">No tags yet.</div> : list.slice(0, 200).map(t => (
        <div className="switch-row" key={t.tag}>
          {editing === t.tag ? (
            <div style={{ display: "flex", gap: 8, flex: 1 }}><input className="input" autoFocus value={to} onChange={e => setTo(e.target.value)} onKeyDown={e => e.key === "Enter" && rename()} placeholder="New name" /><button className="btn btn-sm btn-primary" onClick={rename}>Rename</button><button className="btn btn-sm" onClick={() => setEditing(null)}>Cancel</button></div>
          ) : (
            <>
              <div><div className="t">{t.tag}</div><div className="d">{t.count} {t.count === 1 ? "lead" : "leads"}</div></div>
              <div style={{ display: "flex", gap: 6 }}><button className="btn btn-sm" onClick={() => { setEditing(t.tag); setTo(t.tag); }}>Rename</button><button className="icon-btn" aria-label={`Delete ${t.tag}`} onClick={() => remove(t.tag)}><Icon name="trash" size={15} /></button></div>
            </>
          )}
        </div>
      ))}
    </div>
  );
}

function ImportLayoutsCard({ toast }) {
  const [layouts, setLayouts] = useState(null);
  const [rules, setRules] = useState(null);
  const load = () => { API.importLayouts().then(setLayouts).catch(() => setLayouts([])); API.assignRules().then(setRules).catch(() => setRules([])); };
  useEffect(() => { load(); }, []);
  return (
    <div className="card">
      <div className="card-head"><span className="label">Saved import layouts and assign rules</span></div>
      {layouts && layouts.length ? layouts.map(l => (
        <div className="switch-row" key={l.id}>
          <div><div className="t">{l.name}</div><div className="d">{l.headerRow ? "Files with these column names" : `Files with ${String(l.signature).replace("w:", "")} columns and no header row`} · saved {fmt.date(l.savedAt)}</div></div>
          <button className="icon-btn" aria-label={`Delete ${l.name}`} onClick={async () => { await API.deleteImportLayout(l.id); toast("Layout deleted"); load(); }}><Icon name="trash" size={15} /></button>
        </div>
      )) : <div className="card-body dim" style={{ fontSize: 13 }}>No saved layouts. Name one on the column step of an import.</div>}
      {rules && rules.length ? rules.map(r => (
        <div className="switch-row" key={r.name}>
          <div><div className="t">{r.name}</div><div className="d">{r.ranges.length} revenue {r.ranges.length === 1 ? "range" : "ranges"} for Assign</div></div>
          <button className="icon-btn" aria-label={`Delete ${r.name}`} onClick={async () => { await API.saveAssignRules(rules.filter(x => x.name !== r.name)); toast("Rule deleted"); load(); }}><Icon name="trash" size={15} /></button>
        </div>
      )) : null}
    </div>
  );
}

function DataToolsCard({ s, toast, confirm }) {
  const [pii, setPii] = useState(null);
  const [q, setQ] = useState("");
  const [deals, setDeals] = useState(null);
  const [pick, setPick] = useState(new Set());
  const [tags, setTags] = useState(["Oct 15 offers"]);
  const [busy, setBusy] = useState(false);
  const keyReady = !!(s && s.pii && s.pii.available);
  // The server works through the bucket a page at a time; this keeps asking until it says it's done.
  const walkPii = async (apply) => {
    const sum = { total: 0, plain: 0, encrypted: 0 };
    let cursor = { p: 0 };
    for (let guard = 0; cursor && guard < 2000; guard++) { const r = await API.encryptPii(apply, cursor); sum.total += r.total; sum.plain += r.plain; sum.encrypted += r.encrypted; cursor = r.cursor; }
    return sum;
  };
  const countPii = async () => { setBusy(true); try { setPii(await walkPii(false)); } catch (err) { toast(err.message, true); } setBusy(false); };
  const encrypt = () => confirm({ plain: true, title: "Encrypt the stored SSNs?", confirmLabel: "Encrypt now",
    body: `${pii.plain} stored ${pii.plain === 1 ? "record holds" : "records hold"} an SSN (or EIN or date of birth) unencrypted. They are rewritten encrypted with the key set on the server. This can be run again safely.`,
    onConfirm: async () => { const r = await walkPii(true); toast(`Encrypted ${r.encrypted}`); setPii(r); } });
  const find = async () => { setBusy(true); try { setDeals(await API.dealsBackCandidates(q)); setPick(new Set()); } catch (err) { toast(err.message, true); } setBusy(false); };
  const move = () => confirm({ plain: true, title: `Move ${pick.size} ${pick.size === 1 ? "deal" : "deals"} back to Leads?`, confirmLabel: "Move to Leads",
    body: `They become leads${tags.length ? ` tagged ${tags.join(", ")}` : ""}, with their files, notes and offer links. Links already sent keep working.`,
    onConfirm: async () => {
      const parts = await inParts([...pick], 20, (part) => API.dealsBackToLeads(part, tags, true));
      const moved = parts.reduce((t, r) => t + r.moved.length, 0), skipped = parts.reduce((t, r) => t + r.skipped.length, 0);
      toast(`${moved} moved to Leads${skipped ? `, ${skipped} skipped` : ""}`); find();
    } });
  return (
    <div className="card">
      <div className="card-head"><span className="label">Data tools</span></div>
      <div className="switch-row">
        <div><div className="t">SSN, EIN and date of birth encryption {keyReady ? <Pill status="active" label="Key set" /> : <Pill status="pending" label="Key not set" />}</div>
          <div className="d">{keyReady ? "New numbers are stored encrypted. Numbers saved before the key was set can be encrypted here." : "Set the PII_KEY secret on the server (see DEPLOYMENT.md). Until then imports can't bring in SSN, EIN or date of birth columns."}{pii ? ` · ${pii.total} stored, ${pii.plain} not encrypted` : ""}</div></div>
        {keyReady ? (pii && pii.plain ? <button className="btn btn-sm btn-primary" onClick={encrypt}>Encrypt {pii.plain}</button> : <button className="btn btn-sm" disabled={busy} onClick={countPii}>{busy ? "Checking…" : "Check"}</button>) : null}
      </div>
      <div className="card-body">
        <div className="t" style={{ fontWeight: 600 }}>Move deals back to Leads</div>
        <div className="dim" style={{ fontSize: 13, margin: "4px 0 10px" }}>For deals that were never sent to a lender (no submissions, nothing funded or paid). Search by business, contact or source.</div>
        <div style={{ display: "flex", gap: 8 }}><input className="input" value={q} onChange={e => setQ(e.target.value)} onKeyDown={e => e.key === "Enter" && find()} placeholder="e.g. October 15" /><button className="btn" disabled={busy} onClick={find}>{busy ? "Finding…" : "Find"}</button></div>
        {deals ? (
          <>
            <div className="section-bar" style={{ marginTop: 12 }}>
              <label className="check"><input type="checkbox" checked={deals.length > 0 && deals.every(d => pick.has(d.id))} onChange={() => setPick(deals.every(d => pick.has(d.id)) ? new Set() : new Set(deals.map(d => d.id)))} />{deals.length} {deals.length === 1 ? "deal" : "deals"} found</label>
            </div>
            <div className="card card-flush" style={{ marginTop: 6, maxHeight: 320, overflowY: "auto" }}>
              {deals.map(d => (
                <label className="row check" key={d.id}><input type="checkbox" checked={pick.has(d.id)} onChange={() => setPick(prev => { const n = new Set(prev); n.has(d.id) ? n.delete(d.id) : n.add(d.id); return n; })} />
                  <div className="grow"><div className="title">{d.biz}</div><div className="when">{d.source || "—"} · {fmt.date(d.createdAt)}</div></div></label>
              ))}
            </div>
            {pick.size ? (
              <div style={{ marginTop: 10 }}>
                <span className="label">Tag them</span><TagPicker value={tags} onChange={setTags} options={[]} allowCreate placeholder="Tags" />
                <button className="btn btn-primary" style={{ marginTop: 10 }} onClick={move}>Move {pick.size} to Leads</button>
              </div>
            ) : null}
          </>
        ) : null}
      </div>
    </div>
  );
}

Object.assign(window, { ImportView, ExportSheet, TagsCard, ImportLayoutsCard, DataToolsCard });
