[object Object]

← back to Commercialrealestate

broker-grid: tight-default columns, drag-to-reorder, search all fields

982582ec5c21cf55793f34f62e54bde485bc7ff5 · 2026-07-31 08:40:20 -0700 · Steve Abrams

- START TIGHT: default to essentials (name/firm/type/title/state/listings); toggle rest from rail
- move columns around: HTML5 drag-to-reorder headers, order persisted (brokerColOrder)
- search ALL data: filter scans every field the API returns (incl. crexi_id + nested), space=AND
- data-no-sort on table so col-resize.js stops double-firing sort; page owns sort
- Reset button clears order+widths+visibility back to tight default

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files touched

Diff

commit 982582ec5c21cf55793f34f62e54bde485bc7ff5
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 31 08:40:20 2026 -0700

    broker-grid: tight-default columns, drag-to-reorder, search all fields
    
    - START TIGHT: default to essentials (name/firm/type/title/state/listings); toggle rest from rail
    - move columns around: HTML5 drag-to-reorder headers, order persisted (brokerColOrder)
    - search ALL data: filter scans every field the API returns (incl. crexi_id + nested), space=AND
    - data-no-sort on table so col-resize.js stops double-firing sort; page owns sort
    - Reset button clears order+widths+visibility back to tight default
    
    Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
 public/broker-grid.html | 78 ++++++++++++++++++++++++++++++++++++++++++++-----
 1 file changed, 70 insertions(+), 8 deletions(-)

diff --git a/public/broker-grid.html b/public/broker-grid.html
index cb093eb..335f3c3 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -24,6 +24,11 @@
   table.g thead th{position:sticky;top:0;background:#1b2230;color:var(--mut);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:8px 10px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;z-index:2;}
   table.g thead th:hover{color:var(--ink);}
   table.g thead th.asc::after{content:' ▲';color:var(--gold);} table.g thead th.desc::after{content:' ▼';color:var(--gold);}
+  /* draggable column reorder (Steve rule 2026-07-31: move columns around) */
+  table.g thead th.dragcol{cursor:grab;}
+  table.g thead th.dragging{opacity:.45;}
+  table.g thead th.dropL{box-shadow:inset 3px 0 0 var(--gold);}
+  table.g thead th.dropR{box-shadow:inset -3px 0 0 var(--gold);}
   table.g tbody td{padding:7px 10px;border-bottom:1px solid #20262f;}
   table.g tbody tr:hover td{background:rgba(200,162,75,.07);}
   table.g td.n{text-align:right;font-variant-numeric:tabular-nums;}
@@ -89,7 +94,7 @@
   <h1><b>👥 Brokers</b> &amp; Agents</h1>
   <div class="nav"><a href="/crcp.html">← CRCP</a><a href="/">Explorer</a><a href="/mls.html">📋 Listings</a><a href="/gov-agents.html">🏛️ Gov-Licensed</a><a href="/linkedin.html">🔗 LinkedIn</a></div>
   <span class="spacer"></span>
-  <input class="search" id="q" type="text" placeholder="search name, firm, phone, email, specialty…">
+  <input class="search" id="q" type="text" placeholder="search all fields — name, firm, phone, email, license, source… (space = AND)">
   <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
   <select id="sortsel" title="Sort brokers"></select>
   <label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
@@ -102,12 +107,13 @@
   <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 style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset columns to the tight default — order, widths &amp; visibility">↺ Reset</button></div>
+    <div class="mut" style="font-size:10.5px;margin-top:7px;line-height:1.35">Drag a column header to reorder · drag its right edge to resize · click to sort.</div>
   </div>
 </aside>
 <div class="wrap">
   <div class="statsbar" id="statsbar"></div>
-  <div id="tableView" class="tblwrap"><table class="g"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+  <div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
   <div id="gridView" class="grid hide"></div>
 </div>
 </div>
@@ -175,12 +181,33 @@ const COLS=[
   {k:'source',l:'Source',t:'s',g:'Details'},
   {k:'created_at',l:'Added',t:'date',g:'Details'},
 ];
-let DATA=[], view='table', sortKey='listings', sortDir=-1, q='';
+let DATA=[], view='table', sortKey='listings', sortDir=-1, q='', lastColSig='';
 const F={types:new Set(),firms:new Set()};
+// START TIGHT (Steve rule 2026-07-31): only the essentials show on first load; the
+// user toggles the rest on from the rail. A saved pref in VISCOL always wins.
+const DEFAULT_ON=new Set(['name','firm','agent_type','title','state','listings']);
 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 colVis(k){ if(k in VISCOL) return !!VISCOL[k]; return DEFAULT_ON.has(k); }
+// Draggable column ORDER (Steve rule: move columns around). Keys in display order;
+// auto-discovered/new keys are appended, stale keys pruned. Persisted per browser.
+let COLORDER=(function(){let a=[];try{a=JSON.parse(localStorage.getItem('brokerColOrder')||'[]');}catch(e){}return Array.isArray(a)?a:[];})();
+function syncColOrder(){
+  const keys=COLS.map(c=>c.k);
+  COLORDER=COLORDER.filter(k=>keys.includes(k));                    // prune removed
+  keys.forEach(k=>{ if(!COLORDER.includes(k)) COLORDER.push(k); }); // append new
+}
+function orderedCols(){ syncColOrder(); return COLORDER.map(k=>COLS.find(c=>c.k===k)).filter(Boolean); }
+function visCols(){ return orderedCols().filter(c=>colVis(c.k)); }
 function saveCols(){ try{localStorage.setItem('brokerCols',JSON.stringify(VISCOL));}catch(e){} }
+function saveColOrder(){ try{localStorage.setItem('brokerColOrder',JSON.stringify(COLORDER));}catch(e){} }
+// When the visible column SET or ORDER changes, hand the table back to col-resize.js
+// so it rebuilds its <colgroup> + drag handles for the current columns.
+function reinitResize(){
+  const tbl=document.querySelector('#tableView table.g'); if(!tbl) return;
+  const cg=tbl.querySelector('colgroup[data-cr]'); if(cg) cg.remove();
+  delete tbl.dataset.crDone; tbl.style.tableLayout=''; tbl.style.width='';
+  if(window.ColResize && window.ColResize.refresh) window.ColResize.refresh();
+}
 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']);
@@ -237,7 +264,15 @@ function cell(r,c){
 }
 function filtered(){
   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);})); }
