// crm.jsx — Leads (CRM) view: kanban-by-stage + drawer + new-lead modal.
//
// Visual language matches the rest of Flowboard: tokens.css drives colour, the
// header mirrors .project-header, the toolbar mirrors .board-toolbar, and the
// primary button reuses the project's .btn / .btn-primary instead of inventing
// a new purple gradient.  Only the truly CRM-specific surfaces (kanban cards,
// drawer, lost-reason modal) live in crm.css.

function CRMView({ currentUserId, people = [] }) {
  const [version, setVersion] = React.useState(0);  // bumped after mutations
  const [openLeadId, setOpenLeadId] = React.useState(null);
  const [newOpen, setNewOpen] = React.useState(false);
  const [metaOpen, setMetaOpen] = React.useState(false);
  const [filter, setFilter] = React.useState({ q: "", mine: false });
  // "board" = the kanban-by-stage view (default).
  // "queue" = the Action Queue: a focused triage list bucket-sorted by
  //           urgency. Lives alongside the board so the rep can flip
  //           depending on whether they're planning ("show me the funnel")
  //           or working ("what should I do RIGHT NOW"). Choice is
  //           remembered per-browser so power users don't re-pick every
  //           session.
  const [mode, setMode] = React.useState(() => {
    try { return localStorage.getItem("flowboard.crm.mode") || "board"; }
    catch { return "board"; }
  });
  function setModePersisted(next) {
    setMode(next);
    try { localStorage.setItem("flowboard.crm.mode", next); } catch {}
  }

  // Real-time subscription — the SSE stream fires flowboard:rt:leads.changed
  // whenever any lead is created/updated/deleted (including via the Meta
  // webhook + sync). Reload + bump the render counter so the kanban / table
  // pick up the change without the user touching anything.
  React.useEffect(() => {
    let cancelled = false;
    let pending = null;
    function onLeadsChanged() {
      if (pending) return;                          // coalesce burst events
      pending = setTimeout(() => {
        pending = null;
        if (cancelled) return;
        if (typeof reloadLeads === "function") {
          reloadLeads().then(() => { if (!cancelled) setVersion(v => v + 1); }).catch(() => {});
        }
      }, 200);
    }
    window.addEventListener("flowboard:rt:leads.changed", onLeadsChanged);
    return () => {
      cancelled = true;
      if (pending) clearTimeout(pending);
      window.removeEventListener("flowboard:rt:leads.changed", onLeadsChanged);
    };
  }, []);
  // pendingLost: { leadId, fromStage, alreadyMoved? }
  // Set whenever any code path tries to push a lead into the "Lost" stage.
  // The LostReasonModal renders while this is non-null; nothing is committed
  // until the user picks a reason or cancels.
  const [pendingLost, setPendingLost] = React.useState(null);

  const leads = getLeads();
  const filtered = React.useMemo(() => {
    let list = leads;
    if (filter.mine && currentUserId) list = list.filter(l => l.owner_id === currentUserId);
    const q = filter.q.trim().toLowerCase();
    if (q) list = list.filter(l =>
      (l.name || "").toLowerCase().includes(q) ||
      (l.phone || "").toLowerCase().includes(q) ||
      (l.email || "").toLowerCase().includes(q));
    return list;
  }, [leads, filter, version, currentUserId]);

  // counts for the header chips
  const counts = React.useMemo(() => {
    const out = { total: filtered.length, today: 0, overdue: 0, hot: 0 };
    for (const l of filtered) {
      if (leadOverdue(l)) out.overdue++;
      if (leadDueToday(l)) out.today++;
      if (l.stage === "demo_scheduled" || l.stage === "demo_done") out.hot++;
    }
    return out;
  }, [filtered]);

  // Single chokepoint for stage transitions. Both drag-drop and the drawer's
  // Stage <select> route through this so the lost-reason modal is honoured
  // wherever the user might trigger a "→ lost" move.
  async function requestStageChange(leadId, newStage) {
    const arr = getLeads();
    const idx = arr.findIndex(l => l.id === leadId);
    if (idx === -1) return;
    const prev = arr[idx];
    if (prev.stage === newStage) return;
    if (newStage === "lost") {
      setPendingLost({ leadId, fromStage: prev.stage, alreadyMoved: false });
      return;
    }
    await applyStageChange(leadId, newStage);
  }

  async function applyStageChange(leadId, newStage) {
    const arr = getLeads();
    const idx = arr.findIndex(l => l.id === leadId);
    if (idx === -1) return;
    const prev = arr[idx];
    arr[idx] = { ...prev, stage: newStage };
    setVersion(v => v + 1);
    try {
      const updated = await api.leads.patch(leadId, { stage: newStage });
      arr[idx] = updated;
      setVersion(v => v + 1);
    } catch (e) {
      arr[idx] = prev;
      setVersion(v => v + 1);
      alert("Could not update lead: " + (e && e.message));
    }
  }

  async function handleCreate(payload) {
    try {
      const created = await api.leads.create(payload);
      const arr = getLeads();
      arr.unshift(created);   // mutate in place — same ref, same array
      setNewOpen(false);
      setVersion(v => v + 1);
      setOpenLeadId(created.id);
    } catch (e) {
      alert("Could not create lead: " + (e && e.message));
    }
  }

  async function handlePatch(leadId, body) {
    const arr = getLeads();
    const idx = arr.findIndex(l => l.id === leadId);
    if (idx === -1) return;
    const prev = arr[idx];
    arr[idx] = { ...prev, ...body };
    setVersion(v => v + 1);
    try {
      const updated = await api.leads.patch(leadId, body);
      arr[idx] = updated;
      setVersion(v => v + 1);
    } catch (e) {
      arr[idx] = prev;
      setVersion(v => v + 1);
      alert("Update failed: " + (e && e.message));
    }
  }

  async function handleDelete(leadId) {
    if (!confirm("Delete this lead? This cannot be undone.")) return;
    try {
      await api.leads.remove(leadId);
      const arr = getLeads();
      const idx = arr.findIndex(l => l.id === leadId);
      if (idx !== -1) arr.splice(idx, 1);
      setOpenLeadId(null);
      setVersion(v => v + 1);
    } catch (e) {
      alert("Delete failed: " + (e && e.message));
    }
  }

  // Confirm handler for the lost-reason modal.
  // If alreadyMoved (the backend auto-moved the lead via demo_done outcome),
  // we only persist the reason; otherwise we push the stage change too.
  //
  // Three artefacts are written, all in one PATCH so the lead row stays
  // consistent on the client even if the SSE echo is delayed:
  //   - lost_reason / lost_reason_note  — the queryable structured pair
  //                                       (migration 023). Reports group
  //                                       on these.
  //   - notes                            — narrative line appended for
  //                                       the lead drawer's timeline UI.
  //   - stage = 'lost'                   — only when the move hasn't
  //                                       already been applied.
  async function handleConfirmLost({ reason, note }) {
    if (!pendingLost) return;
    const { leadId, alreadyMoved } = pendingLost;
    const lead = getLeads().find(l => l.id === leadId);
    if (!lead) { setPendingLost(null); return; }

    const stamp = new Date().toLocaleDateString();
    const reasonLabel = reason || "Other";
    const noteFrag = note ? ` — ${note}` : "";
    const append = `[Lost ${stamp}: ${reasonLabel}${noteFrag}]`;
    const newNotes = (lead.notes ? lead.notes + "\n" : "") + append;

    const patch = {
      notes: newNotes,
      lost_reason: reasonLabel,
      lost_reason_note: note ? note : null,
    };
    if (!alreadyMoved) patch.stage = "lost";

    try {
      await handlePatch(leadId, patch);
      // Activity log entry — kept for backwards compatibility with any
      // older report code reading from lead_activity.
      await api.leads.addActivity(leadId, {
        kind: "note",
        details: { lost_reason: reasonLabel, lost_note: note || null },
      }).catch(() => {});
    } catch (e) {
      alert("Could not save lost reason: " + (e && e.message));
    }
    setPendingLost(null);
  }

  function handleCancelLost() {
    // If the demo_done flow already moved the lead to "lost" and the user
    // bails on the reason modal, we leave the lead in "lost" — the move did
    // happen, the user just chose not to annotate it. That matches the rest
    // of the CRM where activities are append-only.
    setPendingLost(null);
  }

  const openLead = openLeadId ? leads.find(l => l.id === openLeadId) : null;

  return (
    <div className="crm-root">
      <div className="crm-header">
        <div className="crm-title-row">
          <div className="crm-title">Leads</div>
          <div className="crm-stat-chips">
            {counts.today   > 0 && <span className="crm-stat-chip is-warn"><b>{counts.today}</b> due today</span>}
            {counts.overdue > 0 && <span className="crm-stat-chip is-danger"><b>{counts.overdue}</b> overdue</span>}
            <span className="crm-stat-chip"><b>{counts.total}</b> total</span>
          </div>
        </div>
      </div>

      <div className="crm-toolbar">
        <input className="crm-search" placeholder="Search name, phone, email…"
               value={filter.q}
               onChange={(e) => setFilter(f => ({ ...f, q: e.target.value }))}/>
        <label className={`crm-toggle ${filter.mine ? "is-on" : ""}`}>
          <input type="checkbox" checked={filter.mine}
                 onChange={(e) => setFilter(f => ({ ...f, mine: e.target.checked }))}/>
          <span>My leads</span>
        </label>
        {/* Board / Queue / Dashboard mode toggle. Same visual idiom as
            the sprint/backlog tab strip elsewhere in the app. Dashboard
            gives an at-a-glance insight view — daily counts, funnel,
            owner leaderboard — for managers / team leads. */}
        <div className="crm-mode-toggle" role="tablist" aria-label="View mode">
          <button type="button" role="tab"
                  aria-selected={mode === "board"}
                  className={`crm-mode-btn ${mode === "board" ? "is-on" : ""}`}
                  onClick={() => setModePersisted("board")}>
            Board
          </button>
          <button type="button" role="tab"
                  aria-selected={mode === "queue"}
                  className={`crm-mode-btn ${mode === "queue" ? "is-on" : ""}`}
                  onClick={() => setModePersisted("queue")}>
            Action queue
          </button>
          <button type="button" role="tab"
                  aria-selected={mode === "dashboard"}
                  className={`crm-mode-btn ${mode === "dashboard" ? "is-on" : ""}`}
                  onClick={() => setModePersisted("dashboard")}>
            Dashboard
          </button>
        </div>
        <div className="crm-toolbar-right">
          <button className="btn" onClick={() => exportLeadsCsv(filtered)} title="Export filtered leads as CSV">
            <span aria-hidden="true" style={{ fontSize: 13 }}>⬇</span> Export CSV
          </button>
          <button className="btn" onClick={() => setMetaOpen(true)} title="Meta Ads integration">
            <span aria-hidden="true" style={{ fontSize: 13 }}>📣</span> Meta Ads
          </button>
          <button className="btn btn-primary" onClick={() => setNewOpen(true)}>
            <Icons.Plus size={14}/> New lead
          </button>
        </div>
      </div>

      {mode === "board" && (
        <CRMBoard leads={filtered} onOpen={(l) => setOpenLeadId(l.id)} onStageChange={requestStageChange}/>
      )}
      {mode === "queue" && (
        <CRMActionQueue leads={filtered} people={people} onOpen={(l) => setOpenLeadId(l.id)}/>
      )}
      {mode === "dashboard" && (
        <CRMDashboard leads={filtered} people={people} onOpen={(l) => setOpenLeadId(l.id)} currentUserId={currentUserId}/>
      )}

      {openLead && (
        <CRMDrawer
          lead={openLead}
          people={people}
          currentUserId={currentUserId}
          onClose={() => setOpenLeadId(null)}
          onPatch={handlePatch}
          onStageChange={requestStageChange}
          onAfterDemoLost={(leadId, fromStage) =>
            setPendingLost({ leadId, fromStage, alreadyMoved: true })}
          onDelete={() => handleDelete(openLead.id)}
        />
      )}

      {newOpen && (
        <NewLeadModal
          onClose={() => setNewOpen(false)}
          onCreate={handleCreate}
          currentUserId={currentUserId}
          people={people}
        />
      )}

      {pendingLost && (
        <LostReasonModal
          leadName={(leads.find(l => l.id === pendingLost.leadId) || {}).name || "this lead"}
          alreadyMoved={pendingLost.alreadyMoved}
          onConfirm={handleConfirmLost}
          onCancel={handleCancelLost}
        />
      )}

      {metaOpen && (
        <MetaIntegrationModal
          onClose={() => setMetaOpen(false)}
          onLeadsChanged={() => reloadLeads().then(() => setVersion(v => v + 1))}
          currentUserId={currentUserId}
        />
      )}
    </div>
  );
}

