// Bylt Capital CRM — Leads (Round 9): the list with tags, filters and selection,
// bulk assign, bulk tags and bulk offer links, and the lead file.
//
// Lead > Deal > Client: a lead becomes a deal when its first lender submission
// is logged here ("Log lender submission"). Admins can also convert by hand.

const LEAD_FILTERS_KEY = "ef.leads.filters";
const readSaved = () => { try { return JSON.parse(localStorage.getItem(LEAD_FILTERS_KEY) || "{}") || {}; } catch (e) { return {}; } };
const writeSaved = (v) => { try { localStorage.setItem(LEAD_FILTERS_KEY, JSON.stringify(v)); } catch (e) { /* private window */ } };
const leadName = (l) => l.name || [l.firstName, l.lastName].filter(Boolean).join(" ") || l.ownerName || "";
const leadBiz = (l) => l.biz || l.businessName || leadName(l) || "—";
// Sorting the Leads list. Each sort has a default direction; leads with nothing to sort by go last either way.
const leadRevenue = (l) => (l.bank && l.bank.avgSales) || fmt.num(l.monthlyRevenue) || null;   // the statements' sales when read, else what was stated (as shown)
const offerRank = (o) => !o ? null : o.state === "accepted" ? 3000 : o.state === "open" ? (o.opened ? 2000 + Math.min(999, o.views || 0) : 1000) : 500;
const LEAD_SORTS = {
  recent:    { label: "Newest", dir: "desc", val: (l) => String(l.lastAppliedAt || l.createdAt || "") },
  revenue:   { label: "Revenue", dir: "desc", val: leadRevenue },
  leverage:  { label: "Leverage", dir: "desc", val: (l) => (l.bank && l.bank.share !== null && l.bank.share !== undefined ? l.bank.share : null) },
  positions: { label: "Positions", dir: "desc", val: (l) => (l.bank && l.bank.share !== null && l.bank.share !== undefined ? l.bank.positions : null) },
  offer:     { label: "Offer opened", dir: "desc", val: (l) => offerRank(l.offer) },
  rep:       { label: "Rep", dir: "asc", val: null },   // by the rep's name (needs the team), set in LeadsView
  biz:       { label: "Business", dir: "asc", val: (l) => leadBiz(l).toLowerCase() },
};
function sortLeads(list, key, dir, valOf) {
  const val = valOf || LEAD_SORTS[key].val, sign = dir === "asc" ? 1 : -1;
  const keyed = list.map(l => ({ l, v: val(l) }));
  keyed.sort((a, b) => {
    const na = a.v === null || a.v === undefined || a.v === "", nb = b.v === null || b.v === undefined || b.v === "";
    if (na || nb) return na === nb ? 0 : na ? 1 : -1;
    return (a.v < b.v ? -1 : a.v > b.v ? 1 : 0) * sign;
  });
  return keyed.map(x => x.l);
}
// ── What the statements say, for the filters and the "Not analyzed" label ──
// `bank` is the snapshot kept on the lead (api-statements.js bankSnapshot): `read` statements read into figures,
// `pending` waiting or being read. Snapshots from before those counts existed are judged by `months`.
const leadAnalyzed = (l) => !!(l.bank && ((l.bank.read || 0) > 0 || (l.bank.months || 0) > 0 || (l.bank.share !== null && l.bank.share !== undefined)));
// "Reading…" for a day at most after the snapshot: a read that died is put right when the lead is opened, and until then
// the lead shows "Not analyzed" rather than reading for ever.
const leadReading = (l) => !!(l.bank && (l.bank.pending || 0) > 0 && Date.now() - Date.parse(l.bank.at || 0) < 864e5);
const leadNotAnalyzed = (l) => !!(l.files && l.files.statements > 0) && !leadAnalyzed(l) && !leadReading(l);
const leadShare = (l) => (l.bank && l.bank.share !== null && l.bank.share !== undefined ? l.bank.share : null);
// Leverage filter: the same bands as the remit label (20% and under green, 21–40 orange, 41–50 red, over 50 over-leveraged).
const LEVERAGE_FILTERS = [
  ["", "Leverage: any"],
  ["le20", "Leverage: 20% and under", (v) => v !== null && v <= 20],
  ["le40", "Leverage: 40% and under", (v) => v !== null && v <= 40],
  ["le50", "Leverage: 50% and under", (v) => v !== null && v <= 50],
  ["b21", "Leverage: 21–40%", (v) => v !== null && v > 20 && v <= 40],
  ["b41", "Leverage: 41–50%", (v) => v !== null && v > 40 && v <= 50],
  ["over", "Leverage: over 50% (Over-Leveraged)", (v) => v !== null && v > 50],
  ["na", "Not analyzed", null, leadNotAnalyzed],
  ["reading", "Reading now", null, leadReading],
];
const levTest = (key) => { const f = LEVERAGE_FILTERS.find(x => x[0] === key); return !f || !key ? () => true : f[3] ? f[3] : (l) => f[2](leadShare(l)); };
// Revenue by what was stated, or by the statements (falling back to stated where none are read yet).
const revenueOn = (l, basis) => (basis === "verified" && l.bank && l.bank.avgSales ? l.bank.avgSales : fmt.num(l.monthlyRevenue) || null);
const positionsOf = (l) => (leadAnalyzed(l) && l.bank ? Number(l.bank.positions) || 0 : null);
const bound = (v) => (v === "" || v === null || v === undefined ? null : Number(String(v).replace(/[$,\s]/g, "")));
const inRange = (v, r) => { const lo = bound(r.min), hi = bound(r.max); if (lo === null && hi === null) return true; if (v === null || v === undefined) return false; return (lo === null || v >= lo) && (hi === null || v <= hi); };
const rangeOn = (r) => bound(r.min) !== null || bound(r.max) !== null;
// The small label beside a lead with statements that haven't been read into figures.
function StatementState({ l, rail }) {
  if (leadReading(l)) return <span className={`st-chip reading${rail ? " small" : ""}`} title="Statements are being read; the figures appear when done">Reading…</span>;
  if (leadNotAnalyzed(l)) return <span className={`st-chip na${rail ? " small" : ""}`} title={`${l.files.statements} ${l.files.statements === 1 ? "statement" : "statements"} on file, not read yet`}>Not analyzed</span>;
  return null;
}
// A filter button with a small panel: a range (min–max), and for revenue which figure it goes by.
function RangePick({ label, value, onChange, money, basis, onBasis }) {
  const [open, setOpen] = useState(false);
  const box = useRef(null);
  useEffect(() => {
    if (!open) return;
    const off = (e) => { if (box.current && !box.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", off); document.addEventListener("touchstart", off);
    return () => { document.removeEventListener("mousedown", off); document.removeEventListener("touchstart", off); };
  }, [open]);
  const show = (v) => (money ? fmt.moneyShort(bound(v)) : String(bound(v)));
  const lo = bound(value.min), hi = bound(value.max);
  const text = lo === null && hi === null ? "any" : lo !== null && hi !== null ? `${show(value.min)}–${show(value.max)}` : lo !== null ? `${show(value.min)}+` : `up to ${show(value.max)}`;
  const tidy = (v) => (money ? v.replace(/[^\d.,$]/g, "") : v.replace(/\D/g, "").slice(0, 3));
  return (
    <div className="tagpick rangepick" ref={box}>
      <button type="button" className={`tagpick-btn${lo !== null || hi !== null ? " on" : ""}`} onClick={() => setOpen(!open)} aria-expanded={open} aria-label={`${label} filter`}>
        <span>{label}{basis && (lo !== null || hi !== null) ? ` (${basis === "verified" ? "verified" : "stated"})` : ""}: <b>{text}</b></span><Icon name="chevdown" size={14} />
      </button>
      {open ? (
        <div className="tagpick-pop range-pop">
          {onBasis ? <div style={{ marginBottom: 10 }}><Segmented value={basis} onChange={onBasis} label="Revenue by" options={[{ id: "stated", label: "Stated" }, { id: "verified", label: "Verified" }]} />
            <div className="hint" style={{ marginTop: 6 }}>{basis === "verified" ? "Average monthly sales on the statements; leads not read yet go by what they stated." : "What the lead stated (or the list said)."}</div></div> : null}
          <div className="minmax-row">
            <input className="input" inputMode="numeric" placeholder="Min" aria-label={`${label} from`} value={value.min} onChange={e => onChange({ ...value, min: tidy(e.target.value) })} autoFocus />
            <span className="muted">to</span>
            <input className="input" inputMode="numeric" placeholder="Max" aria-label={`${label} to`} value={value.max} onChange={e => onChange({ ...value, max: tidy(e.target.value) })} onKeyDown={e => { if (e.key === "Enter") setOpen(false); }} />
          </div>
          {money ? <div className="range-quick">{[25000, 50000, 100000, 250000].map(v => <button type="button" key={v} className="chip" onClick={() => onChange({ ...value, min: String(v), max: "" })}>{fmt.moneyShort(v)}+</button>)}</div>
            : <div className="range-quick">{[["0", "0", "None"], ["1", "1", "1"], ["", "2", "2 or fewer"], ["3", "", "3+"]].map(([a, b, t]) => <button type="button" key={t} className="chip" onClick={() => onChange({ min: a, max: b })}>{t}</button>)}</div>}
          {!money ? <div className="hint" style={{ marginTop: 6 }}>From the statements. Leads not read yet are left out while this is set.</div> : null}
          <div className="tagpick-foot"><button type="button" className="link-btn" onClick={() => onChange({ min: "", max: "" })}>Clear</button><button type="button" className="btn btn-sm" onClick={() => setOpen(false)}>Done</button></div>
        </div>
      ) : null}
    </div>
  );
}

// Big selections go to the server in parts (Cloudflare caps the database work one request may do).
// Must match the server's sizes: BULK_PARTS in src/api-leads.js, OFFERS_PER_REQUEST in src/api-lead-bulk.js.
const PARTS = { tag: 400, delete: 50, assign: 250, offers: 150, analyze: 25 };
const MAX_PICK = 5000;   // one bulk action at a time (the server's MAX_BULK)
async function inParts(list, size, fn, onProgress) {
  const out = [];
  for (let i = 0; i < list.length; i += size) {
    const chunk = list.slice(i, i + size);
    if (onProgress && list.length > size) onProgress(i + chunk.length, list.length);
    try { out.push(await fn(chunk, i + size >= list.length)); }
    catch (err) { if (i > 0) err.message = `Stopped after ${i} of ${list.length}: ${err.message} Run it again to finish; what was done stays done.`; throw err; }
  }
  return out;
}

// ── Tags: a searchable dropdown (there will be a lot of them) ──────────────
function TagPicker({ value, onChange, options, placeholder, allowCreate, label, compact }) {
  const [open, setOpen] = useState(false);
  const [q, setQ] = useState("");
  const box = useRef(null);
  useEffect(() => {
    if (!open) return;
    const off = (e) => { if (box.current && !box.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", off); document.addEventListener("touchstart", off);
    return () => { document.removeEventListener("mousedown", off); document.removeEventListener("touchstart", off); };
  }, [open]);
  const chosen = value || [];
  const has = (t) => chosen.some(x => x.toLowerCase() === t.toLowerCase());
  const toggle = (t) => onChange(has(t) ? chosen.filter(x => x.toLowerCase() !== t.toLowerCase()) : [...chosen, t]);
  const needle = q.trim().toLowerCase();
  const list = (options || []).filter(o => !needle || o.tag.toLowerCase().includes(needle)).slice(0, 80);
  const canCreate = allowCreate && q.trim() && !(options || []).some(o => o.tag.toLowerCase() === needle) && !has(q.trim());
  return (
    <div className={`tagpick${compact ? " compact" : ""}`} ref={box}>
      <button type="button" className="tagpick-btn" onClick={() => setOpen(!open)} aria-expanded={open} aria-label={label || "Tags"}>
        <Icon name="tag" size={14} />
        {chosen.length ? <span className="tagpick-chosen">{chosen.map(t => <span key={t} className="chip">{t}</span>)}</span> : <span className="muted">{placeholder || "Tags"}</span>}
        <Icon name="chevdown" size={14} />
      </button>
      {open ? (
        <div className="tagpick-pop" role="listbox" aria-multiselectable="true">
          <input className="input" autoFocus value={q} onChange={e => setQ(e.target.value)} placeholder={allowCreate ? "Search or type a new tag" : "Search tags"}
                 onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); if (canCreate) { toggle(q.trim()); setQ(""); } else if (list[0]) toggle(list[0].tag); } if (e.key === "Escape") setOpen(false); }} />
          <div className="tagpick-list">
            {canCreate ? <button type="button" className="tagpick-item create" onClick={() => { toggle(q.trim()); setQ(""); }}><Icon name="plus" size={13} />Create “{q.trim()}”</button> : null}
            {list.map(o => (
              <label key={o.tag} className="tagpick-item">
                <input type="checkbox" checked={has(o.tag)} onChange={() => toggle(o.tag)} />
                <span className="grow">{o.tag}</span>{o.count !== undefined ? <span className="muted tnum">{o.count}</span> : null}
              </label>
            ))}
            {!list.length && !canCreate ? <div className="muted" style={{ padding: "8px 10px", fontSize: 13 }}>{options && options.length ? "No tag matches" : "No tags yet"}</div> : null}
          </div>
          {chosen.length ? <div className="tagpick-foot"><button type="button" className="link-btn" onClick={() => onChange([])}>Clear</button><button type="button" className="btn btn-sm" onClick={() => setOpen(false)}>Done</button></div> : null}
        </div>
      ) : null}
    </div>
  );
}

