← back to Rentv 2026

public/deals.html

408 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Deals — Backend Editor · RENTV.com</title>
<meta name="robots" content="noindex,nofollow">
<meta name="description" content="Backend editor for RENTV CRE deals — the live deal feed plus a manual add / edit / hide override layer. Admin-only.">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;1,400&family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
  :root{--red:#c8102e;--ink:#14171a;--sub:#5b636b;--line:#e4e7ea;--bg:#fff;--wash:#f5f6f8;--sans:'Inter',system-ui,sans-serif;--serif:'Playfair Display',Georgia,serif}
  *{box-sizing:border-box;margin:0;padding:0}
  body{font-family:var(--sans);color:var(--ink);background:var(--wash);line-height:1.45}
  a{color:inherit;text-decoration:none}
  .wrap{max-width:1200px;margin:0 auto;padding:0 20px}
  .util{background:var(--ink);color:#cfd4d9;font-size:12px}
  .util .wrap{display:flex;justify-content:space-between;align-items:center;height:34px}
  .util .live{display:inline-flex;align-items:center;gap:7px;letter-spacing:.05em;text-transform:uppercase;font-size:11px}
  .util .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
  @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.6)}70%{box-shadow:0 0 0 7px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
  .util .acts a{margin-left:14px;color:#cfd4d9}
  .mast{background:#fff;border-bottom:2px solid var(--ink);padding:16px 0 12px}
  .mast .wrap{display:flex;align-items:center;justify-content:space-between}
  .logo{font-family:var(--serif);font-weight:700;font-size:32px;letter-spacing:-1px}.logo span{color:var(--red)}
  .logo small{display:block;font-size:11px;font-weight:600;letter-spacing:3px;color:var(--sub);text-transform:uppercase;margin-top:2px;font-family:var(--sans)}
  .backtag{font-size:11px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:#fff;background:var(--ink);padding:5px 11px;border-radius:999px}
  nav{background:#fff;border-bottom:1px solid var(--line)}
  nav .wrap{display:flex;gap:2px;align-items:center;height:46px;overflow-x:auto}
  nav a{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:13px 8px;border-bottom:3px solid transparent;white-space:nowrap}
  nav a:hover,nav a.on{color:var(--red);border-color:var(--red)}
  .head{padding:30px 0 6px}
  .head h1{font-family:var(--serif);font-size:34px;letter-spacing:-.5px}
  .head p{color:var(--sub);font-size:14px;margin-top:4px;max-width:820px}
  .head .fresh{font-size:12px;color:var(--sub);margin-top:8px}
  .controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:18px 0 6px}
  .pill{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:7px 13px;border:1px solid var(--line);border-radius:999px;background:#fff;cursor:pointer;color:var(--sub)}
  .pill.on{background:var(--ink);color:#fff;border-color:var(--ink)}
  .ctl{display:inline-flex;align-items:center;gap:7px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--sub)}
  .ctl select{font-family:var(--sans);font-size:13px;font-weight:600;text-transform:none;letter-spacing:0;color:var(--ink);padding:7px 10px;border:1px solid var(--line);border-radius:6px;background:#fff;cursor:pointer}
  .ctl.dens input[type=range]{width:110px;accent-color:var(--red);cursor:pointer}
  .ctl.chk{cursor:pointer}.ctl.chk input{accent-color:var(--red);cursor:pointer}
  .controls input[type=search]{margin-left:auto;font-family:var(--sans);font-size:13px;padding:8px 12px;border:1px solid var(--line);border-radius:6px;min-width:200px}
  .btn{font-family:var(--sans);font-size:13px;font-weight:700;padding:8px 15px;border-radius:6px;border:1px solid var(--ink);background:var(--ink);color:#fff;cursor:pointer}
  .btn.add{background:var(--red);border-color:var(--red)}
  .btn.ghost{background:#fff;color:var(--ink)}
  .count{font-size:12px;color:var(--sub);margin:6px 0 16px}
  .grid{display:grid;grid-template-columns:repeat(var(--cols,3),1fr);gap:16px}
  .card{background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s,transform .15s}
  .card:hover{box-shadow:0 8px 24px rgba(0,0,0,.09);transform:translateY(-2px)}
  .card.hid{opacity:.5;filter:grayscale(.5)}
  .card.man{border-color:var(--red)}
  .card .ph{aspect-ratio:16/9;background:var(--wash) center/cover no-repeat;position:relative}
  .card .badges{position:absolute;top:10px;left:10px;display:flex;gap:6px;flex-wrap:wrap;max-width:80%}
  .badge{font-size:10px;font-weight:800;text-transform:uppercase;letter-spacing:.05em;padding:4px 8px;border-radius:4px;color:#fff;background:var(--ink)}
  .badge.txn{background:var(--red)}
  .badge.man{background:#7c3aed}.badge.hid{background:#6b7280}.badge.edt{background:#0369a1}
  .card .date{position:absolute;top:10px;right:10px;background:rgba(255,255,255,.94);color:var(--ink);font-size:11px;font-weight:800;letter-spacing:.03em;padding:4px 9px;border-radius:4px}
  .card .amt{position:absolute;bottom:10px;right:10px;background:rgba(20,23,26,.92);color:#fff;font-family:var(--serif);font-weight:700;font-size:18px;padding:4px 10px;border-radius:5px}
  .card .body{padding:14px 16px 14px;display:flex;flex-direction:column;gap:7px;flex:1}
  .card .addr{font-family:var(--serif);font-size:18px;line-height:1.2;font-weight:700}
  .card h3{font-size:13px;color:var(--sub);font-weight:600;line-height:1.35}
  .card .meta{font-size:12px;color:var(--sub);font-weight:600;display:flex;flex-wrap:wrap;gap:4px 10px}
  .card .meta b{color:var(--ink)}
  .card .when{font-size:11px;color:var(--sub);font-weight:600;letter-spacing:.02em}
  .card .meta.parties{font-size:11px;gap:3px 8px}
  .card .meta.parties span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block}
  .drill{cursor:pointer}.badge.drill:hover{filter:brightness(1.25)}.card .meta .drill:hover{text-decoration:underline;color:var(--red)}
  .card .cardfoot{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:8px;padding-top:10px;border-top:1px solid var(--line)}
  .card .more{font-size:12px;font-weight:700;color:var(--red);text-transform:uppercase;letter-spacing:.04em}
  .card .acts{display:flex;gap:6px}
  .card .acts button{font-family:var(--sans);font-size:11px;font-weight:700;padding:5px 10px;border-radius:5px;border:1px solid var(--line);background:#fff;color:var(--ink);cursor:pointer}
  .card .acts button:hover{border-color:var(--ink)}
  .card .acts button.del{color:var(--red);border-color:#f0c9ce}
  .card .acts button.res{color:#0369a1;border-color:#bfe0f0}
  .empty{background:#fff;border:1px dashed var(--line);border-radius:10px;padding:40px;text-align:center;color:var(--sub);grid-column:1/-1}
  footer{background:var(--ink);color:#8b939b;margin-top:40px;padding:26px 0;font-size:12px}
  footer .wrap{display:flex;justify-content:space-between;flex-wrap:wrap;gap:10px}
  /* modal editor */
  .ov{position:fixed;inset:0;background:rgba(20,23,26,.55);display:none;align-items:flex-start;justify-content:center;padding:40px 16px;overflow:auto;z-index:50}
  .ov.on{display:flex}
  .modal{background:#fff;border-radius:14px;width:100%;max-width:640px;box-shadow:0 24px 60px rgba(0,0,0,.3)}
  .modal .mh{display:flex;align-items:center;justify-content:space-between;padding:18px 22px;border-bottom:1px solid var(--line)}
  .modal .mh h2{font-family:var(--serif);font-size:22px}
  .modal .mh button{font-size:22px;background:none;border:none;cursor:pointer;color:var(--sub);line-height:1}
  .modal form{padding:18px 22px 4px}
  .modal .row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
  .modal label{display:block;margin-bottom:12px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--sub)}
  .modal input,.modal select,.modal textarea{width:100%;font-family:var(--sans);font-size:14px;font-weight:500;text-transform:none;letter-spacing:0;color:var(--ink);margin-top:5px;padding:9px 11px;border:1px solid var(--line);border-radius:7px;background:#fff}
  .modal textarea{min-height:70px;resize:vertical}
  .modal .mf{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:14px 22px 20px;border-top:1px solid var(--line);margin-top:8px}
  .modal .mf .msg{font-size:12px;color:var(--sub)}
  @media(max-width:900px){.grid{grid-template-columns:1fr 1fr}}
  @media(max-width:600px){.grid{grid-template-columns:1fr!important}.controls input[type=search]{margin-left:0;width:100%}.modal .row{grid-template-columns:1fr}}
</style>
</head>
<body>
  <div class="util"><div class="wrap">
    <span class="live"><span class="dot"></span> Backend · Deals editor</span>
    <div class="acts"><a href="/backend">← Backend hub</a><a href="/deals-log.html">Deals Log</a></div>
  </div></div>
  <header class="mast"><div class="wrap">
    <a class="logo" href="/">REN<span>TV</span>.com<small>Commercial Real Estate News</small></a>
    <span class="backtag">🔒 Backend only</span>
  </div></header>
  <nav><div class="wrap">
    <a href="/backend">Backend</a><a href="/deals" class="on">Deals</a><a href="/closings">Closings</a><a href="/deals-log.html">Deals Log</a><a href="/desk">Broker &amp; Owner Desk</a><a href="/admin">Admin</a><a href="/blog">Blog</a>
  </div></nav>
  <main class="wrap">
    <div class="head">
      <h1>CRE Deals</h1>
      <p>The live deal feed from RENTV.com reporting across all transaction types, plus your manual layer. <b>Add</b> a deal the scraper missed, <b>Edit</b> any card (your changes stick — the 20-min auto-pull never overwrites them), or <b>Hide</b> ones that are wrong. Backend-only; not shown to the public site.</p>
      <div class="fresh" id="fresh">Loading…</div>
    </div>
    <!-- txn-type filter pills -->
    <div class="controls" id="txns"></div>
    <!-- property-type filter pills -->
    <div class="controls" id="types"></div>
    <div class="controls">
      <button class="btn add" id="addBtn">+ Add deal</button>
      <label class="ctl"><span>Sort</span>
        <select id="sort">
          <option value="newest">Newest</option>
          <option value="price-desc">Price ↓</option>
          <option value="price-asc">Price ↑</option>
          <option value="type">Property Type A→Z</option>
          <option value="loc">Location A→Z</option>
          <option value="title">Title A→Z</option>
        </select>
      </label>
      <label class="ctl dens"><span>Density</span>
        <input id="dens" type="range" min="2" max="5" step="1" value="3" aria-label="Grid density (columns)">
      </label>
      <label class="ctl chk"><input type="checkbox" id="showHidden"> Show hidden</label>
      <input id="q" type="search" placeholder="Search address, city, property, party…" autocomplete="off">
    </div>
    <div class="count" id="count"></div>
    <div class="grid" id="grid"></div>
  </main>
  <footer><div class="wrap"><span>© RENTV.com — Commercial Real Estate News · Backend</span><span>Base feed refreshed every 20 min · manual adds/edits/hides persist in deals-overrides.json</span></div></footer>

  <!-- editor modal -->
  <div class="ov" id="ov">
    <div class="modal">
      <div class="mh"><h2 id="mTitle">Add deal</h2><button id="mClose" aria-label="Close">×</button></div>
      <form id="mForm" autocomplete="off">
        <input type="hidden" id="f_id">
        <label>Transaction type
          <select id="f_txn_type">
            <option value="">—</option>
            <option value="Sale">Sale</option>
            <option value="Lease">Lease</option>
            <option value="Financing">Financing</option>
            <option value="Development">Development</option>
            <option value="Acquisition">Acquisition</option>
          </select>
        </label>
        <label>Address / property name<input id="f_address" placeholder="1200 Wilshire Blvd"></label>
        <div class="row">
          <label>City<input id="f_city" placeholder="Los Angeles"></label>
          <label>State<input id="f_state" placeholder="CA"></label>
        </div>
        <div class="row">
          <label>Property type
            <select id="f_property_type">
              <option value="">—</option>
              <option>Multifamily</option><option>Office</option><option>Industrial</option>
              <option>Retail</option><option>Hospitality</option><option>Mixed-Use</option><option>Land</option>
            </select>
          </label>
          <label>Amount (label)<input id="f_amount_label" placeholder="$12.5M"></label>
        </div>
        <div class="row">
          <label>Close date<input id="f_close_date" type="date"></label>
          <label>Size (label)<input id="f_size_label" placeholder="120,000 SF"></label>
        </div>
        <div class="row">
          <label>Year built<input id="f_year_built" placeholder="1998"></label>
          <label>Headline / title<input id="f_title" placeholder="Investor buys downtown tower"></label>
        </div>
        <label>Image URL<input id="f_image" placeholder="https://… (optional)"></label>
        <label>Summary / notes<textarea id="f_summary" placeholder="Optional details about the transaction"></textarea></label>
      </form>
      <div id="deal-notes-wrap" style="padding:2px 22px 14px;display:none">
        <h3 style="font-size:13px;text-transform:uppercase;letter-spacing:.04em;color:var(--sub);margin:6px 0 10px">Notes <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--sub)">(team notes — timestamped &amp; attributed)</span></h3>
        <div id="deal-notethread"></div>
      </div>
      <div class="mf">
        <span class="msg" id="mMsg"></span>
        <span><button class="btn ghost" id="mCancel">Cancel</button> <button class="btn" id="mSave">Save deal</button></span>
      </div>
    </div>
  </div>

<script src="/csrf.js"></script>
<script src="/admin/pr-intelligence/pr-shared.js"></script>
<script>
const $=s=>document.querySelector(s);
const MONTHS={Jan:0,Feb:1,Mar:2,Apr:3,May:4,Jun:5,Jul:6,Aug:7,Sep:8,Oct:9,Nov:10,Dec:11};
let ALL=[], fTxn='', fType='', fQ='',
    sVal=(localStorage.getItem('rentv.deals.sort')||'newest'),
    dens=(localStorage.getItem('rentv.deals.dens')||'3'),
    showHidden=(localStorage.getItem('rentv.deals.hidden')==='1');
const TXNS=['Sale','Lease','Financing','Development','Acquisition'];
const TYPES=['Multifamily','Office','Industrial','Retail','Hospitality','Mixed-Use','Land'];

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

function amtNum(d){
  const s=String(d.amount_label||d.amount||'').replace(/[,\s$]/g,'');
  const m=s.match(/([\d.]+)\s*([MmKkBb]?)/);
  if(!m)return -1;
  let n=parseFloat(m[1]);
  const u=m[2].toUpperCase();
  if(u==='B')n*=1e9;else if(u==='M')n*=1e6;else if(u==='K')n*=1e3;
  return isNaN(n)?-1:n;
}

function dateOf(d){
  if(d.close_date){const m=String(d.close_date).match(/^(\d{4})-(\d{2})-(\d{2})/);if(m)return new Date(Date.UTC(+m[1],+m[2]-1,+m[3]));}
  const m=String(d.image||'').match(/([A-Z][a-z]+?)(\d{1,2})(\d{4})\.(?:jpg|jpeg|png|webp)/);if(!m)return null;const mo=MONTHS[m[1].slice(0,3)];if(mo==null)return null;return new Date(Date.UTC(+m[3],mo,+m[2]));
}
const fmtDate=dt=>dt?dt.toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}):'';
// Admin-card rule: show the RECORD's created date AND time in local tz. A full ISO timestamp (feed
// deals, from the registry first_seen) renders date+time; a DATE-ONLY value (corpus article date) renders
// date-only — parsed as a LOCAL date so a UTC-midnight string doesn't render as the prior evening, and
// with no fabricated clock so we never show a precision we don't actually have.
const fmtDateTime=v=>{if(!v)return'';const s=String(v);
  const md=s.match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if(md)return new Date(+md[1],+md[2]-1,+md[3]).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'});
  const dt=new Date(s);return isNaN(dt)?'':dt.toLocaleString(undefined,{month:'short',day:'numeric',year:'numeric',hour:'numeric',minute:'2-digit'});};
// Party drill filters by the FIRM (strip a "(agent, agent…)" roster) so clicking a broker/party chip
// finds the firm's OTHER deals, not just this one — the full-string value matched only itself (1 deal).
const firmKey=v=>{const t=String(v||'').split('(')[0].trim().replace(/[,;]\s*$/,'');return t||String(v||'');};
// Match a SHORT firm code (CT/JLL/PRI) as a WHOLE WORD so a party drill doesn't hit substrings — "ct"
// inside "transaction" matched 1,611 deals (36%); \bct\b matches only the 13 real CT parties, while
// \bjll\b still finds all 406 JLL deals. Longer queries keep plain substring search.
const matchQ=(hay,q)=>{ if(!q)return true; if(q.length<=4&&/^[a-z0-9&]+$/.test(q))return new RegExp('\\b'+q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')+'\\b').test(hay); return hay.includes(q); };
const isManual=d=>d._manual||String(d.id||'').startsWith('md');

function pillRow(hostId,list,cur,pick){
  const host=$(('#'+hostId));
  host.innerHTML='<button class="pill'+(cur===''?' on':'')+'" data-v="">All</button>'+
    list.map(v=>`<button class="pill${cur===v?' on':''}" data-v="${esc(v)}">${esc(v)}</button>`).join('');
  host.querySelectorAll('.pill').forEach(b=>b.onclick=()=>pick(b.dataset.v));
}

function sorted(list){
  return list.slice().sort((a,b)=>{
    if(sVal==='price-desc')return amtNum(b)-amtNum(a);
    if(sVal==='price-asc'){const x=amtNum(a),y=amtNum(b);return (x<0?1e18:x)-(y<0?1e18:y);}
    if(sVal==='type')return (a.property_type||'').localeCompare(b.property_type||'');
    if(sVal==='loc')return `${a.city} ${a.state}`.localeCompare(`${b.city} ${b.state}`);
    if(sVal==='title')return (a.title||'').localeCompare(b.title||'');
    // newest = natural feed order (ALL already newest-first); stable fallback by dateOf
    const da=dateOf(a),db=dateOf(b);return (db?db.getTime():0)-(da?da.getTime():0);
  });
}

function render(){
  let rows=ALL.filter(d=>
    (!fTxn||d.txn_type===fTxn)&&
    (!fType||d.property_type===fType)&&
    matchQ(`${d.address} ${d.title} ${d.city} ${d.state} ${d.property_type} ${d.buyer} ${d.seller} ${d.broker} ${d.summary}`.toLowerCase(),fQ)
  );
  rows=sorted(rows);
  document.documentElement.style.setProperty('--cols',dens);
  const manC=ALL.filter(isManual).length, hidC=ALL.filter(d=>d._hidden).length;
  $('#count').textContent=`${rows.length} deal${rows.length===1?'':'s'}${fTxn?' · '+fTxn:''}${fType?' · '+fType:''} · ${manC} manual${hidC?` · ${hidC} hidden`:''}`;
  $('#grid').innerHTML=rows.length?rows.map(d=>{
    const loc=[d.city,d.state].filter(Boolean).join(', ');
    const ph=d.image?`background-image:url('${esc(d.image)}')`:'';
    const dt=fmtDate(dateOf(d));
    const man=isManual(d), hidden=!!d._hidden, edited=!!d._edited;
    // HARD RULE: never link out to rentv.com; feed/corpus rows link /news/:id in-app only
    const story=(!man&&d.id)?`<a class="more" href="/news/${encodeURIComponent(d.id)}">Read the full story →</a>`:'<span class="more" style="color:var(--sub)">Manual entry</span>';
    return `<div class="card ${hidden?'hid':''} ${man?'man':''}" data-id="${esc(String(d.id))}">
      <div class="ph" style="${ph}">
        <div class="badges">
          ${d.txn_type?`<span class="badge txn drill" data-filt="txn" data-val="${esc(d.txn_type)}" title="Filter by ${esc(d.txn_type)}">${esc(d.txn_type)}</span>`:'<span class="badge txn">Deal</span>'}
          ${d.property_type?`<span class="badge drill" data-filt="type" data-val="${esc(d.property_type)}" title="Filter by ${esc(d.property_type)}">${esc(d.property_type)}</span>`:''}
          ${man?'<span class="badge man">Manual</span>':''}
          ${edited?'<span class="badge edt">Edited</span>':''}
          ${hidden?'<span class="badge hid">Hidden</span>':''}
        </div>
        ${dt?`<span class="date">🗓 ${esc(dt)}</span>`:''}
        ${d.amount_label?`<span class="amt">${esc(d.amount_label)}</span>`:''}
      </div>
      <div class="body">
        <div class="addr">${esc(d.address||d.title)}</div>
        ${loc?`<div class="meta"><span class="drill" data-filt="q" data-val="${esc(loc)}" title="Filter by ${esc(loc)}">📍 <b>${esc(loc)}</b></span>${d.size_label?`<span>📐 ${esc(d.size_label)}</span>`:''}${d.year_built?`<span>🏗 ${esc(d.year_built)}</span>`:''}</div>`:''}
        ${(d.buyer||d.seller||d.broker)?`<div class="meta parties">${d.buyer?`<span class="drill" data-filt="q" data-val="${esc(firmKey(d.buyer))}" title="Buyer — filter by ${esc(firmKey(d.buyer))}">🟢 <b>${esc(d.buyer)}</b></span>`:''}${d.seller?`<span class="drill" data-filt="q" data-val="${esc(firmKey(d.seller))}" title="Seller — filter by ${esc(firmKey(d.seller))}">🔴 ${esc(d.seller)}</span>`:''}${d.broker?`<span class="drill" data-filt="q" data-val="${esc(firmKey(d.broker))}" title="Broker — filter by ${esc(firmKey(d.broker))}">🤝 ${esc(d.broker)}</span>`:''}</div>`:''}
        ${d.acquirer?`<div class="meta"><a href="/firm?name=${encodeURIComponent(d.acquirer)}" title="See every deal RENTV has ${esc(d.acquirer)} in" style="color:#c8102e;font-weight:700">🏢 ${esc(d.acquirer)}</a></div>`:''}
        ${(d.address&&d.title)?`<h3>${esc(d.title)}</h3>`:''}
        ${d.created_at?`<div class="when" title="${esc(d.created_at)}">🕓 Added ${esc(fmtDateTime(d.created_at))}</div>`:''}
        <div class="cardfoot">
          ${story}
          <div class="acts">
            <button data-act="edit">Edit</button>
            ${hidden?'<button class="res" data-act="restore">Restore</button>':`<button class="del" data-act="del">${man?'Delete':'Hide'}</button>`}
          </div>
        </div>
      </div></div>`;
  }).join(''):'<div class="empty">No deals match your filters yet. Add one with + Add deal, or clear the filters.</div>';

  // wire per-card actions — buttons are NOT inside an <a>, so no navigation side-effects
  $('#grid').querySelectorAll('.card .acts button').forEach(b=>{
    b.onclick=()=>{
      const card=b.closest('.card'), id=card.dataset.id, act=b.dataset.act;
      if(act==='edit')return openEdit(ALL.find(x=>String(x.id)===String(id)));
      if(act==='del')return removeDeal(id);
      if(act==='restore')return restoreDeal(id);
    };
  });
  // Datum drilldown — txn badge → txn filter, property badge → type filter, location/parties → search (href-everything rule).
  $('#grid').querySelectorAll('.drill').forEach(el=>{
    el.onclick=(e)=>{e.stopPropagation();const f=el.dataset.filt,v=(el.dataset.val||'').trim();
      if(f==='txn'){fTxn=v;renderPills();}
      else if(f==='type'){fType=v;renderPills();}
      else if(f==='q'){fQ=v.toLowerCase();$('#q').value=v;}
      render();};
  });
}

// ── editor modal ──
const F=['id','txn_type','address','city','state','property_type','amount_label','close_date','size_label','year_built','title','image','summary'];
function openAdd(){
  $('#mTitle').textContent='Add deal'; $('#mMsg').textContent='';
  F.forEach(k=>{const el=$('#f_'+k);if(el)el.value='';});
  $('#deal-notes-wrap').style.display='none'; // no id yet — notes need a saved deal to attach to
  $('#ov').classList.add('on'); $('#f_txn_type').focus();
}
function openEdit(d){
  if(!d)return; $('#mTitle').textContent='Edit deal'; $('#mMsg').textContent='';
  F.forEach(k=>{const el=$('#f_'+k);if(el)el.value=d[k]||'';});
  if(!d.close_date){const dt=dateOf(d);if(dt)$('#f_close_date').value=dt.toISOString().slice(0,10);}
  // Notes attach to the persisted deal by its (string) id — the same reusable widget as org/broker/person.
  const nw=$('#deal-notes-wrap');
  if(d.id&&window.PR&&PR.notes){ nw.style.display='block'; PR.notes($('#deal-notethread'), 'deal', String(d.id)); }
  else nw.style.display='none';
  $('#ov').classList.add('on'); $('#f_txn_type').focus();
}
function closeModal(){$('#ov').classList.remove('on');}

async function saveDeal(){
  const id=$('#f_id').value.trim();
  const body={}; F.filter(k=>k!=='id').forEach(k=>body[k]=$('#f_'+k).value.trim());
  if(!body.address&&!body.title){$('#mMsg').textContent='Address or title is required.';return;}
  $('#mSave').disabled=true; $('#mMsg').textContent='Saving…';
  try{
    const url=id?('/api/deals-manage/'+encodeURIComponent(id)):'/api/deals-manage';
    const r=await fetch(url,{method:id?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
    const j=await r.json();
    if(!r.ok||!j.ok)throw new Error(j.error||('HTTP '+r.status));
    closeModal(); await load();
  }catch(e){$('#mMsg').textContent='Failed: '+e.message;}
  finally{$('#mSave').disabled=false;}
}

async function removeDeal(id){
  const man=isManual({id});
  if(!confirm(man?'Delete this manual deal permanently?':'Hide this deal from the list? You can restore it with "Show hidden".'))return;
  try{
    const r=await fetch('/api/deals-manage/'+encodeURIComponent(id),{method:'DELETE'});
    const j=await r.json(); if(!r.ok||!j.ok)throw new Error(j.error||('HTTP '+r.status));
    await load();
  }catch(e){alert('Failed: '+e.message);}
}

async function restoreDeal(id){
  try{
    const r=await fetch('/api/deals-manage/'+encodeURIComponent(id)+'/restore',{method:'POST'});
    const j=await r.json(); if(!r.ok||!j.ok)throw new Error(j.error||('HTTP '+r.status));
    await load();
  }catch(e){alert('Failed: '+e.message);}
}

// init persistent controls
$('#sort').value=sVal; $('#dens').value=dens; $('#showHidden').checked=showHidden;
$('#sort').onchange=e=>{sVal=e.target.value;localStorage.setItem('rentv.deals.sort',sVal);render();};
$('#dens').oninput=e=>{dens=e.target.value;localStorage.setItem('rentv.deals.dens',dens);render();};
$('#q').oninput=e=>{fQ=e.target.value.toLowerCase().trim();render();};
$('#showHidden').onchange=e=>{showHidden=e.target.checked;localStorage.setItem('rentv.deals.hidden',showHidden?'1':'0');load();};
$('#addBtn').onclick=openAdd;
$('#mClose').onclick=closeModal; $('#mCancel').onclick=closeModal; $('#mSave').onclick=saveDeal;
$('#ov').onclick=e=>{if(e.target===$('#ov'))closeModal();};
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('#ov').classList.contains('on'))closeModal();});

function renderPills(){
  pillRow('txns',TXNS,fTxn,v=>{fTxn=v;renderPills();render();});
  pillRow('types',TYPES,fType,v=>{fType=v;renderPills();render();});
}

async function load(){
  try{
    const r=await fetch('/api/deals-manage'+(showHidden?'?include_hidden=1':''));
    if(r.status===403){$('#grid').innerHTML='<div class="empty">Admin login required — this is a backend-only page.</div>';return;}
    const data=await r.json();
    ALL=data.deals||[];
    $('#fresh').textContent=`${data.count??ALL.length} deals · ${data.from_feed||0} live feed · ${data.from_corpus||0} article archive · ${data.manual||0} manual · ${data.hidden||0} hidden`;
    render();
  }catch(e){$('#grid').innerHTML='<div class="empty">Failed to load deals: '+esc(e.message)+'</div>';}
}

(async()=>{renderPills();await load();})();
</script>
</body>
</html>