← back to Commercialrealestate
Broker grid+list view — all 2,537 brokers, every field sortable & searchable
ddcd86b563c90cb964d1a34159e675dedf6d4b76 · 2026-06-30 16:45:36 -0700 · Steve Abrams
public/broker-grid.html: List (spreadsheet, 15 cols: name/firm/type/listings/total-assets/phone/email/
website/linkedin/title/DRE-MLS/specialties/office/source/added) + Grid toggle. Every column click-sorts
(numeric-aware), global search across all fields. Backed by new GET /api/brokers/all (all fields + listings
count). Linked from CRCP nav + front-page header. Verified headless: sort/search/toggle, 0 errors.
Files touched
A public/broker-grid.htmlM public/crcp.htmlM public/index.html
Diff
commit ddcd86b563c90cb964d1a34159e675dedf6d4b76
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jun 30 16:45:36 2026 -0700
Broker grid+list view — all 2,537 brokers, every field sortable & searchable
public/broker-grid.html: List (spreadsheet, 15 cols: name/firm/type/listings/total-assets/phone/email/
website/linkedin/title/DRE-MLS/specialties/office/source/added) + Grid toggle. Every column click-sorts
(numeric-aware), global search across all fields. Backed by new GET /api/brokers/all (all fields + listings
count). Linked from CRCP nav + front-page header. Verified headless: sort/search/toggle, 0 errors.
---
public/broker-grid.html | 132 ++++++++++++++++++++++++++++++++++++++++++++++++
public/crcp.html | 2 +-
public/index.html | 1 +
3 files changed, 134 insertions(+), 1 deletion(-)
diff --git a/public/broker-grid.html b/public/broker-grid.html
new file mode 100644
index 0000000..64e0c78
--- /dev/null
+++ b/public/broker-grid.html
@@ -0,0 +1,132 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%91%A5%3C/text%3E%3C/svg%3E">
+<title>Brokers & Agents — Grid / List</title>
+<style>
+ :root{ --bg:#0c0f15; --card:#141925; --line:#232a38; --ink:#E6E8EC; --mut:#8A93A2; --gold:#C8A24B; --acc:#70AD47; --blue:#58a6ff; --warn:#d29922; }
+ *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
+ header{display:flex;align-items:center;gap:12px;padding:11px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:40;background:rgba(12,15,21,.98);backdrop-filter:blur(8px);flex-wrap:wrap;}
+ header h1{font-size:16px;margin:0;} header h1 b{color:var(--gold);}
+ header .nav{display:flex;gap:12px;margin-left:6px;} header .nav a{color:var(--blue);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--gold);}
+ .spacer{flex:1;}
+ .search{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:14px;min-width:220px;}
+ .toggle{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
+ .toggle button{background:var(--card);border:0;color:var(--mut);padding:8px 13px;font-size:13px;cursor:pointer;}
+ .toggle button.on{background:var(--gold);color:#1a1205;font-weight:700;}
+ .count{color:var(--mut);font-size:12px;}
+ .wrap{padding:14px 18px 60px;}
+ .tblwrap{overflow:auto;max-height:calc(100vh - 130px);border:1px solid var(--line);border-radius:10px;}
+ table.g{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px;white-space:nowrap;}
+ 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);}
+ 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;}
+ table.g a{color:var(--blue);text-decoration:none;} table.g a:hover{text-decoration:underline;}
+ .atype{display:inline-block;font-size:10px;font-weight:700;text-transform:uppercase;padding:1px 6px;border-radius:8px;}
+ .atype.res{background:rgba(88,166,255,.16);color:var(--blue);} .atype.com{background:rgba(200,162,75,.16);color:var(--gold);}
+ .mut{color:var(--mut);} .miss{color:#5A6472;}
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;}
+ .gc{background:var(--card);border:1px solid var(--line);border-radius:11px;padding:12px 14px;}
+ .gc .nm{font-weight:700;} .gc .fm{color:var(--mut);font-size:12px;margin-bottom:6px;}
+ .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;}
+</style>
+</head>
+<body>
+<header>
+ <h1><b>👥 Brokers</b> & Agents</h1>
+ <div class="nav"><a href="/crcp.html">← CRCP</a><a href="/">Explorer</a><a href="/mls.html">📋 MLS</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…">
+ <div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
+ <span class="count" id="count">…</span>
+</header>
+<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>
+<script>
+const $=s=>document.querySelector(s);
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&').replace(/</g,'<').replace(/"/g,'"');
+const num=n=>(n==null||n==='')?'—':Number(n).toLocaleString();
+const tel=v=>v?`<a href="tel:${esc(v)}">${esc(v)}</a>`:'<span class="miss">—</span>';
+const mail=v=>v?`<a href="mailto:${esc(v)}">${esc(v)}</a>`:'<span class="miss">—</span>';
+const url=(v,lbl)=>{ if(!v) return '<span class="miss">—</span>'; const h=/^https?:/.test(v)?v:'https://'+v; return `<a href="${esc(h)}" target="_blank" rel="noopener noreferrer">${lbl}</a>`; };
+const dt=s=>{ if(!s) return '—'; try{ return new Date(s).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}); }catch(e){ return '—'; } };
+const COLS=[
+ {k:'name',l:'Name',t:'s'},
+ {k:'firm',l:'Company / Firm',t:'s'},
+ {k:'agent_type',l:'Type',t:'atype'},
+ {k:'listings',l:'Listings',t:'n'},
+ {k:'total_assets',l:'Total Assets',t:'n'},
+ {k:'phone',l:'Phone',t:'tel'},
+ {k:'email',l:'Email',t:'mail'},
+ {k:'website',l:'Website',t:'web'},
+ {k:'linkedin',l:'LinkedIn',t:'li'},
+ {k:'title',l:'Title',t:'s'},
+ {k:'license',l:'DRE/MLS',t:'s'},
+ {k:'specialties',l:'Specialties',t:'s'},
+ {k:'office_addr',l:'Office',t:'s'},
+ {k:'source',l:'Source',t:'s'},
+ {k:'created_at',l:'Added',t:'date'},
+];
+let DATA=[], view='table', sortKey='listings', sortDir=-1, q='';
+function cell(r,c){
+ const v=r[c.k];
+ if(c.t==='n') return num(v);
+ if(c.t==='tel') return tel(v);
+ if(c.t==='mail') return mail(v);
+ if(c.t==='web') return url(v,'site↗');
+ if(c.t==='li') return url(v,'in↗');
+ if(c.t==='date') return dt(v);
+ if(c.t==='atype') return v==='residential'?'<span class="atype res">Residential</span>':'<span class="atype com">Commercial</span>';
+ return esc(v==null||v===''?'—':v);
+}
+function filtered(){
+ let rows=DATA;
+ 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)=>{
+ let x=a[c.k], y=b[c.k];
+ const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+ if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+ if(x<y) return -sortDir; if(x>y) return sortDir; return 0;
+ });
+ return rows;
+}
+function render(){
+ const rows=filtered();
+ $('#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>';
+ if(view==='table'){
+ $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
+ $('#tbody').innerHTML=rows.map(r=>'<tr>'+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');
+ $('#gridView').innerHTML=rows.map(r=>`<div class="gc">
+ <div class="nm">${esc(r.name)} ${r.agent_type==='residential'?'<span class="atype res">Res</span>':'<span class="atype com">Com</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)} ${r.website?url(r.website,'site↗'):''} ${r.linkedin?url(r.linkedin,'in↗'):''}</div>
+ </div>`).join('');
+ }
+}
+$('#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();});
+$('#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();})
+ .catch(()=>{$('#count').textContent='failed to load /api/brokers/all';});
+</script>
+</body>
+</html>
diff --git a/public/crcp.html b/public/crcp.html
index 0559a74..46b80ca 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -82,7 +82,7 @@
<h1><b>CRCP</b> · Commercial Real Estate Control Panel</h1>
<span class="live"><span class="pulse"></span><span id="livetxt">live</span></span>
<nav class="nav">
- <a href="/">Properties</a><a href="/map.html">Map</a><a href="/deals.html">Deals</a><a href="/rent-rolls.html">Rent Rolls</a><a href="/brokers.html">Mind-map</a><a href="/graphics.html">Graphics</a><a href="/condos.html">Condos</a><a href="/fha-loans.html">FHA Loans</a>
+ <a href="/">Properties</a><a href="/mls.html">📋 MLS</a><a href="/broker-grid.html">👥 Brokers grid</a><a href="/map.html">Map</a><a href="/deals.html">Deals</a><a href="/rent-rolls.html">Rent Rolls</a><a href="/brokers.html">Mind-map</a><a href="/graphics.html">Graphics</a><a href="/condos.html">Condos</a><a href="/fha-loans.html">FHA Loans</a>
<span class="denslider" title="chip / tile density">⊞ <input type="range" id="dens" min="120" max="300" value="150" aria-label="Tile density"></span>
<button class="expandall" id="expandall">Expand all ▾</button>
</nav>
diff --git a/public/index.html b/public/index.html
index 172343d..4128c10 100644
--- a/public/index.html
+++ b/public/index.html
@@ -134,6 +134,7 @@
<a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="MLS-style spreadsheet + grid — all property data, sortable & searchable">📋 MLS</a>
<a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
<a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
+ <a href="/broker-grid.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All brokers/agents — grid + list, sortable by every field">👥 Broker grid</a>
<a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
<a href="/graphics.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / firm / warrantability charts">📊 Graphics</a>
<a href="/condos.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="LA condos — FHA warrantability">🏢 Condos</a>
← 4191307 FHA loans: add /fha-loans route + /api/fha-loans JSON endpoi
·
back to Commercialrealestate
·
Make the grids actionable: MLS row → 100-yr property history 5a26b03 →