// ── Offer link settings (bulk generator and the import): one set for the whole batch ──
const OFFER_DEFAULTS = { pct: 150, round: 10000, min: 10000, mode: "adjust", tmin: 44, tmax: 60, tstep: 4, fmin: 1.25, fmax: 1.35, tf: {}, term: 52, factor: 1.3, freq: "Weekly", remit: { Daily: true, Weekly: true }, days: 14 };
const OFFER_SETTINGS_KEY = "ef.offerSettings";
const savedOfferSettings = () => { try { return { ...OFFER_DEFAULTS, ...(JSON.parse(localStorage.getItem(OFFER_SETTINGS_KEY) || "{}") || {}) }; } catch (e) { return { ...OFFER_DEFAULTS }; } };
const rememberOfferSettings = (s) => { try { localStorage.setItem(OFFER_SETTINGS_KEY, JSON.stringify(s)); } catch (e) { /* private window */ } };
function termWeeksOf(s) {
  const lo = Math.round(Number(s.tmin)), hi = Math.round(Number(s.tmax)), st = Math.max(1, Math.round(Number(s.tstep)));
  if (!(lo >= 1 && hi >= lo)) return { err: "The shortest term must be 1 week or more and no longer than the longest." };
  const out = []; for (let w = lo; w <= hi; w += st) out.push(w); if (out[out.length - 1] !== hi) out.push(hi);
  if (out.length > 6) return { err: `That makes ${out.length} term choices. The offer page shows up to 6, so use a bigger step.` };
  return { weeks: out };
}
const r2 = (n) => Math.round(n * 100) / 100;
function factorAt(s, w) {
  const t = s.tf && s.tf[w] !== undefined && s.tf[w] !== "" ? Number(s.tf[w]) : NaN;
  if (Number.isFinite(t)) return t;
  const lo = Number(s.tmin), hi = Number(s.tmax);
  return hi === lo ? Number(s.fmin) : r2(Number(s.fmin) + (Number(s.fmax) - Number(s.fmin)) * (w - lo) / (hi - lo));
}
function offerSettingsError(s) {
  if (!(Number(s.min) >= 1000)) return "The minimum offer must be at least $1,000.";
  if (s.mode === "fixed") return Number(s.term) >= 1 && Number(s.factor) >= 1 ? "" : "Enter the term and factor rate.";
  const t = termWeeksOf(s);
  if (t.err) return t.err;
  if (!s.remit.Daily && !s.remit.Weekly) return "Allow daily payments, weekly payments, or both.";
  if (t.weeks.some(w => !(factorAt(s, w) >= 1 && factorAt(s, w) <= 3))) return "Factor rates must be between 1.00 and 3.00.";
  return "";
}
function offerExample(s, revenue) {
  const amt = Math.round(revenue * Number(s.pct) / 100 / Number(s.round)) * Number(s.round);
  if (amt < Number(s.min)) return `${fmt.money(revenue)} a month → under the ${fmt.money(s.min)} minimum, skipped`;
  if (s.mode === "fixed") { const t = offerTerms(amt, Number(s.factor), Number(s.term) * 5, s.freq); return `${fmt.money(revenue)} a month → ${fmt.money(amt)} · ${s.term} weeks at ${Number(s.factor).toFixed(2)} · ${t.payments} ${s.freq.toLowerCase()} payments of ${fmt.money(t.payment)}`; }
  const t = termWeeksOf(s); if (t.err) return t.err;
  const last = t.weeks[t.weeks.length - 1], fq = s.remit.Weekly ? "Weekly" : "Daily";
  const low = offerTerms(amt, factorAt(s, last), last * 5, fq);
  return `${fmt.money(revenue)} a month → up to ${fmt.money(amt)} (slider from ${fmt.money(Math.min(Number(s.min), amt))}) · ${t.weeks[0]}–${last} weeks · payments from ${fmt.money(low.payment)} ${fq === "Weekly" ? "a week" : "a day"}`;
}
function OfferSettingsForm({ s, set }) {
  const tw = termWeeksOf(s);
  const num = (k, label, step, hint) => <Field label={label} hint={hint}><input className="input" inputMode="decimal" value={s[k]} onChange={e => set({ ...s, [k]: e.target.value, ...(["tmin", "tmax", "tstep", "fmin", "fmax"].includes(k) ? { tf: {} } : {}) })} /></Field>;
  return (
    <>
      <div className="form-grid">
        {num("pct", "% of monthly revenue")}
        <Field label="Round to nearest"><Select value={String(s.round)} onChange={x => set({ ...s, round: Number(x) })} options={[1000, 5000, 10000, 25000].map(v => ({ value: String(v), label: fmt.money(v) }))} /></Field>
        {num("min", "Minimum offer", null, "Smaller offers are skipped. No maximum.")}
        {num("days", "Link good for (days)")}
      </div>
      <div className="field" style={{ marginTop: 12 }}>
        <span className="label">Offer type</span>
        <Segmented value={s.mode} onChange={x => set({ ...s, mode: x })} label="Offer type" options={[{ id: "adjust", label: "Merchant chooses" }, { id: "fixed", label: "Fixed" }]} />
        <div className="hint">{s.mode === "adjust" ? "The merchant moves the amount up to the offer and picks a term and daily or weekly payments within your ranges." : "One amount, term and factor."}</div>
      </div>
      {s.mode === "fixed" ? (
        <div className="form-grid" style={{ marginTop: 12 }}>
          {num("term", "Term (weeks)")}
          {num("factor", "Factor rate")}
          <Field label="Payments"><Select value={s.freq} onChange={x => set({ ...s, freq: x })} options={[{ value: "Weekly", label: "Weekly" }, { value: "Daily", label: "Daily" }]} /></Field>
        </div>
      ) : (
        <>
          <div className="form-grid" style={{ marginTop: 12 }}>
            {num("tmin", "Shortest term (weeks)")}
            {num("tmax", "Longest term (weeks)")}
            {num("tstep", "Every (weeks)")}
            {num("fmin", "Factor at shortest")}
            {num("fmax", "Factor at longest")}
            <div className="field"><span className="label">Payments</span>
              <div className="check-row">
                {["Daily", "Weekly"].map(k => <label key={k}><input type="checkbox" checked={!!s.remit[k]} onChange={e => set({ ...s, remit: { ...s.remit, [k]: e.target.checked } })} />{k}</label>)}
              </div>
            </div>
          </div>
          {tw.err ? <div className="banner" role="alert" style={{ marginTop: 10 }}>{tw.err}</div> : (
            <div className="table-wrap" style={{ marginTop: 10 }}>
              <table className="list compact">
                <thead><tr><th>Term</th>{tw.weeks.map(w => <th key={w} className="num">{w} wk</th>)}</tr></thead>
                <tbody><tr><td>Factor</td>{tw.weeks.map(w => (
                  <td key={w} className="num"><input className="input tf" inputMode="decimal" aria-label={`Factor for ${w} weeks`} value={s.tf && s.tf[w] !== undefined ? s.tf[w] : factorAt(s, w).toFixed(2)} onChange={e => set({ ...s, tf: { ...(s.tf || {}), [w]: e.target.value } })} /></td>
                ))}</tr></tbody>
              </table>
            </div>
          )}
        </>
      )}
      <div className="hint" style={{ marginTop: 10 }}>{offerSettingsError(s) || `Example: ${offerExample(s, 40000)}.`}</div>
    </>
  );
}

// ── Assign several leads ────────────────────────────────────────────────────
function AssignSheet({ ids, team, user, onClose, onDone }) {
  const isAdmin = !!canOf(user).manage;
  const reps = (team || []).filter(t => t.active !== false);
  const [mode, setMode] = useState("one");
  const [repId, setRepId] = useState(reps[0] ? reps[0].id : "");
  const [repIds, setRepIds] = useState([]);
  const [ranges, setRanges] = useState([{ min: "0", max: "30000", repIds: [] }, { min: "30000", max: "80000", repIds: [] }, { min: "80000", max: "", repIds: [] }]);
  const [noRevenue, setNoRevenue] = useState("leave");
  const [rules, setRules] = useState([]);
  const [ruleName, setRuleName] = useState("");
  const [plan, setPlan] = useState(null);
  const [progress, setProgress] = useState("");
  const save = useSave(onDone);
  useEffect(() => { API.assignRules().then(setRules).catch(() => {}); }, []);
  const toggleRep = (list, id) => (list.includes(id) ? list.filter(x => x !== id) : [...list, id]);
  const body = () => ({ ids, action: "assign", mode, repId: repId || null, repIds, noRevenue,
    ranges: ranges.map(r => ({ min: fmt.parseMoney(r.min) || 0, max: r.max === "" ? null : fmt.parseMoney(r.max), repIds: r.repIds })) });
  const check = async () => {
    save.setError(""); setPlan(null);
    try { setPlan(await API.bulkLeads({ ...body(), dryRun: true })); } catch (err) { save.setError(err.message); }
  };
  useEffect(() => { setPlan(null); }, [mode, repId, repIds.join(), JSON.stringify(ranges), noRevenue]);
  const submit = () => {
    if (!plan) return check();
    save.run(async () => {
      if (ids.length <= PARTS.assign) return API.bulkLeads({ ...body(), seed: plan.seed });
      const done = await inParts(ids, PARTS.assign, (part, last) => API.bulkLeads({ ...body(), seed: plan.seed, part, notify: last }), (n, of) => setProgress(`Assigning ${n} of ${of}…`));
      return { changed: done.reduce((t, r) => t + (r.changed || 0), 0) };
    });
  };
  const loadRule = (r) => { setMode("ranges"); setRanges(r.ranges.map(x => ({ min: String(x.min || 0), max: x.max === null || x.max === undefined ? "" : String(x.max), repIds: x.repIds || [] }))); setNoRevenue(r.noRevenue || "leave"); };
  const saveRule = async () => {
    if (!ruleName.trim()) return save.setError("Name the rule first.");
    try { const next = await API.saveAssignRules([{ name: ruleName.trim(), ranges: body().ranges, noRevenue }, ...rules.filter(r => r.name !== ruleName.trim())]); setRules(next); setRuleName(""); } catch (err) { save.setError(err.message); }
  };
  const repChecks = (list, onToggle) => (
    <div className="check-row">{reps.map(t => <label key={t.id}><input type="checkbox" checked={list.includes(t.id)} onChange={() => onToggle(t.id)} />{t.name}</label>)}</div>
  );
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Assign leads" onClose={onClose} onSubmit={submit}
           submitLabel={plan ? `Assign ${plan.total - plan.untouched} leads` : "Preview"} busyLabel={progress || undefined} busy={save.busy} error={save.error}>
      <div className="form-section">
        <Segmented value={mode} onChange={setMode} label="How" options={[{ id: "one", label: "To one rep" }, { id: "even", label: "Split evenly" }, { id: "ranges", label: "By revenue" }]} />
        <div className="hint" style={{ marginTop: 8 }}>{mode === "one" ? "Every selected lead goes to this rep." : mode === "even" ? "The leads are shuffled, then dealt out in turn, so each rep gets the same number (give or take one)." : "Each lead goes by its monthly revenue. A range with more than one rep is split evenly between them."}</div>
      </div>
      {mode === "one" ? (
        <div className="form-section"><Field label="Rep"><Select value={repId} onChange={setRepId} options={(canOf(user).wide ? [{ value: "", label: "Unassigned (Admins only)" }] : []).concat(reps.map(t => ({ value: t.id, label: t.name })))} /></Field></div>
      ) : mode === "even" ? (
        <div className="form-section"><span className="label">Reps to split between</span>{repChecks(repIds, id => setRepIds(toggleRep(repIds, id)))}</div>
      ) : (
        <div className="form-section">
          {rules.length ? <div className="chips" style={{ marginTop: 0, marginBottom: 10 }}><span className="muted" style={{ fontSize: 12.5 }}>Saved:</span>{rules.map(r => <button type="button" key={r.name} className="chip as-btn" onClick={() => loadRule(r)}>{r.name}</button>)}</div> : null}
          {ranges.map((r, i) => (
            <div className="range-row" key={i}>
              <div className="range-amt">
                <input className="input" inputMode="decimal" aria-label="From" value={r.min} onChange={e => setRanges(ranges.map((x, k) => k === i ? { ...x, min: e.target.value } : x))} placeholder="$0" />
                <span className="muted">to</span>
                <input className="input" inputMode="decimal" aria-label="To" value={r.max} onChange={e => setRanges(ranges.map((x, k) => k === i ? { ...x, max: e.target.value } : x))} placeholder="no limit" />
                {ranges.length > 1 ? <button type="button" className="icon-btn" aria-label="Remove range" onClick={() => setRanges(ranges.filter((_, k) => k !== i))}><Icon name="x" size={14} /></button> : null}
              </div>
              {repChecks(r.repIds, id => setRanges(ranges.map((x, k) => k === i ? { ...x, repIds: toggleRep(x.repIds, id) } : x)))}
            </div>
          ))}
          <button type="button" className="btn btn-sm" onClick={() => setRanges([...ranges, { min: ranges.length ? ranges[ranges.length - 1].max || "" : "0", max: "", repIds: [] }])}><Icon name="plus" size={14} />Add range</button>
          <Field label="Leads with no revenue"><Select value={noRevenue} onChange={setNoRevenue} options={[{ value: "leave", label: "Leave as they are" }].concat(reps.map(t => ({ value: t.id, label: t.name })))} /></Field>
          {isAdmin ? (
            <div className="range-save"><input className="input" value={ruleName} onChange={e => setRuleName(e.target.value)} placeholder="Name these ranges to reuse them" /><button type="button" className="btn btn-sm" onClick={saveRule}>Save rule</button></div>
          ) : null}
        </div>
      )}
      {plan ? (
        <div className="form-section">
          <span className="label">What will happen</span>
          <div className="card card-flush">
            {plan.plan.map(p => <div className="row" key={p.repId || "none"}><div className="grow"><div className="title">{p.name}</div></div><div className="right tnum"><b>{p.count}</b> {p.count === 1 ? "lead" : "leads"}</div></div>)}
            {plan.untouched ? <div className="row"><div className="grow"><div className="title muted">Left as they are (no revenue, or no range)</div></div><div className="right tnum">{plan.untouched}</div></div> : null}
          </div>
          <p className="hint" style={{ marginTop: 8 }}>Each rep gets one alert in their bell.</p>
        </div>
      ) : null}
    </Sheet>
  );
}

