← back to Commercialrealestate
feat(broker-grid+index): Amazon left rail on broker-grid (agent-type/Arcstone/firm facets + per-column show/hide, auto-discovers every scalar field driving table+grid+CSV); Condos added as an asset class in the index left rail
f57111168e90003061b7c435edf34dbfb985a381 · 2026-07-02 09:49:41 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M public/broker-grid.htmlM public/index.html
Diff
commit f57111168e90003061b7c435edf34dbfb985a381
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jul 2 09:49:41 2026 -0700
feat(broker-grid+index): Amazon left rail on broker-grid (agent-type/Arcstone/firm facets + per-column show/hide, auto-discovers every scalar field driving table+grid+CSV); Condos added as an asset class in the index left rail
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
public/broker-grid.html | 88 +++++++++++++++++++++++++++++++++++++++++--------
public/index.html | 3 +-
2 files changed, 77 insertions(+), 14 deletions(-)
diff --git a/public/broker-grid.html b/public/broker-grid.html
index 9efb86d..f3ec5f4 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -48,6 +48,20 @@
.gc .row{display:flex;justify-content:space-between;gap:8px;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
.gc .row .k{color:var(--mut);} .gc .ci{display:flex;gap:12px;margin-top:8px;flex-wrap:wrap;font-size:12.5px;}
.hide{display:none;}
+ /* ---- Amazon-style left rail ---- */
+ .shell{display:flex;align-items:flex-start;}
+ .rail{flex:0 0 260px;background:#0a0d13;border-right:1px solid var(--line);align-self:stretch;position:sticky;top:53px;height:calc(100vh - 53px);overflow-y:auto;padding:0 0 40px;}
+ .rail .rsec{padding:13px 16px;border-bottom:1px solid var(--line);}
+ .rail h4{margin:0 0 9px;font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--mut);}
+ .chips{display:flex;flex-wrap:wrap;gap:6px;}
+ .chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
+ .chip:hover{color:var(--ink);} .chip.active{border-color:var(--gold);color:var(--gold);background:rgba(200,162,75,.10);}
+ .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
+ .fieldtoggles{display:flex;flex-direction:column;gap:1px;max-height:320px;overflow-y:auto;}
+ .ftog{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink);cursor:pointer;padding:3px 4px;border-radius:6px;} .ftog:hover{background:rgba(200,162,75,.08);} .ftog input{accent-color:var(--gold);}
+ .minibtn{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:7px;padding:4px 9px;font-size:11px;cursor:pointer;margin-right:5px;} .minibtn:hover{border-color:var(--gold);color:var(--ink);}
+ .wrap{flex:1 1 auto;min-width:0;}
+ @media(max-width:820px){ .rail{display:none;} }
</style>
</head>
<body>
@@ -60,10 +74,21 @@
<button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
<span class="count" id="count">…</span>
</header>
+<div class="shell">
+<aside class="rail">
+ <div class="rsec"><h4>Agent type</h4><div class="chips" id="fType"></div></div>
+ <div class="rsec"><h4>Relationship</h4><div class="chips" id="fArc"><span class="chip" data-arc="1">★ Arcstone only</span></div></div>
+ <div class="rsec"><h4>Company / firm</h4><div class="chips" id="fFirm"></div></div>
+ <div class="rsec"><h4>Fields / columns — show / hide</h4>
+ <div class="fieldtoggles" id="fFields"></div>
+ <div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button></div>
+ </div>
+</aside>
<div class="wrap">
<div id="tableView" class="tblwrap"><table class="g"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
<div id="gridView" class="grid hide"></div>
</div>
+</div>
<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
<script>
const $=s=>document.querySelector(s);
@@ -91,6 +116,37 @@ const COLS=[
{k:'created_at',l:'Added',t:'date'},
];
let DATA=[], view='table', sortKey='listings', sortDir=-1, q='';
+const F={types:new Set(),firms:new Set(),arc:false};
+let VISCOL=(function(){let s={};try{s=JSON.parse(localStorage.getItem('brokerCols')||'{}');}catch(e){}return s;})();
+function colVis(k){ if(k in VISCOL) return !!VISCOL[k]; const c=COLS.find(x=>x.k===k); return c?c.def!==0:true; }
+function visCols(){ return COLS.filter(c=>colVis(c.k)); }
+function saveCols(){ try{localStorage.setItem('brokerCols',JSON.stringify(VISCOL));}catch(e){} }
+function tog(set,v){ set.has(v)?set.delete(v):set.add(v); }
+function autoCols(){
+ const known=new Set(COLS.map(c=>c.k)); const skip=new Set(['id','arcstone_flag']);
+ const seen=new Set();
+ DATA.slice(0,60).forEach(r=>Object.keys(r||{}).forEach(k=>{
+ if(known.has(k)||skip.has(k)||seen.has(k)) return; const v=r[k]; if(v==null) return;
+ if(typeof v==='object') return; seen.add(k);
+ const isNum = typeof v==='number' || (v!==''&&!isNaN(+v));
+ COLS.push({k, l:k.replace(/_/g,' ').replace(/\b\w/g,ch=>ch.toUpperCase()).slice(0,20), t:isNum?'n':'s', def:0});
+ }));
+}
+function buildRail(){
+ const tc={},fc={};
+ DATA.forEach(r=>{ if(r.agent_type)tc[r.agent_type]=(tc[r.agent_type]||0)+1; if(r.firm)fc[r.firm]=(fc[r.firm]||0)+1; });
+ const lbl={residential:'Residential',commercial:'Commercial'};
+ $('#fType').innerHTML=Object.entries(tc).sort((a,b)=>b[1]-a[1]).map(([k,c])=>`<span class="chip" data-ty="${esc(k)}">${esc(lbl[k]||k)}<span class="ct">${c}</span></span>`).join('');
+ $('#fFirm').innerHTML=Object.entries(fc).sort((a,b)=>b[1]-a[1]).slice(0,16).map(([k,c])=>`<span class="chip" data-fm="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
+ buildFieldToggles();
+}
+function buildFieldToggles(){ $('#fFields').innerHTML=COLS.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join(''); }
+function passFacets(r){
+ if(F.types.size && !F.types.has(r.agent_type)) return false;
+ if(F.firms.size && !F.firms.has(r.firm)) return false;
+ if(F.arc && !r.arcstone_flag) return false;
+ return true;
+}
function cell(r,c){
const v=r[c.k];
if(c.k==='name') return esc(v==null?'':v)+(r.arcstone_flag?' <span class="atype arc" title="Prior Arcstone relationship">★ Arcstone</span>':'');
@@ -104,7 +160,7 @@ function cell(r,c){
return esc(v==null||v===''?'—':v);
}
function filtered(){
- let rows=DATA;
+ let rows=DATA.filter(passFacets);
if(q){ const t=q.toLowerCase(); rows=rows.filter(r=>COLS.some(c=>{const v=r[c.k];return v!=null&&String(v).toLowerCase().includes(t);})); }
const c=COLS.find(x=>x.k===sortKey)||COLS[0];
rows=rows.slice().sort((a,b)=>{
@@ -116,22 +172,21 @@ function filtered(){
return rows;
}
function render(){
- const rows=filtered();
+ const rows=filtered(); const cols=visCols();
$('#count').textContent=`${rows.length} of ${DATA.length} brokers/agents`;
- $('#thead').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${c.l}</th>`).join('')+'</tr>';
+ $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
if(view==='table'){
$('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
- $('#tbody').innerHTML=rows.map(r=>`<tr class="brow" data-id="${r.id}" data-name="${esc(r.name)}" style="cursor:pointer">`+COLS.map(c=>{const cls=(c.t==='n')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
+ $('#tbody').innerHTML=rows.map(r=>`<tr class="brow" data-id="${r.id}" data-name="${esc(r.name)}" style="cursor:pointer">`+cols.map(c=>{const cls=(c.t==='n')?' class="n"':'';return `<td${cls}>${cell(r,c)}</td>`;}).join('')+'</tr>').join('');
} else {
$('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
+ const skipGrid=new Set(['name','firm','agent_type','phone','email','website','linkedin','title']);
+ const bodyCols=cols.filter(c=>!skipGrid.has(c.k));
$('#gridView').innerHTML=rows.map(r=>`<div class="gc brow" data-id="${r.id}" data-name="${esc(r.name)}" style="cursor:pointer">
<div class="nm">${esc(r.name)} ${r.agent_type==='residential'?'<span class="atype res">Res</span>':'<span class="atype com">Com</span>'}${r.arcstone_flag?'<span class="atype arc" title="Prior Arcstone relationship">★ Arcstone</span>':''}</div>
- <div class="fm">${esc(r.firm||'—')}${r.title?' · '+esc(r.title):''}</div>
- <div class="row"><span class="k">Listings</span><span>${num(r.listings)}</span></div>
- <div class="row"><span class="k">Total assets</span><span>${num(r.total_assets)}</span></div>
- ${r.license?`<div class="row"><span class="k">DRE/MLS</span><span>${esc(r.license)}</span></div>`:''}
- ${r.specialties?`<div class="row"><span class="k">Specialties</span><span class="mut">${esc(r.specialties)}</span></div>`:''}
- <div class="ci">${tel(r.phone)} ${mail(r.email)} ${url(r.website,'site↗')} ${r.linkedin?url(r.linkedin,'in↗'):''}</div>
+ <div class="fm">${esc(r.firm||'—')}${r.title?' · '+esc(r.title):''}</div>`
+ +bodyCols.map(c=>`<div class="row"><span class="k">${esc(c.l)}</span><span>${cell(r,c)}</span></div>`).join('')
+ +`<div class="ci">${tel(r.phone)} ${mail(r.email)} ${url(r.website,'site↗')} ${r.linkedin?url(r.linkedin,'in↗'):''}</div>
</div>`).join('');
}
}
@@ -168,13 +223,20 @@ document.addEventListener('click',e=>{ const row=e.target.closest('.brow'); if(r
$('#thead').addEventListener('click',e=>{const th=e.target.closest('th');if(!th)return;const k=th.dataset.k;
if(sortKey===k)sortDir*=-1;else{sortKey=k;sortDir=(k==='listings'||k==='total_assets'||k==='created_at')?-1:1;}render();});
$('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
-function exportCSV(){ const rows=filtered(); const cv=(r,c)=>{let v=r[c.k]; v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
- const csv=COLS.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>COLS.map(c=>cv(r,c)).join(',')).join('\n');
+function exportCSV(){ const rows=filtered(); const cols=visCols(); const cv=(r,c)=>{let v=r[c.k]; v=(v==null?'':String(v)); return '"'+v.replace(/"/g,'""')+'"';};
+ const csv=cols.map(c=>'"'+c.l+'"').join(',')+'\n'+rows.map(r=>cols.map(c=>cv(r,c)).join(',')).join('\n');
const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='cre-brokers-'+rows.length+'.csv'; a.click(); URL.revokeObjectURL(a.href); }
$('#csv').addEventListener('click',exportCSV);
$('#view').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;
document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b));view=b.dataset.v;render();});
-fetch('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];render();})
+// left-rail facets + column toggles
+$('#fType').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.types,c.dataset.ty);c.classList.toggle('active');render();});
+$('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');render();});
+$('#fArc').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.arc=!F.arc;c.classList.toggle('active',F.arc);render();});
+$('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-ck]');if(!cb)return;VISCOL[cb.dataset.ck]=cb.checked;saveCols();render();});
+$('#fAll').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=true);saveCols();buildFieldToggles();render();});
+$('#fNone').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=false);saveCols();buildFieldToggles();render();});
+fetch('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];autoCols();buildRail();render();})
.catch(()=>{$('#count').textContent='failed to load /api/brokers/all';});
</script>
</body>
diff --git a/public/index.html b/public/index.html
index a829ffa..3baa82d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -200,7 +200,8 @@
<aside id="drawer">
<div class="dhead"><b>Filters</b><button class="x" id="dclose" aria-label="Close">✕</button></div>
- <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div></div>
+ <div class="fsec"><h4>Asset type</h4><div class="chips" id="fType"></div>
+ <a href="/condos" style="display:inline-flex;align-items:center;gap:5px;margin-top:9px;background:rgba(210,153,34,.10);border:1px solid rgba(210,153,34,.4);color:var(--warn);border-radius:18px;padding:5px 11px;font-size:12px;text-decoration:none;font-weight:600">🏢 Condos — FHA warrantability →</a></div>
<div class="fsec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
<div class="fsec"><h4>Submarket / city</h4>
<input type="text" id="fCity" placeholder="search city or street… e.g. Encino, Ventura Blvd">
← d74ce02 auto-save: 2026-07-02T09:46:55 (1 files) — public/mls.html
·
back to Commercialrealestate
·
feat(condos): add Unwarrantable (non-FHA-approved) filter — a2c781e →