[object Object]

← 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

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 →