// ── Add or remove tags on several leads ─────────────────────────────────────
function TagSheet({ ids, tagOptions, onClose, onDone }) {
  const [tags, setTags] = useState([]);
  const [action, setAction] = useState("tag");
  const [progress, setProgress] = useState("");
  const save = useSave(onDone);
  const submit = () => {
    if (!tags.length) return save.setError("Pick or type at least one tag.");
    save.run(async () => { const done = await inParts(ids, PARTS.tag, (part) => API.bulkLeads({ ids: part, action, tags }), (n, of) => setProgress(`Tagging ${n} of ${of}…`)); return { changed: done.reduce((t, r) => t + (r.changed || 0), 0) }; });
  };
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Tags" onClose={onClose} onSubmit={submit} submitLabel={action === "tag" ? "Add tags" : "Remove tags"} busyLabel={progress || undefined} busy={save.busy} error={save.error}>
      <div className="form-section">
        <Segmented value={action} onChange={setAction} label="Add or remove" options={[{ id: "tag", label: "Add" }, { id: "untag", label: "Remove" }]} />
        <div style={{ marginTop: 12 }}><TagPicker value={tags} onChange={setTags} options={tagOptions} allowCreate={action === "tag"} placeholder="Pick tags" /></div>
      </div>
    </Sheet>
  );
}

// ── Send to Constant Contact (DEC-095) ──────────────────────────────────────
// The chosen leads with a live offer link and an email go into the list picked in Settings; Constant Contact's
// automation on that list sends the offer email. Counted first, then sent.
function BulkCcSheet({ ids, cc, onClose, onDone }) {
  const [count, setCount] = useState(null);
  const save = useSave(onDone);
  const parts = (list) => { const out = []; for (let i = 0; i < list.length; i += 5000) out.push(list.slice(i, i + 5000)); return out; };
  useEffect(() => {
    (async () => {
      try {
        const rs = []; for (const p of parts(ids)) rs.push(await API.ccPush({ ids: p, dryRun: true }));
        setCount(rs.reduce((t, r) => ({ sent: t.sent + r.sent, noEmail: t.noEmail + r.noEmail, noLink: t.noLink + r.noLink }), { sent: 0, noEmail: 0, noLink: 0 }));
      } catch (err) { save.setError(err.message); }
    })();
  }, []);
  const run = () => save.run(async () => { let sent = 0, listName = ""; for (const p of parts(ids)) { const r = await API.ccPush({ ids: p }); sent += r.sent; listName = r.listName || listName; } return { sent, listName }; });
  const n = count ? count.sent : 0;
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Send to Constant Contact" onClose={onClose}
           onSubmit={cc.listId && n ? run : null} submitLabel={`Send ${n} ${n === 1 ? "lead" : "leads"}`} busy={save.busy} error={save.error}>
      <div className="form-section">
        {!cc.listId ? <div className="banner" role="alert">No list is picked yet. An Admin picks it in Settings → Constant Contact.</div> : <p>To the list <b>{cc.listName || "picked in Settings"}</b>. Constant Contact sends each one the offer email with their own link.</p>}
        {!count ? <p className="muted">Counting…</p> : (
          <>
            <p style={{ marginTop: 10 }}><b>{n ? `${n} ${n === 1 ? "lead" : "leads"} to send` : "Nothing to send"}</b> of the {ids.length} selected.</p>
            {count.noEmail || count.noLink ? (
              <ul className="dim" style={{ fontSize: 13, margin: "6px 0 0 18px" }}>
                {count.noLink ? <li>{count.noLink} with no open offer link (make links first)</li> : null}
                {count.noEmail ? <li>{count.noEmail} with no email address</li> : null}
              </ul>
            ) : null}
            <p className="hint" style={{ marginTop: 10 }}>Someone already in the list is updated with their newest link, not added twice.</p>
          </>
        )}
      </div>
    </Sheet>
  );
}

// ── Bulk offer links ────────────────────────────────────────────────────────
function BulkOffersSheet({ ids, leads, toast, onClose, onDone, onEditOffer }) {
  const [s, setS] = useState(savedOfferSettings);
  const [existing, setExisting] = useState("skip");
  const [plan, setPlan] = useState(null);
  const [result, setResult] = useState(null);
  const [progress, setProgress] = useState("");
  const save = useSave(null);
  useEffect(() => { setPlan(null); }, [JSON.stringify(s), existing]);
  const settings = () => ({ ...s, pct: Number(s.pct), round: Number(s.round), min: fmt.parseMoney(s.min), days: Number(s.days), tmin: Number(s.tmin), tmax: Number(s.tmax), tstep: Number(s.tstep), fmin: Number(s.fmin), fmax: Number(s.fmax), term: Number(s.term), factor: Number(s.factor) });
  const submit = async () => {
    const err = offerSettingsError(s); if (err) return save.setError(err);
    save.setError("");
    if (!plan) { try { setPlan(await API.bulkOffers({ ids, settings: settings(), existing, dryRun: true })); } catch (e) { save.setError(e.message); } return; }
    save.run(async () => {
      const parts = await inParts(ids, PARTS.offers, (part) => API.bulkOffers({ ids: part, settings: settings(), existing }), (n, of) => setProgress(`Making links ${n} of ${of}…`));
      // One CSV: the first part's column names, then every part's rows.
      const body = (csv) => { const t = String(csv || "").replace(/(\r?\n)+$/, ""); const nl = t.indexOf("\n"); return { head: nl < 0 ? t : t.slice(0, nl).replace(/\r$/, ""), rows: nl < 0 ? "" : t.slice(nl + 1) }; };
      const csvRows = parts.map(r => body(r.csv).rows).filter(Boolean);
      const r = { made: parts.reduce((t, x) => t + x.made, 0), skipped: parts.flatMap(x => x.skipped), rows: parts.flatMap(x => x.rows), csv: [body(parts[0].csv).head, ...csvRows].join("\r\n") + "\r\n" };
      rememberOfferSettings(s); setResult(r); onDone && onDone();
    });
  };
  const byId = new Map((leads || []).map(l => [l.id, l]));
  if (result) {
    return (
      <Sheet eyebrow="Offer links" title={`${result.made} ${result.made === 1 ? "link" : "links"} ready`} onClose={onClose}>
        <div className="form-section">
          <div className="section-bar"><span className="label">Links</span><button type="button" className="btn btn-primary btn-sm" onClick={() => API.saveText(result.csv, `offer-links-${fmt.localDay(new Date().toISOString())}.csv`)}><Icon name="download" size={14} />Download CSV</button></div>
          <p className="hint">Business, first and last name, phone, email, the offer, payment from, good until and the link, plus ID and Extra. Edit any one offer below; its link stays the same.</p>
          <div className="card card-flush" style={{ marginTop: 10 }}>
            {result.rows.map(r => { const l = byId.get(r.leadId) || {}; return (
              <div className="row" key={r.slug}>
                <div className="grow"><div className="title">{leadBiz(l)}</div><div className="line tnum">Up to {fmt.money(r.top)} · {r.terms} · {r.factor} · {r.remit} · from {r.paymentFrom}</div></div>
                <div className="row-actions"><button type="button" className="btn btn-sm" onClick={() => onEditOffer(r.leadId)}>Edit</button></div>
              </div>
            ); })}
          </div>
          {result.skipped.length ? <><div className="section-bar" style={{ marginTop: 16 }}><span className="label">Skipped ({result.skipped.length})</span></div>
            <div className="card card-flush">{result.skipped.slice(0, 60).map(x => <div className="row" key={x.id}><div className="grow"><div className="title">{x.biz}</div><div className="line">{x.reason}</div></div></div>)}</div></> : null}
        </div>
      </Sheet>
    );
  }
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Make offer links" onClose={onClose} onSubmit={submit}
           submitLabel={plan ? `Create ${plan.make} ${plan.make === 1 ? "link" : "links"}` : "Preview"} busyLabel={progress || undefined} busy={save.busy} error={save.error}>
      <div className="form-section"><OfferSettingsForm s={s} set={setS} /></div>
      <div className="form-section">
        <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 (close the old link)" }]} />
      </div>
      {plan ? (
        <div className="form-section">
          <div className="stats three card">
            <div className="stat"><div className="label">Links to create</div><div className="v tnum">{plan.make}</div></div>
            <div className="stat"><div className="label">Skipped</div><div className="v tnum">{plan.total - plan.make}</div></div>
            <div className="stat"><div className="label">Why skipped</div><div className="dim" style={{ fontSize: 12.5, marginTop: 6 }}>{[plan.noRevenue ? `${plan.noRevenue} no revenue` : "", plan.under ? `${plan.under} under the minimum` : "", plan.hasLink ? `${plan.hasLink} have a link` : ""].filter(Boolean).join(" · ") || "None"}</div></div>
          </div>
        </div>
      ) : null}
    </Sheet>
  );
}

// ── The lead file ───────────────────────────────────────────────────────────
// The Private details card is shared with deals and clients (people.jsx).
function PrivateDetails({ lead, canReveal, onEnter }) {
  return <PrivateDetailsCard kind="lead" id={lead.id} summary={piiSummary.lead(lead)} canReveal={canReveal} onEnter={onEnter} />;
}

function LeadSubmitSheet({ lead, lenders, onClose, onSaved }) {
  const [names, setNames] = useState([""]);
  const [notes, setNotes] = useState("");
  const [dup, setDup] = useState(false);
  const save = useSave(onSaved);
  const submit = () => {
    const list = names.map(x => x.trim()).filter(Boolean);
    if (!list.length) return save.setError("Enter the lender it was sent to.");
    save.run(async () => {
      try { return await API.submitLead(lead.id, { lenders: list, notes, ...(dup ? { allowDuplicate: true } : {}) }); }
      catch (err) { if (err.status === 409 && err.data && err.data.duplicate) { setDup(true); throw Object.assign(new Error(err.message + " Press Submit again to go ahead anyway."), {}); } throw err; }
    });
  };
  return (
    <Sheet eyebrow={leadBiz(lead)} title="Log lender submission" onClose={onClose} onSubmit={submit} submitLabel={dup ? "Submit anyway" : "Submit"} busy={save.busy} error={save.error}>
      <div className="form-section">
        <p className="dim" style={{ marginBottom: 12 }}>Logging the first lender submission makes this lead a deal. Its files, offer links and notes move to the deal.</p>
        {names.map((n, i) => (
          <Field key={i} label={i ? `Lender ${i + 1}` : "Lender"} full>
            <input className="input" list="ef-lenders-lead" value={n} onChange={e => setNames(names.map((x, k) => k === i ? e.target.value : x))} placeholder="Start typing…" autoComplete="off" />
          </Field>
        ))}
        <datalist id="ef-lenders-lead">{(lenders || []).map(l => <option key={l.id} value={l.name} />)}</datalist>
        <button type="button" className="btn btn-sm" onClick={() => setNames([...names, ""])}><Icon name="plus" size={14} />Another lender</button>
        <Field label="Notes" full><textarea className="textarea" value={notes} onChange={e => setNotes(e.target.value)} /></Field>
      </div>
    </Sheet>
  );
}

