← back to Rentv 2026

public/admin/pr-intelligence/campaigns.html

83 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 — Campaigns</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 () {
  const c = PR.frame('campaigns', 'Campaigns', 'Target definitions over verified contacts. Enrollment skips suppressed people; drafts generate per-member and go through review → approval → provider draft → explicit send.');
  const meta = await PR.api('/meta');
  const CA = meta.states.find((s) => s.key === 'CA');

  c.innerHTML = `
    <div class="filters">
      <button class="btn primary" id="new-camp">+ New campaign</button>
    </div>
    <div id="list"><span class="spin"></span></div>
    <div id="detail"></div>`;

  async function load() {
    const rows = (await PR.api('/campaigns')).rows;
    document.getElementById('list').innerHTML = rows.length ? `<table class="grid"><thead><tr>
      <th>Campaign</th><th>Targets</th><th>Status</th><th>Members</th><th>Sent</th><th>Replies</th><th>Created</th><th></th></tr></thead><tbody>` +
      rows.map((x) => `<tr>
        <td><b>${PR.esc(x.name)}</b></td>
        <td style="font-size:12px">${PR.esc(x.state || 'any')} · ${(x.metros || []).join(',') || 'all metros'} · ${(x.organization_types || []).slice(0, 3).join(',') || 'all types'}</td>
        <td>${PR.statusBadge(x.status)}</td><td>${x.member_count}</td><td>${x.sent_count}</td><td>${x.reply_count}</td>
        <td title="${PR.esc(x.created_at)}">🕓 ${PR.date(x.created_at)}</td>
        <td><button class="btn sm" data-open="${x.id}">Open</button></td></tr>`).join('') + '</tbody></table>'
      : '<div class="empty">No campaigns yet</div>';
    document.querySelectorAll('[data-open]').forEach((b) => b.addEventListener('click', () => openCamp(Number(b.dataset.open))));
  }

  async function openCamp(id) {
    const camp = await PR.api('/campaigns/' + id);
    document.getElementById('detail').innerHTML = `
      <div class="panel" style="margin-top:16px">
        <h3>${PR.esc(camp.name)} ${PR.statusBadge(camp.status)}</h3>
        <div class="filters">
          <select id="c-status">${['draft', 'active', 'paused', 'archived'].map((s) => `<option ${s === camp.status ? 'selected' : ''}>${s}</option>`).join('')}</select>
          <button class="btn sm" id="c-setstatus">Set status</button>
          <button class="btn sm" id="c-autodraft">Generate drafts for members without one</button>
          <a class="btn sm" href="/api/pr/export/outreach?campaign_id=${camp.id}">⬇ Export outreach history</a>
        </div>
        <h3 style="margin:12px 0 6px">Members (${camp.members.length})</h3>
        ${camp.members.length ? `<table class="grid"><thead><tr><th>Person</th><th>Org</th><th>Email status</th><th>Lifecycle</th><th>Outreach</th><th></th></tr></thead><tbody>` +
          camp.members.map((m) => `<tr>
            <td><a href="/admin/pr-intelligence/people?id=${m.person_id}">${PR.esc(m.full_name)}</a><br><span style="color:var(--muted);font-size:11.5px">${PR.esc(m.exact_title || '')}</span></td>
            <td>${PR.esc(m.organization_name || '')}</td>
            <td>${PR.verifBadge(m.email_verification_status)}</td>
            <td>${PR.statusBadge(m.lifecycle_status)}</td>
            <td>${m.outreach_status ? PR.statusBadge(m.outreach_status) : '—'}</td>
            <td><button class="btn sm danger" data-rm="${m.person_id}">Remove</button></td></tr>`).join('') + '</tbody></table>'
          : '<div class="empty">No members — enroll from the People or Organizations pages</div>'}
      </div>`;
    document.getElementById('c-setstatus').addEventListener('click', async () => {
      await PR.guard(() => PR.api('/campaigns/' + id, { method: 'PUT', body: { status: document.getElementById('c-status').value } }), 'Status set');
      load(); openCamp(id);
    });
    document.getElementById('c-autodraft').addEventListener('click', async () => {
      await PR.guard(() => PR.api('/jobs', { method: 'POST', body: { job_type: 'generate-draft', payload: { campaign_id: id } } }), 'Draft-generation job queued (drafts appear in Letter Builder for review)');
    });
    document.querySelectorAll('[data-rm]').forEach((b) => b.addEventListener('click', async () => {
      await PR.guard(() => PR.api('/campaigns/' + id + '/members/' + b.dataset.rm, { method: 'DELETE' }), 'Removed');
      openCamp(id);
    }));
  }

  document.getElementById('new-camp').addEventListener('click', async () => {
    const name = prompt('Campaign name (e.g. "CA Phase 1 — LA County brokerages"):'); if (!name) return;
    const r = await PR.guard(() => PR.api('/campaigns', { method: 'POST', body: { name, state: 'CA', metros: [], organization_types: [] } }), 'Campaign created');
    load(); openCamp(r.id);
  });

  load();
})();
</script>
</body>
</html>