← back to Rentv 2026

public/admin/pr-intelligence/organizations.html

530 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>CRE PR Intelligence — Organizations</title>
<link rel="stylesheet" href="/admin/pr-intelligence/pr-shared.css">
</head>
<body>
<script src="/admin/pr-intelligence/pr-shared.js"></script>
<script>
(async function () {
  // ── Constants ────────────────────────────────────────────────────────────────
  const LS = {
    view:    'prorg:view',
    sort:    'prorg:sort',
    dir:     'prorg:dir',
    density: 'prorg:density',
    rail:    'prorg:rail',
  };
  const LIMIT = 50;

  // ── Rail group definitions ───────────────────────────────────────────────────
  // Each group maps a label to a set of org_type keys sent as types=a,b,c
  const RAIL_GROUPS = [
    { id: 'pr',       label: 'PR Firms',                 types: ['pr_agency','marketing_agency','public_affairs'] },
    { id: 'builders', label: 'Builders / Developers',    types: ['developer_owner','general_contractor','construction_mgmt','architecture_firm','engineering_firm'] },
    { id: 'brok',     label: 'Brokerages',               types: ['brokerage'] },
    { id: 'owners',   label: 'Owners / Investors / REITs', types: ['developer_owner','reit','asset_property_mgmt'] },
    { id: 'lenders',  label: 'Lenders / Finance',        types: ['commercial_mortgage_bank','debt_equity_broker','construction_lender','bridge_lender','private_lender','cmbs_structured','life_co_lender','bank','credit_union'] },
    { id: 'title',    label: 'Title & Escrow',           types: ['title_insurer','title_agency','escrow_company','commercial_escrow_dept','exchange_1031'] },
    { id: 'legal',    label: 'Legal / Advisory',         types: ['law_firm','accounting_consulting','appraiser','insurance_risk'] },
    { id: 'media',    label: 'Media / Associations',     types: ['trade_publication','association','economic_development','municipal_re','proptech'] },
  ];

  // ── Helpers ──────────────────────────────────────────────────────────────────
  const esc = PR.esc;
  const lsGet = (k, def) => { try { const v = localStorage.getItem(k); return v != null ? v : def; } catch { return def; } };
  const lsSet = (k, v) => { try { localStorage.setItem(k, v); } catch {} };
  const siteName = (u) => { try { return new URL(/^https?:/.test(u) ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return u; } };
  const web = (v) => v ? `<a href="${esc(/^https?:/.test(v) ? v : 'https://' + v)}" target="_blank" rel="noopener noreferrer">${esc(siteName(v))} ↗</a>` : '<span style="color:var(--muted)">—</span>';

  // Steve HARD rule: admin cards show created date AND time with full ISO in title=
  const adminDt = (s) => {
    if (!s) return '—';
    try {
      const d = new Date(s);
      if (isNaN(d)) return '—';
      const t = d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
      return `<span title="${esc(d.toISOString())}">🕓 ${esc(t)}</span>`;
    } catch { return '—'; }
  };

  // ── Frame + meta ─────────────────────────────────────────────────────────────
  const c = PR.frame('organizations', 'Organizations', 'Filter, sort, review, merge, enroll, export. Click a row for the full profile with source evidence.');
  const meta = await PR.api('/meta');
  const CA = meta.states.find((s) => s.key === 'CA') || { metros: [] };
  const AZ = meta.states.find((s) => s.key === 'AZ') || { metros: [] };

  // ── State ────────────────────────────────────────────────────────────────────
  let sort   = lsGet(LS.sort, 'priority');
  let dir    = lsGet(LS.dir, 'desc');
  let offset = 0;
  let view   = lsGet(LS.view, 'list');   // 'list' | 'grid'
  let railSel = lsGet(LS.rail, '');      // '' = all, 'group:id' or 'type:key'
  let totalRows = 0;
  let lastRows  = [];

  // density: 1 (compact) → 10 (comfortable). Map to row padding + font size.
  const DENSITY_MIN_PAD  = 4,  DENSITY_MAX_PAD  = 16;
  const DENSITY_MIN_FONT = 12, DENSITY_MAX_FONT = 14.5;
  const CARD_MIN_MIN = 220, CARD_MIN_MAX = 380;
  let density = parseInt(lsGet(LS.density, '5'), 10);
  if (isNaN(density) || density < 1 || density > 10) density = 5;

  function applyDensity(d) {
    const t = (d - 1) / 9;
    const pad  = Math.round(DENSITY_MIN_PAD  + t * (DENSITY_MAX_PAD  - DENSITY_MIN_PAD));
    const font = (DENSITY_MIN_FONT + t * (DENSITY_MAX_FONT - DENSITY_MIN_FONT)).toFixed(1);
    const card = Math.round(CARD_MIN_MIN + t * (CARD_MIN_MAX - CARD_MIN_MIN));
    document.documentElement.style.setProperty('--org-row-pad', pad + 'px');
    document.documentElement.style.setProperty('--org-font', font + 'px');
    document.documentElement.style.setProperty('--org-card-min', card + 'px');
  }
  applyDensity(density);

  // ── Build page layout ────────────────────────────────────────────────────────
  // Top filter bar (existing PR.filters — keep as-is)
  const { bar, state } = PR.filters([
    { key: 'q',              placeholder: 'Search name / domain…' },
    { key: 'state',          type: 'select', options: [['', 'State: all'], ['CA', 'California'], ['AZ', 'Arizona']] },
    { key: 'metro',          type: 'select', options: [['', 'Metro: all'], ...CA.metros.map((m) => [m.key, 'CA · ' + m.label]), ...AZ.metros.map((m) => [m.key, 'AZ · ' + m.label])] },
    { key: 'type',           type: 'select', options: [['', 'Category: all'], ...meta.org_types.map((t) => [t.key, t.label])] },
    { key: 'asset_class',    type: 'select', options: [['', 'Asset class: all'], ...meta.asset_classes.map((a) => [a, a.replace(/_/g, ' ')])] },
    { key: 'status',         type: 'select', options: [['', 'Status: all']].concat('discovered,needs_review,verified,incomplete,duplicate,archived,ready_for_outreach,contacted,active_relationship'.split(',').map((s) => [s, s.replace(/_/g, ' ')])) },
    { key: 'min_confidence', type: 'select', options: [['', 'Confidence: any'], ['40', '≥40'], ['70', '≥70']] },
    { key: 'min_priority',   type: 'select', options: [['', 'Priority: any'], ['40', '≥40'], ['60', '≥60'], ['80', '≥80']] },
    { key: 'no_pr_contact',  type: 'select', options: [['', 'PR contact: any'], ['1', 'No PR contact yet']] },
  ], () => { offset = 0; load(); });
  c.appendChild(bar);

  // Action row (bulk actions + export)
  const actions = document.createElement('div'); actions.className = 'filters';
  actions.innerHTML = `
    <button class="btn" id="add-org">+ Add organization</button>
    <button class="btn" id="verify-sel" disabled>Queue verify</button>
    <button class="btn" id="tag-sel" disabled>Bulk tag</button>
    <button class="btn" id="campaign-sel" disabled>Add contacts to campaign</button>
    <a class="btn" id="export" href="#">⬇ Export CSV (current filters)</a>
    <span id="selcount" style="color:var(--muted);font-size:12px"></span>`;
  c.appendChild(actions);

  // Toolbar: view toggle + density slider + row count + Reset
  const toolbar = document.createElement('div'); toolbar.className = 'org-toolbar';
  toolbar.innerHTML = `
    <div class="org-view-toggle" id="viewtoggle" role="group" aria-label="View mode">
      <button data-v="list" title="List / table view" aria-pressed="${view === 'list'}">▤ List</button>
      <button data-v="grid" title="Card grid view" aria-pressed="${view === 'grid'}">▦ Grid</button>
    </div>
    <label class="org-density-wrap" title="Row density (List) / card width (Grid)">
      Density
      <input type="range" id="density-slider" min="1" max="10" step="1" value="${density}" aria-label="Display density">
    </label>
    <button class="btn sm" id="reset-prefs" title="Reset all saved view preferences (sort, density, view, rail selection)">↺ Reset</button>
    <span class="org-row-count" id="rowcount"></span>`;
  c.appendChild(toolbar);

  // Sync view toggle button states
  function syncViewBtns() {
    toolbar.querySelectorAll('#viewtoggle button').forEach((b) => {
      const on = b.dataset.v === view;
      b.classList.toggle('on', on);
      b.setAttribute('aria-pressed', on);
    });
  }
  syncViewBtns();

  // Density slider
  const densitySlider = toolbar.querySelector('#density-slider');
  densitySlider.addEventListener('input', () => {
    density = parseInt(densitySlider.value, 10);
    applyDensity(density);
    lsSet(LS.density, density);
  });

  // View toggle
  toolbar.querySelector('#viewtoggle').addEventListener('click', (e) => {
    const b = e.target.closest('button[data-v]'); if (!b) return;
    view = b.dataset.v;
    lsSet(LS.view, view);
    syncViewBtns();
    renderView();
  });

  // Reset prefs
  toolbar.querySelector('#reset-prefs').addEventListener('click', () => {
    [LS.view, LS.sort, LS.dir, LS.density, LS.rail].forEach((k) => { try { localStorage.removeItem(k); } catch {} });
    sort = 'priority'; dir = 'desc'; view = 'list'; railSel = ''; density = 5;
    applyDensity(density);
    densitySlider.value = density;
    syncViewBtns();
    syncRailActive();
    // clear the type filter in the top bar
    const typeEl = bar.querySelector('[data-key="type"]');
    if (typeEl) { typeEl.value = ''; state.type = ''; }
    tbl.setSortIndicator(sort, dir);
    offset = 0; load();
  });

  // ── Shell: rail + main ───────────────────────────────────────────────────────
  const shell = document.createElement('div'); shell.className = 'org-shell';

  // -- LEFT RAIL --
  const railEl = document.createElement('aside');
  railEl.className = 'org-rail'; railEl.setAttribute('aria-label', 'Category filter');

  // Build rail groups HTML
  let railHtml = `<div class="org-rail-inner">
    <h4>Quick filter</h4>
    <button class="org-rail-item${railSel === '' ? ' active' : ''}" data-rail="" aria-current="${railSel === '' ? 'true' : 'false'}">All organizations</button>`;
  RAIL_GROUPS.forEach((g) => {
    const active = railSel === 'group:' + g.id;
    railHtml += `<button class="org-rail-item${active ? ' active' : ''}" data-rail="group:${g.id}" title="${esc(g.types.join(', '))}" aria-current="${active ? 'true' : 'false'}">${esc(g.label)}</button>`;
  });
  railHtml += `<div class="org-rail-sep"></div>
    <button class="org-rail-group-toggle collapsed" id="allcats-toggle" aria-expanded="false" aria-controls="allcats">All categories</button>
    <div class="org-all-cats collapsed" id="allcats">`;
  meta.org_types.forEach((t) => {
    const active = railSel === 'type:' + t.key;
    railHtml += `<button class="org-rail-item${active ? ' active' : ''}" data-rail="type:${esc(t.key)}" aria-current="${active ? 'true' : 'false'}">${esc(t.label)}</button>`;
  });
  railHtml += `</div></div>`;
  railEl.innerHTML = railHtml;

  // All categories expand/collapse
  railEl.querySelector('#allcats-toggle').addEventListener('click', () => {
    const tog = railEl.querySelector('#allcats-toggle');
    const cats = railEl.querySelector('#allcats');
    const collapsed = cats.classList.toggle('collapsed');
    tog.classList.toggle('collapsed', collapsed);
    tog.setAttribute('aria-expanded', !collapsed);
  });

  // Rail item click
  railEl.addEventListener('click', (e) => {
    const btn = e.target.closest('[data-rail]');
    if (!btn || e.target.closest('#allcats-toggle')) return;
    railSel = btn.dataset.rail;
    lsSet(LS.rail, railSel);
    syncRailActive();
    // Sync the top-bar "type" dropdown where possible (single-type rail items)
    const typeEl = bar.querySelector('[data-key="type"]');
    if (typeEl) {
      if (railSel.startsWith('type:')) {
        const key = railSel.slice(5);
        typeEl.value = key; state.type = key;
      } else {
        // group or all: clear the single-type filter (the types= param handles it)
        typeEl.value = ''; state.type = '';
      }
    }
    offset = 0; load();
  });

  function syncRailActive() {
    railEl.querySelectorAll('[data-rail]').forEach((btn) => {
      const on = btn.dataset.rail === railSel;
      btn.classList.toggle('active', on);
      btn.setAttribute('aria-current', on ? 'true' : 'false');
    });
  }

  // -- MAIN area --
  const mainEl = document.createElement('div'); mainEl.className = 'org-main';

  shell.appendChild(railEl);
  shell.appendChild(mainEl);
  c.appendChild(shell);

  // ── Selected set ─────────────────────────────────────────────────────────────
  const selected = new Set();

  // ── Table columns ─────────────────────────────────────────────────────────────
  const COLS = [
    { key: '_sel', label: '',          render: (r) => `<input type="checkbox" data-sel="${r.id}" ${selected.has(r.id) ? 'checked' : ''}>` },
    { key: 'name', label: 'Organization', render: (r) => `<b>${esc(r.display_name)}</b><br><span style="color:var(--muted);font-size:12px">${esc(r.domain || '')}</span>` },
    { key: 'type', label: 'Category',  render: (r) => PR.badge(r.organization_type, 'blue') },
    { key: '_geo', label: 'Metros',    render: (r) => (r.metros || []).map((m) => PR.badge(m)).join(' ') || (r.state_presence || []).join(',') },
    { key: 'pr',   label: 'PR',        render: (r) => r.internal_pr_status === 'has_internal_pr' ? PR.badge('internal PR', 'green') : r.general_press_email ? PR.badge('press email', 'blue') : PR.badge(r.internal_pr_status, 'gray') },
    { key: 'people', label: '👤',      render: (r) => String(r.people_count || 0) },
    { key: 'confidence', label: 'Conf',   render: (r) => PR.score(r.confidence_score) },
    { key: 'priority',   label: 'Priority', render: (r) => PR.score(r.priority_score) },
    { key: 'status',     label: 'Status',   render: (r) => PR.statusBadge(r.lifecycle_status) },
    { key: 'verified',   label: 'Verified', render: (r) => PR.dateShort(r.last_verified_at) },
    { key: 'created',    label: 'Added', render: (r) => adminDt(r.created_at) },
  ];

  // ── Expand renderer (inline detail strip) ───────────────────────────────────
  function expandRow(r) {
    const assetBadges = (r.asset_classes || []).map((a) => PR.badge(a, 'gray')).join(' ') || '<span style="color:var(--muted)">—</span>';
    const tagBadges   = (r.tags || []).map((t) => PR.badge(t, 'purple')).join(' ') || '<span style="color:var(--muted)">—</span>';
    const metros      = (r.metros || []).join(', ') || '—';
    return `<div class="xstrip">
      <div class="xrow"><span class="xk">Website</span><span class="xv">${web(r.website_url)}</span></div>
      <div class="xrow"><span class="xk">Press email</span><span class="xv">${r.general_press_email ? `<a href="mailto:${esc(r.general_press_email)}">${esc(r.general_press_email)}</a>` : '<span style="color:var(--muted)">—</span>'}</span></div>
      <div class="xrow"><span class="xk">Press page</span><span class="xv">${web(r.press_page_url)}</span></div>
      <div class="xrow"><span class="xk">Asset classes</span><span class="xv"><div class="badges">${assetBadges}</div></span></div>
      <div class="xrow"><span class="xk">All metros</span><span class="xv">${esc(metros)}</span></div>
      <div class="xrow"><span class="xk">Confidence</span><span class="xv">${PR.score(r.confidence_score)}</span></div>
      <div class="xrow"><span class="xk">Priority</span><span class="xv">${PR.score(r.priority_score)}</span></div>
      <div class="xrow"><span class="xk">Tags</span><span class="xv"><div class="badges">${tagBadges}</div></span></div>
      <div class="xrow"><span class="xk">People</span><span class="xv">${r.people_count || 0} · Evidence: ${r.evidence_count || 0}</span></div>
      <div class="xrow"><span class="xk">Created</span><span class="xv">${adminDt(r.created_at)}</span></div>
    </div>`;
  }

  // ── Table (list view) ────────────────────────────────────────────────────────
  const tbl = PR.table(COLS, {
    onSort: (s, d) => {
      sort = s; dir = d;
      lsSet(LS.sort, sort); lsSet(LS.dir, dir);
      load();
    },
    onRow: (r) => openOrg(r.id),
    expand: expandRow,
  });
  // Set initial sort indicator
  tbl.setSortIndicator(sort, dir);

  // ── Card grid view ────────────────────────────────────────────────────────────
  const gridEl = document.createElement('div'); gridEl.className = 'org-card-grid';

  function renderCards(rows) {
    gridEl.innerHTML = rows.length ? rows.map((r) => {
      const metroStr = (r.metros || []).slice(0, 3).join(', ') + ((r.metros || []).length > 3 ? ` +${r.metros.length - 3}` : '');
      return `<div class="org-card" data-id="${r.id}" tabindex="0" role="button" aria-label="${esc(r.display_name)}">
        <div class="oc-name">${esc(r.display_name)}</div>
        <div class="oc-domain">${esc(r.domain || r.website_url || '')}</div>
        <div class="oc-badges">
          ${PR.badge(r.organization_type, 'blue')}
          ${r.internal_pr_status === 'has_internal_pr' ? PR.badge('internal PR', 'green') : r.general_press_email ? PR.badge('press email', 'blue') : ''}
          ${PR.statusBadge(r.lifecycle_status)}
        </div>
        <div class="oc-metros">${esc(metroStr) || '<span style="color:var(--muted)">No metro</span>'}</div>
        <div class="oc-scores">
          Conf: ${PR.score(r.confidence_score)} &nbsp; Pri: ${PR.score(r.priority_score)}
        </div>
        <div class="oc-when" title="${esc(r.created_at || '')}">${adminDt(r.created_at)}</div>
      </div>`;
    }).join('') : '<div class="empty">Nothing here yet</div>';

    gridEl.querySelectorAll('.org-card').forEach((card) => {
      const handler = (e) => {
        if (!e.target.closest('a,button,input,select')) openOrg(Number(card.dataset.id));
      };
      card.addEventListener('click', handler);
      card.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(e); } });
    });
  }

  // Containers for both views, placed inside mainEl
  const listWrap = document.createElement('div'); listWrap.id = 'org-list-wrap'; listWrap.appendChild(tbl.el);
  const gridWrap = document.createElement('div'); gridWrap.id = 'org-grid-wrap'; gridWrap.appendChild(gridEl);
  mainEl.appendChild(listWrap);
  mainEl.appendChild(gridWrap);

  const pagerBox = document.createElement('div'); mainEl.appendChild(pagerBox);

  function renderView() {
    const isList = view === 'list';
    listWrap.style.display = isList ? '' : 'none';
    gridWrap.style.display = isList ? 'none' : '';
    if (!isList) renderCards(lastRows);
  }
  renderView();

  // ── Build query params for load ───────────────────────────────────────────────
  function buildQuery() {
    const params = { ...Object.fromEntries(Object.entries(state).filter(([, v]) => v)) };
    params.sort   = sort;
    params.dir    = dir;
    params.limit  = LIMIT;
    params.offset = offset;
    // Rail group → types= param (overrides state.type when a group is selected)
    if (railSel.startsWith('group:')) {
      const gid = railSel.slice(6);
      const grp = RAIL_GROUPS.find((g) => g.id === gid);
      if (grp) { params.types = grp.types.join(','); delete params.type; }
    } else if (railSel.startsWith('type:')) {
      params.type = railSel.slice(5);
    }
    return new URLSearchParams(params);
  }

  // ── Load ──────────────────────────────────────────────────────────────────────
  async function load() {
    const q = buildQuery();
    let j;
    try { j = await PR.api('/organizations?' + q); } catch (e) { PR.toast('Load failed: ' + e.message, true); return; }
    lastRows = j.rows || [];
    totalRows = j.total || 0;

    // Update row count
    document.getElementById('rowcount').textContent = `${totalRows.toLocaleString()} organization${totalRows === 1 ? '' : 's'}`;

    if (view === 'list') { tbl.render(lastRows); } else { renderCards(lastRows); }

    // Reattach checkbox listeners after table render
    listWrap.querySelectorAll('[data-sel]').forEach((cb) => cb.addEventListener('change', () => {
      const id = Number(cb.dataset.sel);
      cb.checked ? selected.add(id) : selected.delete(id);
      syncSel();
    }));

    // Pager
    pagerBox.innerHTML = '';
    pagerBox.appendChild(PR.pager(totalRows, LIMIT, offset, (o) => { offset = o; load(); }));

    // Export href
    document.getElementById('export').href = '/api/pr/export/organizations?' + q;
  }

  function syncSel() {
    ['verify-sel', 'tag-sel', 'campaign-sel'].forEach((id) => document.getElementById(id).disabled = !selected.size);
    document.getElementById('selcount').textContent = selected.size ? selected.size + ' selected' : '';
  }

  // ── Drawer / org detail ──────────────────────────────────────────────────────
  const drawer = PR.drawer();
  async function openOrg(id) {
    let o;
    try { o = await PR.api('/organizations/' + id); } catch (e) { PR.toast('Could not load org: ' + e.message, true); return; }
    const comp = (o.score_components || {});
    const body = drawer.open(`${esc(o.display_name)} ${PR.statusBadge(o.lifecycle_status)}`, `
      <div class="kv">
        <dt>Legal name</dt><dd>${esc(o.legal_name || '—')}</dd>
        <dt>Website</dt><dd>${o.website_url ? `<a href="${esc(o.website_url)}" target="_blank" rel="noopener noreferrer">${esc(o.website_url)}</a>` : '—'}</dd>
        <dt>LinkedIn</dt><dd>${o.linkedin_company_url ? `<a href="${esc(o.linkedin_company_url)}" target="_blank" rel="noopener noreferrer">${esc(o.linkedin_company_url)}</a> ${PR.verifBadge(o.linkedin_status)}` : '—'}</dd>
        <dt>Category</dt><dd>${PR.badge(o.organization_type, 'blue')} ${esc(o.organization_subtype || '')}</dd>
        <dt>Asset classes</dt><dd>${(o.asset_classes || []).map((a) => PR.badge(a)).join(' ') || '—'}</dd>
        <dt>Geography</dt><dd>${(o.state_presence || []).join(', ')} · ${(o.metros || []).join(', ') || 'no metro'} · ${(o.counties || []).join(', ') || 'no county'}</dd>
        <dt>HQ</dt><dd>${esc(o.headquarters || '—')}</dd>
        <dt>Internal PR</dt><dd>${PR.badge(o.internal_pr_status, o.internal_pr_status === 'has_internal_pr' ? 'green' : 'gray')}</dd>
        <dt>Press page</dt><dd>${o.press_page_url ? `<a href="${esc(o.press_page_url)}" target="_blank" rel="noopener noreferrer">${esc(o.press_page_url.slice(0, 70))}</a>` : '—'}</dd>
        <dt>Press email</dt><dd>${esc(o.general_press_email || '—')} ${o.general_press_phone ? '· ' + esc(o.general_press_phone) : ''}</dd>
        <dt>Contact form</dt><dd>${o.contact_form_url ? `<a href="${esc(o.contact_form_url)}" target="_blank" rel="noopener noreferrer">${esc(o.contact_form_url.slice(0, 70))}</a>` : '—'}</dd>
        <dt>Scores</dt><dd>Confidence ${PR.score(o.confidence_score)} · Priority ${PR.score(o.priority_score)}
          <details style="margin-top:4px"><summary style="cursor:pointer;font-size:12px;color:var(--muted)">score components</summary>
          <pre style="font-size:11px;background:var(--bg);padding:8px;border-radius:6px;overflow:auto">${esc(JSON.stringify(comp, null, 1))}</pre></details></dd>
        <dt>Verification</dt><dd>${PR.verifBadge(o.verification_status)} · last verified ${PR.date(o.last_verified_at)}</dd>
        <dt>Created</dt><dd title="${esc(o.created_at)}">🕓 ${PR.date(o.created_at)}</dd>
      </div>
      <div class="filters">
        <button class="btn sm" id="d-verify">Queue website verify</button>
        <button class="btn sm" id="d-people">Queue person discovery</button>
        <button class="btn sm" id="d-matches">Find duplicates</button>
        <a class="btn sm" href="/admin/pr-intelligence/review?kind=organizations&focus=${o.id}">Open in review</a>
      </div>
      <div id="d-matches-out"></div>
      <h3 style="margin:14px 0 8px">People (${o.people.length})</h3>
      ${o.people.map((p) => `<div class="gatechk"><b>${esc(p.full_name)}</b>
        <span style="color:var(--muted)">${esc(p.exact_title || '')}</span>
        ${PR.verifBadge(p.email_verification_status)} ${PR.statusBadge(p.lifecycle_status)}
        ${p.linkedin_url ? `<a href="${esc(p.linkedin_url)}" target="_blank" rel="noopener noreferrer" title="LinkedIn profile" style="margin-left:auto">🔗</a>` : ''}
        <a href="/admin/pr-intelligence/people?id=${p.id}" style="margin-left:${p.linkedin_url ? '8px' : 'auto'}">open →</a></div>`).join('') || '<div class="empty">No people yet</div>'}
      <h3 style="margin:14px 0 8px">Agency / parent relationships</h3>
      ${o.relationships.map((r) => `<div class="gatechk">${PR.badge(r.relationship_type, 'purple')}
        <span>${esc(r.source_name_org)} → ${esc(r.target_name_org)}</span>
        ${PR.badge(r.current_status, r.current_status === 'current' ? 'green' : 'gray')}<span style="color:var(--muted)">conf ${r.confidence}</span></div>`).join('') || '<div class="empty">None recorded</div>'}
      <h3 style="margin:14px 0 8px">Source evidence</h3>
      ${PR.evidenceHtml(o.evidence, { addToLetter: true })}
      <h3 style="margin:14px 0 8px">Outreach history</h3>
      ${o.outreach.map((m) => `<div class="gatechk">${PR.statusBadge(m.status)} <span>${esc(m.subject || '(no subject)')}</span>
        <span style="color:var(--muted);margin-left:auto">${PR.dateShort(m.sent_at || m.drafted_at)}</span></div>`).join('') || '<div class="empty">No outreach yet</div>'}
      <h3 style="margin:14px 0 8px">Notes <span style="font-weight:400;color:var(--muted);font-size:12px">(team notes — timestamped &amp; attributed)</span></h3>
      <div id="o-notethread"></div>
      <h3 style="margin:14px 0 8px">Recent activity</h3>
      ${o.recent_activity.slice(0, 12).map((a) => `<div class="gatechk"><span style="color:var(--muted)">${PR.dateShort(a.created_at)}</span>
        <span>${esc(a.activity)}</span><span style="color:var(--muted)">${esc(a.actor || '')}</span></div>`).join('')}`);

    PR.notes(body.querySelector('#o-notethread'), 'organization', o.id);
    body.querySelector('#d-verify').addEventListener('click', () =>
      PR.guard(() => PR.api('/jobs', { method: 'POST', body: { job_type: 'verify-organization', payload: { organization_id: o.id } } }), 'Verify job queued'));
    body.querySelector('#d-people').addEventListener('click', () =>
      PR.guard(() => PR.api('/jobs', { method: 'POST', body: { job_type: 'discover-people', payload: { organization_id: o.id, team_pages: [] } } }), 'Person-discovery job queued'));
    body.querySelector('#d-matches').addEventListener('click', async () => {
      const j = await PR.guard(() => PR.api('/organizations/' + o.id + '/matches'));
      const out = body.querySelector('#d-matches-out');
      out.innerHTML = j.matches.length ? j.matches.map((m) => `
        <div class="evi"><b>${esc(m.org.display_name)}</b> ${PR.badge(m.verdict, m.verdict === 'exact' ? 'red' : 'amber')} score ${m.score}
          <div class="x">${m.reasons.map(esc).join(' · ')}</div>
          <button class="btn sm" data-merge="${m.org.id}">Merge that one into this</button></div>`).join('')
        : '<div class="empty">No duplicate candidates</div>';
      out.querySelectorAll('[data-merge]').forEach((b) => b.addEventListener('click', async () => {
        if (!confirm('Merge #' + b.dataset.merge + ' into "' + o.display_name + '"? The other record becomes a duplicate pointer.')) return;
        await PR.guard(() => PR.api('/organizations/' + o.id + '/merge', { method: 'POST', body: { duplicate_id: Number(b.dataset.merge) } }), 'Merged');
        drawer.close(); load();
      }));
    });
    body.querySelectorAll('[data-addletter]').forEach((a) => a.addEventListener('click', (e) => {
      e.preventDefault();
      localStorage.setItem('pr-letter-clip', JSON.stringify({ org_id: o.id, org_name: o.display_name, source_id: Number(a.dataset.addletter), excerpt: a.dataset.excerpt }));
      PR.toast('Added to letter clipboard — open the Letter Builder to insert it');
    }));
  }

  // ── Toolbar actions ──────────────────────────────────────────────────────────
  document.getElementById('add-org').addEventListener('click', () => {
    const body = drawer.open('Add organization (manual research entry)', `
      <div class="form">
        <div><label>Display name *</label><input id="f-name"></div>
        <div class="frow"><div><label>Website URL</label><input id="f-web" placeholder="https://…"></div>
          <div><label>Category</label><select id="f-type">${meta.org_types.map((t) => `<option value="${t.key}">${t.label}</option>`).join('')}</select></div></div>
        <div class="frow"><div><label>State</label><select id="f-state"><option>CA</option><option>AZ</option></select></div>
          <div><label>Metro</label><select id="f-metro">${CA.metros.map((m) => `<option value="${m.key}">${m.label}</option>`).join('')}</select></div></div>
        <div><label>Verification URL (the page you checked) *</label><input id="f-src" placeholder="https://…"></div>
        <div><label>Evidence excerpt</label><textarea id="f-exc" placeholder="Short quote from the page backing this entry"></textarea></div>
        <button class="btn primary" id="f-save">Save organization</button>
      </div>`);
    body.querySelector('#f-save').addEventListener('click', async () => {
      const name = body.querySelector('#f-name').value.trim();
      const src  = body.querySelector('#f-src').value.trim();
      if (!name) return PR.toast('Name required', true);
      if (!src)  return PR.toast('A verification URL is required for manual entries', true);
      await PR.guard(() => PR.api('/organizations', { method: 'POST', body: {
        display_name: name, website_url: body.querySelector('#f-web').value.trim() || null,
        organization_type: body.querySelector('#f-type').value,
        state_presence: [body.querySelector('#f-state').value], metros: [body.querySelector('#f-metro').value],
        evidence: { source: { source_type: 'manual', source_name: 'Manual research entry', url: src, short_excerpt: body.querySelector('#f-exc').value.trim() || null, is_primary_source: false, adapter: 'manual' }, fields: { organization: 85 } },
      } }), 'Organization saved');
      drawer.close(); load();
    });
  });

  document.getElementById('verify-sel').addEventListener('click', async () => {
    for (const id of selected) await PR.api('/jobs', { method: 'POST', body: { job_type: 'verify-organization', payload: { organization_id: id } } });
    PR.toast(selected.size + ' verify job(s) queued'); selected.clear(); syncSel(); load();
  });

  document.getElementById('tag-sel').addEventListener('click', async () => {
    const tag = prompt('Tag to add to ' + selected.size + ' organization(s):'); if (!tag) return;
    for (const id of selected) {
      const o = await PR.api('/organizations/' + id);
      await PR.api('/organizations/' + id, { method: 'PUT', body: { tags: [...new Set([...(o.tags || []), tag])] } });
    }
    PR.toast('Tagged'); selected.clear(); syncSel(); load();
  });

  document.getElementById('campaign-sel').addEventListener('click', async () => {
    const camps = (await PR.api('/campaigns')).rows;
    if (!camps.length) return PR.toast('Create a campaign first (Campaigns page)', true);
    const cid = prompt('Campaign id to enroll each org\'s PEOPLE into:\n' + camps.map((x) => `#${x.id} ${x.name}`).join('\n'));
    if (!cid) return;
    let n = 0;
    for (const id of selected) {
      const o = await PR.api('/organizations/' + id);
      const ids = o.people.map((p) => p.id);
      if (ids.length) { const r = await PR.api('/campaigns/' + cid + '/enroll', { method: 'POST', body: { person_ids: ids } }); n += r.added.length; }
    }
    PR.toast(n + ' people enrolled');
  });

  // ── Initial load + deep link ?id= ────────────────────────────────────────────
  const deepId = new URLSearchParams(location.search).get('id');
  await load();
  if (deepId) openOrg(Number(deepId));
})();
</script>
</body>
</html>