← back to Commercialrealestate
CRCP deals-flow.html: add Grid/List/Table views + sortable table headers (card grid + compact list + dense table)
9e31cc7a1421d5c7c1690fbaacbff3248876489b · 2026-08-18 21:07:04 -0700 · Steve Abrams
Files touched
Diff
commit 9e31cc7a1421d5c7c1690fbaacbff3248876489b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 18 21:07:04 2026 -0700
CRCP deals-flow.html: add Grid/List/Table views + sortable table headers (card grid + compact list + dense table)
---
public/deals-flow.html | 46 ++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
diff --git a/public/deals-flow.html b/public/deals-flow.html
index 643b3c3..4a45b60 100644
--- a/public/deals-flow.html
+++ b/public/deals-flow.html
@@ -103,6 +103,21 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
.roleopt.sel{border-color:var(--acc);background:rgba(63,185,80,.12)}
.roleopt b{display:block;color:var(--ink,#fff);font-size:12.5px}
.roleopt span{font-size:10px;color:var(--mut)}
+.viewseg{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden}
+.viewseg button{background:var(--card);color:var(--mut);border:0;border-right:1px solid var(--line);padding:7px 11px;font-size:12px;cursor:pointer}
+.viewseg button:last-child{border-right:0}.viewseg button.on{background:var(--blue);color:#0e1116;font-weight:600}
+.dftable{overflow:auto;margin-top:6px}
+.dftable table{width:100%;border-collapse:collapse;font-size:12.5px}
+.dftable th{text-align:left;color:var(--mut);text-transform:uppercase;font-size:10px;letter-spacing:.4px;padding:7px 9px;border-bottom:1px solid var(--line);white-space:nowrap;position:sticky;top:0;background:var(--card)}
+.dftable td{padding:7px 9px;border-bottom:1px solid var(--line)}
+.dftable tr:hover td{background:rgba(120,160,255,.06)}
+.lst{display:flex;flex-direction:column;margin-top:6px}
+.lrow{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;padding:10px 6px;border-bottom:1px solid var(--line);cursor:pointer}
+.lrow:hover{background:rgba(120,160,255,.06)}
+.lnm{font-weight:700;min-width:210px;flex:0 0 auto;color:var(--ink)}
+.lmeta{display:flex;flex-wrap:wrap;gap:5px 16px;flex:1 1 320px;min-width:0}
+.lm{font-size:12.5px;white-space:nowrap;color:var(--ink)}
+.lk{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-right:5px}
</style> <link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
<link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
</head><body>
@@ -129,6 +144,7 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
<div class="prov">Every card shows its <b>source</b> (🏛️ LA County Assessor · 📄 SEC EDGAR) — all public-record, resellable, $0. County price = Prop-13 reassessment estimate (assessed value de-escalated to transfer year ≈ sale price). SEC cards are REIT filings that mention an acquisition (price/property pending parse — not yet extracted). County "est" price is an ESTIMATE (assessed value ÷ 1.02^yrs), not a recorded sale price; parcels with a building built after the transfer are excluded. <b style="color:var(--gold)">Resale rights pending legal review</b> (CA R&T §408.3(c) restricts commercial-solicitation use of assessor bulk data) — do not resell until cleared.</div>
<div class="toolbar">
<input class="search" id="q" placeholder="search address, company, use, year…">
+ <div class="viewseg" id="viewseg"><button data-v="grid" class="on">▦ Grid</button><button data-v="list">☰ List</button><button data-v="table">▤ Table</button></div>
<select id="sort"></select>
<label style="font-size:11px;color:var(--mut)">density <input type="range" id="dens" min="2" max="7" step="1"></label>
<button id="saveBtn" title="Save this filter set — get alerts on new matching deals" style="background:var(--card);border:1px solid var(--line);color:var(--acc);border-radius:8px;padding:7px 10px;font-size:12px;cursor:pointer">★ Save search</button>
@@ -141,6 +157,8 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
<button id="propLookupBtn" style="background:var(--card);border:1px solid var(--blue);color:var(--blue);border-radius:8px;padding:7px 14px;font-size:12px;cursor:pointer">Look up →</button>
</div>
<div class="grid" id="grid"></div>
+ <div id="tableView" class="dftable hide"><table><thead id="dfthead"></thead><tbody id="dftbody"></tbody></table></div>
+ <div id="listView" class="lst hide"></div>
</div></div>
<!-- property-detail drill (public-record dossier) -->
@@ -182,7 +200,10 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
const $=s=>document.querySelector(s);
const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<');
const money=n=>n==null||isNaN(+n)?null:'$'+Math.round(+n).toLocaleString();
-let DATA=[],q='',sortKey='date',sortDir=-1;
+let DATA=[],q='',sortKey='date',sortDir=-1,view=(localStorage.getItem('dfView')||'grid');
+// columns for the table + list views (the grid keeps its rich cards)
+const DCOLS=[{k:'name',l:'Deal / Property'},{k:'price',l:'Price',money:true},{k:'units',l:'Units',num:true},{k:'use',l:'Use'},{k:'deal_city',l:'City'},{k:'year',l:'Year'},{k:'source_label',l:'Source'},{k:'confidence',l:'Confidence'},{k:'holder',l:'Holder'}];
+const dfmt=(r,c)=>{const v=r[c.k];if(c.money)return money(v)||'—';if(c.num)return v!=null&&v!==''?(+v).toLocaleString():'—';return v==null||v===''?'—':esc(String(v));};
let ME=null, WATCH=new Set(), watchOnly=false;
const dealKey=r=>String(r.ain||r.id||(r.kind+'|'+(r.name||'')+'|'+(r.date||'')));
let dens=(function(){const v=parseInt(localStorage.getItem('dfDens'),10);return(v>=2&&v<=7)?v:4;})();
@@ -287,7 +308,20 @@ function render(){
`<div class="stat"><b style="color:var(--violet)">${rows.filter(r=>r.kind==='reit-filing').length}</b><span>SEC REIT filings</span></div>`+
`<div class="stat"><b>${new Set(rows.map(r=>r.year)).size}</b><span>years</span></div>`;
$('#count').textContent=`${rows.length.toLocaleString()} of ${DATA.length.toLocaleString()}`;
- $('#grid').innerHTML=rows.slice(0,600).map(card).join('')||'<div style="color:var(--mut);padding:40px">No deals match.</div>';
+ render._rows=rows;
+ const g=$('#grid'),tv=$('#tableView'),lv=$('#listView');
+ g.classList.toggle('hide',view!=='grid');
+ if(tv) tv.classList.toggle('hide',view!=='table');
+ if(lv) lv.classList.toggle('hide',view!=='list');
+ const slice=rows.slice(0,600);
+ if(view==='grid'){
+ g.innerHTML=slice.map(card).join('')||'<div style="color:var(--mut);padding:40px">No deals match.</div>';
+ } else if(view==='list' && lv){
+ lv.innerHTML=slice.map(r=>`<div class="lrow drow" data-k="${esc(dealKey(r))}" style="cursor:pointer"><div class="lnm">${esc(r.name||'—')}</div><div class="lmeta">`+DCOLS.slice(1).map(c=>`<span class="lm"><span class="lk">${esc(c.l)}</span>${dfmt(r,c)}</span>`).join('')+`</div></div>`).join('')||'<div style="color:var(--mut);padding:40px">No deals match.</div>';
+ } else if(tv){
+ $('#dfthead').innerHTML='<tr>'+DCOLS.map(c=>`<th data-k="${c.k}" style="cursor:pointer" title="Click to sort">${esc(c.l)}${sortKey===c.k?(sortDir<0?' ↓':' ↑'):''}</th>`).join('')+'</tr>';
+ $('#dftbody').innerHTML=slice.map(r=>`<tr class="drow" data-k="${esc(dealKey(r))}" style="cursor:pointer">`+DCOLS.map(c=>`<td>${dfmt(r,c)}</td>`).join('')+`</tr>`).join('')||`<tr><td colspan="${DCOLS.length}" style="color:var(--mut);padding:30px">No deals match.</td></tr>`;
+ }
}
$('#fSrc').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.src,c.dataset.s);c.classList.toggle('active');writeURLFilters(true);render();});
$('#fUse').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.use,c.dataset.u);c.classList.toggle('active');writeURLFilters(true);render();});
@@ -297,6 +331,14 @@ $('#pMin').addEventListener('input',()=>{F.pMin=$('#pMin').value?+$('#pMin').val
$('#uMin').addEventListener('input',()=>{F.uMin=$('#uMin').value?+$('#uMin').value:null;writeURLFilters();render();});
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();writeURLFilters();render();});
$('#sort').addEventListener('change',()=>{sortKey=$('#sort').value;sortDir=(sortKey==='name'||sortKey==='use')?1:-1;render();});
+// grid/list/table view toggle
+document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x.dataset.v===view));
+$('#viewseg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;document.querySelectorAll('#viewseg button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;localStorage.setItem('dfView',view);render();});
+// table header → sort on any deal field; row → open parcel/filing detail (delegated so it survives re-render)
+document.addEventListener('click',e=>{
+ const th=e.target.closest('#dfthead th'); if(th){const k=th.dataset.k;if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='name'||k==='use'||k==='deal_city'||k==='source_label'||k==='confidence')?1:-1;}const so=$('#sort');if(so&&Array.from(so.options).some(o=>o.value===k))so.value=k;render();return;}
+ const row=e.target.closest('.drow'); if(row && !e.target.closest('a,button')){const r=(render._rows||[]).find(x=>dealKey(x)===row.dataset.k);if(r){const ain=r.ain?String(r.ain).replace(/\D/g,''):'';if(ain)location.href='/parcel/'+ain;else if(r.url)window.open(r.url,'_blank','noopener');}}
+});
$('#dens').addEventListener('input',()=>{dens=+$('#dens').value;localStorage.setItem('dfDens',dens);render();});
$('#sort').innerHTML=[['date','Date'],['price','Price'],['units','Units'],['year','Year'],['name','Name']].map(([k,l])=>`<option value="${k}"${k==='date'?' selected':''}>Sort: ${l}</option>`).join('');
$('#dens').value=dens;
← 284092d CRCP: generic BuildOut scraper (class-cracker) + auto-discov
·
back to Commercialrealestate
·
CRCP contractors.html: add Grid/List/Table to browse mode + 0e9fa7b →