function LeadEditSheet({ lead, team, user, tagOptions, onClose, onSaved }) {
  const l = lead;
  const isAdmin = !!canOf(user).manage;
  const people = team || [];
  const ownerOptions = (canOf(user).wide || !l.repId ? [{ value: "", label: canOf(user).wide ? "Unassigned (Admins only)" : "Unassigned" }] : []).concat(people.filter(t => t.active !== false || t.id === l.repId).map(t => ({ value: t.id, label: t.name })));
  const [v, set] = useForm({
    biz: l.biz || "", dba: l.dba || "", firstName: l.firstName || "", lastName: l.lastName || (l.firstName ? "" : leadName(l)), phone: l.phone || "", phone2: l.phone2 || "", email: l.email || "",
    address: l.address || "", city: l.city || "", state: l.state || "", zip: l.zip || "", industry: l.industry || "", startDate: l.startDate || "",
    monthlyRevenue: l.monthlyRevenue ? String(l.monthlyRevenue) : "", fundingAmount: l.fundingAmount ? String(l.fundingAmount) : "", source: l.source || "",
    xid: l.xid || "", extra: l.extra || "", note: l.note || "", repId: l.repId || "", tags: l.tags || [],
  });
  const save = useSave(onSaved);
  const submit = () => {
    if (!v.biz.trim() && !v.firstName.trim() && !v.lastName.trim()) return save.setError("Enter a business name or the owner's name.");
    const rev = fmt.parseMoney(v.monthlyRevenue), amt = fmt.parseMoney(v.fundingAmount);
    if (Number.isNaN(rev) || Number.isNaN(amt)) return save.setError("Revenue and amount must be numbers.");
    const body = { ...v, monthlyRevenue: rev || 0, fundingAmount: amt || 0 };
    if ((v.repId || null) === (l.repId || null)) delete body.repId; else body.repId = v.repId || null;
    save.run(() => API.updateLead(l.id, body));
  };
  const f = (k, label, extra = {}) => <Field label={label} full={extra.full} hint={extra.hint}><TextInput value={v[k]} onChange={x => set(k, x)} {...(extra.attrs || {})} /></Field>;
  return (
    <Sheet eyebrow="Lead" title="Edit lead" onClose={onClose} onSubmit={submit} submitLabel="Save lead" busy={save.busy} error={save.error}>
      <div className="form-section">
        <div className="form-grid">
          {f("biz", "Business name", { full: true })}
          {f("dba", "DBA")}{f("industry", "Industry")}
          {f("firstName", "Owner first name")}{f("lastName", "Owner last name")}
          {f("phone", "Phone", { attrs: { type: "tel", inputMode: "tel" } })}{f("phone2", "Phone 2", { attrs: { type: "tel", inputMode: "tel" } })}
          {f("email", "Email", { full: true, attrs: { type: "email" } })}
          {f("address", "Address", { full: true })}
          {f("city", "City")}{f("state", "State")}{f("zip", "ZIP")}{f("startDate", "Business start")}
          {f("monthlyRevenue", "Monthly revenue", { attrs: { inputMode: "decimal", placeholder: "$" } })}{f("fundingAmount", "Amount wanted", { attrs: { inputMode: "decimal", placeholder: "$" } })}
          {f("source", "Source")}
          {canOf(user).wide || ownerOptions.length > 1 ? <Field label="Assigned to"><Select value={v.repId} onChange={x => set("repId", x)} options={ownerOptions} /></Field> : null}
          {f("xid", "ID", { hint: "A spare field for your own use" })}{f("extra", "Extra", { hint: "A spare field for your own use" })}
          <div className="field full"><span className="label">Tags</span><TagPicker value={v.tags} onChange={x => set("tags", x)} options={tagOptions} allowCreate placeholder="Add tags" /></div>
          <Field label="Notes" full><textarea className="textarea" value={v.note} onChange={e => set("note", e.target.value)} /></Field>
        </div>
      </div>
    </Sheet>
  );
}