// ── Action Queue ─────────────────────────────────────────────────────────
// A focused triage view that answers "what should this rep do RIGHT NOW?".
// Pure derivation from the leads we already have — no new API. Bucket
// priority (high → low):
//   1. Overdue reminders          — next_action_at in the past
//   2. Due today                  — next_action_at on today's date
//   3. Untouched new leads        — stage = "new" AND no "call" activity
//                                   AND created > 30 minutes ago. The
//                                   30-min grace stops the queue from
//                                   yelling at reps the moment a lead
//                                   lands; after that, time-to-first-
//                                   contact starts hurting conversion.
//   4. Stalling in stage          — same stage > 3 days, not in
//                                   converted/lost. Includes a chip
//                                   showing how long it's been parked.
//
// Buckets that are empty just don't render. Within each bucket we sort
// by oldest-first so the most-overdue / longest-stalled lead is at the
// top, which is what reps actually want to attack first.
function CRMActionQueue({ leads, people, onOpen }) {
  const NOW = Date.now();
  const MIN_30 = 30 * 60 * 1000;
  const DAY_3  = 3 * 24 * 3600 * 1000;
  const STALL_TERMINAL = new Set(["converted", "lost"]);

  function ageMs(iso) {
    if (!iso) return 0;
    const t = new Date(iso).getTime();
    return Number.isFinite(t) ? Math.max(0, NOW - t) : 0;
  }
  function fmtAge(ms) {
    if (ms < 60_000)        return "just now";
    if (ms < 3_600_000)     return Math.round(ms / 60_000) + "m";
    if (ms < 86_400_000)    return Math.round(ms / 3_600_000) + "h";
    return Math.round(ms / 86_400_000) + "d";
  }

  const buckets = React.useMemo(() => {
    const overdue   = [];
    const today     = [];
    const untouched = [];
    const stalling  = [];

    for (const l of leads) {
      if (l.stage === "converted" || l.stage === "lost") continue;

      if (leadOverdue(l))      { overdue.push(l); continue; }
      if (leadDueToday(l))     { today.push(l);   continue; }

      if (l.stage === "new") {
        const age = ageMs(l.created_at);
        if (age > MIN_30) untouched.push(l);
        continue;
      }

      if (!STALL_TERMINAL.has(l.stage)) {
        const sinceUpdate = ageMs(l.updated_at || l.created_at);
        if (sinceUpdate > DAY_3) stalling.push(l);
      }
    }

    // Oldest-first within each bucket — deepest pain at the top.
    overdue  .sort((a, b) => new Date(a.next_action_at) - new Date(b.next_action_at));
    today    .sort((a, b) => new Date(a.next_action_at) - new Date(b.next_action_at));
    untouched.sort((a, b) => new Date(a.created_at)     - new Date(b.created_at));
    stalling .sort((a, b) => new Date(a.updated_at || a.created_at)
                           - new Date(b.updated_at || b.created_at));

    return { overdue, today, untouched, stalling };
  }, [leads]);

  function ownerName(id) {
    if (!id) return "Unassigned";
    const p = (people || []).find(x => x.id === id);
    return (p && p.name) || id;
  }

  function Row({ lead, hint, hintClass }) {
    const stage = LEAD_STAGE_BY_ID[lead.stage] || LEAD_STAGES[0];
    return (
      <div className="aq-row" onClick={() => onOpen(lead)}
           role="button" tabIndex={0}
           onKeyDown={e => { if (e.key === "Enter") onOpen(lead); }}>
        <span className="aq-stage-dot" style={{ background: stage.color }} title={stage.label}/>
        <div className="aq-row-main">
          <div className="aq-row-name">{lead.name}</div>
          <div className="aq-row-sub">
            {lead.course_interest ? lead.course_interest + " · " : ""}
            {ownerName(lead.owner_id)}
            {lead.phone ? " · " + lead.phone : ""}
          </div>
        </div>
        <span className={"aq-hint " + (hintClass || "")}>{hint}</span>
      </div>
    );
  }

  function Bucket({ title, count, leads, badge, render }) {
    if (!leads.length) return null;
    return (
      <div className="aq-bucket">
        <div className={"aq-bucket-head " + (badge || "")}>
          <span className="aq-bucket-title">{title}</span>
          <span className="aq-bucket-count">{count}</span>
        </div>
        <div className="aq-bucket-body">
          {leads.map(render)}
        </div>
      </div>
    );
  }

  const total = buckets.overdue.length + buckets.today.length
              + buckets.untouched.length + buckets.stalling.length;

  if (total === 0) {
    return (
      <div className="crm-aq-empty">
        <div className="crm-aq-empty-emoji" aria-hidden="true">🎯</div>
        <div className="crm-aq-empty-title">Inbox zero</div>
        <div className="crm-aq-empty-sub">
          No overdue reminders, no untouched new leads, nothing stalling.
          Check back after the next intake batch.
        </div>
      </div>
    );
  }

  return (
    <div className="crm-aq">
      <Bucket title="Overdue reminders" count={buckets.overdue.length}
              leads={buckets.overdue} badge="is-danger"
              render={l => (
                <Row key={l.id} lead={l}
                     hintClass="is-danger"
                     hint={"⏰ " + fmtAge(NOW - new Date(l.next_action_at).getTime()) + " late"}/>
              )}/>
      <Bucket title="Due today" count={buckets.today.length}
              leads={buckets.today} badge="is-warn"
              render={l => (
                <Row key={l.id} lead={l}
                     hintClass="is-warn"
                     hint={l.next_action_note ? "⏰ " + l.next_action_note : "⏰ Due today"}/>
              )}/>
      <Bucket title="Untouched new leads" count={buckets.untouched.length}
              leads={buckets.untouched} badge="is-info"
              render={l => (
                <Row key={l.id} lead={l}
                     hintClass="is-info"
                     hint={"🆕 " + fmtAge(NOW - new Date(l.created_at).getTime()) + " old"}/>
              )}/>
      <Bucket title="Stalling in stage (3+ days)" count={buckets.stalling.length}
              leads={buckets.stalling} badge=""
              render={l => (
                <Row key={l.id} lead={l}
                     hint={"💤 idle " + fmtAge(NOW - new Date(l.updated_at || l.created_at).getTime())}/>
              )}/>
    </div>
  );
}

// ── CSV export ───────────────────────────────────────────────────────────
// Streams the currently-filtered leads as a UTF-8 CSV. Excel-friendly
// (BOM prefix → it auto-detects UTF-8 and renders Devanagari / accents
// correctly). RFC 4180 escaping: values are double-quoted; embedded
// quotes are doubled; commas/newlines inside quotes are preserved.
//
// We deliberately export a stable, hand-picked column set instead of
// dumping every field — keeps the CSV usable in spreadsheet pivots
// without cluttering it with Meta plumbing IDs.
function exportLeadsCsv(leads) {
  const cols = [
    ["id",               l => l.id],
    ["name",             l => l.name],
    ["phone",            l => l.phone],
    ["email",            l => l.email],
    ["course",           l => l.course_interest],
    ["source",           l => l.source],
    ["stage",            l => l.stage],
    ["owner_id",         l => l.owner_id],
    ["value",            l => l.value],
    ["demo_at",          l => l.demo_at],
    ["next_action_at",   l => l.next_action_at],
    ["next_action_note", l => l.next_action_note],
    ["lost_reason",      l => l.lost_reason],
    ["lost_reason_note", l => l.lost_reason_note],
    ["created_at",       l => l.created_at],
    ["updated_at",       l => l.updated_at],
  ];
  function esc(v) {
    if (v == null) return "";
    const s = String(v);
    if (/[",\r\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
    return s;
  }
  const header = cols.map(c => c[0]).join(",");
  const rows = (leads || []).map(l => cols.map(c => esc(c[1](l))).join(","));
  const csv = "﻿" + [header, ...rows].join("\r\n");

  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const url  = URL.createObjectURL(blob);
  const a    = document.createElement("a");
  const ts   = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
  a.href = url;
  a.download = `leads_${ts}.csv`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  // Defer revoke a tick so the browser has a chance to start the download.
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// ── Kanban board ─────────────────────────────────────────────────────────
function CRMBoard({ leads, onOpen, onStageChange }) {
  const [dragId, setDragId] = React.useState(null);
  const [overStage, setOverStage] = React.useState(null);

  function byStage(stageId) {
    return leads.filter(l => l.stage === stageId);
  }

  function handleDrop(stageId) {
    if (dragId) {
      const lead = leads.find(l => l.id === dragId);
      if (lead && lead.stage !== stageId) onStageChange(dragId, stageId);
    }
    setDragId(null);
    setOverStage(null);
  }

  return (
    <div className="crm-board">
      {LEAD_STAGES.map(s => {
        const col = byStage(s.id);
        return (
          <div key={s.id}
               className={`crm-col ${overStage === s.id ? "is-drop" : ""}`}
               onDragOver={(e) => { e.preventDefault(); setOverStage(s.id); }}
               onDragLeave={() => setOverStage(o => o === s.id ? null : o)}
               onDrop={(e) => { e.preventDefault(); handleDrop(s.id); }}>
            <div className="crm-col-head" style={{ "--stage-color": s.color }}>
              <span className="crm-col-dot"/>
              <span className="crm-col-name">{s.label}</span>
              <span className="crm-col-count">{col.length}</span>
            </div>
            <div className="crm-col-body">
              {col.length === 0 ? (
                <div className="crm-empty">No leads</div>
              ) : col.map(l => (
                <CRMCard key={l.id} lead={l}
                         onClick={() => onOpen(l)}
                         onDragStart={() => setDragId(l.id)}
                         onDragEnd={() => { setDragId(null); setOverStage(null); }}
                         dragging={dragId === l.id}/>
              ))}
            </div>
          </div>
        );
      })}
    </div>
  );
}

// ── Phone → country flag ─────────────────────────────────────────────
// Parses the leading country code from a phone number and returns
// { iso, flag, name }. Falls back to lead.country / lead.city / lead.location
// when the phone has no usable prefix. Used by the kanban card so the
// sales team can scan country at a glance without opening the drawer.
//
// Coverage is the top ~50 country dialling codes — common WhatsApp/SMS
// origins for South Asian + Gulf + Western markets, which covers > 95%
// of real-world leads we see. Anything unknown returns null and the UI
// gracefully drops the flag.
const PHONE_PREFIX_ISO = (() => {
  // Order matters for longest-match. Longer codes first.
  const entries = [
    // 4-digit codes
    ["1684","AS"],["1242","BS"],["1246","BB"],["1264","AI"],["1268","AG"],
    ["1284","VG"],["1340","VI"],["1345","KY"],["1441","BM"],["1473","GD"],
    ["1649","TC"],["1664","MS"],["1670","MP"],["1671","GU"],["1684","AS"],
    ["1721","SX"],["1758","LC"],["1767","DM"],["1784","VC"],["1787","PR"],
    ["1809","DO"],["1829","DO"],["1849","DO"],["1868","TT"],["1869","KN"],
    ["1876","JM"],["1939","PR"],
    // 3-digit codes
    ["971","AE"],["973","BH"],["974","QA"],["965","KW"],["966","SA"],["968","OM"],
    ["962","JO"],["961","LB"],["963","SY"],["964","IQ"],["967","YE"],["970","PS"],
    ["972","IL"],["960","MV"],["975","BT"],["977","NP"],["852","HK"],["853","MO"],
    ["855","KH"],["856","LA"],["886","TW"],["880","BD"],["960","MV"],
    // 2-digit codes
    ["91","IN"],["92","PK"],["93","AF"],["94","LK"],["95","MM"],["98","IR"],
    ["20","EG"],["27","ZA"],["30","GR"],["31","NL"],["32","BE"],["33","FR"],
    ["34","ES"],["36","HU"],["39","IT"],["40","RO"],["41","CH"],["43","AT"],
    ["44","GB"],["45","DK"],["46","SE"],["47","NO"],["48","PL"],["49","DE"],
    ["51","PE"],["52","MX"],["53","CU"],["54","AR"],["55","BR"],["56","CL"],
    ["57","CO"],["58","VE"],["60","MY"],["61","AU"],["62","ID"],["63","PH"],
    ["64","NZ"],["65","SG"],["66","TH"],["81","JP"],["82","KR"],["84","VN"],
    ["86","CN"],["90","TR"],
    // 1-digit codes
    ["1","US"],["7","RU"],
  ];
  return entries;
})();
function isoToFlag(iso) {
  if (!iso || iso.length !== 2) return "";
  // Regional Indicator Symbols: A=0x1F1E6 .. Z=0x1F1FF
  const a = iso.toUpperCase().charCodeAt(0) - 65;
  const b = iso.toUpperCase().charCodeAt(1) - 65;
  if (a < 0 || a > 25 || b < 0 || b > 25) return "";
  return String.fromCodePoint(0x1F1E6 + a) + String.fromCodePoint(0x1F1E6 + b);
}
// Friendly country name lookup keyed by ISO-3166-1 alpha-2. Used by the
// CRM dashboard so users see "United Arab Emirates" instead of "AE" in
// the country breakdown table. Limited to the same countries the
// phone-prefix table can detect — anything else just shows the ISO
// code, which is still better than nothing.
const ISO_TO_COUNTRY_NAME = {
  IN: "India", PK: "Pakistan", BD: "Bangladesh", LK: "Sri Lanka",
  NP: "Nepal", BT: "Bhutan", AF: "Afghanistan", MV: "Maldives", IR: "Iran",
  AE: "United Arab Emirates", SA: "Saudi Arabia", QA: "Qatar", KW: "Kuwait",
  BH: "Bahrain", OM: "Oman", JO: "Jordan", LB: "Lebanon", SY: "Syria",
  IQ: "Iraq", YE: "Yemen", PS: "Palestine", IL: "Israel", EG: "Egypt",
  GB: "United Kingdom", US: "United States", CA: "Canada", AU: "Australia",
  NZ: "New Zealand", SG: "Singapore", MY: "Malaysia", ID: "Indonesia",
  PH: "Philippines", TH: "Thailand", VN: "Vietnam", JP: "Japan",
  KR: "South Korea", CN: "China", HK: "Hong Kong", MO: "Macao",
  TW: "Taiwan", MM: "Myanmar", KH: "Cambodia", LA: "Laos",
  NL: "Netherlands", BE: "Belgium", FR: "France", ES: "Spain", IT: "Italy",
  DE: "Germany", AT: "Austria", CH: "Switzerland", SE: "Sweden",
  NO: "Norway", DK: "Denmark", FI: "Finland", IE: "Ireland", PT: "Portugal",
  PL: "Poland", CZ: "Czechia", HU: "Hungary", RO: "Romania", GR: "Greece",
  RU: "Russia", TR: "Turkey", UA: "Ukraine", ZA: "South Africa",
  NG: "Nigeria", KE: "Kenya", GH: "Ghana", MX: "Mexico", BR: "Brazil",
  AR: "Argentina", CL: "Chile", CO: "Colombia", PE: "Peru", VE: "Venezuela",
  CU: "Cuba", DO: "Dominican Republic", JM: "Jamaica", TT: "Trinidad and Tobago",
  BS: "Bahamas", BB: "Barbados", PR: "Puerto Rico",
};
function isoToCountryName(iso) {
  if (!iso) return "";
  return ISO_TO_COUNTRY_NAME[iso.toUpperCase()] || iso.toUpperCase();
}
function phoneCountry(phoneRaw) {
  if (!phoneRaw) return null;
  // Strip everything but digits and the leading +. UAE etc. often arrives as
  // "+971 50 ..." or "00971 50 ..." (international prefix variant).
  let s = String(phoneRaw).replace(/[^\d+]/g, "");
  if (s.startsWith("00")) s = "+" + s.slice(2);
  if (!s.startsWith("+")) return null;
  const digits = s.slice(1);
  for (const [code, iso] of PHONE_PREFIX_ISO) {
    if (digits.startsWith(code)) return { iso, flag: isoToFlag(iso) };
  }
  return null;
}
// Last-resort country detection from text (lead.country / city / location
// columns aren't in schema today but the meta-ads / sales-portal feeds
// sometimes leak a country string into one of these fields).
function locationCountry(text) {
  if (!text) return null;
  const t = String(text).toLowerCase();
  const map = [
    ["india","IN"],["pakistan","PK"],["bangladesh","BD"],["sri lanka","LK"],
    ["nepal","NP"],["uae","AE"],["united arab","AE"],["dubai","AE"],
    ["abu dhabi","AE"],["saudi","SA"],["riyadh","SA"],["jeddah","SA"],
    ["qatar","QA"],["doha","QA"],["kuwait","KW"],["bahrain","BH"],
    ["oman","OM"],["muscat","OM"],["uk","GB"],["united kingdom","GB"],
    ["london","GB"],["usa","US"],["united states","US"],["canada","CA"],
    ["australia","AU"],["singapore","SG"],["malaysia","MY"],
  ];
  for (const [needle, iso] of map) {
    if (t.includes(needle)) return { iso, flag: isoToFlag(iso) };
  }
  return null;
}
function leadCountry(lead) {
  return phoneCountry(lead.phone)
    || locationCountry(lead.country)
    || locationCountry(lead.city)
    || locationCountry(lead.location)
    || null;
}
// Compact created-at label for the card. Today → "Today, 3:45 PM",
// yesterday → "Yesterday, 9:12 AM", older → "Apr 28, 2:00 PM".
function fmtCardCreated(iso) {
  if (!iso) return "";
  const d = new Date(iso);
  if (!isFinite(d.getTime())) return "";
  const now = new Date();
  const sameDay = d.toDateString() === now.toDateString();
  const yest = new Date(now); yest.setDate(yest.getDate() - 1);
  const isYest = d.toDateString() === yest.toDateString();
  const time = d.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
  if (sameDay) return "Today, " + time;
  if (isYest) return "Yesterday, " + time;
  const date = d.toLocaleDateString(undefined, { month: "short", day: "numeric" });
  // Show year only if it's not the current year — keeps the card tight.
  if (d.getFullYear() !== now.getFullYear()) {
    return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }) + ", " + time;
  }
  return date + ", " + time;
}

function CRMCard({ lead, onClick, onDragStart, onDragEnd, dragging }) {
  const stage = LEAD_STAGE_BY_ID[lead.stage] || LEAD_STAGES[0];
  const overdue = leadOverdue(lead);
  const today = leadDueToday(lead);
  const cc = leadCountry(lead);
  const createdLabel = fmtCardCreated(lead.created_at);
  return (
    <div className={`crm-card ${dragging ? "is-dragging" : ""} ${overdue ? "is-overdue" : ""}`}
         draggable
         onClick={onClick}
         onDragStart={(e) => { e.dataTransfer.setData("text/plain", lead.id); onDragStart(); }}
         onDragEnd={onDragEnd}
         style={{ "--stage-color": stage.color }}>
      <div className="crm-card-head">
        {cc && (
          <span className="crm-card-flag"
                title={`Country: ${cc.iso}`}
                aria-label={`Country ${cc.iso}`}>
            {cc.flag}
          </span>
        )}
        <div className="crm-card-name" title={lead.name}>{lead.name}</div>
        {lead.value != null && <div className="crm-card-value">₹{Number(lead.value).toLocaleString()}</div>}
      </div>
      <div className="crm-card-meta">
        {lead.course_interest && <span className="crm-pill">{lead.course_interest}</span>}
        {lead.source === "meta_ad" && <span className="crm-pill crm-pill-meta">Meta</span>}
      </div>
      {(lead.phone || lead.email) && (
        <div className="crm-card-contact">
          {lead.phone && <a href={"tel:" + lead.phone} onClick={(e) => e.stopPropagation()}>📞 {lead.phone}</a>}
          {lead.email && <span className="crm-card-email">{lead.email}</span>}
        </div>
      )}
      {createdLabel && (
        <div className="crm-card-created" title={"Created " + (lead.created_at || "")}>
          🗓 {createdLabel}
        </div>
      )}
      {lead.next_action_at && (
        <div className={`crm-card-due ${overdue ? "overdue" : today ? "today" : ""}`}>
          ⏰ {fmtRel(lead.next_action_at)}
          {lead.next_action_note ? " · " + lead.next_action_note : ""}
        </div>
      )}
    </div>
  );
}

// ── Drawer ───────────────────────────────────────────────────────────────
function CRMDrawer({ lead, people, currentUserId, onClose, onPatch, onStageChange, onAfterDemoLost, onDelete }) {
  const [tab, setTab] = React.useState("details");
  const [activity, setActivity] = React.useState([]);
  const [loadingAct, setLoadingAct] = React.useState(false);

  React.useEffect(() => {
    let cancelled = false;
    setLoadingAct(true);
    api.leads.get(lead.id).then(full => {
      if (cancelled) return;
      setActivity(Array.isArray(full.activity) ? full.activity : []);
      setLoadingAct(false);
    }).catch(() => { if (!cancelled) setLoadingAct(false); });
    return () => { cancelled = true; };
  }, [lead.id]);

  async function logActivity(kind, details) {
    try {
      const created = await api.leads.addActivity(lead.id, { kind, details });
      setActivity(prev => [created, ...prev]);
      // Most activities now have side-effects on the lead row (stage, demo_at,
      // next_action_at). Always refresh after writing so the kanban card and
      // the drawer header reflect the new stage immediately.
      const fresh = await api.leads.get(lead.id);
      const arr = getLeads();
      const idx = arr.findIndex(l => l.id === lead.id);
      const fromStage = lead.stage;
      if (idx !== -1) {
        const merged = { ...arr[idx], ...fresh };
        delete merged.activity;
        arr[idx] = merged;
      }
      onPatch && onPatch(lead.id, {});  // bump parent's render counter cheaply

      // Demo-done with "not interested" → backend moves the lead to "lost".
      // Surface the lost-reason modal so we still capture WHY.
      if (kind === "demo_done" && details && details.outcome === "not interested" && fresh.stage === "lost") {
        onAfterDemoLost && onAfterDemoLost(lead.id, fromStage);
      }
    } catch (e) {
      alert("Could not log: " + (e && e.message));
    }
  }

  return (
    <div className="crm-drawer-shade" onClick={onClose}>
      <aside className="crm-drawer" onClick={(e) => e.stopPropagation()}>
        <div className="crm-drawer-head">
          <div>
            <div className="crm-drawer-name">{lead.name}</div>
            <div className="crm-drawer-sub">{lead.id} · {LEAD_STAGE_BY_ID[lead.stage]?.label}</div>
          </div>
          <button className="crm-icon-btn" onClick={onClose} aria-label="Close">✕</button>
        </div>

        <div className="crm-drawer-tabs">
          {[
            { id: "details",  label: "Details" },
            { id: "activity", label: "Activity" },
            { id: "reminder", label: "Reminders" },
          ].map(t => (
            <button key={t.id} className={tab === t.id ? "is-active" : ""} onClick={() => setTab(t.id)}>
              {t.label}
            </button>
          ))}
        </div>

        <div className="crm-drawer-body">
          {tab === "details" && (
            <DetailsTab lead={lead} people={people}
                        onPatch={onPatch}
                        onStageChange={onStageChange}
                        onDelete={onDelete}/>
          )}
          {tab === "activity" && (
            <ActivityTab loading={loadingAct} activity={activity}
                         onLogCall={(d) => logActivity("call", d)}
                         onLogNote={(d) => logActivity("note", d)}
                         onScheduleDemo={(d) => logActivity("demo_scheduled", d)}
                         onCompleteDemo={(d) => logActivity("demo_done", d)}/>
          )}
          {tab === "reminder" && (
            <ReminderTab lead={lead}
                         onSet={(when, note) => logActivity("reminder_set", { when, note })}/>
          )}
        </div>
      </aside>
    </div>
  );
}

// Format helpers for the lead's created_at / updated_at timestamps.
// Kept top-level so they can be reused if the lead row or the queue
// view ever wants the same labels.
function _fmtLeadDateTime(iso) {
  if (!iso) return "—";
  const d = new Date(iso);
  if (!isFinite(d.getTime())) return "—";
  // Locale string + explicit time so the user always sees the date AND
  // the time of day. Browser picks IST formatting on this user's
  // profile (their timezone is set in /api/users/me).
  return d.toLocaleString(undefined, {
    year: "numeric", month: "short", day: "numeric",
    hour: "numeric", minute: "2-digit",
  });
}
function _fmtLeadAgo(iso) {
  if (!iso) return "";
  const t = new Date(iso).getTime();
  if (!isFinite(t)) return "";
  const ms = Math.max(0, Date.now() - t);
  if (ms < 60_000)      return "just now";
  if (ms < 3_600_000)   return Math.round(ms / 60_000) + "m ago";
  if (ms < 86_400_000)  return Math.round(ms / 3_600_000) + "h ago";
  if (ms < 2_592_000_000) return Math.round(ms / 86_400_000) + "d ago";
  return Math.round(ms / 2_592_000_000) + "mo ago";
}

function DetailsTab({ lead, people, onPatch, onStageChange, onDelete }) {
  const [form, setForm] = React.useState({
    name: lead.name || "",
    phone: lead.phone || "",
    email: lead.email || "",
    course_interest: lead.course_interest || "",
    value: lead.value == null ? "" : lead.value,
    owner_id: lead.owner_id || "",
    stage: lead.stage,
    notes: lead.notes || "",
  });
  React.useEffect(() => {
    setForm({
      name: lead.name || "",
      phone: lead.phone || "",
      email: lead.email || "",
      course_interest: lead.course_interest || "",
      value: lead.value == null ? "" : lead.value,
      owner_id: lead.owner_id || "",
      stage: lead.stage,
      notes: lead.notes || "",
    });
  }, [lead.id]);

  function commit(field, raw) {
    const v = field === "value" ? (raw === "" ? null : Number(raw)) : raw;
    setForm(f => ({ ...f, [field]: raw }));
    if (field === "stage") {
      // Route stage edits through the parent's chokepoint so the lost-reason
      // modal still fires when the user picks "Lost" from this dropdown.
      onStageChange && onStageChange(lead.id, raw);
      return;
    }
    onPatch(lead.id, { [field]: v });
  }

  return (
    <div className="crm-form">
      <label className="crm-row">
        <span>Name</span>
        <input value={form.name} onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))}
               onBlur={(e) => commit("name", e.target.value)}/>
      </label>
      <label className="crm-row">
        <span>Phone</span>
        <input value={form.phone} onChange={(e) => setForm(f => ({ ...f, phone: e.target.value }))}
               onBlur={(e) => commit("phone", e.target.value)}/>
      </label>
      <label className="crm-row">
        <span>Email</span>
        <input type="email" value={form.email} onChange={(e) => setForm(f => ({ ...f, email: e.target.value }))}
               onBlur={(e) => commit("email", e.target.value)}/>
      </label>
      <label className="crm-row">
        <span>Course interest</span>
        <CoursePicker value={form.course_interest}
                      onChange={(v) => commit("course_interest", v)}/>
      </label>
      <label className="crm-row">
        <span>Stage</span>
        <select value={form.stage} onChange={(e) => commit("stage", e.target.value)}>
          {LEAD_STAGES.map(s => <option key={s.id} value={s.id}>{s.label}</option>)}
        </select>
      </label>
      <label className="crm-row">
        <span>Owner</span>
        <select value={form.owner_id} onChange={(e) => commit("owner_id", e.target.value)}>
          <option value="">— Unassigned —</option>
          {people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
        </select>
      </label>
      {/* Sales-portal rep — separate from Owner. Owner is the
          workspace user accountable internally; the sales rep is
          the field rep with a /sales mobile login. */}
      <SalesRepPicker
        value={lead.sales_owner_id || ""}
        onChange={(v) => commit("sales_owner_id", v || null)}/>
      <label className="crm-row">
        <span>Value (₹)</span>
        <input type="number" value={form.value}
               onChange={(e) => setForm(f => ({ ...f, value: e.target.value }))}
               onBlur={(e) => commit("value", e.target.value)}/>
      </label>
      <label className="crm-row crm-row-full">
        <span>Notes</span>
        <textarea rows={4} value={form.notes}
                  onChange={(e) => setForm(f => ({ ...f, notes: e.target.value }))}
                  onBlur={(e) => commit("notes", e.target.value)}/>
      </label>

      {/* When this lead landed in the CRM. Both the absolute date /
          time (in the user's locale, IST by default) and a relative
          "X ago" label so the row reads as "added on May 8, 2026 at
          3:42 PM (2h ago)". Read-only — the timestamp is set by the
          DB on insert and isn't user-editable. */}
      <label className="crm-row crm-row-full">
        <span>Created</span>
        <div style={{
          display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap",
          padding: "8px 10px", color: "var(--ink-strong)", fontSize: 13,
        }}>
          <span style={{ fontWeight: 600 }}>{_fmtLeadDateTime(lead.created_at)}</span>
          {lead.created_at && (
            <span style={{
              fontSize: 11.5, color: "var(--ink-muted)",
              background: "rgba(0,0,0,.04)",
              padding: "1px 8px", borderRadius: 999,
              fontWeight: 500,
            }}>{_fmtLeadAgo(lead.created_at)}</span>
          )}
          {lead.updated_at && lead.updated_at !== lead.created_at && (
            <span style={{ fontSize: 11.5, color: "var(--ink-muted)", marginLeft: "auto" }}>
              Last updated {_fmtLeadAgo(lead.updated_at)}
            </span>
          )}
        </div>
      </label>

      {/* Campaign details — shown for any lead that came from Meta. Fields
          can be empty if the page access token wasn't configured at intake
          time; the Refresh button re-fetches them on demand. */}
      <CampaignDetails lead={lead} onRefreshed={() => onPatch && onPatch(lead.id, {})}/>

      <div className="crm-row crm-row-full">
        <button className="crm-btn-danger" onClick={onDelete}>Delete lead</button>
      </div>
    </div>
  );
}

