← back to Rentv 2026

public/audience.html

287 lines

<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>RENTV · Audience &amp; CRM</title>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@800&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
  :root{--red:#c8102e;--line:#232a32}*{box-sizing:border-box;margin:0}
  body{background:#0d1014;color:#e7ecf1;font-family:'Inter',system-ui,sans-serif;font-size:13.5px}
  a{color:inherit;text-decoration:none}
  .top{position:sticky;top:0;z-index:900;display:flex;align-items:center;justify-content:space-between;height:52px;padding:0 14px;background:#0b0e12;border-bottom:1px solid var(--line)}
  .burger{display:inline-flex;align-items:center;gap:9px;border:0;background:none;color:#e7ecf1;cursor:pointer;font:inherit;padding:6px 8px;border-radius:8px}.burger:hover{background:rgba(255,255,255,.08)}
  .burger .lbl{font-size:12px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:#8b939b}
  .brand{position:absolute;left:50%;transform:translateX(-50%);font-family:'Playfair Display',serif;font-weight:800;font-size:19px}.brand span{color:var(--red)}
  .user{width:35px;height:35px;border-radius:50%;border:1.5px solid #2a323b;background:#11161c;color:#cfd6dd;cursor:pointer;display:inline-flex;align-items:center;justify-content:center}.user:hover{border-color:var(--red);color:#fff}
  .scrim{position:fixed;inset:0;background:rgba(6,9,12,.5);opacity:0;pointer-events:none;transition:.22s;z-index:950}.scrim.open{opacity:1;pointer-events:auto}
  .drawer{position:fixed;top:0;left:0;height:100%;width:340px;max-width:88vw;background:#0b0e12;transform:translateX(-102%);transition:transform .26s cubic-bezier(.4,0,.2,1);z-index:951;display:flex;flex-direction:column;overflow-y:auto;box-shadow:2px 0 40px rgba(0,0,0,.4)}
  .drawer.open{transform:none}
  .drawer .dh{display:flex;justify-content:space-between;padding:16px 18px;border-bottom:1px solid var(--line)}.drawer .dh .w{font-family:'Playfair Display',serif;font-size:21px;font-weight:800}.drawer .dh .w span{color:var(--red)}.drawer .dh button{background:none;border:0;color:#8b949e;font-size:22px;cursor:pointer}
  .gt{font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:#6b747d;padding:14px 18px 6px}
  .drawer nav a{display:block;padding:9px 18px;color:#dbe1e7;border-left:3px solid transparent}.drawer nav a:hover{background:#141a21;border-left-color:var(--red);color:#fff}
  .shell{display:grid;grid-template-columns:210px 1fr;height:calc(100vh - 52px)}
  .rail{border-right:1px solid var(--line);padding:14px 10px;display:flex;flex-direction:column;gap:6px;overflow-y:auto}
  .rail .h{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:#6b747d;padding:8px 10px 4px}
  .rail button{display:flex;align-items:center;gap:10px;width:100%;background:none;border:0;color:#c3cbd3;font:700 13px 'Inter';padding:10px 12px;border-radius:9px;cursor:pointer;text-align:left}
  .rail button:hover{background:#12161c}.rail button.on{background:#12161c;color:#fff;box-shadow:inset 3px 0 0 var(--red)}
  .rail button .i{font-size:15px;width:18px;text-align:center}.rail .c{margin-left:auto;font-size:11px;color:#8b939b}
  .content{overflow:hidden;position:relative}
  .view{position:absolute;inset:0;overflow-y:auto;display:none}.view.on{display:block}
  .act{display:inline-flex;align-items:center;gap:6px;background:#12161c;border:1px solid #2a333d;border-radius:8px;padding:7px 11px;color:#dbe1e7;font-weight:700;font-size:12.5px;white-space:nowrap}.act:hover{border-color:var(--red);color:#fff}.act .i{font-size:14px}
  .acts{display:flex;gap:7px;flex-wrap:wrap}
  .field-link{color:#7cc0ff;border-bottom:1px dotted #3a5064}.field-link:hover{color:#a9d5ff}
  .fresh{color:#6b747d;font-size:12px;padding:10px 14px}
  /* master-detail (prospects + profiles) */
  .md{display:grid;grid-template-columns:380px 1fr;height:100%}
  .md .list{border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:0}
  .md .search{padding:12px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:9px}
  .md .search input,.md .search select{width:100%;background:#11161c;border:1px solid var(--line);border-radius:9px;color:#e7ecf1;padding:9px 12px;font:inherit}
  .srow{display:flex;gap:8px}.srow select{flex:1}
  .md .rows{overflow-y:auto;flex:1}
  .md .row{padding:11px 14px;border-bottom:1px solid #171d24;cursor:pointer}.md .row:hover{background:#141b22}.md .row.sel{background:#11161c;border-left:3px solid var(--red)}
  .md .row .nm{font-weight:700;font-size:14px;display:flex;align-items:center;gap:7px}
  .md .row .f{font-size:12px;color:#9aa4ad;margin-top:2px}
  .md .row .when{font-size:11px;color:#6b747d;margin-top:3px}
  .pill{display:inline-block;background:#0e141a;border:1px solid #2a333d;border-radius:20px;padding:2px 9px;font-size:11px;font-weight:700;color:#c3cbd3}
  .pill.persona{color:#ffd39b;border-color:#4a3a24}
  .md .detail{overflow-y:auto;padding:30px 36px}
  .md .detail .k{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--red);font-weight:800}
  .md .detail h2{font-size:30px;font-weight:800;margin:5px 0}.md .detail .meta{color:#9aa4ad;font-size:15px}
  .md .detail .acts{margin:20px 0}
  .md .detail .grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}.md .detail .cell{background:#11161c;border:1px solid var(--line);border-radius:10px;padding:13px}.md .detail .cell .kk{font-size:11px;color:#8b939b;text-transform:uppercase}.md .detail .cell .vv{font-size:16px;font-weight:700;margin-top:3px;word-break:break-word}
  .seglist{display:flex;flex-wrap:wrap;gap:7px;margin-top:14px}
  .seg-chip{background:#0e141a;border:1px solid #2a333d;border-radius:8px;padding:5px 10px;font-size:12px;font-weight:700;color:#c3cbd3;cursor:pointer}.seg-chip:hover{border-color:var(--red);color:#fff}
  .h3{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:#8b939b;margin:22px 0 10px}
  .lcard{background:#11161c;border:1px solid var(--line);border-radius:10px;padding:12px 14px;margin-bottom:10px}
  .lcard .a{font-weight:700}.lcard .b{font-size:12px;color:#9aa4ad;margin-top:3px}
  /* segments grid */
  .segwrap{padding:20px 22px}
  .grp-h{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--red);font-weight:800;margin:22px 0 12px}.grp-h:first-child{margin-top:2px}
  .seggroup{border:1px solid var(--line);border-radius:12px;margin:0 0 12px;overflow:hidden;background:#0e141a}
  .grp-tab{width:100%;display:flex;align-items:center;gap:9px;background:#11161c;border:0;color:#e7ecf1;font:inherit;font-weight:800;font-size:13px;letter-spacing:.06em;text-transform:uppercase;padding:13px 16px;cursor:pointer;text-align:left}
  .grp-tab:hover{background:#151b23}.seggroup.open .grp-tab{color:var(--red);border-bottom:1px solid var(--line)}
  .grp-tab .caret{color:var(--red);font-size:12px;width:12px}
  .grp-tab .cnt{margin-left:auto;font-weight:600;font-size:11px;letter-spacing:.02em;text-transform:none;color:#8b939b}
  .seggroup .segcards{padding:14px}
  .segcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:14px}
  .segcard{background:#11161c;border:1px solid var(--line);border-radius:12px;padding:15px;cursor:pointer;transition:.15s}.segcard:hover{border-color:var(--red);transform:translateY(-1px)}
  .segcard .e{font-size:20px}.segcard .l{font-weight:800;font-size:15px;margin-top:6px}
  .segcard .n{font-size:26px;font-weight:800;margin-top:6px}.segcard .n small{font-size:13px;color:#8b939b;font-weight:600}
  .segcard .track{height:6px;background:#0e141a;border-radius:4px;overflow:hidden;margin-top:10px}.segcard .fill{height:100%;background:linear-gradient(90deg,var(--red),#e0455f)}
  .segcard .g{font-size:11px;color:#8b939b;margin-top:8px}.segcard .g b{color:#37d67a}
  /* analytics */
  .an{padding:18px 22px}.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px}.kpi{background:#11161c;border:1px solid var(--line);border-radius:12px;padding:16px}.kpi .k{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:#8b939b}.kpi .v{font-size:28px;font-weight:800;margin-top:4px}
  .p2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  .panel{background:#11161c;border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:14px}.panel h3{font-size:12px;letter-spacing:.08em;text-transform:uppercase;color:#8b939b;margin-bottom:14px}
  .barwrap .bar{display:flex;align-items:center;gap:10px;margin-bottom:9px;cursor:pointer}.bar .lab{width:150px;font-size:12.5px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar .track{flex:1;height:20px;background:#0e141a;border-radius:5px;overflow:hidden}.bar .fill{height:100%;background:linear-gradient(90deg,var(--red),#e0455f)}.bar .val{width:52px;text-align:right;font-weight:800}
  .empty{color:#6b747d;font-size:13px;padding:30px 20px;line-height:1.7;max-width:560px}
  @media(max-width:900px){.shell{grid-template-columns:1fr;height:auto}.md{grid-template-columns:1fr}.kpis,.p2{grid-template-columns:1fr 1fr}}
</style></head>
<body>
<header class="top">
  <button class="burger" id="burger" aria-label="Menu"><svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg><span class="lbl">Menu</span></button>
  <a class="brand" href="/">REN<span>TV</span> · Audience</a>
  <button class="user" id="user" title="Home"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="3.5"/><path d="M4.5 20a7.5 7.5 0 0 1 15 0" stroke-linecap="round"/></svg></button>
</header>
<div class="scrim" id="scrim"></div>
<aside class="drawer" id="drawer">
  <div class="dh"><span class="w">REN<span>TV</span></span><button id="dclose">✕</button></div>
  <div class="gt">Intelligence</div><nav><a href="/desk">Broker &amp; Owner Desk</a><a href="/audience">Audience &amp; CRM</a><a href="/la-commercial">LA Commercial</a><a href="/markets">Markets</a></nav>
  <div class="gt">Newsroom</div><nav><a href="/">Home</a><a href="/review">The REview</a><a href="/blog">Blog</a><a href="/subscribe">Newsletter</a></nav>
</aside>
<div class="shell">
  <nav class="rail" id="rail">
    <div class="h">Audience</div>
    <button data-v="prospects" class="on"><span class="i">👥</span>Clients &amp; Prospects <span class="c" id="cP">0</span></button>
    <button data-v="segments"><span class="i">🎯</span>Audience Segments <span class="c" id="cS">0</span></button>
    <button data-v="profiles"><span class="i">👤</span>Contact Profiles <span class="c" id="cC">0</span></button>
    <button data-v="performance"><span class="i">📈</span>Segment Performance</button>
    <div class="fresh" id="fresh"></div>
  </nav>
  <div class="content">
    <div class="view on" id="v-prospects"></div>
    <div class="view" id="v-segments"></div>
    <div class="view" id="v-profiles"></div>
    <div class="view" id="v-performance"></div>
  </div>
</div>
<script>
const $=s=>document.getElementById(s), enc=encodeURIComponent;
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const dstr=d=>d?new Date(d).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}):'—';
const fmtWhen=d=>{try{return new Date(d).toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'});}catch(e){return d||'—';}};
const LI_P=(n,f)=>`https://www.linkedin.com/search/results/people/?keywords=${enc([n,f].filter(Boolean).join(' '))}`,LI_C=f=>`https://www.linkedin.com/search/results/companies/?keywords=${enc(f)}`,GOOG=q=>`https://www.google.com/search?q=${enc(q)}`,MAPS=q=>`https://www.google.com/maps/search/?api=1&query=${enc(q)}`;
const A=(h,i,l)=>`<a class="act" href="${h}" target="_blank" rel="noopener" onclick="event.stopPropagation()"><span class="i">${i}</span>${l}</a>`;
function contactActions(c){
  return [
    c.email?A('mailto:'+c.email,'✉️','Email'):'',
    c.phone?A('tel:'+String(c.phone).replace(/[^\d+]/g,''),'📞','Call'):'',
    // CRM contacts drill into their full pr-intelligence record (Steve's "every data point hrefs deeper" rule)
    c.origin==='crm'?A('/admin/pr-intelligence/people?id='+encodeURIComponent(String(c.id).replace(/^crm-/,'')),'🗂️','Open in CRM'):'',
    c.origin==='crm'&&c.firm?A('/admin/pr-intelligence/organizations?q='+encodeURIComponent(c.firm),'🏢','Firm in CRM'):'',
    A(c.linkedin||LI_P(c.name,c.firm),'👤',c.linkedin?'LinkedIn ✓':'LinkedIn'),
    c.firm?A(LI_C(c.firm),'🏢','Firm'):'',
    (c.listings&&c.listings[0]&&c.listings[0].listing_url&&/^https?:/i.test(c.listings[0].listing_url))?A(c.listings[0].listing_url,'🔗','Listing'):'',
    A(GOOG(c.name+' '+(c.firm||'')+' commercial real estate'),'🔎','Google'),
  ].filter(Boolean).join('');
}
const personaEmoji=c=>c.origin==='sublease'?'🔑':c.origin==='crm'?'🏢':'📩';
let A_={data:null,view:'prospects',selP:0,selC:0,filterSeg:null,sort:localStorage.getItem('aud.sort')||'newest'};
// chrome
$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
const cd=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('open');};$('dclose').onclick=cd;$('scrim').onclick=cd;
$('user').onclick=()=>location.href='/';
async function jget(u,fb){try{const r=await fetch(u,{credentials:'same-origin'});if(!r.ok)return fb;return await r.json();}catch(e){return fb;}}
(async()=>{
  A_.data=await jget('/api/audience',{stats:{},segments:[],contacts:[]});
  // Precompute a lean lowercase search string per contact ONCE (was JSON.stringify per keystroke
  // over 2,162 contacts — a real perf drag; Cody-flagged). Search now filters on this index.
  (A_.data.contacts||[]).forEach(c=>{c._q=[c.name,c.email,c.phone,c.firm,c.title,c.interest,c.persona,c.source,(c.segments||[]).join(' ')].filter(Boolean).join(' ').toLowerCase();});
  const st=A_.data.stats||{};
  $('cP').textContent=A_.data.contacts.length;$('cS').textContent=A_.data.segments.length;$('cC').textContent=A_.data.contacts.length;
  $('fresh').textContent=(st.crm||0).toLocaleString()+' CRM contacts · '+(st.newsletter||0)+' newsletter · '+(st.subleaseLinked||0)+' sublease';
  renderAll();
})();
function switchView(v){A_.view=v;
  document.querySelectorAll('#rail [data-v]').forEach(x=>x.classList.toggle('on',x.dataset.v===v));
  document.querySelectorAll('.view').forEach(x=>x.classList.toggle('on',x.id==='v-'+v));
}
$('rail').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;switchView(b.dataset.v);renderAll();});
function renderAll(){prospects();segments();profiles();performance();}
// ── sort + filter helpers shared by prospects/profiles ──
function sortContacts(rows){
  const s=A_.sort;const cp=rows.slice();
  if(s==='name')cp.sort((a,b)=>a.name.localeCompare(b.name));
  else if(s==='persona')cp.sort((a,b)=>a.persona.localeCompare(b.persona)||a.name.localeCompare(b.name));
  else if(s==='segments')cp.sort((a,b)=>(b.segments.length-a.segments.length));
  else cp.sort((a,b)=>String(b.created_at||'').localeCompare(String(a.created_at||''))); // newest
  return cp;
}
function filteredContacts(term){
  let r=A_.data.contacts.slice();
  if(A_.filterSeg)r=r.filter(c=>c.segments.includes(A_.filterSeg));
  const v=(term||'').trim().toLowerCase();
  if(v)r=r.filter(c=>(c._q||"").includes(v));
  return sortContacts(r);
}
// ── 1. Clients & Prospects (master-detail) ──
function prospects(){
  const segName=A_.filterSeg?(A_.data.segments.find(s=>s.key===A_.filterSeg)||{}).label:'';
  $('v-prospects').innerHTML=`<div class="md"><div class="list"><div class="search">
    <input id="pq" placeholder="Search name, email, firm, interest…">
    <div class="srow">
      <select id="psort">
        <option value="newest">Newest first</option><option value="name">Name A→Z</option>
        <option value="persona">Persona</option><option value="segments">Most segments</option>
      </select>
    </div>
    ${A_.filterSeg?`<div class="fresh" style="padding:2px 2px 0">Filtered to <b style="color:#e7ecf1">${esc(segName)}</b> · <a class="field-link" id="clrf" href="#">clear</a></div>`:''}
    <div class="fresh" id="pcount" style="padding:2px 2px 0"></div>
  </div><div class="rows" id="prows"></div></div><div class="detail" id="pdetail"></div></div>`;
  $('psort').value=A_.sort;
  $('psort').onchange=e=>{A_.sort=e.target.value;localStorage.setItem('aud.sort',A_.sort);draw();};
  $('pq').oninput=debounce(()=>{A_.selP=0;draw();},250);
  if($('clrf'))$('clrf').onclick=e=>{e.preventDefault();A_.filterSeg=null;prospects();};
  function detail(c){if(!c){$('pdetail').innerHTML='';return;}
    const grid=[['Email',c.email||'—'],['Phone',c.phone||'—'],['Firm',c.firm||'—'],['Source',c.source||'—'],['Interest',c.interest||'—'],['Listings',c.origin==='sublease'?String(c.listings.length):'—']];
    $('pdetail').innerHTML=`<div class="k">${esc(c.persona)}</div><h2>${esc(c.name)}</h2>
      <div class="meta">${esc([c.firm,c.email].filter(Boolean).join(' · ')||'No contact info on file')}</div>
      <div class="acts">${contactActions(c)}</div>
      <div class="grid">${grid.map(([k,v])=>`<div class="cell"><div class="kk">${k}</div><div class="vv">${esc(v)}</div></div>`).join('')}</div>
      <div class="h3">Segments (${c.segments.length})</div>
      <div class="seglist">${c.segmentLabels.map(s=>`<span class="seg-chip" data-k="${esc(s.key)}">${s.emoji} ${esc(s.label)}</span>`).join('')}</div>
      <div class="h3">Added</div><div class="pill">🕓 ${esc(fmtWhen(c.created_at))}</div>
      <div style="margin-top:16px"><a class="act" id="toprofile" href="#"><span class="i">👤</span>Open full profile</a></div>`;
    $('pdetail').querySelectorAll('.seg-chip').forEach(ch=>ch.onclick=()=>{A_.filterSeg=ch.dataset.k;A_.selP=0;prospects();});
    $('toprofile').onclick=e=>{e.preventDefault();A_.selC=A_.data.contacts.indexOf(c);switchView('profiles');profiles();};
  }
  function draw(){const r=filteredContacts($('pq').value);
    $('pcount').textContent=r.length?`${r.length} contact${r.length===1?'':'s'}`:'';
    $('prows').innerHTML=r.length?r.map((c,i)=>`<div class="row${i===A_.selP?' sel':''}" data-i="${i}">
      <div class="nm">${personaEmoji(c)} ${esc(c.name)} <span class="pill persona">${esc(c.persona)}</span></div>
      <div class="f">${esc([c.firm,c.interest].filter(Boolean).join(' · ')||c.email||'—')}</div>
      <div class="when">🕓 ${esc(fmtWhen(c.created_at))}</div></div>`).join('')
      :emptyState();
    $('prows').querySelectorAll('.row').forEach(row=>row.onclick=()=>{A_.selP=+row.dataset.i;$('prows').querySelectorAll('.row').forEach(x=>x.classList.remove('sel'));row.classList.add('sel');detail(r[A_.selP]);});
    detail(r[A_.selP]||r[0]||null);
    if(!r.length)$('pdetail').innerHTML='';
  }
  draw();
}
function emptyState(){
  return `<div class="empty">No contacts yet.<br><br>This CRM fills automatically from three live streams:<br>• <b style="color:#e7ecf1">PR Intelligence CRM</b> (orgs &amp; contacts on the <a class="field-link" href="/admin/pr-intelligence/organizations">Organizations</a> page)<br>• <b style="color:#e7ecf1">Newsletter signups</b> (the subscribe form across the site)<br>• <b style="color:#e7ecf1">Sublease listing brokers</b> (added on the <a class="field-link" href="/desk-admin">Sublease Desk</a>)<br><br>Every contact is auto-segmented by category, interest, source, and recency.</div>`;
}
const debounce=(fn,ms)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms);};};
// ── 2. Audience Segments (grid of cards, grouped) ──
function segments(){
  const segs=A_.data.segments;
  if(!segs.length){$('v-segments').innerHTML=`<div class="segwrap"><div class="empty">No segments yet — they appear as soon as the first contact lands.</div></div>`;return;}
  const groups=[...new Set(segs.map(s=>s.group))];
  const max=Math.max(...segs.map(s=>s.size),1);
  // Collapsible tabs — each segment group opens/closes; default COLLAPSED, choice persisted.
  const open=JSON.parse(localStorage.getItem('aud.segopen')||'{}');
  $('v-segments').innerHTML=`<div class="segwrap">${groups.map(g=>{
    const gs=segs.filter(s=>s.group===g);
    const isOpen=!!open[g], gtot=gs.reduce((n,s)=>n+s.size,0);
    return `<div class="seggroup${isOpen?' open':''}" data-g="${esc(g)}">
      <button class="grp-tab" data-gt="${esc(g)}"><span class="caret">${isOpen?'▾':'▸'}</span> ${esc(g)} <span class="cnt">${gs.length} segment${gs.length===1?'':'s'} · ${gtot.toLocaleString()}</span></button>
      <div class="segcards"${isOpen?'':' style="display:none"'}>${gs.map(s=>`
        <div class="segcard" data-k="${esc(s.key)}">
          <div class="e">${s.emoji}</div><div class="l">${esc(s.label)}</div>
          <div class="n">${s.size}<small> · ${s.share}%</small></div>
          <div class="track"><div class="fill" style="width:${Math.round(s.size/max*100)}%"></div></div>
          <div class="g">${s.new30?`<b>${s.new30} new</b> in 30d`:'&nbsp;'}</div>
        </div>`).join('')}</div>
    </div>`;}).join('')}</div>`;
  $('v-segments').querySelectorAll('.grp-tab').forEach(t=>t.onclick=()=>{
    const g=t.dataset.gt;open[g]=!open[g];localStorage.setItem('aud.segopen',JSON.stringify(open));
    const grp=t.closest('.seggroup'),cards=grp.querySelector('.segcards'),now=open[g];
    grp.classList.toggle('open',now);cards.style.display=now?'':'none';t.querySelector('.caret').textContent=now?'▾':'▸';
  });
  $('v-segments').querySelectorAll('.segcard').forEach(c=>c.onclick=()=>{A_.filterSeg=c.dataset.k;A_.selP=0;switchView('prospects');prospects();});
}
// ── 3. Contact Profiles (deep single-contact view + picker) ──
function profiles(){
  const all=sortContacts(A_.data.contacts);
  if(!all.length){$('v-profiles').innerHTML=`<div class="md"><div class="list"><div class="search"><input id="cq" placeholder="Search contacts…"></div><div class="rows" id="crows"></div></div><div class="detail">${emptyState()}</div></div>`;return;}
  $('v-profiles').innerHTML=`<div class="md"><div class="list"><div class="search"><input id="cq" placeholder="Search contacts…"><div class="fresh" id="ccount" style="padding:2px 2px 0"></div></div><div class="rows" id="crows"></div></div><div class="detail" id="cdetail"></div></div>`;
  function detail(c){if(!c){$('cdetail').innerHTML='';return;}
    const grid=[['Email',c.email||'—'],['Phone',c.phone||'—'],['Firm',c.firm||'—'],['Source',c.source||'—'],['Persona',c.persona],['First seen',dstr(c.created_at)]];
    const listings=(c.listings||[]).filter(l=>l&&(l.address||l.submarket));
    $('cdetail').innerHTML=`<div class="k">${esc(c.persona)}</div><h2>${esc(c.name)}</h2>
      <div class="meta">${esc([c.firm,c.email,c.phone].filter(Boolean).join(' · ')||'No contact info on file')}</div>
      <div class="acts">${contactActions(c)}</div>
      <div class="grid">${grid.map(([k,v])=>`<div class="cell"><div class="kk">${k}</div><div class="vv">${esc(v)}</div></div>`).join('')}</div>
      <div class="h3">Segment membership (${c.segments.length})</div>
      <div class="seglist">${c.segmentLabels.map(s=>`<span class="seg-chip" data-k="${esc(s.key)}">${s.emoji} ${esc(s.label)}</span>`).join('')}</div>
      ${listings.length?`<div class="h3">Sublease listings (${listings.length})</div>${listings.map(l=>`<div class="lcard"><div class="a">${esc(l.address||l.submarket||'Listing')}</div><div class="b">${esc([l.submarket,l.city,l.sqft?Number(l.sqft).toLocaleString()+' SF':'',l.asking_rate?'$'+Number(l.asking_rate).toFixed(2)+'/'+(l.rate_period||'yr')+' SF':''].filter(Boolean).join(' · '))}</div></div>`).join('')}`:''}
      <div class="h3">Timeline</div><div class="pill">🕓 Added ${esc(fmtWhen(c.created_at))}</div>`;
    $('cdetail').querySelectorAll('.seg-chip').forEach(ch=>ch.onclick=()=>{A_.filterSeg=ch.dataset.k;A_.selP=0;switchView('prospects');prospects();});
  }
  function draw(){let r=all;const v=($('cq').value||'').trim().toLowerCase();if(v)r=all.filter(c=>(c._q||"").includes(v));
    $('ccount').textContent=`${r.length} contact${r.length===1?'':'s'}`;
    $('crows').innerHTML=r.map((c,i)=>`<div class="row${c===all[A_.selC]?' sel':''}" data-i="${A_.data.contacts.indexOf(c)}"><div class="nm">${personaEmoji(c)} ${esc(c.name)}</div><div class="f">${esc([c.firm,c.email].filter(Boolean).join(' · ')||'—')}</div></div>`).join('')||'<div class="fresh">No matches.</div>';
    $('crows').querySelectorAll('.row').forEach(row=>row.onclick=()=>{A_.selC=+row.dataset.i;$('crows').querySelectorAll('.row').forEach(x=>x.classList.remove('sel'));row.classList.add('sel');detail(A_.data.contacts[A_.selC]);});
  }
  $('cq').oninput=debounce(draw,200);draw();detail(A_.data.contacts[A_.selC]||all[0]);
}
// ── 4. Segment Performance (analytics) ──
function performance(){
  const st=A_.data.stats||{},segs=A_.data.segments;
  const kpis=[['Total contacts',st.total||0],['New (30d)',st.newThisMonth||0],['Segments',st.segmentCount||0],['Sublease brokers',st.subleaseLinked||0]];
  const top=segs.filter(s=>s.group!=='Lifecycle').slice().sort((a,b)=>b.size-a.size).slice(0,10);
  const maxT=Math.max(...top.map(s=>s.size),1);
  const life=segs.filter(s=>s.group==='Lifecycle');const maxL=Math.max(...life.map(s=>s.size),1);
  const sources=Object.entries(st.sources||{}).sort((a,b)=>b[1]-a[1]);const maxS=Math.max(...sources.map(s=>s[1]),1);
  $('v-performance').innerHTML=`<div class="an">
    <div class="kpis">${kpis.map(([k,v])=>`<div class="kpi"><div class="k">${k}</div><div class="v">${Number(v).toLocaleString()}</div></div>`).join('')}</div>
    <div class="panel"><h3>Top segments by size</h3><div class="barwrap" id="tbars">${top.length?top.map(s=>bar(s.emoji+' '+s.label,s.size,maxT,s.key)):'<div class="fresh">No segments yet.</div>'}</div></div>
    <div class="p2">
      <div class="panel"><h3>Lifecycle mix</h3><div class="barwrap" id="lbars">${life.length?life.map(s=>bar(s.label,s.size,maxL,s.key)):'<div class="fresh">—</div>'}</div></div>
      <div class="panel"><h3>Acquisition source</h3><div class="barwrap">${sources.length?sources.map(([k,v])=>bar(prettify(k),v,maxS)):'<div class="fresh">—</div>'}</div></div>
    </div></div>`;
  $('v-performance').querySelectorAll('.bar[data-k]').forEach(b=>b.onclick=()=>{A_.filterSeg=b.dataset.k;A_.selP=0;switchView('prospects');prospects();});
}
const prettify=s=>String(s).replace(/[-_]+/g,' ').replace(/\b\w/g,c=>c.toUpperCase());
function bar(lab,val,max,key){return `<div class="bar"${key?` data-k="${esc(key)}" style="cursor:pointer"`:''}><span class="lab" title="${esc(lab)}">${esc(lab)}</span><span class="track"><span class="fill" style="width:${Math.round(val/max*100)}%"></span></span><span class="val">${Number(val).toLocaleString()}</span></div>`;}
</script>
</body></html>