// The lead's file, laid out like the client file: header with the revenue, tabs, and on wide screens a
// right rail with the actions. The Overview is the statements' picture of the business: deposits and
// positions by month, the current positions and the banking figures.
function LeadFile({ leadId, user, team, office, lenders, toast, confirm, tagOptions, onClose, onChanged, onConverted, focus }) {
  const [data, setData] = useState(null);
  const [offers, setOffers] = useState([]);
  const [error, setError] = useState("");
  const [sheet, setSheet] = useState(null);
  const [note, setNote] = useState("");
  const [tab, setTab] = useState("overview");
  const canWrite = !!canOf(user).write;
  const isAdmin = !!canOf(user).manage;
  const showing = useRef(leadId); showing.current = leadId;
  useEffect(() => { API.markLead(leadId); }, [leadId]);
  const load = useCallback(async () => {
    try {
      const d = await API.lead(leadId); if (showing.current !== leadId) return;
      setData(d); setError("");
      const o = await API.offers(leadId); if (showing.current === leadId) setOffers(o);
    } catch (err) { if (showing.current === leadId) setError(err.status === 404 ? "This lead isn't here any more (it may have become a deal)." : err.message); }
  }, [leadId]);
  useEffect(() => { setData(null); setOffers([]); setError(""); setSheet(null); setTab("overview"); load(); }, [leadId]);
  const lead = data && data.lead;
  // The shared file, offer and statement screens take a deal; a lead is shaped like one for them.
  const holder = lead ? { ...lead, _kind: "lead", contact: leadName(lead), docs: data.documents || [], amount: lead.fundingAmount || 0, status: "new", products: (lead.application && lead.application.products) || [] } : null;
  const statements = useStatements(leadId, holder ? holder.docs.length : 0);
  // When statements finish reading here, the list beside it picks up the new remit and positions.
  const wasReading = useRef(false);
  useEffect(() => { if (wasReading.current && !statements.pending && onChanged) onChanged(); wasReading.current = statements.pending; }, [statements.pending]);
  // Opened to edit its offer (from bulk offer links): the offer links card is brought into view.
  useEffect(() => {
    if (focus !== "offers" || !lead) return;
    const t = setTimeout(() => { const el = document.getElementById("offer-links"); if (el) el.scrollIntoView({ block: "start", behavior: "smooth" }); }, 150);
    return () => clearTimeout(t);
  }, [focus, !!lead, leadId]);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !sheet && !document.querySelector(".scrim") && !/^(INPUT|TEXTAREA|SELECT)$/.test((e.target && e.target.tagName) || "")) onClose(); };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [sheet]);
  const refresh = async () => { await load(); onChanged && onChanged(); };
  const close = () => setSheet(null);
  const repOf = (id) => (team || []).find(t => t.id === id);
  const analyze = async () => {
    try { const r = await API.analyzeAll(leadId); await statements.reload(); toast(r.queued ? `Reading ${r.queued} ${r.queued === 1 ? "statement" : "statements"}` : "Every statement here has been read already"); }
    catch (err) { toast(err.message, true); }
  };
  const addNote = async () => {
    if (!note.trim()) return;
    try { await API.leadNote(leadId, note.trim()); setNote(""); await load(); } catch (err) { toast(err.message, true); }
  };
  const convert = () => confirm({ plain: true, title: "Make this lead a deal now?", confirmLabel: "Convert to deal",
    body: "Normally a lead becomes a deal when its first lender submission is logged. Everything filed under it moves to the deal.",
    onConfirm: async () => {
      try { const deal = await API.convertLead(leadId, {}); toast("Lead converted to a deal"); onConverted(deal); }
      catch (err) { if (err.status === 409 && err.data && err.data.duplicate) { const deal = await API.convertLead(leadId, { allowDuplicate: true }); toast("Lead converted to a deal"); onConverted(deal); } else throw err; }
    } });
  const askDelete = () => confirm({ title: "Delete this lead?", body: `${leadBiz(lead)} will be removed, with its files and offer links.`, confirmLabel: "Delete lead",
    onConfirm: async () => { await API.deleteLead(leadId); toast("Lead deleted"); onChanged && onChanged(); onClose(); } });
  const askDeleteDoc = (f) => confirm({ title: "Delete this file?", body: `${f.name} will be removed from the lead.`, confirmLabel: "Delete file",
    onConfirm: async () => { await API.deleteDocument(f.objectKey); await load(); toast("File deleted"); } });

  if (!lead) {
    return (
      <>
        <section className="file lfile"><div className="tab-body" style={{ display: "grid", placeItems: "center", height: "100%" }}>
          {error ? <Empty title="Couldn't open this lead">{error} <button className="link-btn" onClick={onClose}>Back to the list</button></Empty> : <div className="empty"><p>Loading…</p></div>}
        </div></section>
        <aside className="context" />
      </>
    );
  }

  const hist = lead.history || [];
  const activity = data.activity || [];
  const stFiles = holder.docs.filter(isStatementFile);
  const stData = statements.data;
  const readKeys = stData ? new Set(stData.statements.filter(x => x.status).map(x => x.objectKey)) : null;
  const unread = readKeys ? stFiles.filter(f => !readKeys.has(f.objectKey)).length : 0;
  const reading = stData ? new Set(stData.statements.filter(x => ST_PENDING.includes(x.status)).map(x => x.objectKey)).size : 0;
  const sum = stData && stData.summary;
  const fromStatements = sum && sum.avgMonthlyRevenue != null;
  const rep = repOf(lead.repId);
  const canAnalyze = canWrite && unread > 0 && !(stData && stData.ready === false);
  const tabs = [
    { id: "overview", label: "Overview", short: "Overview" },
    { id: "documents", label: "Documents", short: "Docs", n: holder.docs.length },
    { id: "details", label: "Details", short: "Details" },
    { id: "activity", label: "Activity", short: "Log", n: activity.length },
    { id: "map", label: "Map", short: "Map" },
  ];
  const actions = canWrite ? [
    { label: "Log lender submission", short: "Submit", icon: "offer", run: () => setSheet({ kind: "submit" }) },
    { label: "Offer link", short: "Offer link", icon: "link", run: () => setSheet({ kind: "offer" }) },
    ...(canAnalyze ? [{ label: "Analyze statements", short: "Analyze", icon: "spark", run: analyze }] : []),
    { label: "Upload doc", short: "Upload", icon: "upload", run: () => setSheet({ kind: "upload", role: "statement" }) },
    { label: "Edit lead", short: "Edit", icon: "edit", run: () => setSheet({ kind: "edit" }) },
  ] : [];

  return (
    <>
      <section className="file lfile">
        <div className="file-head">
          <button className="back" onClick={onClose}><Icon name="back" size={15} />All leads</button>
          <div className="file-top">
            <div style={{ minWidth: 0, display: "flex", gap: 14, alignItems: "center" }}>
              <Avatar name={leadBiz(lead)} size="lg" />
              <div style={{ minWidth: 0 }}>
                <div className="file-id">
                  <span className="label">Lead · {lead.id}</span>
                  {lead.reapplied ? <span className="re-badge">Re-applied ×{lead.reapplied + 1}</span> : null}
                  {lead.existing ? <span className="re-badge">Already a {lead.existing.kind === "client" || lead.existing.kind === "client-deal" ? "client" : "deal"}</span> : null}
                </div>
                <div className="name-row"><h1 className="file-name">{leadBiz(lead)}</h1><RemitBadge share={remitShare(sum)} withPlatforms={remitShareAll(sum)} /><BankLabels reverse={bankLabelsOf(sum).reverse} debtSettlement={bankLabelsOf(sum).debtSettlement} highRisk={!!(stData && stData.callNotes && stData.callNotes.redFlagSummary && stData.callNotes.redFlagSummary.highRisk)} /></div>
                <div className="file-contact">
                  {leadName(lead) ? <span>{leadName(lead)}</span> : null}
                  {lead.phone ? <a href={`tel:${lead.phone}`}>{lead.phone}</a> : null}
                  {lead.email ? <a href={`mailto:${lead.email}`}>{lead.email}</a> : null}
                  {!leadName(lead) && !lead.phone && !lead.email ? <span className="muted">No contact details on file</span> : null}
                  {lead.geo && lead.geo.lat != null ? <ShowOnMap kind="lead" id={lead.id} /> : null}
                </div>
              </div>
            </div>
            <div className="file-money">
              <div className="label">{fromStatements ? "Avg monthly sales" : "Monthly revenue"}</div>
              <div className="amount tnum">{fromStatements || lead.monthlyRevenue ? <><span className="cur">$</span>{Math.round(fromStatements ? sum.avgMonthlyRevenue : fmt.num(lead.monthlyRevenue)).toLocaleString("en-US")}</> : <span className="muted">—</span>}</div>
              <div className="facts tnum">
                {fromStatements ? <span>{sum.months} {sum.months === 1 ? "month" : "months"} of statements</span> : lead.monthlyRevenue ? <span>As stated</span> : null}
                {fromStatements && lead.monthlyRevenue ? <span>Stated <b>{fmt.moneyShort(lead.monthlyRevenue)}</b></span> : null}
                {lead.fundingAmount ? <span>Wants <b>{fmt.moneyShort(lead.fundingAmount)}</b></span> : null}
                {sum ? <span style={{ color: (sum.positions || []).length ? "var(--st-declined)" : undefined }}>Positions <b style={{ color: "inherit" }}>{(sum.positions || []).length}</b></span> : null}
              </div>
            </div>
          </div>
          {actions.length ? (
            <div className="file-actions" style={{ "--n": actions.length }}>
              {actions.map((a, i) => <button key={a.label} className={`btn btn-sm${i === 0 ? " btn-primary" : ""}`} onClick={a.run} aria-label={a.label}><Icon name={a.icon} size={15} /><span className="lg">{a.label}</span><span className="sm" aria-hidden="true">{a.short}</span></button>)}
            </div>
          ) : null}
          <CallNotesStrip notes={stData && stData.callNotes} />
        </div>

        <div className="tabs" role="tablist">
          {tabs.map(t => <button key={t.id} role="tab" className="tab" aria-label={t.n ? `${t.label} ${t.n}` : t.label} aria-selected={tab === t.id} onClick={() => setTab(t.id)}><span className="lg">{t.label}</span><span className="sm" aria-hidden="true">{t.short}</span>{t.n ? <span className="n tnum">{t.n}</span> : null}</button>)}
        </div>

        <div className="tab-body scroll" role="tabpanel">
          {error ? <div className="banner" role="alert">{error} <button className="link-btn" onClick={load}>Try again</button></div> : null}
          {tab === "overview" ? (
            <>
              {lead.reapplied ? <div className="banner info"><b>Re-applied {lead.reapplied} {lead.reapplied === 1 ? "time" : "times"}.</b> Last {fmt.when(lead.lastAppliedAt)}. New details filled what was blank; anything changed is under Details.</div> : null}
              {lead.existing ? <div className="banner info">This business is already in the CRM as a {lead.existing.kind === "client" || lead.existing.kind === "client-deal" ? "client" : "deal"}{lead.existing.biz ? ` (${lead.existing.biz})` : ""}.{!lead.existing.id ? null : lead.existing.kind !== "client" ? <> <button className="link-btn" onClick={() => go("deals", lead.existing.id)}>Open it</button></> : <> <button className="link-btn" onClick={() => go("clients", lead.existing.id)}>Open the client</button></>}</div> : null}
              {lead.fieldIssues && lead.fieldIssues.length ? <div className="banner">Left blank on import: {lead.fieldIssues.join("; ")}.</div> : null}
              {reading ? <div className="banner info working"><span className="spinner" />Reading {reading} {reading === 1 ? "statement" : "statements"}. The chart and figures fill in as each one finishes.</div> : null}
              {canAnalyze && !reading ? (
                <div className="analyze-bar">
                  <div><b>{unread === stFiles.length ? `${unread} bank ${unread === 1 ? "statement" : "statements"} stored, not read yet.` : `${unread} of ${stFiles.length} statements not read yet.`}</b> Reading costs a little each time, so it waits until you are working this lead.</div>
                  <button className="btn btn-sm btn-primary" onClick={analyze}><Icon name="spark" size={14} />Analyze statements</button>
                </div>
              ) : null}
              {!stFiles.length ? <div className="banner">No bank statements on this lead.{canWrite ? <> <button className="link-btn" onClick={() => setSheet({ kind: "upload", role: "statement" })}>Upload one</button></> : null}</div> : null}
              <StatementOverview statements={statements} onOpen={() => setTab("documents")} />
              {stFiles.length && !sum && !reading && !canAnalyze && stData ? <div className="card card-body dim" style={{ fontSize: 13 }}>None of the statements could be read into figures. See Documents for why.</div> : null}
              <OfferLinksCard deal={holder} offers={offers} canWrite={canWrite} isAdmin={isAdmin} office={office} toast={toast} confirm={confirm}
                onNew={() => setSheet({ kind: "offer" })} onEdit={(o) => setSheet({ kind: "offer", offer: o })} onChanged={load} onApply={() => {}} />
            </>
          ) : null}

          {tab === "documents" ? <DocumentsTab deal={holder} canWrite={canWrite} onUpload={role => setSheet({ kind: "upload", role })} onDelete={askDeleteDoc} statements={statements} toast={toast} /> : null}

          {tab === "details" ? (
            <>
              <div className="card card-flush">
                <div className="card-head"><span className="label">Details</span>{canWrite ? <button className="link-btn" onClick={() => setSheet({ kind: "edit" })}>Edit</button> : null}</div>
                <dl>
                  {[["Owner", leadName(lead)], ["Phone", lead.phone], ["Phone 2", lead.phone2], ["Email", lead.email], ["Address", [lead.address, lead.city, lead.state, lead.zip].filter(Boolean).join(", ") + (lead.addressFrom === "statement" && (lead.address || lead.city) ? " · from bank statement" : "")],
                    ["Industry", lead.industry], ["Business start", lead.startDate], ["Monthly revenue", lead.monthlyRevenue ? fmt.money(lead.monthlyRevenue) : ""], ["Amount wanted", lead.fundingAmount ? fmt.money(lead.fundingAmount) : ""],
                    ["Assigned to", lead.repId ? (rep && rep.name) || "Someone off the team" : "Unassigned"], ["Source", lead.source], ["ID", lead.xid], ["Extra", lead.extra]]
                    .filter(([, x]) => x).map(([k, x]) => <div className="kv" key={k}><dt>{k}</dt><dd>{x}</dd></div>)}
                  {Object.entries(lead.more || {}).map(([k, x]) => <div className="kv" key={"more-" + k}><dt>{k}</dt><dd>{String(x)}</dd></div>)}
                  <div className="kv"><dt>Tags</dt><dd>{(lead.tags || []).length ? <span className="chips" style={{ marginTop: 0 }}>{lead.tags.map(t => <span className="chip" key={t}>{t}</span>)}</span> : <span className="muted">None</span>}</dd></div>
                </dl>
                {lead.note ? <div className="card-body notes" style={{ whiteSpace: "pre-wrap", fontSize: 13.5 }}>{lead.note}</div> : null}
              </div>
              <PrivateDetails lead={lead} canReveal={!!canOf(user).pii} onEnter={() => setSheet({ kind: "pii" })} />
              {lead.application && lead.application.business ? <div className="card card-flush"><div className="card-head"><span className="label">Website application</span></div><div className="card-body"><ApplicationDetails app={lead.application} hidePrivate canReveal={!!canOf(user).pii} reveal={() => API.leadSsn(lead.id)} /></div></div> : null}
              <div className="section-bar"><span className="label">Applications</span></div>
              <div className="card card-flush">
                {hist.length ? hist.slice().reverse().map((h, i) => (
                  <div className="row" key={i}>
                    <span className={`hicon ${h.kind === "reapplied" ? "t-warn" : ""}`}><Icon name={h.kind === "reapplied" ? "swap" : "funnel"} size={14} /></span>
                    <div className="grow"><div className="title">{h.kind === "reapplied" ? "Re-applied" : "Applied"}{h.on ? <span className="chip">matched on {h.on}</span> : null}</div>
                      <div className="line">{h.src || ""}{h.changed ? ` · changed: ${Object.keys(h.changed).join(", ")}` : ""}</div>
                      {h.offered || h.otherSsnLast4 !== undefined || h.otherEinLast4 !== undefined ? <div className="line warn-text">Gave different details, not saved over what was on file: {[...Object.entries(h.offered || {}).map(([k, v]) => `${{ biz: "business", name: "name", phone: "phone", email: "email", fundingAmount: "amount" }[k] || k} ${k === "fundingAmount" ? fmt.money(v) : v}`), h.otherSsnLast4 !== undefined ? `SSN ending ${h.otherSsnLast4}` : "", h.otherEinLast4 !== undefined ? `EIN ending ${h.otherEinLast4}` : ""].filter(Boolean).join(" · ")}</div> : null}
                      <div className="when">{fmt.when(h.at)}</div></div>
                  </div>
                )) : <div className="card-body dim">Created {fmt.when(lead.createdAt)}.</div>}
              </div>
              {canWrite ? (
                <div className="card"><div className="card-head"><span className="label">Lead tools</span></div>
                  <div className="card-body" style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                    {isAdmin ? <button className="btn btn-sm" onClick={convert}><Icon name="swap" size={14} />Convert to deal</button> : null}
                    <button className="btn btn-sm btn-quiet btn-danger" style={{ marginLeft: "auto" }} onClick={askDelete}><Icon name="trash" size={14} />Delete lead</button>
                  </div></div>
              ) : null}
            </>
          ) : null}

          {tab === "map" ? <FileMapTab kind="lead" id={lead.id} geo={lead.geo} gplace={lead.gplace} biz={leadBiz(lead)} /> : null}

          {tab === "activity" ? (
            <>
              {canWrite ? <div className="note-add"><input className="input" value={note} onChange={e => setNote(e.target.value)} placeholder="Add a note (who you spoke to, what's next)" onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addNote(); } }} /><button className="btn" onClick={addNote}>Add</button></div> : null}
              <div className="card card-flush">
                {activity.length ? activity.map((a, i) => (
                  <div className="row" key={i}><span className="act-icon"><Icon name={a.tag === "OFFER" ? "link" : a.tag === "DOC" ? "file" : a.tag === "SSN" ? "eye" : "note"} size={14} /></span>
                    <div className="grow"><div className="line" style={{ color: "var(--ink)" }}>{a.text}</div><div className="when">{fmt.when(a.ts)}</div></div></div>
                )) : <div className="card-body dim">Nothing logged yet.</div>}
              </div>
            </>
          ) : null}
        </div>
      </section>

      <aside className="context scroll">
        {actions.length ? <div><span className="label">Actions</span><div className="action-grid">{actions.map(a => <button key={a.label} className="action" onClick={a.run}><Icon name={a.icon} />{a.label}</button>)}</div></div> : null}
        <CallNotesPanel notes={stData && stData.callNotes} />
        <div>
          <span className="label">Stage</span>
          <div className="stage-line"><span className="pill s-new">Lead</span><Icon name="chev" size={12} /><span className="pill plain muted-pill">Deal</span><Icon name="chev" size={12} /><span className="pill plain muted-pill">Client</span></div>
          <div className="dim" style={{ fontSize: 12.5 }}>Becomes a deal when its first lender submission is logged.</div>
        </div>
        <div>
          <span className="label">Bank statements</span>
          <div className="mini"><div className="mini-row">
            {!stFiles.length ? <span className="muted">None on file</span>
              : <>{stFiles.length - unread} of {stFiles.length} read{reading ? ` · reading ${reading}` : ""}{stData && stData.spent ? <div className="when">Reading cost {readCost(stData.spent)}</div> : null}{sum && sum.needReview ? <div className="when">{sum.needReview} {sum.needReview === 1 ? "doesn't" : "don't"} tie out</div> : null}</>}
          </div></div>
        </div>
        <div>
          <span className="label">Assigned rep</span>
          <div className="mini"><div className="mini-row" style={{ display: "flex", alignItems: "center", gap: 10 }}>
            {rep ? <><Avatar name={rep.name} size="sm" /><div><b>{rep.name}</b><div className="when">{rep.role}</div></div></> : <span className="muted">{lead.repId ? "Someone outside your team" : "Unassigned · Admins only"}</span>}
          </div></div>
        </div>
        {(lead.tags || []).length ? <div><span className="label">Tags</span><span className="chips" style={{ marginTop: 0 }}>{lead.tags.map(t => <span className="chip" key={t}>{t}</span>)}</span></div> : null}
        <div>
          <span className="label">Recent activity</span>
          <div className="mini">
            {activity.length ? activity.slice(0, 5).map((a, i) => <div className="mini-row" key={i}>{a.text}<div className="when">{fmt.when(a.ts)}</div></div>)
              : <div className="mini-row muted">Nothing logged yet.</div>}
          </div>
        </div>
      </aside>

      {sheet && sheet.kind === "submit" ? <LeadSubmitSheet lead={lead} lenders={lenders} onClose={close} onSaved={(r) => { close(); toast("Submitted: this lead is now a deal"); onConverted(r.deal); }} /> : null}
      {sheet && sheet.kind === "pii" ? <EnterPiiSheet kind="lead" id={leadId} biz={leadBiz(lead)} has={(() => { const x = piiSummary.lead(lead); return { ssn: !!x.ssnLast4, ein: !!x.einLast4, dob: x.dob }; })()} onClose={close} onSaved={() => { close(); load(); toast("Saved encrypted"); }} /> : null}
      {sheet && sheet.kind === "edit" ? <LeadEditSheet lead={lead} team={team} user={user} tagOptions={tagOptions} onClose={close} onSaved={() => { close(); refresh(); toast("Lead saved"); }} /> : null}
      {sheet && sheet.kind === "offer" ? <OfferSheet deal={holder} offer={sheet.offer} subs={[]} user={user} office={office} onClose={close} onSaved={() => { close(); load(); toast(sheet.offer ? "Offer saved. Same link." : "Offer link created"); }} /> : null}
      {sheet && sheet.kind === "upload" ? <UploadSheet deal={holder} role={sheet.role} onClose={close} onSaved={() => { close(); load(); toast("Uploaded"); }} /> : null}
    </>
  );
}