// ── Campaign details panel ──────────────────────────────────────────────
// Rendered inside the Details tab. Shows where a lead came from in the
// Meta hierarchy (Campaign → Adset → Ad → Form), platform, and the time
// Meta says the lead was submitted. A "Refresh" button hits the new
// /api/meta/leads/:id/refresh endpoint to re-pull the latest names from
// the Graph API — useful after a campaign rename or for legacy leads
// ingested before campaign-detail enrichment shipped.
function CampaignDetails({ lead, onRefreshed }) {
  const [open, setOpen]       = React.useState(true);
  const [busy, setBusy]       = React.useState(false);
  const [flash, setFlash]     = React.useState("");
  const [error, setError]     = React.useState("");

  // Hide entirely for non-Meta leads — keeps the drawer clean for manual
  // / Zapier intake. We accept either the canonical source = meta_ad or
  // the presence of any Meta identifier on the lead.
  const isMeta =
    lead.source === "meta_ad" ||
    !!lead.meta_lead_id || !!lead.meta_ad_id || !!lead.meta_form_id;
  if (!isMeta) return null;

  // Compute the human-readable summary inside the row labels so missing
  // fields collapse to a faint "—".
  const empty = "—";
  const prettyTime = lead.meta_lead_created_time
    ? new Date(lead.meta_lead_created_time).toLocaleString()
    : empty;
  const platform = lead.meta_platform
    ? lead.meta_platform.charAt(0).toUpperCase() + lead.meta_platform.slice(1)
    : empty;

  const hasNoNames =
    !lead.meta_campaign_name && !lead.meta_adset_name && !lead.meta_ad_name && !lead.meta_form_name;

  async function refresh() {
    setBusy(true); setError(""); setFlash("");
    try {
      const r = await api.meta.refreshLead(lead.id);
      setFlash("✓ Refreshed");
      setTimeout(() => setFlash(""), 1600);
      // Surface the freshly-stored names back onto the lead row so the
      // labels update without a full reload. We mutate in-place because
      // the parent already owns the LEADS array.
      const arr = (typeof getLeads === "function" ? getLeads() : []) || [];
      const idx = arr.findIndex(l => l.id === lead.id);
      if (idx !== -1 && r && r.details) {
        const d = r.details;
        Object.assign(arr[idx], {
          meta_ad_name           : d.ad_name           || null,
          meta_adset_id          : d.adset_id          || null,
          meta_adset_name        : d.adset_name        || null,
          meta_real_campaign_id  : d.campaign_id       || null,
          meta_campaign_name     : d.campaign_name     || null,
          meta_form_name         : d.form_name         || null,
          meta_platform          : d.platform          || arr[idx].meta_platform,
          meta_lead_created_time : d.lead_created_time || arr[idx].meta_lead_created_time,
        });
      }
      onRefreshed && onRefreshed();
    } catch (e) {
      const msg = (e && e.message) || "Failed to refresh.";
      // Most likely failure is the page access token isn't set on the
      // server. Surface a tailored hint so the user knows where to go.
      if (msg === "meta_token_unset") {
        setError("Set the Meta page access token in CRM → Meta integration first.");
      } else {
        setError(msg);
      }
    } finally {
      setBusy(false);
    }
  }

  return (
    <div className="crm-row crm-row-full crm-camp-wrap">
      <div className="crm-camp-head" onClick={() => setOpen(o => !o)} role="button">
        <span className="crm-camp-title">
          📣 Campaign details
          {lead.meta_platform && (
            <span className={`crm-camp-platform-tag is-${lead.meta_platform}`}>
              {platform}
            </span>
          )}
        </span>
        <span className={`crm-camp-caret ${open ? "is-open" : ""}`}>▾</span>
      </div>

      {open && (
        <div className="crm-camp-body">
          {hasNoNames && (
            <div className="crm-camp-hint">
              Campaign names haven't been fetched yet. Click <b>Refresh</b> to
              pull them from Meta — make sure the page access token has the
              <code> ads_read</code> permission.
            </div>
          )}

          <div className="crm-camp-grid">
            <div className="crm-camp-field">
              <div className="crm-camp-label">Campaign</div>
              <div className="crm-camp-value">
                {lead.meta_campaign_name || empty}
                {lead.meta_real_campaign_id && (
                  <code className="crm-camp-id">{lead.meta_real_campaign_id}</code>
                )}
              </div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Ad set</div>
              <div className="crm-camp-value">
                {lead.meta_adset_name || empty}
                {lead.meta_adset_id && (
                  <code className="crm-camp-id">{lead.meta_adset_id}</code>
                )}
              </div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Ad</div>
              <div className="crm-camp-value">
                {lead.meta_ad_name || empty}
                {lead.meta_ad_id && (
                  <code className="crm-camp-id">{lead.meta_ad_id}</code>
                )}
              </div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Lead form</div>
              <div className="crm-camp-value">
                {lead.meta_form_name || empty}
                {lead.meta_form_id && (
                  <code className="crm-camp-id">{lead.meta_form_id}</code>
                )}
              </div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Submitted on Meta</div>
              <div className="crm-camp-value">{prettyTime}</div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Page</div>
              <div className="crm-camp-value">
                {lead.meta_page_id ? <code className="crm-camp-id">{lead.meta_page_id}</code> : empty}
              </div>
            </div>
            <div className="crm-camp-field">
              <div className="crm-camp-label">Leadgen ID</div>
              <div className="crm-camp-value">
                {lead.meta_lead_id ? <code className="crm-camp-id">{lead.meta_lead_id}</code> : empty}
              </div>
            </div>
          </div>

          <div className="crm-camp-actions">
            <button type="button" className="btn"
                    onClick={refresh} disabled={busy}>
              {busy ? "Refreshing…" : "Refresh from Meta"}
            </button>
            {flash && <span className="crm-camp-flash">{flash}</span>}
            {error && <span className="crm-camp-error">{error}</span>}
            {lead.meta_real_campaign_id && (
              <a className="crm-camp-link"
                 href={`https://business.facebook.com/adsmanager/manage/campaigns?selected_campaign_ids=${encodeURIComponent(lead.meta_real_campaign_id)}`}
                 target="_blank" rel="noopener noreferrer">
                Open in Ads Manager ↗
              </a>
            )}
          </div>

          {/* ── Ad creative panel ─────────────────────────────────
              Shows the actual content the lead clicked on — image
              or video thumbnail, headline, body copy, and a link to
              the live FB post. Hidden when no creative was captured
              (older leads pre-migration 032, or fetch failures). */}
          <CreativePanel lead={lead}/>
        </div>
      )}
    </div>
  );
}

