← back to Ventura Corridor

public/pitches.html

557 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Ventura Boulevard · DW pitch pipeline</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script>(function(){try{var t=localStorage.getItem('vc-theme');document.documentElement.dataset.theme=(t==='light'||t==='dark')?t:'dark';}catch(e){document.documentElement.dataset.theme='dark';}})();</script>
<link rel="stylesheet" href="/theme.css">
<script src="/theme.js" defer></script>
<style>
  :root {
    --noir: #0a0a0c; --noir-rise: #131316; --ink: #f0ece2;
    --ink-mute: #888475; --metal: #b89968; --metal-glow: #d4b683;
    --rule: #2a2622;
    --serif: 'Cormorant Garamond', 'Times New Roman', serif;
    --sans: 'Inter', -apple-system, system-ui, sans-serif;
    --mono: 'JetBrains Mono', ui-monospace, monospace;
    --green: #6a9b73; --red: #b66565; --amber: #c4a05c;
  }
  @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;1,300;1,400&family=Inter:wght@200;300;400;500;600&family=JetBrains+Mono:wght@200;300;400&display=swap');
  html, body { margin: 0; background: var(--noir); color: var(--ink); font-family: var(--sans); font-weight: 300; }
  * { box-sizing: border-box; }
  header {
    position: sticky; top: 0; z-index: 10;
    background: linear-gradient(180deg, rgba(10,10,12,.97) 70%, rgba(10,10,12,0));
    padding: 18px 32px 14px; border-bottom: 1px solid var(--rule);
    display: flex; justify-content: space-between; align-items: baseline; gap: 24px; flex-wrap: wrap;
  }
  header h1 { font-family: var(--serif); font-weight: 400; font-size: 24px; margin: 0; }
  header h1 em { font-style: italic; color: var(--metal); }
  .sub { font-size: 10px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); margin-top: 2px; }
  nav.tabs { display: flex; gap: 4px; flex-wrap: wrap; }
  nav.tabs a { font-size: 9px; letter-spacing: .26em; text-transform: uppercase;
    color: var(--ink-mute); text-decoration: none; padding: 5px 10px; border: 1px solid var(--rule); }
  nav.tabs a.active, nav.tabs a:hover { color: var(--metal); border-color: var(--metal); }

  .controls { padding: 14px 32px; border-bottom: 1px solid var(--rule); display: flex; gap: 16px; flex-wrap: wrap; align-items: center; font-size: 11px; }
  .controls label { display: flex; align-items: center; gap: 6px; font-size: 9px; letter-spacing: .22em; text-transform: uppercase; color: var(--ink-mute); }
  .controls select, .controls input { background: var(--noir-rise); color: var(--ink); border: 1px solid var(--rule); padding: 5px 8px; font-family: var(--sans); font-size: 11px; }
  .controls input[type=search] { min-width: 220px; }
  .controls .ct { margin-left: auto; font-size: 10px; letter-spacing: .22em; text-transform: uppercase; color: var(--ink-mute); }
  .controls .ct b { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 16px; font-weight: 400; margin-right: 6px; font-variant-numeric: tabular-nums; }

  /* Quick stats by status */
  .stats { padding: 12px 32px; border-bottom: 1px solid var(--rule); display: flex; gap: 24px; flex-wrap: wrap; font-size: 10px; letter-spacing: .15em; color: var(--ink-mute); text-transform: uppercase; }
  .stats span b { color: var(--metal); font-family: var(--serif); font-style: italic; font-size: 14px; margin-right: 6px; font-variant-numeric: tabular-nums; }

  table { width: 100%; border-collapse: collapse; font-size: 12px; }
  th, td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--rule); vertical-align: top; }
  th { font-size: 9px; letter-spacing: .26em; text-transform: uppercase; color: var(--ink-mute); font-weight: 400; background: var(--noir); position: sticky; top: 60px; z-index: 1; }
  tr:hover { background: var(--noir-rise); cursor: pointer; }
  tr.expanded { background: var(--noir-rise); }
  tr.sel { background: rgba(212,182,131,0.06); }
  tr.sel.expanded { background: rgba(212,182,131,0.1); }
  .nm { font-weight: 400; color: var(--ink); }
  .addr { font-family: var(--mono); font-size: 9px; color: var(--ink-mute); letter-spacing: .08em; }
  .ptag { font-size: 8px; letter-spacing: .22em; text-transform: uppercase; padding: 2px 5px; border: 1px solid var(--rule); color: var(--ink-mute); display: inline-block; }
  .ptag.t-trade-partner, .ptag.t-trade-client { color: var(--metal-glow); border-color: var(--metal-glow); }
  .ptag.t-cross-sell, .ptag.t-showroom-partner { color: var(--metal); border-color: var(--metal); }
  .ptag.t-law-office { color: #c4b07b; border-color: #c4b07b; }
  .ptag.t-medical-clinic { color: #7ab8a3; border-color: #7ab8a3; }
  .ptag.t-beauty-salon { color: #c47ba6; border-color: #c47ba6; }
  .ptag.t-real-estate { color: #7b9bc4; border-color: #7b9bc4; }
  .ptag.t-hospitality { color: #c47b7b; border-color: #c47b7b; }
  .ptag.t-fitness { color: #a3c47b; border-color: #a3c47b; }
  .stag { font-size: 8px; letter-spacing: .22em; text-transform: uppercase; padding: 2px 5px; display: inline-block; }
  .stag.s-draft { color: var(--ink-mute); }
  .stag.s-researched { color: var(--metal); }
  .stag.s-scrubbed { color: var(--amber); }
  .stag.s-approved { color: var(--green); }
  .stag.s-sent { color: var(--metal-glow); }
  .stag.s-replied { color: var(--green); font-weight: 600; }
  .stag.s-won { color: var(--green); font-weight: 600; }
  .stag.s-lost { color: var(--red); }
  .stag.s-skip { color: var(--red); opacity: 0.6; }

  /* Expanded detail panel */
  .detail { padding: 18px 22px; background: var(--noir); border-left: 2px solid var(--metal); }
  .detail h4 { font-family: var(--serif); font-style: italic; font-weight: 400; font-size: 16px; color: var(--metal); margin: 0 0 10px; }
  .detail .why { font-style: italic; color: var(--ink-mute); margin-bottom: 14px; }
  .detail pre { background: rgba(0,0,0,0.3); padding: 14px 16px; border: 1px solid var(--rule); white-space: pre-wrap; font-family: var(--mono); font-size: 11px; line-height: 1.55; color: var(--ink); }
  .detail textarea { width: 100%; background: var(--noir-rise); border: 1px solid var(--rule); color: var(--ink); padding: 8px 10px; font-family: var(--mono); font-size: 11px; min-height: 60px; }
  .detail .links { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
  .detail .links a { font-size: 9px; letter-spacing: .22em; text-transform: uppercase; padding: 4px 8px; border: 1px solid var(--metal); color: var(--metal); text-decoration: none; }
  .detail .links a:hover { background: rgba(184,153,104,0.08); }
  .detail .actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 14px; }
  .detail .actions button {
    background: transparent; border: 1px solid var(--rule); color: var(--ink-mute);
    font-size: 9px; letter-spacing: .22em; text-transform: uppercase;
    padding: 6px 12px; cursor: pointer; font-family: var(--sans);
  }
  .detail .actions button.primary { color: var(--metal); border-color: var(--metal); }
  .detail .actions button.danger { color: var(--red); border-color: var(--red); }
  .detail .actions button:hover { background: rgba(184,153,104,0.06); }

  .empty { padding: 80px 32px; text-align: center; font-style: italic; color: var(--ink-mute); }
</style>
</head>
<body>

<header>
  <div>
    <h1>DW <em>pitch pipeline</em> · 5,353 corridor targets</h1>
    <div class="sub">admin only · status workflow · per-vertical pre-drafted bodies</div>
  </div>
  <nav class="tabs">
    <a href="/">map</a>
    <a href="/atlas.html">atlas</a>
    <a href="/dw-radar.html">dw-radar</a>
    <a href="/signals.html">signals</a>
    <a href="/pitches.html" class="active">pitches</a>
    <a href="/today.html">today</a>
    <a href="/walk-route.html">walk route</a>
    <a href="/responses.html">responses</a>
    <a href="/postcards.html">postcards</a>
    <a href="/buildings.html">buildings</a>
    <a href="/crawl-derby.html">🏇 derby</a>
    <a href="/linkedin.html">linkedin</a>
    <span class="theme-toggle-host" style="display:inline-flex;align-items:center;margin-left:6px"></span>
  </nav>
</header>

<div class="stats" id="stats">—</div>

<div class="controls">
  <label>vertical
    <select id="f-type">
      <option value="">All</option>
      <option>trade-partner</option>
      <option>trade-client</option>
      <option>showroom-partner</option>
      <option>cross-sell</option>
      <option>trade-supplier</option>
      <option>law-office</option>
      <option>medical-clinic</option>
      <option>beauty-salon</option>
      <option>real-estate</option>
      <option>hospitality</option>
      <option>fitness</option>
    </select>
  </label>
  <label>status
    <select id="f-status">
      <option value="">All</option>
      <option>draft</option>
      <option>researched</option>
      <option>scrubbed</option>
      <option>approved</option>
      <option>sent</option>
      <option>replied</option>
      <option>won</option>
      <option>lost</option>
      <option>skip</option>
    </select>
  </label>
  <label>priority
    <select id="f-priority">
      <option value="">All</option>
      <option value="1">1 — DW peer (paint/wallpaper)</option>
      <option value="2">2 — Interior design</option>
      <option value="3">3 — Specialized design</option>
      <option value="4">4 — Home furnishings</option>
      <option value="5">5 — Furniture stores</option>
      <option value="10">10 — Law office</option>
      <option value="11">11 — Medical/clinic</option>
      <option value="12">12 — Beauty/salon</option>
      <option value="13">13 — Real estate</option>
      <option value="14">14 — Hospitality</option>
      <option value="15">15 — Fitness</option>
    </select>
  </label>
  <label>channel
    <select id="f-channel">
      <option value="">All</option>
      <option value="walk-in">🚶 walk-in</option>
      <option value="postcard">✉️ postcard</option>
      <option value="email">📧 email</option>
      <option value="linkedin">💼 linkedin</option>
      <option value="phone">📞 phone</option>
    </select>
  </label>
  <label>DW proximity
    <select id="f-prox">
      <option value="">All</option>
      <option value="same_building">⚡ Same building (15442)</option>
      <option value="same_block">★ Same block (15400-15500)</option>
      <option value="walk_5min">5-min walk (≤0.25 mi)</option>
      <option value="walk_10min">10-min walk (≤0.5 mi)</option>
      <option value="walk_15min">15-min walk (≤1 mi)</option>
      <option value="walking">All walking distance (≤1 mi)</option>
      <option value="any">Any proximity tag</option>
    </select>
  </label>
  <input type="search" id="q" placeholder="search name, city, address…" autocomplete="off">
  <a href="/api/pitches/hot-list.csv" download style="font-size:9px;letter-spacing:.26em;text-transform:uppercase;padding:5px 10px;border:1px solid var(--metal);color:var(--metal);text-decoration:none">Hot list CSV ↓</a>
  <a href="/api/pitches/postcard-batch.csv?min_proximity=walk_5min" download style="font-size:9px;letter-spacing:.26em;text-transform:uppercase;padding:5px 10px;border:1px solid var(--metal);color:var(--metal);text-decoration:none" title="Lob-format CSV for postcards within 0.25 mi of DW">Postcard 5min ↓</a>
  <a href="/api/pitches/postcard-batch.csv?min_proximity=walk_10min" download style="font-size:9px;letter-spacing:.26em;text-transform:uppercase;padding:5px 10px;border:1px solid var(--metal);color:var(--metal);text-decoration:none" title="Lob-format CSV for postcards within 0.5 mi of DW">Postcard 10min ↓</a>
  <span class="ct"><b id="ct">—</b> targets</span>
</div>

<div id="mass-toolbar" style="padding:14px 24px;border-bottom:1px solid var(--rule);background:rgba(184,153,104,0.04);display:flex;gap:12px;flex-wrap:wrap;align-items:center;font-size:11px">
  <span style="font-family:var(--serif);font-style:italic;font-size:16px;color:var(--metal-glow)">Mass actions</span>
  <span style="color:var(--ink-mute);letter-spacing:.18em;text-transform:uppercase;font-size:9px" id="mass-mode-label">applies to all <b id="mass-count" style="color:var(--metal)">—</b> visible</span>
  <button onclick="selectAllVisible()" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 9px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">Select all visible</button>
  <button onclick="clearSelection()" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 9px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">Clear</button>
  <span style="flex:1"></span>
  <select id="mass-field" style="background:var(--noir);border:1px solid var(--rule);color:var(--ink);padding:6px 10px;font-size:11px">
    <option value="status">Set status</option>
    <option value="outreach_channel">Set channel</option>
    <option value="priority">Set priority</option>
    <option value="pitch_type">Set pitch_type</option>
    <option value="next_followup_at">⏰ Schedule follow-up</option>
  </select>
  <select id="mass-value" style="background:var(--noir);border:1px solid var(--rule);color:var(--ink);padding:6px 10px;font-size:11px"></select>
  <input id="mass-date" type="date" style="background:var(--noir);border:1px solid var(--rule);color:var(--ink);padding:6px 10px;font-size:11px;display:none">
  <span id="mass-date-presets" style="display:none;gap:4px">
    <button onclick="setFollowupDate(3)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+3d</button>
    <button onclick="setFollowupDate(7)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+1w</button>
    <button onclick="setFollowupDate(14)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+2w</button>
    <button onclick="setFollowupDate(30)" style="background:transparent;border:1px solid var(--rule);color:var(--ink-mute);padding:4px 8px;font-size:9px;letter-spacing:.15em;text-transform:uppercase;cursor:pointer;font-family:var(--sans)">+1mo</button>
  </span>
  <button onclick="bulkApply(true)" style="background:transparent;border:1px solid var(--metal);color:var(--metal);padding:6px 12px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer">Preview</button>
  <button onclick="bulkApply(false)" style="background:transparent;border:1px solid var(--metal-glow);color:var(--metal-glow);padding:6px 12px;font-size:9px;letter-spacing:.18em;text-transform:uppercase;cursor:pointer">⚡ Apply</button>
</div>

<table id="grid">
  <thead>
    <tr>
      <th><input type="checkbox" id="cb-all" onchange="toggleAllVisible(this.checked)" style="accent-color:var(--metal);cursor:pointer"></th>
      <th>★</th><th>Status</th><th>Vertical</th><th>Name</th><th>Address</th><th>City</th><th>Ready</th>
    </tr>
  </thead>
  <tbody id="rows"></tbody>
</table>
<div class="empty" id="empty" style="display:none">No matches.</div>

<script>
let DATA = [];
const SELECTED = new Set();   // pitch ids selected for surgical bulk action
let EXPANDED = null;

function escapeHtml(s) { return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }

function statusActions(p) {
  const id = p.id;
  const s = p.status;
  // Allowed transitions
  const next = {
    draft:      [['researched','primary','✓ researched'], ['skip','danger','skip']],
    researched: [['scrubbed','primary','✓ DNC scrubbed'], ['draft','','← back to draft'], ['skip','danger','skip']],
    scrubbed:   [['approved','primary','✓ approved by Steve'], ['researched','','← back']],
    approved:   [['sent','primary','✓ sent via George'], ['scrubbed','','← back']],
    sent:       [['replied','primary','✓ got reply'], ['won','primary','✓ closed won'], ['lost','danger','closed lost']],
    replied:    [['won','primary','✓ closed won'], ['lost','danger','closed lost']],
    won:        [['replied','','← back']],
    lost:       [['replied','','← back']],
    skip:       [['draft','','← reactivate']],
  };
  return (next[s] || []).map(([nextStatus, cls, label]) =>
    `<button class="${cls}" data-id="${id}" data-status="${nextStatus}">${label}</button>`
  ).join('');
}

function render() {
  const ftype = document.getElementById('f-type').value;
  const fstatus = document.getElementById('f-status').value;
  const fpri = document.getElementById('f-priority').value;
  const q = (document.getElementById('q').value || '').trim().toLowerCase();
  let rows = DATA;
  if (ftype) rows = rows.filter(r => r.pitch_type === ftype);
  if (fstatus) rows = rows.filter(r => r.status === fstatus);
  if (fpri) rows = rows.filter(r => String(r.priority) === fpri);
  const fprox = document.getElementById('f-prox').value;
  const WALK = ['same_building','same_block','walk_2min','walk_5min','walk_10min','walk_15min'];
  if (fprox === 'same_building')   rows = rows.filter(r => r.dw_proximity === 'same_building');
  else if (fprox === 'same_block') rows = rows.filter(r => r.dw_proximity === 'same_block');
  else if (fprox === 'walk_5min')  rows = rows.filter(r => ['same_building','same_block','walk_2min','walk_5min'].includes(r.dw_proximity));
  else if (fprox === 'walk_10min') rows = rows.filter(r => ['same_building','same_block','walk_2min','walk_5min','walk_10min'].includes(r.dw_proximity));
  else if (fprox === 'walk_15min' || fprox === 'walking') rows = rows.filter(r => WALK.includes(r.dw_proximity));
  else if (fprox === 'any')        rows = rows.filter(r => r.dw_proximity);
  const fchannel = document.getElementById('f-channel').value;
  if (fchannel) rows = rows.filter(r => r.outreach_channel === fchannel);
  if (q) rows = rows.filter(r => ((r.name || '') + ' ' + (r.city || '') + ' ' + (r.address || '') + ' ' + (r.naics || '')).toLowerCase().includes(q));

  document.getElementById('ct').textContent = rows.length.toLocaleString();
  const body = document.getElementById('rows');
  if (!rows.length) { body.innerHTML = ''; document.getElementById('empty').style.display = 'block'; return; }
  document.getElementById('empty').style.display = 'none';

  body.innerHTML = rows.slice(0, 400).map(r => {
    const isExp = EXPANDED === r.id;
    const proxMap = {
      same_building: ['⚡', 'var(--metal-glow)'], same_block: ['★', 'var(--metal)'],
      walk_2min: ['◉', 'var(--metal)'], walk_5min: ['◉', 'var(--metal)'],
      walk_10min: ['◎', 'var(--ink-mute)'], walk_15min: ['·', 'var(--ink-mute)']
    };
    const proxIcon  = (proxMap[r.dw_proximity] || ['',''])[0];
    const proxColor = (proxMap[r.dw_proximity] || ['',''])[1];
    // Research-completeness ladder: contact_name → email → phone → observation → body
    const dots = [
      r.contact_name ? '●' : '○',
      r.email        ? '●' : '○',
      r.phone        ? '●' : '○',
      r.observation  ? '●' : '○',
      r.body         ? '●' : '○',
    ].join('');
    const filled = [r.contact_name, r.email, r.phone, r.observation, r.body].filter(Boolean).length;
    const dotColor = filled === 5 ? 'var(--metal-glow)' : filled >= 3 ? 'var(--metal)' : 'var(--ink-mute)';
    const checked = SELECTED.has(r.id) ? 'checked' : '';
    const main = `
      <tr class="${isExp ? 'expanded' : ''} ${SELECTED.has(r.id) ? 'sel' : ''}" data-id="${r.id}">
        <td style="text-align:center" data-cb="1"><input type="checkbox" ${checked} data-cb-id="${r.id}" onclick="event.stopPropagation()" style="accent-color:var(--metal);cursor:pointer"></td>
        <td style="color:${proxColor};font-size:14px;text-align:center" title="${r.dw_proximity || ''}">${proxIcon}</td>
        <td><span class="stag s-${r.status}">${r.status}</span></td>
        <td><span class="ptag t-${r.pitch_type}">${r.pitch_type}</span></td>
        <td class="nm">${escapeHtml(r.name)}${(() => {
          const fresh = Number(r.news_fresh_7d || 0);
          const total = Number(r.news_count || 0);
          if (fresh >= 3) return ` <a href="/business/${r.business_id}/news" style="display:inline-block;padding:1px 5px;font-size:8px;letter-spacing:.16em;text-transform:uppercase;background:var(--metal-glow);color:#0a0a0c;text-decoration:none;border-radius:2px;vertical-align:middle" title="${fresh} articles in last 7 days — Hot publisher">🔥${fresh}</a>`;
          if (fresh >= 1) return ` <a href="/business/${r.business_id}/news" style="display:inline-block;padding:1px 5px;font-size:8px;letter-spacing:.16em;text-transform:uppercase;color:var(--metal);border:1px solid var(--metal);text-decoration:none;vertical-align:middle" title="${fresh} articles in last 7 days">📰${fresh}</a>`;
          if (total >= 1) return ` <a href="/business/${r.business_id}/news" style="display:inline-block;padding:1px 5px;font-size:8px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);border:1px solid var(--rule);text-decoration:none;vertical-align:middle" title="${total} articles total">📰${total}</a>`;
          return '';
        })()}</td>
        <td class="addr">${escapeHtml(r.address || '')}</td>
        <td class="addr">${escapeHtml(r.city || '')}</td>
        <td style="color:${dotColor};font-family:var(--mono);font-size:13px;letter-spacing:0.1em" title="contact·email·phone·observation·body — ${filled}/5">${dots}</td>
      </tr>`;
    const detail = isExp ? `
      <tr><td colspan="8">
        <div class="detail">
          <div class="why">${escapeHtml(r.why_dw_fits || '')}</div>
          <div class="links">
            ${Object.entries(r.research_links || {}).map(([k,v]) => `<a href="${v}" target="_blank" rel="noopener">${k} ↗</a>`).join('')}
            ${r.pitch_md_path ? `<a href="/${r.pitch_md_path}" target="_blank">📄 markdown ↗</a>` : ''}
          </div>
          <h4>Contact info <span style="font-family:var(--mono);font-size:9px;letter-spacing:.15em;color:var(--ink-mute);text-transform:uppercase;margin-left:8px">filled during research · saves on blur</span></h4>
          <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px;margin-bottom:14px">
            <input data-field="contact_name" data-id="${r.id}" placeholder="Contact name" value="${escapeHtml(r.contact_name||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--sans);font-size:11px"/>
            <input data-field="email" data-id="${r.id}" placeholder="email@example.com" value="${escapeHtml(r.email||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--mono);font-size:11px"/>
            <input data-field="phone" data-id="${r.id}" placeholder="(818) 555-0123" value="${escapeHtml(r.phone||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--mono);font-size:11px"/>
            <input data-field="website" data-id="${r.id}" placeholder="https://…" value="${escapeHtml(r.website||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--mono);font-size:11px"/>
            <input data-field="instagram" data-id="${r.id}" placeholder="@handle or URL" value="${escapeHtml(r.instagram||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--mono);font-size:11px"/>
            <input data-field="linkedin" data-id="${r.id}" placeholder="LinkedIn URL" value="${escapeHtml(r.linkedin||'')}" style="background:var(--noir-rise);border:1px solid var(--rule);color:var(--ink);padding:6px 8px;font-family:var(--mono);font-size:11px"/>
          </div>
          <h4>Subject</h4>
          <pre>${escapeHtml(r.subject || '(no subject yet)')}</pre>
          <h4>Body</h4>
          <pre>${escapeHtml(r.body || '(no body yet)')}</pre>
          <h4>Observation (what you saw)</h4>
          <textarea data-field="observation" data-id="${r.id}" placeholder="What did Google Maps / Yelp / IG show? Style, budget tier, current walls…">${escapeHtml(r.observation || '')}</textarea>
          <h4>Notes</h4>
          <textarea data-field="notes" data-id="${r.id}" placeholder="Internal notes, follow-ups, context…">${escapeHtml(r.notes || '')}</textarea>
          <div class="actions">${statusActions(r)}</div>
          <div class="cross-actions" style="margin-top:14px;padding-top:12px;border-top:1px dashed var(--rule);display:flex;gap:8px;flex-wrap:wrap;font-size:10px;letter-spacing:.18em;text-transform:uppercase">
            <a href="/postcards.html?id=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">📮 Postcard preview</a>
            <a href="/buildings.html?bldg=${encodeURIComponent((r.address||'').replace(/\s*(SUITE|STE|UNIT|#).*$/i,'').trim())}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">🏢 Building roster</a>
            <a href="/responses.html?id=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">📝 Log response</a>
            <a href="/walk-route.html?focus=${r.id}" target="_blank" style="color:var(--metal);text-decoration:none;padding:5px 10px;border:1px solid var(--rule)">🚶 Walk route</a>
            <a href="/pitch/${r.id}" target="_blank" style="color:var(--metal-glow);text-decoration:none;padding:5px 10px;border:1px solid var(--metal-glow)">📄 1-pager</a>
            <a href="/api/pitches/${r.id}/sheet.md" target="_blank" style="color:var(--ink-mute);text-decoration:none;padding:5px 10px;border:1px solid var(--rule);font-family:var(--mono);font-size:9px">⬇ .md</a>
            <a href="/api/pitches/${r.id}/timeline" target="_blank" style="color:var(--ink-mute);text-decoration:none;padding:5px 10px;border:1px solid var(--rule);font-family:var(--mono);font-size:9px">📜 Timeline JSON</a>
          </div>
          <div style="font-family:var(--mono);font-size:9px;color:var(--ink-mute);margin-top:14px">
            id ${r.id} · biz #${r.business_id} · created ${new Date(r.created_at).toLocaleDateString()} · last update ${new Date(r.updated_at).toLocaleString()}
            ${r.sent_at ? `· sent ${new Date(r.sent_at).toLocaleString()}` : ''}
            ${r.replied_at ? `· replied ${new Date(r.replied_at).toLocaleString()}` : ''}
          </div>
        </div>
      </td></tr>` : '';
    return main + detail;
  }).join('');
}

function updateStats() {
  const counts = {};
  for (const r of DATA) counts[r.status] = (counts[r.status] || 0) + 1;
  const order = ['draft','researched','scrubbed','approved','sent','replied','won','lost','skip'];
  document.getElementById('stats').innerHTML = order
    .filter(s => counts[s])
    .map(s => `<span><b>${counts[s]}</b>${s}</span>`)
    .join('');
}

async function patchPitch(id, fields) {
  await fetch('/api/pitches/' + id, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(fields),
  });
  // Refresh local row
  const i = DATA.findIndex(r => r.id == id);
  if (i >= 0) Object.assign(DATA[i], fields);
  updateStats();
  render();
}

document.getElementById('rows').addEventListener('click', (e) => {
  const t = e.target;
  if (t.tagName === 'BUTTON' && t.dataset.status) {
    e.stopPropagation();
    patchPitch(t.dataset.id, { status: t.dataset.status });
    return;
  }
  // Checkbox handles its own click (stopPropagation) but defend anyway
  if (t.tagName === 'INPUT' && t.type === 'checkbox' && t.dataset.cbId) return;
  if (t.tagName === 'TEXTAREA' || t.tagName === 'A') return;
  const tr = t.closest('tr[data-id]');
  if (!tr) return;
  const id = parseInt(tr.dataset.id, 10);
  EXPANDED = (EXPANDED === id ? null : id);
  render();
});

document.getElementById('rows').addEventListener('change', (e) => {
  // Selection checkbox
  if (e.target.tagName === 'INPUT' && e.target.type === 'checkbox' && e.target.dataset.cbId) {
    const id = parseInt(e.target.dataset.cbId, 10);
    if (e.target.checked) SELECTED.add(id); else SELECTED.delete(id);
    e.target.closest('tr').classList.toggle('sel', e.target.checked);
    updateMassCount();
    return;
  }
  // Field edit
  if ((e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') && e.target.dataset.id) {
    const f = e.target.dataset.field;
    const v = e.target.value;
    patchPitch(e.target.dataset.id, { [f]: v });
  }
});

['f-type','f-status','f-priority','f-prox','f-channel','q'].forEach(id => {
  const el = document.getElementById(id);
  el.addEventListener('input', () => { clearTimeout(window._t); window._t = setTimeout(render, 100); });
  el.addEventListener('change', render);
});

fetch('/api/pitches').then(r => r.json()).then(d => {
  DATA = d.rows || [];
  updateStats();
  render();
  refreshMassValueOptions();
});

// ─── Mass-action toolbar (iter 58) ───────────────────────────────────
const MASS_OPTIONS = {
  status: ['draft','researched','scrubbed','approved','sent','replied','won','lost','skip'],
  outreach_channel: ['walk-in','linkedin-dm','inmail','email','postcard','phone','text'],
  priority: ['1','2','3','4','5','6','7','8','9','10','11','12'],
  pitch_type: ['law-office','medical-clinic','dental-office','restaurant','salon-spa','retail-storefront','office-tenant','real-estate','fitness-studio','beauty-spa','hospitality','building-neighbor']
};
function refreshMassValueOptions() {
  const f = document.getElementById('mass-field').value;
  const v = document.getElementById('mass-value');
  const d = document.getElementById('mass-date');
  const p = document.getElementById('mass-date-presets');
  if (f === 'next_followup_at') {
    v.style.display = 'none';
    d.style.display = 'inline-block';
    p.style.display = 'inline-flex';
    if (!d.value) setFollowupDate(7);
  } else {
    v.style.display = 'inline-block';
    d.style.display = 'none';
    p.style.display = 'none';
    v.innerHTML = (MASS_OPTIONS[f] || []).map(o => `<option value="${o}">${o}</option>`).join('');
  }
}
function setFollowupDate(daysFromNow) {
  const dt = new Date(Date.now() + daysFromNow * 86400000);
  document.getElementById('mass-date').value = dt.toISOString().slice(0, 10);
}
document.getElementById('mass-field').addEventListener('change', refreshMassValueOptions);

// Keep "applies to N visible" in sync with current filter
const _origRender = render;
window.render = function() { _origRender(); updateMassCount(); };
function updateMassCount() {
  const visibleIds = Array.from(document.querySelectorAll('#rows tr[data-id]')).map(tr => parseInt(tr.dataset.id, 10));
  const selN = SELECTED.size;
  const lbl = document.getElementById('mass-mode-label');
  if (selN > 0) {
    lbl.innerHTML = `applies to <b style="color:var(--metal-glow)">${selN}</b> selected · <span style="color:var(--ink-mute);text-transform:none;letter-spacing:0">click checkbox to deselect</span>`;
  } else {
    lbl.innerHTML = `applies to all <b id="mass-count" style="color:var(--metal)">${visibleIds.length.toLocaleString()}</b> visible · <span style="color:var(--ink-mute);text-transform:none;letter-spacing:0">tick rows for surgical mode</span>`;
  }
  // Update header "select all" checkbox state
  const cbAll = document.getElementById('cb-all');
  if (cbAll) {
    const allChecked = visibleIds.length > 0 && visibleIds.every(id => SELECTED.has(id));
    const someChecked = visibleIds.some(id => SELECTED.has(id));
    cbAll.checked = allChecked;
    cbAll.indeterminate = !allChecked && someChecked;
  }
}
setInterval(updateMassCount, 500);

function selectAllVisible() {
  const visibleIds = Array.from(document.querySelectorAll('#rows tr[data-id]')).map(tr => parseInt(tr.dataset.id, 10));
  visibleIds.forEach(id => SELECTED.add(id));
  render();
  updateMassCount();
}
function clearSelection() {
  SELECTED.clear();
  render();
  updateMassCount();
}
function toggleAllVisible(check) {
  if (check) selectAllVisible();
  else clearSelection();
}

async function bulkApply(dryRun) {
  // Use SELECTED if non-empty, else fall back to all visible
  let ids = Array.from(SELECTED);
  let scope = 'selected';
  if (ids.length === 0) {
    ids = Array.from(document.querySelectorAll('#rows tr[data-id]')).map(tr => parseInt(tr.dataset.id, 10));
    scope = 'visible';
  }
  if (!ids.length) { alert('No rows selected and none visible.'); return; }
  const field = document.getElementById('mass-field').value;
  let value;
  if (field === 'next_followup_at') {
    value = document.getElementById('mass-date').value;
    if (!value) { alert('Pick a follow-up date first (or use a preset).'); return; }
  } else {
    value = document.getElementById('mass-value').value;
    if (field === 'priority') value = parseInt(value, 10);
  }
  if (dryRun) {
    alert(`Preview: would set ${field}=${value} on ${ids.length} ${scope} pitch${ids.length === 1 ? '' : 'es'}. No changes made.`);
    return;
  }
  if (!confirm(`Set ${field}=${value} on ${ids.length} ${scope} pitch${ids.length === 1 ? '' : 'es'}?`)) return;
  try {
    const r = await fetch('/api/pitches/bulk', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ ids, fields: { [field]: value } })
    });
    if (!r.ok) throw new Error(await r.text());
    const d = await r.json();
    alert(`Updated ${d.updated} of ${d.ids} pitches.`);
    SELECTED.clear();
    const fresh = await fetch('/api/pitches').then(r => r.json());
    DATA = fresh.rows || [];
    updateStats();
    render();
    updateMassCount();
  } catch (e) {
    alert('Bulk update failed: ' + e.message);
  }
}
</script>
</body>
</html>