// The list folded down beside an open lead: business, owner and revenue, with the same search and filters.
function LeadRail({ rows, total, loading, selectedId, onSelect, q, setQ, filtered, onClear, onFull }) {
  const [limit, setLimit] = useState(200);
  const list = useRef(null);
  // Keep the open lead in view when it is opened from the full list or by link.
  useEffect(() => {
    const el = list.current && list.current.querySelector('[aria-current="true"]');
    if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" });
  }, [selectedId]);
  const at = rows.findIndex(l => l.id === selectedId);
  const shown = rows.slice(0, Math.max(limit, at + 1));
  return (
    <aside className="rail lead-rail">
      <div className="rail-head">
        <div className="rail-title">
          <div><div className="label tnum">{filtered ? `${rows.length.toLocaleString("en-US")} of ${total.toLocaleString("en-US")}` : `${rows.length.toLocaleString("en-US")} leads`}</div><h2>Leads</h2></div>
          <span className="rail-acts"><button className="btn btn-sm" onClick={onFull} title="Back to the full list (Esc)"><Icon name="back" size={14} />Full list</button></span>
        </div>
        <div className="search"><Icon name="search" size={15} /><input type="search" value={q} onChange={e => setQ(e.target.value)} placeholder="Search leads" aria-label="Search leads" /></div>
        {filtered ? <div className="filters"><span className="dim" style={{ fontSize: 12 }}>Filtered</span><button className="link-btn" style={{ fontSize: 12 }} onClick={onClear}>Clear filters</button></div> : null}
      </div>
      <div className="rail-list scroll" ref={list}>
        {!rows.length ? <div className="empty"><p>{loading ? "Loading leads…" : "No leads match."}</p></div> : shown.map(l => (
          <button key={l.id} className="deal-row lead-row" aria-current={l.id === selectedId} onClick={() => onSelect(l.id)}>
            <Avatar name={leadBiz(l)} />
            <span className="main">
              <span className="name" style={{ display: "block" }}>{leadBiz(l)}</span>
              <span className="sub" style={{ display: "block" }}>{leadName(l) || <span className="muted">No owner name</span>}</span>
              {l.bank && ((l.bank.reverse || []).length || l.bank.debtSettlement) ? <span className="meta rail-labels"><BankLabels reverse={l.bank.reverse} debtSettlement={l.bank.debtSettlement} /></span> : null}
              {l.offer && l.offer.state === "open" && l.offer.lastAt ? <span className="meta"><SeenChip at={l.offer.lastAt} via={l.offer.via} views={l.offer.views} /></span> : null}
            </span>
            <span className="rev tnum">{leadRevenue(l) ? fmt.moneyShort(leadRevenue(l)) : ""}{l.bank && l.bank.share !== null && l.bank.share !== undefined ? <span className={`rail-remit remit-${remitBand(l.bank.share)}`} title={`${l.bank.share}% remit · ${l.bank.positions} ${l.bank.positions === 1 ? "position" : "positions"}`}>{l.bank.share}%</span> : <StatementState l={l} rail />}</span>
          </button>
        ))}
        {rows.length > shown.length ? <div className="card-foot"><button className="btn btn-sm" onClick={() => setLimit(shown.length + 400)}>Show more ({rows.length - shown.length})</button></div> : null}
      </div>
    </aside>
  );
}