function ActivityTab({ loading, activity, onLogCall, onLogNote, onScheduleDemo, onCompleteDemo }) {
  // Composer state — inline forms instead of browser prompt() dialogs.
  const [open, setOpen] = React.useState(null);   // "call" | "note" | "demo_sched" | "demo_done" | null
  const [busy, setBusy] = React.useState(false);

  // Per-form scratch state. Reset whenever the active form changes.
  const [callOutcome, setCallOutcome] = React.useState("answered");
  const [callNote, setCallNote] = React.useState("");
  const [noteText, setNoteText] = React.useState("");
  const [demoWhen, setDemoWhen] = React.useState(() => {
    const d = new Date(); d.setDate(d.getDate() + 1); d.setHours(11, 0, 0, 0);
    return d.toISOString().slice(0, 16);
  });
  const [demoNote, setDemoNote] = React.useState("");
  const [demoOutcome, setDemoOutcome] = React.useState("interested");
  const [demoOutcomeNote, setDemoOutcomeNote] = React.useState("");

  function close() { setOpen(null); setBusy(false); }
  async function run(fn) {
    setBusy(true);
    try { await fn(); close(); }
    finally { setBusy(false); }
  }

  function actionButton(id, emoji, label) {
    return (
      <button type="button"
              className={`crm-act-trigger ${open === id ? "is-open" : ""}`}
              onClick={() => setOpen(o => o === id ? null : id)}>
        <span className="crm-act-trigger-emoji">{emoji}</span>{label}
      </button>
    );
  }

  return (
    <div className="crm-activity">
      <div className="crm-activity-actions">
        {actionButton("call",       "📞", "Log call")}
        {actionButton("demo_sched", "📅", "Schedule demo")}
        {actionButton("demo_done",  "🎬", "Demo done")}
        {actionButton("note",       "✏️", "Add note")}
      </div>

      {open === "call" && (
        <form className="crm-act-composer" onSubmit={(e) => {
          e.preventDefault();
          run(() => onLogCall({ outcome: callOutcome, note: callNote || undefined }));
        }}>
          <label className="crm-row">
            <span>Outcome</span>
            <select value={callOutcome} onChange={(e) => setCallOutcome(e.target.value)}>
              <option value="answered">Answered</option>
              <option value="not answered">Not answered</option>
              <option value="busy">Busy / call back</option>
              <option value="wrong number">Wrong number</option>
              <option value="wants demo">Wants demo</option>
              <option value="not interested">Not interested</option>
            </select>
          </label>
          <label className="crm-row crm-row-full">
            <span>Note (optional)</span>
            <input value={callNote} onChange={(e) => setCallNote(e.target.value)}
                   placeholder="e.g. interested in JEE foundation; budget concern"/>
          </label>
          <div className="crm-act-composer-actions">
            <button type="button" className="btn" onClick={close}>Cancel</button>
            <button type="submit" className="btn btn-primary" disabled={busy}>
              {busy ? "Saving…" : "Log call"}
            </button>
          </div>
        </form>
      )}

      {open === "note" && (
        <form className="crm-act-composer" onSubmit={(e) => {
          e.preventDefault();
          if (!noteText.trim()) return;
          run(() => onLogNote({ text: noteText.trim() })).then(() => setNoteText(""));
        }}>
          <label className="crm-row crm-row-full">
            <span>Note</span>
            <textarea autoFocus rows={3} value={noteText}
                      onChange={(e) => setNoteText(e.target.value)}
                      placeholder="Anything important to remember about this lead…"/>
          </label>
          <div className="crm-act-composer-actions">
            <button type="button" className="btn" onClick={close}>Cancel</button>
            <button type="submit" className="btn btn-primary" disabled={busy || !noteText.trim()}>
              {busy ? "Saving…" : "Save note"}
            </button>
          </div>
        </form>
      )}

      {open === "demo_sched" && (
        <form className="crm-act-composer" onSubmit={(e) => {
          e.preventDefault();
          if (!demoWhen) return;
          const when = demoWhen.replace("T", " ") + ":00";
          run(() => onScheduleDemo({ when, note: demoNote || undefined }));
        }}>
          <label className="crm-row">
            <span>Date / time</span>
            <input type="datetime-local" value={demoWhen}
                   onChange={(e) => setDemoWhen(e.target.value)}/>
          </label>
          <label className="crm-row crm-row-full">
            <span>Note (optional)</span>
            <input value={demoNote} onChange={(e) => setDemoNote(e.target.value)}
                   placeholder="e.g. Zoom link sent, parent attending"/>
          </label>
          <div className="crm-act-composer-actions">
            <button type="button" className="btn" onClick={close}>Cancel</button>
            <button type="submit" className="btn btn-primary" disabled={busy || !demoWhen}>
              {busy ? "Saving…" : "Schedule demo"}
            </button>
          </div>
        </form>
      )}

      {open === "demo_done" && (
        <form className="crm-act-composer" onSubmit={(e) => {
          e.preventDefault();
          run(() => onCompleteDemo({ outcome: demoOutcome, note: demoOutcomeNote || undefined }));
        }}>
          <label className="crm-row">
            <span>Outcome</span>
            <select value={demoOutcome} onChange={(e) => setDemoOutcome(e.target.value)}>
              <option value="interested">Interested</option>
              <option value="undecided">Undecided</option>
              <option value="not interested">Not interested</option>
            </select>
          </label>
          <label className="crm-row crm-row-full">
            <span>Note (optional)</span>
            <input value={demoOutcomeNote} onChange={(e) => setDemoOutcomeNote(e.target.value)}
                   placeholder="e.g. wants weekend batch; needs fee discount"/>
          </label>
          <div className="crm-act-composer-actions">
            <button type="button" className="btn" onClick={close}>Cancel</button>
            <button type="submit" className="btn btn-primary" disabled={busy}>
              {busy ? "Saving…" : "Mark demo done"}
            </button>
          </div>
        </form>
      )}

      <div className="crm-timeline">
        {loading && <div className="crm-empty">Loading activity…</div>}
        {!loading && activity.length === 0 && <div className="crm-empty">No activity yet — log a call or note to get started.</div>}
        {activity.map(a => (
          <div key={a.id} className="crm-act">
            <div className="crm-act-head">
              <span className={`crm-act-kind crm-act-${a.kind}`}>{(a.kind || "").replace(/_/g, " ")}</span>
              <span className="crm-act-time">{fmtRel(a.happened_at)}</span>
            </div>
            {a.details && (
              <div className="crm-act-body">
                {Object.entries(a.details).map(([k, v]) => (
                  <div key={k}>
                    <b>{k.replace(/_/g, " ")}:</b>{" "}
                    {v == null ? "—" : (typeof v === "string" ? v : JSON.stringify(v))}
                  </div>
                ))}
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function ReminderTab({ lead, onSet }) {
  const [when, setWhen] = React.useState(lead.next_action_at ? lead.next_action_at.slice(0, 16) : "");
  const [note, setNote] = React.useState(lead.next_action_note || "");

  function quick(daysFromNow) {
    const d = new Date();
    d.setDate(d.getDate() + daysFromNow);
    d.setHours(10, 0, 0, 0);
    setWhen(d.toISOString().slice(0, 16));
  }

  return (
    <div className="crm-reminder">
      {lead.next_action_at && (
        <div className={`crm-reminder-current ${leadOverdue(lead) ? "overdue" : ""}`}>
          Current reminder: <b>{new Date(lead.next_action_at).toLocaleString()}</b>
          {lead.next_action_note ? " — " + lead.next_action_note : ""}
        </div>
      )}
      <div className="crm-quick">
        <button onClick={() => quick(0)}>Today</button>
        <button onClick={() => quick(1)}>Tomorrow</button>
        <button onClick={() => quick(3)}>+3 days</button>
        <button onClick={() => quick(7)}>+1 week</button>
      </div>
      <label className="crm-row">
        <span>Date / time</span>
        <input type="datetime-local" value={when} onChange={(e) => setWhen(e.target.value)}/>
      </label>
      <label className="crm-row crm-row-full">
        <span>Note</span>
        <input value={note} placeholder="e.g. follow up about fee discount"
               onChange={(e) => setNote(e.target.value)}/>
      </label>
      <button className="btn btn-primary" disabled={!when}
              onClick={() => onSet(when.replace("T", " ") + ":00", note)}>
        Set reminder
      </button>
    </div>
  );
}

function NewLeadModal({ onClose, onCreate, currentUserId, people }) {
  const [form, setForm] = React.useState({
    name: "", phone: "", email: "",
    course_interest: COURSE_OPTIONS[0],
    value: "",
    owner_id: currentUserId || "",
    source: "manual",
    notes: "",
  });

  function submit(e) {
    e && e.preventDefault();
    if (!form.name.trim() && !form.phone.trim() && !form.email.trim()) {
      alert("Please enter at least a name, phone, or email.");
      return;
    }
    const payload = {
      ...form,
      value: form.value === "" ? null : Number(form.value),
    };
    onCreate(payload);
  }

  return (
    <div className="crm-modal-shade" onClick={onClose}>
      <div className="crm-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crm-modal-head">
          <h2>New lead</h2>
          <button className="crm-icon-btn" onClick={onClose}>✕</button>
        </div>
        <form className="crm-form" onSubmit={submit}>
          <label className="crm-row">
            <span>Name *</span>
            <input autoFocus value={form.name}
                   onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))}/>
          </label>
          <label className="crm-row">
            <span>Phone</span>
            <input value={form.phone} onChange={(e) => setForm(f => ({ ...f, phone: e.target.value }))}/>
          </label>
          <label className="crm-row">
            <span>Email</span>
            <input type="email" value={form.email}
                   onChange={(e) => setForm(f => ({ ...f, email: e.target.value }))}/>
          </label>
          <label className="crm-row">
            <span>Course</span>
            <CoursePicker value={form.course_interest} allowEmpty={false}
                          onChange={(v) => setForm(f => ({ ...f, course_interest: v }))}/>
          </label>
          <label className="crm-row">
            <span>Source</span>
            <select value={form.source}
                    onChange={(e) => setForm(f => ({ ...f, source: e.target.value }))}>
              <option value="manual">Manual</option>
              <option value="meta_ad">Meta ad</option>
              <option value="referral">Referral</option>
              <option value="walkin">Walk-in</option>
              <option value="other">Other</option>
            </select>
          </label>
          <label className="crm-row">
            <span>Owner</span>
            <select value={form.owner_id}
                    onChange={(e) => setForm(f => ({ ...f, owner_id: e.target.value }))}>
              <option value="">— Unassigned —</option>
              {people.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
            </select>
          </label>
          <label className="crm-row">
            <span>Value (₹)</span>
            <input type="number" value={form.value}
                   onChange={(e) => setForm(f => ({ ...f, value: e.target.value }))}/>
          </label>
          <label className="crm-row crm-row-full">
            <span>Notes</span>
            <textarea rows={3} value={form.notes}
                      onChange={(e) => setForm(f => ({ ...f, notes: e.target.value }))}/>
          </label>
          <div className="crm-modal-actions">
            <button type="button" className="btn" onClick={onClose}>Cancel</button>
            <button type="submit" className="btn btn-primary">Create lead</button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ── Course picker ──────────────────────────────────────────────────────
// A small dropdown wrapper that lets the user add a brand-new course on the
// fly. The built-in list lives in leads-data.jsx; user additions are stored
// in localStorage so they survive reloads.  When the user picks the sentinel
// "+ Add new course…" option we swap to an inline input + Save / Cancel.
function CoursePicker({ value, onChange, allowEmpty = true, autoFocus = false }) {
  const [adding, setAdding]   = React.useState(false);
  const [draft, setDraft]     = React.useState("");
  const [tick, setTick]       = React.useState(0);   // re-render after addCourseOption
  const options = getCourseOptions();
  const isCustom = value && !options.includes(value);

  function commitAdd() {
    const name = addCourseOption(draft);
    if (!name) { setAdding(false); setDraft(""); return; }
    onChange(name);
    setAdding(false);
    setDraft("");
    setTick(t => t + 1);
  }
  function cancelAdd() { setAdding(false); setDraft(""); }

  if (adding) {
    return (
      <div className="crm-course-add">
        <input autoFocus value={draft}
               placeholder="New course name (e.g. CUET — English)"
               onChange={(e) => setDraft(e.target.value)}
               onKeyDown={(e) => {
                 if (e.key === "Enter")  { e.preventDefault(); commitAdd(); }
                 if (e.key === "Escape") { e.preventDefault(); cancelAdd(); }
               }}/>
        <div className="crm-course-add-actions">
          <button type="button" className="btn" onClick={cancelAdd}>Cancel</button>
          <button type="button" className="btn btn-primary"
                  onClick={commitAdd} disabled={!draft.trim()}>Add</button>
        </div>
      </div>
    );
  }

  return (
    <select
      data-tick={tick}
      autoFocus={autoFocus}
      value={isCustom ? value : (value || "")}
      onChange={(e) => {
        const v = e.target.value;
        if (v === "__add__") { setAdding(true); return; }
        onChange(v);
      }}>
      {allowEmpty && <option value="">—</option>}
      {options.map(c => <option key={c} value={c}>{c}</option>)}
      {isCustom && <option value={value}>{value}</option>}
      <option value="__add__">+ Add new course…</option>
    </select>
  );
}

// ── Lost-reason modal ──────────────────────────────────────────────────
// Pops whenever a lead is being moved into "Lost" so we always capture WHY.
// Reasons get stamped into notes (so they show up in lead history) and a
// structured `note` activity is logged with `lost_reason` / `lost_note` for
// later reporting on win/loss.
function LostReasonModal({ leadName, alreadyMoved, onConfirm, onCancel }) {
  const PRESETS = [
    "Price / fees",
    "Wrong fit",
    "Ghosted",
    "Competitor",
    "Timing",
    "Already enrolled",
    "Other",
  ];
  const [reason, setReason] = React.useState("");
  const [note, setNote] = React.useState("");
  const [busy, setBusy] = React.useState(false);

  function submit(e) {
    e && e.preventDefault();
    if (!reason && !note.trim()) return;
    setBusy(true);
    Promise.resolve(onConfirm({ reason: reason || "Other", note: note.trim() }))
      .finally(() => setBusy(false));
  }

  return (
    <div className="crm-modal-shade" onClick={busy ? undefined : onCancel}>
      <div className="crm-lost-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crm-lost-head">
          <span className="crm-lost-head-emoji" aria-hidden="true">📉</span>
          <h2>{alreadyMoved ? "Why was this lost?" : "Mark as lost"}</h2>
        </div>
        <form onSubmit={submit}>
          <div className="crm-lost-body">
            <p>
              {alreadyMoved
                ? <span><b>{leadName}</b> was just marked lost. Add a reason so we can learn from it.</span>
                : <span>Moving <b>{leadName}</b> to <b>Lost</b>. Tell us why — it helps tighten the funnel.</span>}
            </p>
            <div className="crm-lost-presets">
              {PRESETS.map(p => (
                <button type="button" key={p}
                        className={`crm-lost-preset ${reason === p ? "is-on" : ""}`}
                        onClick={() => setReason(p)}>
                  {p}
                </button>
              ))}
            </div>
            <label className="crm-row crm-row-full">
              <span>Details (optional)</span>
              <textarea rows={3} value={note} autoFocus
                        onChange={(e) => setNote(e.target.value)}
                        placeholder="What did they actually say? Worth noting for next time."/>
            </label>
          </div>
          <div className="crm-lost-actions">
            <button type="button" className="btn" onClick={onCancel} disabled={busy}>Cancel</button>
            <button type="submit" className="crm-btn-lost" disabled={busy || (!reason && !note.trim())}>
              {busy ? "Saving…" : "Confirm lost"}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

// ── Meta Ads integration modal ────────────────────────────────────────
// Shows the webhook URL Meta should point at, the current credential
// status, and the most recent intake events. Admins can edit the four
// Meta credentials directly from this panel — the values persist in the
// `app_settings` DB table and override the matching process.env on the
// next webhook event. Non-admins see the same form read-only.
// Single row in the Recent Intake list. Click the header to expand and
// see the raw JSON payload Meta sent (or that the sync run captured).
// Pretty-prints the JSON; copy-to-clipboard for the rare case where the
// user wants to forward it to support.
function RecentIntakeRow({ r }) {
  const [open, setOpen] = React.useState(false);
  const [copied, setCopied] = React.useState(false);
  const tagLabel = (r.source || "unknown").replace(/_/g, " ");

  const isError = r.source === "meta_webhook_error" || r.source === "meta_sync_error";
  const isSync  = r.source === "meta_sync_run";

  function copyRaw(e) {
    e.stopPropagation();
    if (!r.raw) return;
    const text = JSON.stringify(r.raw, null, 2);
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(() => {
        setCopied(true); setTimeout(() => setCopied(false), 1400);
      });
    }
  }

  return (
    <div className={`crm-meta-evt crm-meta-evt-${r.source || "unknown"} ${open ? "is-open" : ""} ${isError ? "is-error" : ""} ${isSync ? "is-sync" : ""}`}
         onClick={() => setOpen(o => !o)}
         role="button" tabIndex={0}
         onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen(o => !o); } }}>
      <div className="crm-meta-evt-row">
        <span className={`crm-meta-evt-tag tag-${r.source || "unknown"}`}>{tagLabel}</span>
        <span className="crm-meta-evt-time">
          {r.received_at ? fmtRel(r.received_at) : "—"}
          <span className="crm-meta-evt-caret">{open ? "▾" : "▸"}</span>
        </span>
      </div>
      <div className="crm-meta-evt-summary">{r.summary || "—"}</div>
      {r.lead_id && <div className="crm-meta-evt-lead">→ {r.lead_id}</div>}

      {open && (
        <div className="crm-meta-evt-detail" onClick={(e) => e.stopPropagation()}>
          <div className="crm-meta-evt-detail-head">
            <span>Raw payload</span>
            <button type="button" className="btn" onClick={copyRaw}
                    disabled={!r.raw}>{copied ? "Copied!" : "Copy JSON"}</button>
          </div>
          <pre className="crm-meta-evt-json">
            {r.raw ? JSON.stringify(r.raw, null, 2) : "(no payload stored)"}
          </pre>
        </div>
      )}
    </div>
  );
}

function MetaIntegrationModal({ onClose, onLeadsChanged, currentUserId }) {
  const [config, setConfig]   = React.useState(null);
  const [recent, setRecent]   = React.useState(null);
  const [error, setError]     = React.useState("");
  const [refreshing, setRefreshing] = React.useState(false);
  const [copied, setCopied]   = React.useState("");
  // Recent intake now lives in its own dedicated modal so it doesn't
  // share scroll real-estate with the long config form. Click the
  // button in the integration modal to open it.
  const [intakeOpen, setIntakeOpen] = React.useState(false);

  // Edit form state — only populated once we've loaded the config.
  // Secrets are *never* round-tripped: we send the field only if the user
  // typed something new (so re-saving the form without touching the secret
  // fields leaves them unchanged).
  const [form, setForm] = React.useState({ verifyToken: "", pageId: "", graphVersion: "" });
  const [secretEdits, setSecretEdits] = React.useState({ appSecret: "", pageAccessToken: "" });
  const [reveal, setReveal]   = React.useState({ appSecret: false, pageAccessToken: false });
  const [saving, setSaving]   = React.useState(false);
  const [savedFlash, setSavedFlash] = React.useState(false);

  // Determine if the acting user can edit (workspace admin or owner).
  const me = (typeof PEOPLE !== "undefined" ? PEOPLE : []).find(p => p.id === currentUserId);
  const canEdit = !me || me.wsRole === "admin" || me.wsRole === "owner";

  async function load() {
    setRefreshing(true);
    setError("");
    try {
      const [cfg, rec] = await Promise.all([
        api.meta.config(),
        api.meta.recent(20).catch(() => []),
      ]);
      setConfig(cfg);
      setForm({
        verifyToken : cfg.verifyToken || "",
        pageId      : cfg.pageId      || "",
        graphVersion: cfg.graphVersion|| "v19.0",
      });
      // Reset draft secret edits — the actual stored values aren't sent back
      // for safety, so the inputs render as empty placeholders below.
      setSecretEdits({ appSecret: "", pageAccessToken: "" });
      setRecent(Array.isArray(rec) ? rec : []);
    } catch (e) {
      setError((e && e.message) || "Failed to load Meta config.");
    } finally {
      setRefreshing(false);
    }
  }

  React.useEffect(() => { load(); /* eslint-disable-next-line */ }, []);

  // Live-refresh the modal whenever the server reports a sync run or a
  // new lead landing — keeps the "last run" line and Recent Intake list
  // honest without making the user click Refresh.
  React.useEffect(() => {
    let pending = null;
    function bump() {
      if (pending) return;
      pending = setTimeout(() => { pending = null; load(); }, 350);
    }
    window.addEventListener("flowboard:rt:meta.sync.complete", bump);
    window.addEventListener("flowboard:rt:leads.created", bump);
    return () => {
      if (pending) clearTimeout(pending);
      window.removeEventListener("flowboard:rt:meta.sync.complete", bump);
      window.removeEventListener("flowboard:rt:leads.created", bump);
    };
  }, []);

  async function save() {
    if (!canEdit || saving) return;
    setSaving(true); setError("");
    try {
      // Only send fields that actually changed. For secrets, "" means the
      // user wants to clear the override (fall back to env); a non-empty
      // value is the new secret.
      const body = {};
      if (form.verifyToken  !== (config.verifyToken  || ""))  body.verifyToken  = form.verifyToken;
      if (form.pageId       !== (config.pageId       || ""))  body.pageId       = form.pageId;
      if (form.graphVersion !== (config.graphVersion || "")) body.graphVersion = form.graphVersion;
      if (secretEdits.appSecret       !== "") body.appSecret       = secretEdits.appSecret;
      if (secretEdits.pageAccessToken !== "") body.pageAccessToken = secretEdits.pageAccessToken;

      if (Object.keys(body).length === 0) {
        setSavedFlash(true);
        setTimeout(() => setSavedFlash(false), 1400);
        return;
      }
      const updated = await api.meta.updateConfig(body);
      setConfig(updated);
      setForm({
        verifyToken : updated.verifyToken || "",
        pageId      : updated.pageId      || "",
        graphVersion: updated.graphVersion|| "v19.0",
      });
      setSecretEdits({ appSecret: "", pageAccessToken: "" });
      setReveal({ appSecret: false, pageAccessToken: false });
      setSavedFlash(true);
      setTimeout(() => setSavedFlash(false), 1800);
    } catch (e) {
      setError((e && e.message) || "Failed to save Meta config.");
    } finally {
      setSaving(false);
    }
  }

  // ── Auto-sync controls ──────────────────────────────────────────────
  const [syncBusy, setSyncBusy] = React.useState(false);
  const [syncMsg, setSyncMsg]   = React.useState("");

  async function runSyncNow(full = false) {
    if (syncBusy) return;
    setSyncBusy(true); setSyncMsg("");
    try {
      const r = await api.meta.sync({ full });
      if (r && r.ok) {
        const s = r.stats || {};
        setSyncMsg(`✓ Synced ${s.inserted || 0} new · ${s.scanned || 0} scanned · ${s.duplicates || 0} duplicates`);
        // Pull a fresh config so syncLastRun and stats refresh too.
        await load();
        onLeadsChanged && onLeadsChanged();
      } else {
        setSyncMsg(r && r.error ? `Failed: ${r.error}` : "Sync failed");
      }
    } catch (e) {
      setSyncMsg("Failed: " + ((e && e.message) || "unknown"));
    } finally {
      setSyncBusy(false);
      setTimeout(() => setSyncMsg(""), 6000);
    }
  }

  async function changeInterval(nextMin) {
    if (saving) return;
    setSaving(true); setError("");
    try {
      const updated = await api.meta.updateConfig({ syncIntervalMin: nextMin });
      setConfig(updated);
    } catch (e) {
      setError((e && e.message) || "Failed to update interval.");
    } finally {
      setSaving(false);
    }
  }

  async function clearSecret(which) {
    // "Clear" sends an empty string for the targeted secret, which removes
    // the DB override and falls back to the env var (if any).
    if (!canEdit || saving) return;
    if (!confirm(`Clear the stored ${which === "appSecret" ? "App Secret" : "Page Access Token"}?\nIt will fall back to the .env value if one is set.`)) return;
    setSaving(true); setError("");
    try {
      const body = {}; body[which] = "";
      const updated = await api.meta.updateConfig(body);
      setConfig(updated);
      setSecretEdits(s => ({ ...s, [which]: "" }));
      setSavedFlash(true);
      setTimeout(() => setSavedFlash(false), 1800);
    } catch (e) {
      setError((e && e.message) || "Failed to clear value.");
    } finally {
      setSaving(false);
    }
  }

  function copy(label, text) {
    if (!text) return;
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(text).then(() => {
        setCopied(label);
        setTimeout(() => setCopied(""), 1400);
      });
    }
  }

  const allReady =
    config && config.verifyTokenSet && config.appSecretSet && config.pageAccessTokenSet;

  return (
    <div className="crm-modal-shade" onClick={onClose}>
      <div className="crm-meta-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crm-modal-head">
          <h2>📣 Meta Ads integration</h2>
          <button className="crm-icon-btn" onClick={onClose}>✕</button>
        </div>

        <div className="crm-meta-body">
          {error && <div className="crm-meta-error">{error}</div>}

          {!config ? (
            <div className="crm-empty">Loading integration status…</div>
          ) : (
            <React.Fragment>
              <div className={`crm-meta-status ${allReady ? "is-ok" : "is-warn"}`}>
                <div className="crm-meta-status-dot"/>
                <div className="crm-meta-status-text">
                  {allReady
                    ? "Live — leads from Meta will appear here automatically."
                    : "Setup not finished — fill in the missing items below."}
                </div>
              </div>

              {/* Recent intake button — opens its own modal so the
                  intake history doesn't have to share scroll space
                  with the (long) config form below. The count chip
                  hints how many events are loaded. */}
              <div className="crm-meta-recent-head">
                <h3 className="crm-meta-h" style={{ margin: 0 }}>
                  Recent intake
                  {recent && (
                    <span style={{
                      marginLeft: 8,
                      background: "rgba(0,0,0,.06)",
                      color: "var(--ink-muted)",
                      borderRadius: 999,
                      padding: "1px 8px",
                      fontSize: 11,
                      fontWeight: 700,
                    }}>{recent.length}</span>
                  )}
                </h3>
                <div style={{ display: "flex", gap: 6 }}>
                  <button type="button" className="btn"
                          onClick={() => setIntakeOpen(true)}
                          disabled={!recent}>
                    View events
                  </button>
                  <button type="button" className="btn"
                          onClick={() => { load(); onLeadsChanged && onLeadsChanged(); }}
                          disabled={refreshing}>
                    {refreshing ? "Refreshing…" : "Refresh"}
                  </button>
                </div>
              </div>

              <h3 className="crm-meta-h">1. Webhook URL</h3>
              <p className="crm-meta-p">
                Paste this URL into Meta → App → Webhooks → Page → <b>leadgen</b> subscription.
              </p>
              <div className="crm-meta-copy-row">
                <code>{config.webhookUrl}</code>
                <button type="button" className="btn"
                        onClick={() => copy("webhook", config.webhookUrl)}>
                  {copied === "webhook" ? "Copied!" : "Copy"}
                </button>
              </div>

              <h3 className="crm-meta-h">2. API credentials</h3>
              <p className="crm-meta-p crm-meta-p-faint">
                Stored encrypted in the database — they override the matching
                environment variable on the next webhook event. Save changes
                instantly take effect; no server restart required.
                {!canEdit && <> <b>You're viewing read-only — only workspace admins can edit.</b></>}
              </p>

              <div className="crm-meta-fields">
                {/* META_VERIFY_TOKEN — plaintext, shareable with Meta */}
                <div className="crm-meta-field">
                  <div className="crm-meta-field-head">
                    <label htmlFor="meta-vt">
                      <code>META_VERIFY_TOKEN</code>
                      <span className={`crm-meta-pill ${config.verifyTokenSet ? "is-ok" : "is-missing"}`}>
                        {config.verifyTokenSet ? "set" : "missing"}
                      </span>
                      {config.sources && config.sources.verifyToken === "env" && (
                        <span className="crm-meta-source">from .env</span>
                      )}
                    </label>
                  </div>
                  <input id="meta-vt" type="text" className="crm-meta-input"
                         placeholder="e.g. zereo_meta_verify_8x4k2p"
                         value={form.verifyToken}
                         disabled={!canEdit || saving}
                         onChange={(e) => setForm(f => ({ ...f, verifyToken: e.target.value }))}/>
                  <div className="crm-meta-field-help">
                    Anything you choose. Paste the same string into Meta when
                    you "Verify and save" the webhook subscription.
                  </div>
                </div>

                {/* META_APP_SECRET — secret, masked when stored */}
                <div className="crm-meta-field">
                  <div className="crm-meta-field-head">
                    <label htmlFor="meta-as">
                      <code>META_APP_SECRET</code>
                      <span className={`crm-meta-pill ${config.appSecretSet ? "is-ok" : "is-missing"}`}>
                        {config.appSecretSet ? "set" : "missing"}
                      </span>
                      {config.sources && config.sources.appSecret === "env" && (
                        <span className="crm-meta-source">from .env</span>
                      )}
                    </label>
                  </div>
                  <div className="crm-meta-input-row">
                    <input id="meta-as"
                           type={reveal.appSecret ? "text" : "password"}
                           className="crm-meta-input"
                           placeholder={config.appSecretMask
                             ? `Stored: ${config.appSecretMask} — leave blank to keep`
                             : "Paste app secret"}
                           value={secretEdits.appSecret}
                           disabled={!canEdit || saving}
                           autoComplete="new-password"
                           onChange={(e) => setSecretEdits(s => ({ ...s, appSecret: e.target.value }))}/>
                    <button type="button" className="btn crm-meta-eye"
                            onClick={() => setReveal(r => ({ ...r, appSecret: !r.appSecret }))}
                            disabled={!secretEdits.appSecret}
                            title={reveal.appSecret ? "Hide" : "Reveal"}>
                      {reveal.appSecret ? "Hide" : "Show"}
                    </button>
                    {config.appSecretSet && config.sources && config.sources.appSecret === "db" && canEdit && (
                      <button type="button" className="btn crm-meta-clear"
                              onClick={() => clearSecret("appSecret")} disabled={saving}
                              title="Remove stored value (falls back to .env)">
                        Clear
                      </button>
                    )}
                  </div>
                  <div className="crm-meta-field-help">
                    Meta App → Settings → Basic → App Secret. Used to verify
                    the X-Hub-Signature-256 HMAC on incoming events.
                  </div>
                </div>

                {/* META_PAGE_ACCESS_TOKEN — long-lived secret */}
                <div className="crm-meta-field">
                  <div className="crm-meta-field-head">
                    <label htmlFor="meta-pat">
                      <code>META_PAGE_ACCESS_TOKEN</code>
                      <span className={`crm-meta-pill ${config.pageAccessTokenSet ? "is-ok" : "is-missing"}`}>
                        {config.pageAccessTokenSet ? "set" : "missing"}
                      </span>
                      {config.sources && config.sources.pageAccessToken === "env" && (
                        <span className="crm-meta-source">from .env</span>
                      )}
                    </label>
                  </div>
                  <div className="crm-meta-input-row">
                    <input id="meta-pat"
                           type={reveal.pageAccessToken ? "text" : "password"}
                           className="crm-meta-input"
                           placeholder={config.pageAccessTokenMask
                             ? `Stored: ${config.pageAccessTokenMask} — leave blank to keep`
                             : "Paste long-lived Page access token"}
                           value={secretEdits.pageAccessToken}
                           disabled={!canEdit || saving}
                           autoComplete="new-password"
                           onChange={(e) => setSecretEdits(s => ({ ...s, pageAccessToken: e.target.value }))}/>
                    <button type="button" className="btn crm-meta-eye"
                            onClick={() => setReveal(r => ({ ...r, pageAccessToken: !r.pageAccessToken }))}
                            disabled={!secretEdits.pageAccessToken}
                            title={reveal.pageAccessToken ? "Hide" : "Reveal"}>
                      {reveal.pageAccessToken ? "Hide" : "Show"}
                    </button>
                    {config.pageAccessTokenSet && config.sources && config.sources.pageAccessToken === "db" && canEdit && (
                      <button type="button" className="btn crm-meta-clear"
                              onClick={() => clearSecret("pageAccessToken")} disabled={saving}
                              title="Remove stored value (falls back to .env)">
                        Clear
                      </button>
                    )}
                  </div>
                  <div className="crm-meta-field-help">
                    Long-lived Page token with the <code>leads_retrieval</code>{" "}
                    permission. Used to fetch the actual lead fields after the
                    webhook fires.
                  </div>
                </div>

                {/* META_PAGE_ID + META_GRAPH_VERSION — non-secret, optional */}
                <div className="crm-meta-field-grid">
                  <div className="crm-meta-field">
                    <div className="crm-meta-field-head">
                      <label htmlFor="meta-pid">
                        <code>META_PAGE_ID</code>
                        <span className={`crm-meta-pill ${config.pageIdSet ? "is-ok" : "is-missing"} is-optional`}>
                          {config.pageIdSet ? "set" : "optional"}
                        </span>
                      </label>
                    </div>
                    <input id="meta-pid" type="text" className="crm-meta-input"
                           placeholder="e.g. 122140555917187150"
                           value={form.pageId}
                           disabled={!canEdit || saving}
                           onChange={(e) => setForm(f => ({ ...f, pageId: e.target.value }))}/>
                  </div>
                  <div className="crm-meta-field">
                    <div className="crm-meta-field-head">
                      <label htmlFor="meta-gv">
                        <code>META_GRAPH_VERSION</code>
                      </label>
                    </div>
                    <input id="meta-gv" type="text" className="crm-meta-input"
                           placeholder="v19.0"
                           value={form.graphVersion}
                           disabled={!canEdit || saving}
                           onChange={(e) => setForm(f => ({ ...f, graphVersion: e.target.value }))}/>
                  </div>
                </div>

                {canEdit && (
                  <div className="crm-meta-save-row">
                    <button type="button" className="btn btn-primary"
                            onClick={save} disabled={saving}>
                      {saving ? "Saving…" : "Save credentials"}
                    </button>
                    {savedFlash && <span className="crm-meta-saved-flash">✓ Saved</span>}
                  </div>
                )}
              </div>

              {/* Auto-sync (scheduled poll) */}
              <h3 className="crm-meta-h">3. Auto-sync (backup poller)</h3>
              <p className="crm-meta-p crm-meta-p-faint">
                Webhooks deliver leads in real time, but if Meta retries fail
                (cold starts, network blips), the poller catches anything missed
                by walking <code>/page/leadgen_forms</code> and pulling new leads.
                Dedup is automatic — running both is always safe.
              </p>

              <div className="crm-meta-sync">
                <div className="crm-meta-sync-row">
                  <label className="crm-meta-sync-interval">
                    <span>Run every</span>
                    <select value={config.syncIntervalMin || 0}
                            disabled={!canEdit || saving}
                            onChange={(e) => changeInterval(parseInt(e.target.value, 10))}>
                      <option value={0}>Off</option>
                      <option value={5}>5 minutes</option>
                      <option value={15}>15 minutes</option>
                      <option value={30}>30 minutes</option>
                      <option value={60}>1 hour</option>
                      <option value={180}>3 hours</option>
                      <option value={360}>6 hours</option>
                      <option value={720}>12 hours</option>
                      <option value={1440}>Daily</option>
                    </select>
                  </label>
                  <button type="button" className="btn btn-primary"
                          onClick={() => runSyncNow(false)}
                          disabled={syncBusy || !config.pageAccessTokenSet || !config.pageIdSet}
                          title={!config.pageAccessTokenSet || !config.pageIdSet
                            ? "Set Page Access Token + Page ID first"
                            : "Pull any leads created since the last sync"}>
                    {syncBusy ? "Syncing…" : "Sync now"}
                  </button>
                  {canEdit && (
                    <button type="button" className="btn"
                            onClick={() => runSyncNow(true)}
                            disabled={syncBusy || !config.pageAccessTokenSet || !config.pageIdSet}
                            title="Re-scan the last 30 days (use after first enabling)">
                      Backfill 30d
                    </button>
                  )}
                </div>

                <div className="crm-meta-sync-status">
                  {config.syncIntervalMin > 0 ? (
                    <span className="crm-meta-sync-pill is-on">
                      ● Auto-sync every {config.syncIntervalMin} min
                    </span>
                  ) : (
                    <span className="crm-meta-sync-pill is-off">○ Auto-sync off</span>
                  )}
                  {config.syncLastRun && (
                    <span className="crm-meta-sync-meta">
                      Last run {fmtRel(config.syncLastRun)}
                      {config.syncLastStats && (
                        <> · <b>{config.syncLastStats.inserted || 0}</b> new
                        · {config.syncLastStats.scanned || 0} scanned
                        {config.syncLastStats.errors > 0 && <> · <span style={{ color: "#c0223a" }}>{config.syncLastStats.errors} errors</span></>}
                        </>
                      )}
                    </span>
                  )}
                </div>
                {syncMsg && (
                  <div className={`crm-meta-sync-msg ${syncMsg.startsWith("✓") ? "is-ok" : "is-err"}`}>
                    {syncMsg}
                  </div>
                )}
                {!canEdit && (
                  <div className="crm-meta-sync-meta">
                    Only workspace admins can change the auto-sync schedule.
                  </div>
                )}
              </div>

              <h3 className="crm-meta-h">4. Or use Zapier / Make</h3>
              <p className="crm-meta-p">
                If you'd rather not touch Meta's developer console, point Zapier or
                Make.com at this URL with field names <code>name</code>, <code>phone</code>,
                <code>email</code>, <code>course_interest</code>:
              </p>
              <div className="crm-meta-copy-row">
                <code>{config.intakeUrl}</code>
                <button type="button" className="btn"
                        onClick={() => copy("intake", config.intakeUrl)}>
                  {copied === "intake" ? "Copied!" : "Copy"}
                </button>
              </div>
              {config.intakeTokenSet && (
                <p className="crm-meta-p crm-meta-p-faint">
                  Intake is protected by <code>LEADS_INTAKE_TOKEN</code> — send it in the
                  <code> X-Intake-Token</code> header.
                </p>
              )}

            </React.Fragment>
          )}
        </div>

        <div className="crm-meta-actions">
          <button type="button" className="btn btn-primary" onClick={onClose}>Done</button>
        </div>
      </div>
      {intakeOpen && (
        <RecentIntakeModal
          recent={recent || []}
          refreshing={refreshing}
          onRefresh={() => { load(); onLeadsChanged && onLeadsChanged(); }}
          onClose={() => setIntakeOpen(false)}/>
      )}
    </div>
  );
}

// Standalone modal for the Recent intake list. Sits on top of the
// integration modal (higher z-index) so the user can flip between
// "configure the integration" and "see what came in" without losing
// their place in the form.
function RecentIntakeModal({ recent, refreshing, onRefresh, onClose }) {
  // Esc to close, click-outside to close.
  React.useEffect(() => {
    function onKey(e) { if (e.key === "Escape") onClose(); }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [onClose]);
  const list = Array.isArray(recent) ? recent : [];
  return (
    <div className="crm-modal-shade" style={{ zIndex: 9200 }} onClick={onClose}>
      <div className="crm-meta-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crm-modal-head">
          <h2>📥 Recent intake — Meta Ads</h2>
          <button className="crm-icon-btn" onClick={onClose}>✕</button>
        </div>
        <div className="crm-meta-body">
          <p className="crm-meta-p crm-meta-p-faint" style={{ margin: 0 }}>
            Last 20 events the server received — webhooks from Meta, sync runs, and any errors. Click a row to expand its raw payload.
          </p>
          <div style={{ display: "flex", justifyContent: "flex-end" }}>
            <button type="button" className="btn"
                    onClick={onRefresh}
                    disabled={refreshing}>
              {refreshing ? "Refreshing…" : "Refresh"}
            </button>
          </div>
          {list.length === 0 ? (
            <div className="crm-empty" style={{ padding: "32px 12px", textAlign: "center" }}>
              No intake events yet.<br/>
              Submit a test lead from Meta's Lead Ads testing tool, or click <b>Sync now</b> in the integration settings, to confirm the wiring.
            </div>
          ) : (
            <div className="crm-meta-recent">
              {list.map(r => <RecentIntakeRow key={r.id} r={r}/>)}
            </div>
          )}
        </div>
        <div className="crm-meta-actions">
          <button type="button" className="btn btn-primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

// ── CreativePanel — Meta ad creative preview on a CRM lead ────────
// Renders inline inside the Details tab when migration-032 fields are
// populated. Three variants: video (thumbnail + play overlay → opens
// FB watch URL), image (clickable to view full-size), and headline/
// body copy. Hidden entirely when nothing useful is stored.
function CreativePanel({ lead }) {
  if (!lead) return null;
  const img    = lead.meta_creative_image_url || lead.meta_creative_thumbnail_url;
  const thumb  = lead.meta_creative_thumbnail_url || img;
  const video  = lead.meta_creative_video_id;
  const title  = lead.meta_creative_title;
  const body   = lead.meta_creative_body;
  const link   = lead.meta_creative_link;
  const story  = lead.meta_effective_story_id;
  // Bail when there's nothing to show — older leads pre-migration 032.
  const hasAnything = img || video || title || body;
  if (!hasAnything) return null;

  // FB watch URL for videos. Story id has shape "<page_id>_<post_id>";
  // facebook.com/<story_id> resolves to the live post with the video.
  const fbStoryUrl = story ? `https://www.facebook.com/${encodeURIComponent(story)}` : null;
  const fbVideoUrl = video ? `https://www.facebook.com/watch/?v=${encodeURIComponent(video)}` : null;

  // Failed-image fallback — Meta CDN URLs expire; if the image 404s
  // we swap to the thumbnail; if that also fails we hide the visual.
  const [imgFailed, setImgFailed] = React.useState(false);
  const [thumbFailed, setThumbFailed] = React.useState(false);
  const visualSrc = !imgFailed
    ? img
    : !thumbFailed
      ? thumb
      : null;

  return (
    <div className="crm-creative-panel">
      <div className="crm-creative-head">
        <span className="crm-creative-pip" aria-hidden="true">🖼️</span>
        Ad creative
        <span className="crm-creative-meta">
          What this lead saw before submitting the form
        </span>
      </div>
      <div className="crm-creative-body">
        {visualSrc && (
          <a className="crm-creative-media"
             href={fbVideoUrl || fbStoryUrl || visualSrc}
             target="_blank" rel="noopener noreferrer"
             title={video ? "Open video on Facebook" : (fbStoryUrl ? "Open ad on Facebook" : "Open full-size image")}>
            <img src={visualSrc}
                 alt="Ad creative"
                 onError={() => {
                   if (!imgFailed) setImgFailed(true);
                   else            setThumbFailed(true);
                 }}/>
            {video && (
              <span className="crm-creative-play" aria-hidden="true">▶</span>
            )}
          </a>
        )}
        <div className="crm-creative-text">
          {title && <div className="crm-creative-title">{title}</div>}
          {body  && <div className="crm-creative-copy">{body}</div>}
          <div className="crm-creative-actions">
            {link && (
              <a className="crm-creative-link"
                 href={link}
                 target="_blank" rel="noopener noreferrer">
                Landing page ↗
              </a>
            )}
            {fbStoryUrl && (
              <a className="crm-creative-link"
                 href={fbStoryUrl}
                 target="_blank" rel="noopener noreferrer">
                Open on Facebook ↗
              </a>
            )}
            {fbVideoUrl && !fbStoryUrl && (
              <a className="crm-creative-link"
                 href={fbVideoUrl}
                 target="_blank" rel="noopener noreferrer">
                Watch video ↗
              </a>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

// CSS injection for the panel — kept inline so this whole feature is
// portable to any Flowboard install without a CSS bundle bump.
if (typeof document !== "undefined" && !document.getElementById("crm-creative-css")) {
  const s = document.createElement("style");
  s.id = "crm-creative-css";
  s.textContent = `
    .crm-creative-panel {
      margin-top: 14px;
      background: white;
      border: 1px solid var(--border);
      border-radius: var(--r-md);
      box-shadow: var(--shadow-sm);
      overflow: hidden;
    }
    .crm-creative-head {
      display: flex; align-items: center; gap: 8px;
      padding: 10px 14px;
      background: #fafbfc;
      border-bottom: 1px solid var(--border-row);
      font-size: 12px; font-weight: 700;
      color: var(--ink-strong);
      letter-spacing: .02em;
    }
    .crm-creative-pip {
      display: inline-flex; align-items: center; justify-content: center;
      width: 22px; height: 22px; border-radius: 6px;
      background: rgba(24,119,242,.10);
      font-size: 13px;
    }
    .crm-creative-meta {
      flex: 1;
      font-size: 11px; font-weight: 500;
      color: var(--ink-muted);
      letter-spacing: 0;
      text-align: right;
    }
    .crm-creative-body {
      display: grid;
      grid-template-columns: 200px 1fr;
      gap: 14px;
      padding: 14px;
      align-items: start;
    }
    @media (max-width: 720px) {
      .crm-creative-body { grid-template-columns: 1fr; }
    }
    .crm-creative-media {
      position: relative;
      display: block;
      width: 200px;
      aspect-ratio: 1 / 1;
      border-radius: var(--r-sm);
      overflow: hidden;
      background: #f4f5f8;
      cursor: zoom-in;
    }
    .crm-creative-media img {
      width: 100%; height: 100%;
      object-fit: cover;
      display: block;
    }
    .crm-creative-play {
      position: absolute; inset: 0;
      display: flex; align-items: center; justify-content: center;
      background: rgba(0,0,0,.30);
      color: white;
      font-size: 36px;
      pointer-events: none;
      transition: background .12s;
    }
    .crm-creative-media:hover .crm-creative-play { background: rgba(0,0,0,.45); }
    .crm-creative-text { min-width: 0; }
    .crm-creative-title {
      font-size: 14px; font-weight: 700;
      color: var(--ink-strong);
      line-height: 1.3;
      margin-bottom: 6px;
    }
    .crm-creative-copy {
      font-size: 12.5px; line-height: 1.5;
      color: var(--ink-body);
      white-space: pre-wrap;
      word-break: break-word;
      max-height: 8em;
      overflow: hidden;
      position: relative;
    }
    .crm-creative-copy:hover { max-height: none; }
    .crm-creative-actions {
      display: flex; gap: 12px; flex-wrap: wrap;
      margin-top: 10px;
    }
    .crm-creative-link {
      font-size: 12px; font-weight: 600;
      color: var(--brand);
      text-decoration: none;
    }
    .crm-creative-link:hover { text-decoration: underline; }
  `;
  document.head.appendChild(s);
}

// ── SalesRepPicker — assign a lead to a sales-portal rep ──────────
// Reads /api/admin/sales (workspace JWT, owner/admin only). The rep
// list is cached on window so opening multiple lead drawers in a row
// doesn't refetch. Click "Manage sales accounts…" inside the picker
// to open the SalesAccountsModal — bottom of this file.
function SalesRepPicker({ value, onChange }) {
  const [reps, setReps] = React.useState(window.__SALES_REPS_CACHE || null);
  const [adminOpen, setAdminOpen] = React.useState(false);
  React.useEffect(() => {
    if (reps !== null) return;
    if (!window.api || !window.api.salesAdmin) { setReps([]); return; }
    let cancelled = false;
    window.api.salesAdmin.list().then(rows => {
      if (cancelled) return;
      const arr = Array.isArray(rows) ? rows : [];
      window.__SALES_REPS_CACHE = arr;
      setReps(arr);
    }).catch(() => { if (!cancelled) setReps([]); });
    return () => { cancelled = true; };
  }, [reps]);

  const active = (reps || []).filter(r => r.status === "active");
  // Currently-assigned rep stays visible even if deactivated, so the
  // row never silently looks unassigned.
  const currentMissing = value && !active.some(r => r.id === value);

  return (
    <>
      <label className="crm-row">
        <span>Sales rep <small style={{ color: "var(--ink-muted)", fontWeight: 500, marginLeft: 4 }}>(mobile)</small></span>
        <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
          <select value={value || ""}
                  onChange={(e) => onChange(e.target.value)}
                  style={{ flex: 1 }}>
            <option value="">— No sales rep —</option>
            {active.map(r => (
              <option key={r.id} value={r.id}>{r.name}</option>
            ))}
            {currentMissing && (
              <option value={value}>(deactivated)</option>
            )}
          </select>
          <button type="button" className="btn"
                  onClick={() => setAdminOpen(true)}
                  title="Manage sales accounts"
                  style={{ padding: "6px 10px", fontSize: 12 }}>
            ⚙
          </button>
        </div>
      </label>
      {adminOpen && (
        <SalesAccountsModal
          onClose={() => setAdminOpen(false)}
          onChanged={() => {
            // Bust the cache so the picker re-pulls the fresh list.
            window.__SALES_REPS_CACHE = null;
            setReps(null);
          }}/>
      )}
    </>
  );
}

// ── SalesAccountsModal — workspace admin manages sales-portal accounts.
// Owner / admin only (the route checks). Lets you create reps, reset
// their password (one-shot plaintext), and deactivate.
function SalesAccountsModal({ onClose, onChanged }) {
  const [list, setList] = React.useState(null);
  const [error, setError] = React.useState("");
  const [creating, setCreating] = React.useState(false);
  const [form, setForm] = React.useState({ name: "", email: "", phone: "" });
  const [revealed, setRevealed] = React.useState(null); // { id, password, kind }

  async function load() {
    setError("");
    try { setList(await window.api.salesAdmin.list()); }
    catch (e) { setError((e && (e.body?.message || e.message)) || "Failed to load"); }
  }
  React.useEffect(() => { load(); }, []);

  async function create() {
    setCreating(true); setError("");
    try {
      const r = await window.api.salesAdmin.create(form);
      setRevealed({ id: r.id, password: r.initial_password, kind: "create" });
      setForm({ name: "", email: "", phone: "" });
      await load();
      onChanged && onChanged();
    } catch (e) { setError((e && (e.body?.message || e.message)) || "Couldn't create"); }
    finally { setCreating(false); }
  }
  async function reset(id) {
    if (!confirm("Reset this user's password? They'll need the new one to sign in.")) return;
    try {
      const r = await window.api.salesAdmin.resetPassword(id);
      setRevealed({ id, password: r.new_password, kind: "reset" });
    } catch (e) { setError((e && (e.body?.message || e.message)) || "Reset failed"); }
  }
  async function deactivate(id) {
    if (!confirm("Deactivate this sales account? They won't be able to sign in.")) return;
    try { await window.api.salesAdmin.deactivate(id); await load(); onChanged && onChanged(); }
    catch (e) { setError((e && (e.body?.message || e.message)) || "Deactivate failed"); }
  }
  async function reactivate(id) {
    try { await window.api.salesAdmin.reactivate(id); await load(); onChanged && onChanged(); }
    catch (e) { setError((e && (e.body?.message || e.message)) || "Reactivate failed"); }
  }

  return (
    <div className="crm-modal-shade" onClick={onClose} style={{ zIndex: 9300 }}>
      <div className="crm-meta-modal" onClick={(e) => e.stopPropagation()}>
        <div className="crm-modal-head">
          <h2>📞 Sales portal accounts</h2>
          <button className="crm-icon-btn" onClick={onClose}>✕</button>
        </div>
        <div className="crm-meta-body">
          <p className="crm-meta-p crm-meta-p-faint" style={{ margin: 0 }}>
            Standalone mobile login at <code>/sales</code>. These accounts are <b>isolated</b> from your workspace — sales reps never see chat, projects, or admin. They only see the leads you assign to them via the <b>Sales rep</b> field on each lead.
          </p>

          {error && <div className="crm-meta-error">{error}</div>}

          {/* Create-account form */}
          <div className="crm-meta-h">Create account</div>
          <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
            <input placeholder="Full name" value={form.name}
                   onChange={(e) => setForm(f => ({ ...f, name: e.target.value }))}/>
            <input placeholder="Email" type="email" value={form.email}
                   onChange={(e) => setForm(f => ({ ...f, email: e.target.value }))}/>
            <input placeholder="Phone (optional)" type="tel" value={form.phone}
                   onChange={(e) => setForm(f => ({ ...f, phone: e.target.value }))}/>
            <button className="btn btn-primary"
                    disabled={creating || !form.name.trim() || !form.email.trim()}
                    onClick={create}>
              {creating ? "Creating…" : "Create + generate password"}
            </button>
          </div>

          {/* Plaintext-password reveal — shown ONCE after create / reset.
              We don't store the plaintext anywhere; if the admin closes
              this card without copying, they have to reset to see one
              again. */}
          {revealed && (
            <div style={{
              padding: 12, borderRadius: 8,
              background: "rgba(0,115,234,.06)",
              border: "1px solid rgba(0,115,234,.30)",
            }}>
              <div style={{ fontSize: 12, fontWeight: 700, color: "#0044a3", marginBottom: 6 }}>
                {revealed.kind === "create" ? "Account created" : "Password reset"}
              </div>
              <div style={{ fontSize: 13, marginBottom: 8 }}>
                Share this with the sales rep — they can change it after first login.
              </div>
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                <code style={{
                  flex: 1, padding: "8px 10px",
                  background: "white", border: "1px solid var(--border)",
                  borderRadius: 6, fontSize: 14, fontWeight: 700,
                  letterSpacing: ".05em", userSelect: "all",
                }}>{revealed.password}</code>
                <button className="btn"
                        onClick={() => navigator.clipboard?.writeText(revealed.password)}>
                  Copy
                </button>
                <button className="btn" onClick={() => setRevealed(null)}>Done</button>
              </div>
            </div>
          )}

          {/* Existing accounts */}
          <div className="crm-meta-h">Existing accounts</div>
          {list === null ? (
            <div className="crm-empty">Loading…</div>
          ) : list.length === 0 ? (
            <div className="crm-empty">No sales accounts yet.</div>
          ) : (
            <div className="crm-meta-recent">
              {list.map(u => (
                <div key={u.id} className={"crm-meta-evt" + (u.status === "deactivated" ? " is-error" : "")}>
                  <div className="crm-meta-evt-row">
                    <span className="crm-meta-evt-tag" style={{
                      background: u.status === "active" ? "rgba(34,197,94,.14)" : "rgba(0,0,0,.06)",
                      color: u.status === "active" ? "#166534" : "var(--ink-muted)",
                    }}>{u.status}</span>
                    <span className="crm-meta-evt-time">
                      {u.lead_count || 0} lead{(u.lead_count || 0) === 1 ? "" : "s"}
                    </span>
                  </div>
                  <div style={{ fontSize: 13, fontWeight: 700, color: "var(--ink-strong)" }}>{u.name}</div>
                  <div style={{ fontSize: 12, color: "var(--ink-muted)" }}>
                    {u.email}{u.phone ? " · " + u.phone : ""}
                  </div>
                  <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
                    <button className="btn" onClick={() => reset(u.id)}>Reset password</button>
                    {u.status === "active" ? (
                      <button className="btn" style={{ color: "#c0223a" }} onClick={() => deactivate(u.id)}>
                        Deactivate
                      </button>
                    ) : (
                      <button className="btn" onClick={() => reactivate(u.id)}>Reactivate</button>
                    )}
                  </div>
                </div>
              ))}
            </div>
          )}
        </div>
        <div className="crm-meta-actions">
          <button className="btn btn-primary" onClick={onClose}>Done</button>
        </div>
      </div>
    </div>
  );
}

// ── CRM Dashboard ─────────────────────────────────────────────────────
// "What's happening in our pipeline?" — a manager / team-lead view that
// rolls up the lead set into KPI cards, a daily-count bar chart, the
// stage funnel, source + country breakdowns, and an owner leaderboard.
// Reads the same `filtered` array the kanban + queue tabs use, so
// the user's existing search / "mine only" filters apply transparently.
//
// Range selector at the top — 7d / 30d / 90d / all — is persisted to
// localStorage so a power user can park their preferred lens.
function CRMDashboard({ leads, people = [], onOpen, currentUserId }) {
  const [range, setRange] = React.useState(() => {
    try { return localStorage.getItem("flowboard.crm.dashRange") || "30"; }
    catch { return "30"; }
  });
  function setRangePersisted(next) {
    setRange(next);
    try { localStorage.setItem("flowboard.crm.dashRange", next); } catch {}
  }
  const days = range === "all" ? null : Number(range) || 30;

  // ── Date-range filter ──────────────────────────────────────────────
  // We keep two slices: `inRange` (created within the window — used for
  // KPIs about new leads, daily count, source mix) and `leads` itself
  // (the full filtered set — used for stage funnel + open-now KPIs).
  const now = React.useMemo(() => new Date(), []);
  const inRange = React.useMemo(() => {
    if (!days) return leads;
    const cutoff = Date.now() - days * 86400000;
    return leads.filter(l => {
      const t = l.created_at ? new Date(l.created_at).getTime() : 0;
      return t >= cutoff;
    });
  }, [leads, days]);

  // ── KPIs ───────────────────────────────────────────────────────────
  const kpi = React.useMemo(() => {
    const startOfDay = new Date(); startOfDay.setHours(0, 0, 0, 0);
    const todayMs = startOfDay.getTime();
    const weekMs  = todayMs - 6 * 86400000;
    const monthMs = todayMs - 29 * 86400000;
    let todayCount = 0, weekCount = 0, monthCount = 0;
    let openCount = 0, wonCount = 0, lostCount = 0;
    let pipelineValue = 0, wonValue = 0;
    for (const l of leads) {
      const t = l.created_at ? new Date(l.created_at).getTime() : 0;
      if (t >= todayMs) todayCount++;
      if (t >= weekMs)  weekCount++;
      if (t >= monthMs) monthCount++;
      if (l.stage === "converted") { wonCount++; wonValue += Number(l.value || 0); }
      else if (l.stage === "lost") { lostCount++; }
      else { openCount++; pipelineValue += Number(l.value || 0); }
    }
    const decided = wonCount + lostCount;
    const conv = decided ? Math.round((wonCount / decided) * 100) : 0;
    return { todayCount, weekCount, monthCount, openCount, wonCount, lostCount, pipelineValue, wonValue, conv };
  }, [leads]);

  // ── Daily count series (last N days) ──────────────────────────────
  // Buckets keyed by YYYY-MM-DD in the user's local tz so the bars
  // line up with the calendar day a rep would expect.
  const daily = React.useMemo(() => {
    const n = days || 30;
    const buckets = [];
    const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
    for (let i = n - 1; i >= 0; i--) {
      const d = new Date(todayStart.getTime() - i * 86400000);
      buckets.push({ date: d, key: d.toDateString(), count: 0 });
    }
    const idxByKey = new Map(buckets.map((b, i) => [b.key, i]));
    for (const l of leads) {
      if (!l.created_at) continue;
      const d = new Date(l.created_at);
      if (!isFinite(d.getTime())) continue;
      d.setHours(0, 0, 0, 0);
      const i = idxByKey.get(d.toDateString());
      if (i != null) buckets[i].count++;
    }
    const peak = Math.max(1, ...buckets.map(b => b.count));
    return { buckets, peak };
  }, [leads, days]);

  // ── Stage funnel ───────────────────────────────────────────────────
  // Top of funnel = total leads in range. Each subsequent stage shows
  // the count plus a width-proportional bar so the drop-off is obvious.
  const funnel = React.useMemo(() => {
    const counts = {};
    for (const l of inRange) counts[l.stage] = (counts[l.stage] || 0) + 1;
    const total = inRange.length || 1;
    return LEAD_STAGES.map(s => ({
      stage: s,
      count: counts[s.id] || 0,
      pct: Math.round(((counts[s.id] || 0) / total) * 100),
    }));
  }, [inRange]);

  // ── Source breakdown ───────────────────────────────────────────────
  const sources = React.useMemo(() => {
    const buckets = new Map();
    for (const l of inRange) {
      const key = l.source || "manual";
      buckets.set(key, (buckets.get(key) || 0) + 1);
    }
    const total = inRange.length || 1;
    const entries = Array.from(buckets.entries())
      .map(([key, n]) => ({ key, n, pct: Math.round((n / total) * 100) }))
      .sort((a, b) => b.n - a.n);
    return entries;
  }, [inRange]);

  // ── Country breakdown (via flag detection) ────────────────────────
  // Reuses the same leadCountry() helper the kanban cards use, so the
  // dashboard counts match the flags users see on the board. Now also
  // tracks open / won / lost per country so the table can show what's
  // happening WITHIN each region, not just raw totals.
  const countries = React.useMemo(() => {
    const buckets = new Map();
    let unknown = 0;
    function rowFor(iso) {
      if (!buckets.has(iso)) {
        buckets.set(iso, { iso, flag: isoToFlag(iso), name: isoToCountryName(iso),
                           total: 0, open: 0, won: 0, lost: 0, pipeline: 0 });
      }
      return buckets.get(iso);
    }
    for (const l of inRange) {
      const cc = leadCountry(l);
      if (!cc) { unknown++; continue; }
      const r = rowFor(cc.iso);
      r.total++;
      if (l.stage === "converted") r.won++;
      else if (l.stage === "lost") r.lost++;
      else { r.open++; r.pipeline += Number(l.value || 0); }
    }
    const entries = Array.from(buckets.values())
      .map(r => {
        const decided = r.won + r.lost;
        return { ...r, conv: decided ? Math.round((r.won / decided) * 100) : 0 };
      })
      .sort((a, b) => b.total - a.total);
    return { entries, unknown };
  }, [inRange]);

  // ── Owner leaderboard ─────────────────────────────────────────────
  // For each owner: total leads in range, won, lost, open, conversion%.
  // Sorted by total leads then by conversion. Unassigned bucket too.
  const leaderboard = React.useMemo(() => {
    const byOwner = new Map();
    function rowFor(id) {
      if (!byOwner.has(id)) byOwner.set(id, { id, total: 0, won: 0, lost: 0, open: 0, pipeline: 0 });
      return byOwner.get(id);
    }
    for (const l of inRange) {
      const r = rowFor(l.owner_id || "_unassigned");
      r.total++;
      if (l.stage === "converted") r.won++;
      else if (l.stage === "lost") r.lost++;
      else { r.open++; r.pipeline += Number(l.value || 0); }
    }
    return Array.from(byOwner.values())
      .map(r => {
        const decided = r.won + r.lost;
        return { ...r, conv: decided ? Math.round((r.won / decided) * 100) : 0 };
      })
      .sort((a, b) => b.total - a.total || b.conv - a.conv);
  }, [inRange]);
  const peopleById = React.useMemo(
    () => Object.fromEntries(people.map(p => [p.id, p])),
    [people]
  );

  // ── Recent activity strip ─────────────────────────────────────────
  // Last 6 lead state-changes (sorted by updated_at desc) so the user
  // can scan "who moved what" today without opening every drawer.
  const recent = React.useMemo(() => {
    return [...leads]
      .filter(l => l.updated_at)
      .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
      .slice(0, 6);
  }, [leads]);

  // ── Render ────────────────────────────────────────────────────────
  return (
    <div className="crm-dashboard">
      {/* Range tabs */}
      <div className="crm-dash-range" role="tablist" aria-label="Date range">
        {[
          { id: "7",   label: "Last 7 days" },
          { id: "30",  label: "Last 30 days" },
          { id: "90",  label: "Last 90 days" },
          { id: "all", label: "All time" },
        ].map(opt => (
          <button key={opt.id} type="button" role="tab"
                  aria-selected={range === opt.id}
                  className={`crm-dash-range-btn ${range === opt.id ? "is-on" : ""}`}
                  onClick={() => setRangePersisted(opt.id)}>
            {opt.label}
          </button>
        ))}
      </div>

      {/* KPI cards */}
      <div className="crm-dash-kpis">
        <KpiCard title="Today" value={kpi.todayCount} accent="#0073ea" hint="new leads created"/>
        <KpiCard title="Last 7 days" value={kpi.weekCount} accent="#a25ddc" hint="new leads"/>
        <KpiCard title="Last 30 days" value={kpi.monthCount} accent="#fdab3d" hint="new leads"/>
        <KpiCard title="Open" value={kpi.openCount} accent="#00c875"
                 hint={"₹" + Number(kpi.pipelineValue || 0).toLocaleString() + " in pipeline"}/>
        <KpiCard title="Won (all-time)" value={kpi.wonCount} accent="#00853d"
                 hint={"₹" + Number(kpi.wonValue || 0).toLocaleString() + " closed"}/>
        <KpiCard title="Conversion" value={kpi.conv + "%"} accent="#5559df"
                 hint={kpi.wonCount + " won · " + kpi.lostCount + " lost"}/>
      </div>

      {/* Daily count bar chart */}
      <section className="crm-dash-card">
        <div className="crm-dash-card-head">
          <h3>Daily new leads</h3>
          <span className="crm-dash-card-sub">
            {days ? `last ${days} days` : "all time"} · peak {daily.peak}
          </span>
        </div>
        <div className="crm-dash-daily">
          {daily.buckets.map((b, i) => {
            const h = Math.round((b.count / daily.peak) * 100);
            const isToday = b.date.toDateString() === new Date().toDateString();
            const isWeekend = b.date.getDay() === 0 || b.date.getDay() === 6;
            const dayLabel = b.date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
            return (
              <div key={i} className="crm-dash-daily-col"
                   title={`${dayLabel} · ${b.count} ${b.count === 1 ? "lead" : "leads"}`}>
                <div className="crm-dash-daily-bar-wrap">
                  {b.count > 0 && (
                    <span className="crm-dash-daily-count">{b.count}</span>
                  )}
                  <div className={`crm-dash-daily-bar ${isToday ? "is-today" : ""} ${isWeekend ? "is-weekend" : ""}`}
                       style={{ height: Math.max(2, h) + "%" }}/>
                </div>
                {/* Only label every Nth bar so the axis doesn't crowd
                    on 30/90-day windows. */}
                {(days <= 7 || i % Math.ceil(daily.buckets.length / 10) === 0 || i === daily.buckets.length - 1) && (
                  <div className="crm-dash-daily-tick">{dayLabel}</div>
                )}
              </div>
            );
          })}
        </div>
      </section>

      {/* Stage funnel + Source breakdown side-by-side */}
      <div className="crm-dash-grid">
        <section className="crm-dash-card">
          <div className="crm-dash-card-head">
            <h3>Stage funnel</h3>
            <span className="crm-dash-card-sub">{inRange.length} leads in range</span>
          </div>
          <div className="crm-dash-funnel">
            {funnel.map(f => (
              <div key={f.stage.id} className="crm-dash-funnel-row">
                <div className="crm-dash-funnel-label">
                  <span className="crm-dash-funnel-emoji">{f.stage.emoji}</span>
                  {f.stage.label}
                </div>
                <div className="crm-dash-funnel-bar-wrap">
                  <div className="crm-dash-funnel-bar"
                       style={{ width: Math.max(2, f.pct) + "%", background: f.stage.color }}/>
                  <span className="crm-dash-funnel-count">{f.count}</span>
                  <span className="crm-dash-funnel-pct">{f.pct}%</span>
                </div>
              </div>
            ))}
          </div>
        </section>

        <section className="crm-dash-card">
          <div className="crm-dash-card-head">
            <h3>Sources</h3>
            <span className="crm-dash-card-sub">where leads came from</span>
          </div>
          <div className="crm-dash-list">
            {sources.length === 0 && (
              <div className="crm-dash-empty">No leads in this range.</div>
            )}
            {sources.map(s => (
              <div key={s.key} className="crm-dash-source-row">
                <span className="crm-dash-source-label">
                  {s.key === "meta_ad" ? "📣 Meta Ad" :
                   s.key === "manual"  ? "✍ Manual" :
                   s.key === "api"     ? "🔌 API"    :
                   s.key === "import"  ? "📥 Import" :
                   s.key === "portal"  ? "🛍 Sales Portal" :
                                          s.key}
                </span>
                <div className="crm-dash-source-bar-wrap">
                  <div className="crm-dash-source-bar"
                       style={{ width: Math.max(2, s.pct) + "%" }}/>
                </div>
                <span className="crm-dash-source-count">{s.n} · {s.pct}%</span>
              </div>
            ))}
          </div>
        </section>
      </div>

      {/* ── Country-wise lead count ──────────────────────────────────
          Full-width section. Detects country from each lead's phone
          country code (with location text fallback), then for every
          country shows: total leads, open / won / lost split, win-rate
          inside that country, and a width-proportional bar so high-
          volume regions stand out at a glance. Sorted by total leads
          descending. All countries shown — table scrolls if there are
          many — so it doubles as an export-ready geo report. */}
      <section className="crm-dash-card">
        <div className="crm-dash-card-head">
          <h3>Country-wise lead count</h3>
          <span className="crm-dash-card-sub">
            {countries.entries.length} {countries.entries.length === 1 ? "country" : "countries"}
            {countries.unknown ? ` · ${countries.unknown} without a country code` : ""}
          </span>
        </div>
        {countries.entries.length === 0 ? (
          <div className="crm-dash-empty">
            No leads with detectable country in this range. Phone numbers
            need a country prefix (e.g. +91, +971) to be classified.
          </div>
        ) : (
          <div className="crm-dash-country-table-wrap">
            <table className="crm-dash-country-table">
              <thead>
                <tr>
                  <th style={{ width: 36 }}></th>
                  <th>Country</th>
                  <th className="num">Total</th>
                  <th>Share</th>
                  <th className="num">Open</th>
                  <th className="num">Won</th>
                  <th className="num">Lost</th>
                  <th className="num">Win-rate</th>
                </tr>
              </thead>
              <tbody>
                {countries.entries.map((c, i) => {
                  const maxTotal = countries.entries[0].total || 1;
                  const sharePct = Math.round((c.total / inRange.length) * 100);
                  return (
                    <tr key={c.iso}>
                      <td className="crm-dash-country-flag-cell">{c.flag}</td>
                      <td>
                        <div className="crm-dash-country-name">{c.name}</div>
                        <div className="crm-dash-country-iso-sub">{c.iso}</div>
                      </td>
                      <td className="num crm-dash-country-total">{c.total}</td>
                      <td className="crm-dash-country-share">
                        <div className="crm-dash-country-share-bar-wrap">
                          <div className="crm-dash-country-share-bar"
                               style={{ width: Math.max(3, Math.round((c.total / maxTotal) * 100)) + "%" }}/>
                        </div>
                        <span className="crm-dash-country-share-pct">{sharePct}%</span>
                      </td>
                      <td className="num">{c.open}</td>
                      <td className="num" style={{ color: c.won ? "#00853d" : "var(--ink-faint)", fontWeight: c.won ? 700 : 500 }}>{c.won}</td>
                      <td className="num" style={{ color: c.lost ? "#c0223a" : "var(--ink-faint)" }}>{c.lost}</td>
                      <td className="num" style={{ fontWeight: 700,
                          color: c.won + c.lost === 0 ? "var(--ink-faint)" :
                                 c.conv >= 50 ? "#00853d" :
                                 c.conv >= 25 ? "#b66f00" : "#c0223a" }}>
                        {c.won + c.lost === 0 ? "—" : c.conv + "%"}
                      </td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
        )}
      </section>

      {/* Owner leaderboard — full width below the country table */}
      <section className="crm-dash-card">
          <div className="crm-dash-card-head">
            <h3>Owner leaderboard</h3>
            <span className="crm-dash-card-sub">per sales rep</span>
          </div>
          {leaderboard.length === 0 ? (
            <div className="crm-dash-empty">No leads in this range.</div>
          ) : (
            <table className="crm-dash-leaderboard">
              <thead>
                <tr>
                  <th>Owner</th>
                  <th>Total</th>
                  <th>Open</th>
                  <th>Won</th>
                  <th>Lost</th>
                  <th>Conv.</th>
                </tr>
              </thead>
              <tbody>
                {leaderboard.map(r => {
                  const p = peopleById[r.id];
                  const name = p ? p.name : r.id === "_unassigned" ? "Unassigned" : r.id;
                  const isMe = r.id === currentUserId;
                  return (
                    <tr key={r.id} className={isMe ? "is-me" : ""}>
                      <td>
                        {p && (
                          <Avatar src={p.avatar} name={p.name} size={20}/>
                        )}
                        <span style={{ marginLeft: p ? 6 : 0 }}>{name}{isMe ? " (you)" : ""}</span>
                      </td>
                      <td>{r.total}</td>
                      <td>{r.open}</td>
                      <td style={{ color: "#00853d", fontWeight: 600 }}>{r.won}</td>
                      <td style={{ color: "#c0223a" }}>{r.lost}</td>
                      <td style={{ fontWeight: 700 }}>{r.conv}%</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          )}
        </section>

      {/* Recent activity */}
      <section className="crm-dash-card">
        <div className="crm-dash-card-head">
          <h3>Recently updated</h3>
          <span className="crm-dash-card-sub">click to open</span>
        </div>
        {recent.length === 0 ? (
          <div className="crm-dash-empty">No recent activity.</div>
        ) : (
          <div className="crm-dash-recent">
            {recent.map(l => {
              const stage = LEAD_STAGE_BY_ID[l.stage] || LEAD_STAGES[0];
              const cc = leadCountry(l);
              return (
                <button key={l.id} type="button"
                        className="crm-dash-recent-row"
                        onClick={() => onOpen && onOpen(l)}>
                  {cc && <span className="crm-dash-recent-flag">{cc.flag}</span>}
                  <span className="crm-dash-recent-name">{l.name || "Untitled lead"}</span>
                  <span className="crm-pill" style={{ background: stage.color + "22", color: stage.color }}>
                    {stage.emoji} {stage.label}
                  </span>
                  <span className="crm-dash-recent-when">{_fmtLeadAgo(l.updated_at || l.created_at)}</span>
                </button>
              );
            })}
          </div>
        )}
      </section>
    </div>
  );
}

function KpiCard({ title, value, hint, accent }) {
  return (
    <div className="crm-dash-kpi" style={{ "--kpi-accent": accent }}>
      <div className="crm-dash-kpi-title">{title}</div>
      <div className="crm-dash-kpi-value">{value}</div>
      {hint && <div className="crm-dash-kpi-hint">{hint}</div>}
    </div>
  );
}

Object.assign(window, {
  CRMView, CRMBoard, CRMCard, CRMDrawer, CRMDashboard,
  DetailsTab, ActivityTab, ReminderTab,
  NewLeadModal, LostReasonModal, CoursePicker, MetaIntegrationModal,
  CreativePanel,
  SalesRepPicker, SalesAccountsModal,
});