+  if(q){
+    // Search ALL data (Steve rule 2026-07-31): every field the API returned on a row —
+    // not just the defined/visible columns — incl. nested objects. Space = AND.
+    const terms=q.toLowerCase().split(/\s+/).filter(Boolean);
+    rows=rows.filter(r=>{
+      const hay=Object.keys(r).filter(k=>k!=='__hay').map(k=>{const v=r[k];return v==null?'':(typeof v==='object'?JSON.stringify(v):String(v));}).join('  ').toLowerCase();
+      return terms.every(t=>hay.includes(t));
+    });
+  }
   const c=COLS.find(x=>x.k===sortKey)||COLS[0];
   rows=rows.slice().sort((a,b)=>{
     let x=a[c.k], y=b[c.k];
@@ -250,10 +285,12 @@ function filtered(){
 function render(){
   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'):''}">${esc(c.l)}</th>`).join('')+'</tr>';
+  $('#thead').innerHTML='<tr>'+cols.map(c=>`<th data-k="${c.k}" draggable="true" title="Drag to reorder · click to sort" class="dragcol${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('');
+    const csig=cols.map(c=>c.k).join(',');
+    if(csig!==lastColSig){ lastColSig=csig; reinitResize(); }
   } else {
     $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
     const skipGrid=new Set(['name','firm','agent_type','phone','email','website','linkedin','title']);
@@ -290,6 +327,27 @@ document.addEventListener('click',e=>{ const fm=e.target.closest('.findmail'); i
 $('#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;}
   localStorage.setItem('brokSortKey',sortKey);localStorage.setItem('brokSortDir',sortDir);if(window.__syncSortSel)window.__syncSortSel();render();});
+// ── drag-to-reorder columns (Steve rule 2026-07-31: move columns around) ──
+let dragKey=null;
+$('#thead').addEventListener('dragstart',e=>{ const th=e.target.closest('th'); if(!th) return;
+  if(e.target.closest('.cr-handle')){ e.preventDefault(); return; }        // that grab is a resize, not a move
+  dragKey=th.dataset.k; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',dragKey);}catch(_){}
+  th.classList.add('dragging'); });
+$('#thead').addEventListener('dragover',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th||th.dataset.k===dragKey) return;
+  e.preventDefault(); e.dataTransfer.dropEffect='move';
+  const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+  th.classList.toggle('dropR',after); th.classList.toggle('dropL',!after); });
+$('#thead').addEventListener('dragleave',e=>{ const th=e.target.closest('th'); if(th) th.classList.remove('dropL','dropR'); });
+$('#thead').addEventListener('drop',e=>{ if(!dragKey) return; const th=e.target.closest('th'); if(!th) return; e.preventDefault();
+  const tgt=th.dataset.k; if(tgt!==dragKey){
+    const r=th.getBoundingClientRect(), after=(e.clientX-r.left)>r.width/2;
+    syncColOrder(); const from=COLORDER.indexOf(dragKey); if(from>=0){
+      COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,dragKey);
+      saveColOrder(); lastColSig=''; render();
+    }
+  }
+  dragKey=null; });
+$('#thead').addEventListener('dragend',()=>{ document.querySelectorAll('#thead th.dragging,#thead th.dropL,#thead th.dropR').forEach(x=>x.classList.remove('dragging','dropL','dropR')); dragKey=null; });
 $('#q').addEventListener('input',()=>{q=$('#q').value.trim();render();});
 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');
@@ -332,6 +390,10 @@ $('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c
 $('#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();});
+// Reset the whole table layout back to the tight default: visibility, order, widths.
+$('#fReset').addEventListener('click',()=>{ VISCOL={}; saveCols(); COLORDER=[]; saveColOrder();
+  try{Object.keys(localStorage).forEach(k=>{ if(k.indexOf('cr:'+location.pathname+':')===0) localStorage.removeItem(k); });}catch(e){}
+  lastColSig=''; buildFieldToggles(); render(); });
 // collapsible rail tabs — all collapsed on load
 const railEl=document.querySelector('.rail');
 if(railEl){ railEl.addEventListener('click',e=>{ const h=e.target.closest('h4'); if(h && h.parentElement.classList.contains('rsec')) h.parentElement.classList.toggle('collapsed'); });

← 73d873b map: CRCP data by B2B-legality — assessor/FHA/DRE/Census + a  ·  back to Commercialrealestate  ·  B2B: assumable-loan lead viewer (/fha-leads.html, 100% HUD p ea76efd →