// ── Leads ───────────────────────────────────────────────────────────────────
function LeadsView({ user, team, office, lenders, toast, confirm, selectedId, select, onConverted }) {
  const saved = useRef(readSaved()).current;
  const [leads, setLeads] = useState(null);
  const [error, setError] = useState("");
  const [q, setQ] = useState("");
  const [tagFilter, setTagFilter] = useState(saved.tags || []);
  const [extraFilter, setExtraFilter] = useState(saved.extra || "");
  const [show, setShow] = useState(saved.show || "all");
  // Who the leads are assigned to: "" anyone, "me", "none" (unassigned), or a rep's id. Only for those who see others' leads.
  const [who, setWho] = useState(user.sees !== "own" && typeof saved.who === "string" ? saved.who : "");
  // Revenue range (stated or from the statements), leverage band, positions range. An older saved "minRev" becomes the revenue minimum.
  const [rev, setRev] = useState(() => (saved.rev && typeof saved.rev === "object" ? { min: String(saved.rev.min || ""), max: String(saved.rev.max || "") } : { min: saved.minRev ? String(saved.minRev) : "", max: "" }));
  const [revBasis, setRevBasis] = useState(saved.revBasis === "verified" ? "verified" : "stated");
  const [lev, setLev] = useState(LEVERAGE_FILTERS.some(f => f[0] === saved.lev) ? saved.lev : "");
  const [pos, setPos] = useState(saved.pos && typeof saved.pos === "object" ? { min: String(saved.pos.min || ""), max: String(saved.pos.max || "") } : { min: "", max: "" });
  const [sort, setSort] = useState(saved.sort && LEAD_SORTS[saved.sort.key] && (saved.sort.dir === "asc" || saved.sort.dir === "desc") && (saved.sort.key !== "rep" || user.sees !== "own") ? saved.sort : { key: "recent", dir: "desc" });
  const [tagOptions, setTagOptions] = useState([]);
  const [picked, setPicked] = useState(new Set());
  const [sheet, setSheet] = useState(null);
  const [limit, setLimit] = useState(200);
  const canWrite = !!canOf(user).write;
  const isAdmin = !!canOf(user).manage;
  const showOwner = user.sees !== "own";
  const repName = (id) => { const t = (team || []).find(x => x.id === id); return t ? t.name : id ? "Someone off the team" : null; };

  const load = useCallback(async () => {
    try { const [l, t] = await Promise.all([API.leads(), API.tags().catch(() => [])]); setLeads(l); setTagOptions(t); setError(""); }
    catch (err) { if (err.status !== 401) setError(err.message); }
  }, []);
  useEffect(() => { load(); }, []);
  // A statement rule added or removed (rules.jsx): every lead's leverage and positions are worked out again over the
  // next minute or two, so the list looks again now, and twice more while that finishes.
  useEffect(() => {
    let timers = [];
    const on = () => { timers.forEach(clearTimeout); timers = [1500, 20000, 90000].map(ms => setTimeout(load, ms)); };
    window.addEventListener("ef:statements-changed", on);
    return () => { window.removeEventListener("ef:statements-changed", on); timers.forEach(clearTimeout); };
  }, [load]);
  // While statements are being read on any lead, the list checks again every 30 seconds (only while the tab is in view),
  // so "Reading…" turns into the leverage and positions without a refresh.
  const anyReading = (leads || []).some(leadReading);
  useEffect(() => {
    if (!anyReading) return;
    const t = setInterval(() => { if (document.visibilityState === "visible") load(); }, 30000);
    return () => clearInterval(t);
  }, [anyReading, load]);
  useEffect(() => { writeSaved({ tags: tagFilter, extra: extraFilter, show, who, sort, rev, revBasis, lev, pos }); setLimit(200); }, [tagFilter.join("|"), extraFilter, show, who, sort.key, sort.dir, rev.min, rev.max, revBasis, lev, pos.min, pos.max]);

  const needle = q.trim().toLowerCase();
  const hasTags = (l) => tagFilter.every(t => (l.tags || []).some(x => x.toLowerCase() === t.toLowerCase()));
  const SHOW = {
    all: () => true, unassigned: (l) => !l.repId, reapplied: (l) => (l.reapplied || 0) > 0, "no-statements": (l) => l.files && l.files.files > 0 && !l.files.statements,
    reverse: (l) => !!(l.bank && (l.bank.reverse || []).length), "debt-settlement": (l) => !!(l.bank && l.bank.debtSettlement),
    "high-risk": (l) => !!(l.bank && l.bank.redFlagSummary && l.bank.redFlagSummary.highRisk),
    "offer-opened": (l) => l.offer && l.offer.opened && l.offer.state === "open", "offer-unopened": (l) => l.offer && !l.offer.opened && l.offer.state === "open", "offer-accepted": (l) => l.offer && l.offer.state === "accepted", "no-offer": (l) => !l.offer || l.offer.state !== "open",
  };
  const levOk = levTest(lev);
  const whoOk = (l) => (!who ? true : who === "me" ? l.repId === user.id : who === "none" ? !l.repId : l.repId === who);
  const filteredRows = (leads || []).filter(l => hasTags(l) && inRange(revenueOn(l, revBasis), rev) && levOk(l) && inRange(positionsOf(l), pos) && (!extraFilter || String(l.extra || "").toLowerCase() === extraFilter.toLowerCase()) && whoOk(l) && (SHOW[show] || SHOW.all)(l)
    && matches(needle, leadBiz(l), leadName(l), l.phone, l.email, l.source, l.extra, l.xid, (l.tags || []).join(" "), Object.values(l.more || {}).join(" ")));
  // Revenue sorts by the same figure the revenue filter goes by (Stated, or Verified from the statements).
  const rows = sortLeads(filteredRows, sort.key, sort.dir, sort.key === "rep" ? (l) => (l.repId ? (repName(l.repId) || "~").toLowerCase() : null) : sort.key === "revenue" ? (l) => revenueOn(l, revBasis) : null);
  const sortBy = (key) => setSort(s => (s.key === key ? { key, dir: s.dir === "asc" ? "desc" : "asc" } : { key, dir: LEAD_SORTS[key].dir }));
  const th = (key, label, cls) => (
    <th className={`${cls || ""} sortable`.trim()} aria-sort={sort.key === key ? (sort.dir === "asc" ? "ascending" : "descending") : "none"}>
      <button type="button" className="th-sort" onClick={() => sortBy(key)} title={`Sort by ${LEAD_SORTS[key].label.toLowerCase()}`}>{label}<span className="sort-arrow" aria-hidden="true">{sort.key === key ? (sort.dir === "asc" ? "▲" : "▼") : ""}</span></button>
    </th>
  );
  const extras = [...new Set((leads || []).map(l => l.extra).filter(Boolean))].sort();
  const counts = { reapplied: (leads || []).filter(SHOW.reapplied).length, unassigned: (leads || []).filter(SHOW.unassigned).length, na: (leads || []).filter(leadNotAnalyzed).length, reading: (leads || []).filter(leadReading).length };
  const withOffers = rows.filter(l => l.offer);
  const filtered = tagFilter.length || extraFilter || show !== "all" || who || needle || rangeOn(rev) || lev || rangeOn(pos);
  const pickedRows = (leads || []).filter(l => picked.has(l.id));
  const allShownPicked = rows.length > 0 && rows.every(l => picked.has(l.id));
  const togglePick = (id) => setPicked(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; });
  const pickAll = () => setPicked(allShownPicked ? new Set() : new Set(rows.map(l => l.id)));
  const after = async (msg) => { setSheet(null); await load(); if (msg) toast(msg); };
  const clearFilters = () => { setTagFilter([]); setExtraFilter(""); setShow("all"); setWho(""); setQ(""); setRev({ min: "", max: "" }); setLev(""); setPos({ min: "", max: "" }); };
  // Opening a lead folds the list down to a rail beside it: on a wide screen the other columns fade off to the
  // right first. Coming back, they fade in again.
  const [leaving, setLeaving] = useState(false);
  const [focus, setFocus] = useState(null);
  const [returning, setReturning] = useState(false);
  const animate = () => window.matchMedia("(min-width: 901px)").matches && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  // Where the list was scrolled, and the last lead opened, so coming back lands in the same place.
  const mainRef = useRef(null);
  const lastOpen = useRef({ top: 0, id: null });
  React.useLayoutEffect(() => {
    const el = mainRef.current;
    if (selectedId || !el || !lastOpen.current.id) return;
    el.scrollTop = lastOpen.current.top;
    const row = el.querySelector(`tr[data-lead="${CSS.escape(lastOpen.current.id)}"]`);
    if (row) { const r = row.getBoundingClientRect(), box = el.getBoundingClientRect(); if (r.top < box.top || r.bottom > box.bottom) row.scrollIntoView({ block: "center" }); }
  }, [selectedId, !!leads]);
  useEffect(() => { if (selectedId) lastOpen.current.id = selectedId; }, [selectedId]);
  const pickFromRail = (id) => { lastOpen.current.id = id; setFocus(null); select(id); };
  // The fade before opening: one at a time, and dropped if the Leads screen is left meanwhile.
  const fadeTimer = useRef(null);
  useEffect(() => () => clearTimeout(fadeTimer.current), []);
  const openLead = (id) => {
    if (fadeTimer.current) return;
    lastOpen.current = { top: mainRef.current ? mainRef.current.scrollTop : 0, id };
    if (selectedId || !animate()) return select(id);
    setLeaving(true);
    fadeTimer.current = setTimeout(() => { fadeTimer.current = null; select(id); }, 230);
  };
  useEffect(() => { if (selectedId) setLeaving(false); }, [selectedId]);
  const closeLead = () => { setFocus(null); if (animate()) { setReturning(true); setTimeout(() => setReturning(false), 500); } select(null); };
  // A click anywhere on a row opens it, except on its controls, the tick box's cell, or when text is being selected.
  const rowClick = (e, id) => {
    if (e.target.closest("button, a, input, label, select, td.pick")) return;
    const sel = window.getSelection && window.getSelection();
    if (sel && String(sel).trim()) return;
    openLead(id);
  };
  const offerState = (o) => !o ? null : o.state === "accepted" ? <Pill status="funded" label="Accepted" /> : o.state === "open" ? (o.opened ? (o.lastAt ? <SeenChip at={o.lastAt} via={o.via} views={o.views} count /> : <Pill status="submitted" label={`Opened ×${o.views}`} />) : <span className="pill plain">Not opened</span>) : <span className="pill plain">{fmt.title(o.state)}</span>;

  const sheets = (
    <>
      {sheet && sheet.kind === "new" ? <LeadSheet team={team} user={user} onClose={() => setSheet(null)} onSaved={() => after("Lead created")} /> : null}
      {sheet && sheet.kind === "assign" ? <AssignSheet ids={[...picked]} team={team} user={user} onClose={() => setSheet(null)} onDone={(r) => { setPicked(new Set()); after(`${r.changed} ${r.changed === 1 ? "lead" : "leads"} assigned`); }} /> : null}
      {sheet && sheet.kind === "tags" ? <TagSheet ids={[...picked]} tagOptions={tagOptions} onClose={() => setSheet(null)} onDone={(r) => after(`Tags changed on ${r.changed} ${r.changed === 1 ? "lead" : "leads"}`)} /> : null}
      {sheet && sheet.kind === "offers" ? <BulkOffersSheet ids={[...picked]} leads={pickedRows} toast={toast} onClose={() => { setSheet(null); load(); }} onDone={load} onEditOffer={(id) => { setSheet(null); setFocus({ id, what: "offers" }); lastOpen.current = { top: mainRef.current ? mainRef.current.scrollTop : 0, id }; select(id); }} /> : null}
      {sheet && sheet.kind === "export" ? <ExportSheet ids={sheet.ids} what="leads" tagOptions={tagOptions} onClose={() => setSheet(null)} toast={toast} /> : null}
      {sheet && sheet.kind === "cc" ? <BulkCcSheet ids={[...picked]} cc={office.constantContact} onClose={() => setSheet(null)} onDone={(r) => { setSheet(null); toast(`${r.sent} ${r.sent === 1 ? "lead" : "leads"} sent to Constant Contact${r.listName ? ` (${r.listName})` : ""}`); }} /> : null}
      {sheet && sheet.kind === "analyze" ? <BulkAnalyzeSheet ids={[...picked]} leads={pickedRows} onClose={() => setSheet(null)} onDone={(r) => { setSheet(null); toast(r.queued ? `Reading ${r.queued} ${r.queued === 1 ? "statement" : "statements"} on ${r.withStatements} ${r.withStatements === 1 ? "lead" : "leads"}. Results appear on each lead as they finish.` : "Every statement on those leads has been read already"); }} /> : null}
      {sheet && sheet.kind === "delete" ? <BulkDeleteSheet ids={[...picked]} onClose={() => setSheet(null)} onDone={(r) => { setPicked(new Set()); after(`${r.deleted} ${r.deleted === 1 ? "lead" : "leads"} deleted`); }} /> : null}
    </>
  );

  if (selectedId) {
    return (
      <main className="workspace leads-ws" data-view="file">
        <LeadRail rows={rows} total={(leads || []).length} loading={leads === null} selectedId={selectedId} onSelect={pickFromRail} q={q} setQ={setQ} filtered={!!filtered} onClear={clearFilters} onFull={closeLead} />
        <LeadFile key={selectedId} leadId={selectedId} user={user} team={team} office={office} lenders={lenders} toast={toast} confirm={confirm} tagOptions={tagOptions}
          onClose={closeLead} onChanged={load} onConverted={(deal) => { load(); onConverted(deal); }} focus={focus && focus.id === selectedId ? focus.what : null} />
        {sheets}
      </main>
    );
  }

  return (
    <main className="workspace single" ref={mainRef}>
      <div className="page leads-page">
        <div className="page-head">
          <div>
            <h1>Leads</h1>
            <p>{isAdmin ? "New enquiries and imported lists. Unassigned leads show for Admins only. A lead becomes a deal when its first lender submission is logged."
              : canOf(user).wide ? "Every lead in the office, unassigned ones too. A lead becomes a deal when its first lender submission is logged."
              : user.sees === "team" ? "Leads assigned to you and your team. A lead becomes a deal when its first lender submission is logged." : "Leads assigned to you. A lead becomes a deal when its first lender submission is logged."}</p>
          </div>
          <div className="page-tools leads-tools">
            <PageSearch value={q} onChange={setQ} placeholder="Search leads" />
            {isAdmin ? <button className="btn" onClick={() => setSheet({ kind: "export" })}><Icon name="download" size={14} />Export</button> : null}
            {canOf(user).import ? <button className="btn" onClick={() => go("import")}><Icon name="upload" size={14} />Import</button> : null}
            {canWrite ? <button className="btn btn-primary" onClick={() => setSheet({ kind: "new" })}><Icon name="plus" size={14} />New lead</button> : null}
          </div>
        </div>
        {error ? <div className="banner" role="alert">{error} <button className="link-btn" onClick={load}>Try again</button></div> : null}

        <div className="lead-filters">
          <TagPicker value={tagFilter} onChange={setTagFilter} options={tagOptions} placeholder="All tags" label="Filter by tags" />
          {extras.length ? <select className="select sm" value={extraFilter} onChange={e => setExtraFilter(e.target.value)} aria-label="Filter by Extra"><option value="">Extra: any</option>{extras.slice(0, 200).map(x => <option key={x} value={x}>{x}</option>)}</select> : null}
          <select className="select sm" value={show} onChange={e => setShow(e.target.value)} aria-label="Show">
            <option value="all">All leads</option>
            <option value="reapplied">Re-applied ({counts.reapplied})</option>
            {showOwner ? <option value="unassigned">Unassigned ({counts.unassigned})</option> : null}
            <option value="offer-opened">Offer opened</option>
            <option value="offer-unopened">Offer not opened</option>
            <option value="offer-accepted">Offer accepted</option>
            <option value="no-offer">No open offer</option>
            <option value="no-statements">Files but no statements</option>
            <option value="reverse">In reverse</option>
            <option value="debt-settlement">Debt settlement</option>
            <option value="high-risk">High risk</option>
          </select>
          {showOwner ? (() => {
            // Assigned to: mine first, then unassigned, then each rep with leads, with how many each has.
            const n = new Map(); for (const l of leads || []) n.set(l.repId || "", (n.get(l.repId || "") || 0) + 1);
            const others = (team || []).filter(t => t.id !== user.id && n.get(t.id)).sort((a, b) => String(a.name).localeCompare(String(b.name)));
            return (
              <select className="select sm" value={who} onChange={e => setWho(e.target.value)} aria-label="Assigned to">
                <option value="">Assigned to: anyone</option>
                <option value="me">Assigned to me ({n.get(user.id) || 0})</option>
                <option value="none">Unassigned ({n.get("") || 0})</option>
                {others.map(t => <option key={t.id} value={t.id}>{t.name} ({n.get(t.id)})</option>)}
              </select>
            );
          })() : null}
          <RangePick label="Revenue" money value={rev} onChange={setRev} basis={revBasis} onBasis={setRevBasis} />
          <select className="select sm" value={lev} onChange={e => setLev(e.target.value)} aria-label="Leverage">
            {LEVERAGE_FILTERS.map(([k, t]) => <option key={k} value={k}>{t}{k === "na" && counts.na ? ` (${counts.na})` : k === "reading" && counts.reading ? ` (${counts.reading})` : ""}</option>)}
          </select>
          <RangePick label="Positions" value={pos} onChange={setPos} />
          <span className="sort-pick">
            <select className="select sm" value={sort.key} onChange={e => setSort({ key: e.target.value, dir: LEAD_SORTS[e.target.value].dir })} aria-label="Sort by">
              {Object.entries(LEAD_SORTS).filter(([k]) => k !== "rep" || showOwner).map(([k, x]) => <option key={k} value={k}>Sort: {x.label}</option>)}
            </select>
            <button type="button" className="btn btn-sm btn-quiet" onClick={() => setSort(s => ({ ...s, dir: s.dir === "asc" ? "desc" : "asc" }))} aria-label={sort.dir === "asc" ? "Lowest first (switch to highest first)" : "Highest first (switch to lowest first)"} title={sort.dir === "asc" ? "Ascending" : "Descending"}>{sort.dir === "asc" ? "▲" : "▼"}</button>
          </span>
          {filtered ? <button className="link-btn" onClick={clearFilters}>Clear filters</button> : null}
          {canWrite && rows.length ? <label className="check pick-all-m"><input type="checkbox" checked={allShownPicked} onChange={pickAll} />Select all {rows.length}</label> : null}
        </div>

        {counts.reapplied && show !== "reapplied" ? <div className="banner info lead-reapplied"><span><b>{counts.reapplied} {counts.reapplied === 1 ? "lead has" : "leads have"} re-applied.</b> Each keeps its history.</span><button className="btn btn-sm" onClick={() => setShow("reapplied")}>Show them</button></div> : null}

        {withOffers.length && filtered ? (
          <div className="card offer-strip">
            <span><b className="tnum">{withOffers.length}</b> offer links</span>
            <span><b className="tnum">{withOffers.filter(l => l.offer.opened).length}</b> opened</span>
            <span><b className="tnum">{withOffers.filter(l => l.offer.state === "accepted").length}</b> accepted</span>
            <span className="muted">{Math.round(withOffers.filter(l => l.offer.opened).length / withOffers.length * 100)}% open rate</span>
            <span className="grow" />
            <button className="btn btn-sm" onClick={() => API.offersCsv({ ids: withOffers.map(l => l.id).join(",") }).catch(err => toast(err.message, true))}><Icon name="download" size={14} />Offer links CSV</button>
            <button className="btn btn-sm" title="For a Constant Contact import: leads with an email and an open offer link" onClick={() => API.constantContactCsv({ ids: withOffers.map(l => l.id).join(",") }).catch(err => toast(err.message, true))}><Icon name="download" size={14} />Constant Contact CSV</button>
          </div>
        ) : null}

        {picked.size && canWrite ? (
          <div className="bulk-bar" role="region" aria-label="Selected leads">
            <b className="tnum">{picked.size} selected</b>
            {picked.size > MAX_PICK ? <span className="bulk-note">Pick {MAX_PICK.toLocaleString("en-US")} or fewer at a time (use a tag or filter to narrow it down).</span> : <>
            <button className="btn btn-sm btn-primary" onClick={() => setSheet({ kind: "assign" })}><Icon name="users" size={14} />Assign</button>
            <button className="btn btn-sm" onClick={() => setSheet({ kind: "tags" })}><Icon name="tag" size={14} />Tags</button>
            <button className="btn btn-sm" onClick={() => setSheet({ kind: "offers" })}><Icon name="link" size={14} />Make offer links</button>
            {office && office.statements ? <button className="btn btn-sm" onClick={() => setSheet({ kind: "analyze" })}><Icon name="spark" size={14} />Analyze statements</button> : null}
            {office && office.constantContact && office.constantContact.connected ? <button className="btn btn-sm" onClick={() => setSheet({ kind: "cc" })}><Icon name="mail" size={14} />Constant Contact</button> : null}
            {isAdmin ? <button className="btn btn-sm" onClick={() => setSheet({ kind: "export", ids: [...picked] })}><Icon name="download" size={14} />Export</button> : null}
            {isAdmin ? <button className="btn btn-sm btn-danger" onClick={() => setSheet({ kind: "delete" })}><Icon name="trash" size={14} />Delete</button> : null}
            </>}
            <button className="link-btn" onClick={() => setPicked(new Set())}>Clear</button>
          </div>
        ) : null}

        <div className="card">
          {leads === null && !error ? <div className="empty"><p>Loading leads…</p></div>
            : !rows.length ? <Empty title={leads && leads.length ? "No leads match" : "No leads waiting"}>{leads && leads.length ? "Try a different search or filter." : canOf(user).import ? "New enquiries land here. Import a list with Import." : "New enquiries assigned to you land here."}</Empty>
            : (
              <div className={`table-wrap${leaving ? " fading" : ""}`}>
                <table className={`list leads-table${leaving ? " leaving" : ""}${returning ? " returning" : ""}`}>
                  <thead><tr>
                    {canWrite ? <th className="pick"><input type="checkbox" aria-label={allShownPicked ? "Unselect all" : `Select all ${rows.length}`} checked={allShownPicked} onChange={pickAll} /></th> : null}
                    {th("biz", "Business", "keep")}<th>Contact</th>{th("revenue", "Revenue", "num keep")}{th("leverage", "Leverage")}<th>Tags</th>{th("offer", "Offer")}<th>Files</th>{showOwner ? th("rep", "Assigned to") : null}{th("recent", "Last applied")}
                  </tr></thead>
                  <tbody>
                    {rows.slice(0, limit).map(l => (
                      <tr key={l.id} data-lead={l.id} className={`lead-tr${picked.has(l.id) ? " picked" : ""}`} onClick={(e) => rowClick(e, l.id)}>
                        {canWrite ? <td className="pick"><input type="checkbox" aria-label={`Select ${leadBiz(l)}`} checked={picked.has(l.id)} onChange={() => togglePick(l.id)} /></td> : null}
                        <td className="keep"><div className="who"><Avatar name={leadBiz(l)} size="sm" /><div><button className="link-btn strong" onClick={() => openLead(l.id)}>{leadBiz(l)}</button>{l.bank ? <BankLabels reverse={l.bank.reverse} debtSettlement={l.bank.debtSettlement} /> : null}<span>{leadName(l)}{l.reapplied ? <> · <span className="re-badge">Re-applied ×{l.reapplied + 1}</span></> : null}{l.existing ? <> · <span className="re-badge">Already a {l.existing.kind === "client" || l.existing.kind === "client-deal" ? "client" : "deal"}</span></> : null}</span></div></div></td>
                        <td data-label="Contact" className="contact-cell">{l.phone ? <div className="tnum">{l.phone}</div> : null}{l.email ? <div className="clip" title={l.email}>{l.email}</div> : null}</td>
                        <td className="num tnum keep" data-label="Revenue">{l.bank && l.bank.avgSales ? <><span title="Average monthly sales on the bank statements">{fmt.moneyShort(l.bank.avgSales)}</span>{l.monthlyRevenue ? <div className="rev-st">stated {fmt.moneyShort(l.monthlyRevenue)}</div> : <div className="rev-st">from statements</div>}</> : l.monthlyRevenue ? <>{fmt.moneyShort(l.monthlyRevenue)}<div className="rev-st">stated</div></> : ""}</td>
                        <td data-label="Leverage" className="lev-cell">{l.bank && l.bank.share !== null && l.bank.share !== undefined ? <><RemitBadge share={l.bank.share} withPlatforms={l.bank.shareAll} compact /><div className="rev-st">{l.bank.positions} {l.bank.positions === 1 ? "position" : "positions"}{l.bank.platforms ? ` + ${l.bank.platforms} platform` : ""}</div></> : <StatementState l={l} />}</td>
                        <td data-label="Tags">{(l.tags || []).length ? <span className="chips tight">{l.tags.slice(0, 3).map(t => <span key={t} className="chip">{t}</span>)}{l.tags.length > 3 ? <span className="chip">+{l.tags.length - 3}</span> : null}</span> : ""}</td>
                        <td data-label="Offer">{l.offer ? <span className="offer-cell" title={`Up to ${fmt.money(l.offer.top)}`}>{offerState(l.offer)}</span> : ""}</td>
                        <td data-label="Files" className="files-cell">{l.files && l.files.files ? <span className={l.files.statements ? "muted" : "warn-text"}>{l.files.statements ? `${l.files.statements} stmt${l.files.statements === 1 ? "" : "s"}` : "No stmts"}{l.files.applications ? " · app" : ""}</span> : l.application ? (l.application.complete ? "Application" : <Pill status="pending" label={`Incomplete · step ${l.application.lastStep || 1}/4`} />) : ""}</td>
                        {showOwner ? <td data-label="Assigned to">{repName(l.repId) || <Pill status="pending" label="Unassigned" />}</td> : null}
                        <td data-label="Last applied" className="nowrap">{fmt.date(l.lastAppliedAt || l.createdAt) || l.date || ""}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
                {rows.length > limit ? <div className="card-foot"><button className="btn btn-sm" onClick={() => setLimit(limit + 400)}>Show more ({rows.length - limit} not shown)</button></div> : null}
              </div>
            )}
        </div>
      </div>

      {sheets}
    </main>
  );
}

// Reads the bank statements on the chosen leads. It counts first (nothing is spent), says what it will read,
// then queues them; reading happens in the background, a few at a time. To keep the cost down it can leave out
// leads whose stated monthly revenue is under a floor, and read only each lead's newest few statements
// (the months come from the file names; a lead whose names don't say reads them all). The choices are remembered.
const ANALYZE_KEY = "ef.analyzeSettings";
const ANALYZE_DEFAULTS = { useMin: true, minRevenue: 25000, skipNoRevenue: false, useNewest: true, newest: 4, highestFirst: true };
const readAnalyze = () => { try { return { ...ANALYZE_DEFAULTS, ...(JSON.parse(localStorage.getItem(ANALYZE_KEY) || "{}") || {}) }; } catch (e) { return { ...ANALYZE_DEFAULTS }; } };
function BulkAnalyzeSheet({ ids: picked, leads, onClose, onDone }) {
  const [opt, setOpt] = useState(readAnalyze);
  // Highest revenue first: the leads are sent (and so read) biggest first, down to the floor; leads with no revenue on file go last.
  const rev = useMemo(() => new Map((leads || []).map(l => [l.id, Number(l.monthlyRevenue) || 0])), [leads]);
  const ids = useMemo(() => (opt.highestFirst ? [...picked].sort((a, b) => (rev.get(b) || 0) - (rev.get(a) || 0)) : picked), [picked, opt.highestFirst, rev]);
  const [count, setCount] = useState(null);
  const [progress, setProgress] = useState("");
  const save = useSave(onDone);
  const asked = useRef(0);
  const set = (k, v) => setOpt(o => ({ ...o, [k]: v }));
  const body = () => ({ lane: picked.length === 1 ? "now" : "bulk", minRevenue: opt.useMin ? Math.max(0, Number(String(opt.minRevenue).replace(/[$,\s]/g, "")) || 0) : 0, skipNoRevenue: opt.useMin && opt.skipNoRevenue, newest: opt.useNewest ? Math.max(1, Math.min(24, Math.floor(Number(opt.newest) || 4))) : 0 });
  const sum = (list) => list.reduce((t, r) => ({ queued: t.queued + (r.queued || 0), already: t.already + (r.already || 0), withStatements: t.withStatements + (r.withStatements || 0), lowRevenue: t.lowRevenue + (r.lowRevenue || 0), olderSkipped: t.olderSkipped + (r.olderSkipped || 0) }), { queued: 0, already: 0, withStatements: 0, lowRevenue: 0, olderSkipped: 0 });
  // Counted again whenever a choice changes (a moment after the last keystroke); an older count that arrives late is dropped.
  useEffect(() => {
    try { localStorage.setItem(ANALYZE_KEY, JSON.stringify(opt)); } catch (e) { /* private window */ }
    const mine = ++asked.current;
    setCount(null);
    const t = setTimeout(() => {
      inParts(ids, PARTS.analyze, (part) => API.bulkLeads({ ids: part, action: "analyze", dryRun: true, ...body() }), (n, of) => mine === asked.current && setProgress(`Counting ${n} of ${of}…`))
        .then(r => { if (mine === asked.current) { setCount(sum(r)); setProgress(""); } }, err => mine === asked.current && save.setError(err.message));
    }, 350);
    return () => clearTimeout(t);
  }, [opt.useMin, opt.minRevenue, opt.skipNoRevenue, opt.useNewest, opt.newest]);
  const run = () => save.run(async () => sum(await inParts(ids, PARTS.analyze, (part) => API.bulkLeads({ ids: part, action: "analyze", ...body() }), (n, of) => setProgress(`Starting ${n} of ${of}…`))));
  const n = count ? count.queued : 0;
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Analyze statements" onClose={onClose}
           onSubmit={count && n ? run : null} submitLabel={`Read ${n} ${n === 1 ? "statement" : "statements"}`} busyLabel={progress || undefined} busy={save.busy} error={save.error}>
      <div className="form-section">
        <span className="label">Keep the cost down</span>
        <div className="switch-row boxed">
          <div><div className="t">Skip leads under a monthly revenue</div><div className="d">By the revenue on the lead (stated or imported).</div></div>
          <Switch checked={opt.useMin} onChange={v => set("useMin", v)} label="Skip leads under a monthly revenue" />
        </div>
        {opt.useMin ? (
          <div className="form-grid" style={{ marginTop: 10 }}>
            <Field label="Monthly revenue under"><TextInput value={String(opt.minRevenue)} onChange={v => set("minRevenue", v)} inputMode="numeric" /></Field>
            <label className="check" style={{ alignSelf: "end", paddingBottom: 10 }}><input type="checkbox" checked={opt.skipNoRevenue} onChange={e => set("skipNoRevenue", e.target.checked)} />Also skip leads with no revenue on file</label>
          </div>
        ) : null}
        <div className="switch-row boxed" style={{ marginTop: 12 }}>
          <div><div className="t">Only each lead's newest statements</div><div className="d">The months come from the file names ("Chase_July2026.pdf"). A lead whose file names don't say has them all read.</div></div>
          <Switch checked={opt.useNewest} onChange={v => set("useNewest", v)} label="Only each lead's newest statements" />
        </div>
        {opt.useNewest ? <div className="form-grid" style={{ marginTop: 10 }}><Field label="How many per lead"><TextInput value={String(opt.newest)} onChange={v => set("newest", v)} inputMode="numeric" /></Field></div> : null}
        <div className="switch-row boxed" style={{ marginTop: 12 }}>
          <div><div className="t">Highest revenue first</div><div className="d">Leads are read biggest first and work down{opt.useMin ? ` to ${fmt.moneyShort(Math.max(0, Number(String(opt.minRevenue).replace(/[$,\s]/g, "")) || 0))}` : ""}, so if credits run out the best leads are already done.</div></div>
          <Switch checked={opt.highestFirst} onChange={v => set("highestFirst", v)} label="Highest revenue first" />
        </div>
      </div>
      <div className="form-section">
        {!count ? <p className="muted">{progress || "Counting the statements on these leads…"}</p> : (
          <>
            <p><b>{n ? `${n} ${n === 1 ? "statement" : "statements"} to read` : "Nothing to read"}</b> on {count.withStatements} of the {ids.length} selected {ids.length === 1 ? "lead" : "leads"}{count.already ? ` (${count.already} already read, skipped)` : ""}.</p>
            {count.lowRevenue || count.olderSkipped || ids.length - count.withStatements - count.lowRevenue > 0 ? (
              <ul className="dim" style={{ fontSize: 13, margin: "6px 0 0 18px" }}>
                {count.lowRevenue ? <li>{count.lowRevenue} {count.lowRevenue === 1 ? "lead" : "leads"} left out for revenue</li> : null}
                {count.olderSkipped ? <li>{count.olderSkipped} older {count.olderSkipped === 1 ? "statement" : "statements"} not read</li> : null}
                {ids.length - count.withStatements - count.lowRevenue > 0 ? <li>{ids.length - count.withStatements - count.lowRevenue} {ids.length - count.withStatements - count.lowRevenue === 1 ? "lead has" : "leads have"} no statements</li> : null}
              </ul>
            ) : null}
            {n ? <p className="dim" style={{ fontSize: 13 }}>Read in the background, a few at a time; each lead's breakdown appears when done, with what its reading cost.</p> : null}
          </>
        )}
      </div>
    </Sheet>
  );
}

function BulkDeleteSheet({ ids, onClose, onDone }) {
  const [typed, setTyped] = useState("");
  const [progress, setProgress] = useState("");
  const save = useSave(onDone);
  const run = () => save.run(async () => {
    const done = await inParts(ids, PARTS.delete, (part) => API.bulkLeads({ ids: part, action: "delete", confirm: "DELETE" }), (n, of) => setProgress(`Deleting ${n} of ${of}…`));
    return { deleted: done.reduce((t, r) => t + (r.deleted || 0), 0) };
  });
  return (
    <Sheet eyebrow={`${ids.length} ${ids.length === 1 ? "lead" : "leads"}`} title="Delete leads" onClose={onClose}
           onSubmit={() => (typed === "DELETE" ? run() : save.setError("Type DELETE to confirm."))}
           submitLabel={`Delete ${ids.length}`} busyLabel={progress || undefined} busy={save.busy} error={save.error}>
      <div className="form-section">
        <p>These leads are removed with their files, statements and offer links. Links already sent stop working. This can't be undone.</p>
        <Field label="Type DELETE to confirm"><TextInput value={typed} onChange={setTyped} autoComplete="off" /></Field>
      </div>
    </Sheet>
  );
}

Object.assign(window, { inParts, LeadsView, LeadFile, TagPicker, OfferSettingsForm, offerSettingsError, savedOfferSettings, rememberOfferSettings, AssignSheet, leadBiz, leadName });
