← back to Commercialrealestate
CRCP broker grid: organize all fields into categories (Identity/Geography/Activity/Contact/Details) + grouped sort menu with optgroups; surface State + CA-Confidence as badged sortable columns; every field sortable
068be0a617f5a9434838491a06a59dfbd5b82a78 · 2026-07-12 07:31:23 -0700 · Steve Abrams
Files touched
M package-lock.jsonM package.jsonM public/broker-grid.html
Diff
commit 068be0a617f5a9434838491a06a59dfbd5b82a78
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 12 07:31:23 2026 -0700
CRCP broker grid: organize all fields into categories (Identity/Geography/Activity/Contact/Details) + grouped sort menu with optgroups; surface State + CA-Confidence as badged sortable columns; every field sortable
---
package-lock.json | 4 +--
package.json | 2 +-
public/broker-grid.html | 79 +++++++++++++++++++++++++++++++++++++------------
3 files changed, 63 insertions(+), 22 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b4e2882..efd2dc0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "commercialrealestate",
- "version": "0.7.1",
+ "version": "0.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "commercialrealestate",
- "version": "0.7.1",
+ "version": "0.8.0",
"dependencies": {
"better-sqlite3": "^12.11.1",
"express": "^4.22.2",
diff --git a/package.json b/package.json
index 6e8fd49..858c974 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "commercialrealestate",
- "version": "0.7.1",
+ "version": "0.8.0",
"private": true,
"description": "LA County CRE investment explorer — multi-firm sourced, Census + assessor enriched, Qwen-analyzed",
"scripts": {
diff --git a/public/broker-grid.html b/public/broker-grid.html
index b4c9413..72c963b 100644
--- a/public/broker-grid.html
+++ b/public/broker-grid.html
@@ -32,6 +32,10 @@
.atype.res{background:rgba(88,166,255,.16);color:var(--blue);} .atype.com{background:rgba(200,162,75,.16);color:var(--gold);}
.atype.arc{background:rgba(127,174,106,.2);color:#8fd06f;border:1px solid rgba(127,174,106,.55);margin-left:5px;}
.mut{color:var(--mut);} .miss{color:#5A6472;}
+ .geo{display:inline-block;font-size:10px;font-weight:700;padding:1px 7px;border-radius:8px;white-space:nowrap;}
+ .geo.ca{background:rgba(127,174,106,.18);color:#8fd06f;border:1px solid rgba(127,174,106,.5);}
+ .geo.non{background:rgba(248,81,73,.16);color:#f77;border:1px solid rgba(248,81,73,.5);}
+ .geo.unk{background:rgba(138,147,162,.14);color:var(--mut);border:1px solid var(--line);}
#ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:60;align-items:flex-start;justify-content:center;padding:40px 14px;overflow-y:auto;}
#ov.on{display:flex;}
#modal{width:min(700px,96vw);background:var(--card);border:1px solid var(--line);border-radius:15px;padding:20px 22px;position:relative;}
@@ -69,6 +73,7 @@
.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);}
+ .fgrp{font-size:9.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);margin:9px 0 3px;font-weight:700;opacity:.85;} .fgrp:first-child{margin-top:0;}
.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;} }
@@ -115,22 +120,32 @@ const tel=v=>v?`<a href="tel:${esc(v)}">${esc(v)}</a>`:'<span class="miss">—</
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 '—'; } };
+// Every field carries a group `g` (for the sort menu optgroups + organized rail toggles).
+// Order below IS the column order. Every field is sortable (raw-value sort, any type).
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'},
+ // ── Identity ──
+ {k:'name',l:'Name',t:'s',g:'Identity'},
+ {k:'firm',l:'Company / Firm',t:'s',g:'Identity'},
+ {k:'agent_type',l:'Type',t:'atype',g:'Identity'},
+ {k:'title',l:'Title',t:'s',g:'Identity'},
+ // ── Geography ── (added by the CA-DRE cross-ref work)
+ {k:'state',l:'State',t:'geo',g:'Geography'},
+ {k:'dre_match',l:'CA Confidence',t:'geo',g:'Geography'},
+ {k:'license',l:'DRE / MLS #',t:'s',g:'Geography'},
+ {k:'dre_license',l:'DRE License #',t:'s',g:'Geography',def:0},
+ // ── Activity ──
+ {k:'listings',l:'Listings',t:'n',g:'Activity'},
+ {k:'total_assets',l:'Total Assets',t:'n',g:'Activity'},
+ // ── Contact ──
+ {k:'phone',l:'Phone',t:'tel',g:'Contact'},
+ {k:'email',l:'Email',t:'mail',g:'Contact'},
+ {k:'website',l:'Website',t:'web',g:'Contact'},
+ {k:'linkedin',l:'LinkedIn',t:'li',g:'Contact'},
+ // ── Details ──
+ {k:'specialties',l:'Specialties',t:'s',g:'Details'},
+ {k:'office_addr',l:'Office',t:'s',g:'Details'},
+ {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='';
const F={types:new Set(),firms:new Set(),arc:false};
@@ -146,7 +161,7 @@ function autoCols(){
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});
+ COLS.push({k, l:k.replace(/_/g,' ').replace(/\b\w/g,ch=>ch.toUpperCase()).slice(0,20), t:isNum?'n':'s', g:'Other', def:0});
}));
}
function buildRail(){
@@ -157,7 +172,13 @@ function buildRail(){
$('#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 buildFieldToggles(){
+ const groups={};
+ COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+ $('#fFields').innerHTML=Object.entries(groups).map(([g,cs])=>
+ `<div class="fgrp">${esc(g)}</div>`+cs.map(c=>`<label class="ftog"><input type="checkbox" data-ck="${c.k}" ${colVis(c.k)?'checked':''}><span>${esc(c.l)}</span></label>`).join('')
+ ).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;
@@ -174,6 +195,17 @@ function cell(r,c){
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>';
+ if(c.t==='geo'){
+ if(c.k==='state'){
+ if(v==='CA') return '<span class="geo ca">CA ✓</span>';
+ if(v==null||v==='') return '<span class="geo unk">unknown</span>';
+ return '<span class="geo non">'+esc(v)+'</span>';
+ }
+ // dre_match / CA-confidence provenance
+ if(v==null||v==='') return '<span class="miss">—</span>';
+ const map={verified:'ca',probable:'ca','probable-fuzzy':'ca','ca-by-city':'ca','ca-by-listing-activity':'ca','address-derived':'ca',unmatched:'unk',error:'non'};
+ return '<span class="geo '+(map[v]||'unk')+'">'+esc(v)+'</span>';
+ }
return esc(v==null||v===''?'—':v);
}
function filtered(){
@@ -250,7 +282,16 @@ $('#view').addEventListener('click',e=>{const b=e.target.closest('button');if(!b
// ── sort <select> + density slider (Steve rule: every grid gets sort + density) ──
(function(){
const sel=$('#sortsel'),dz=$('#density');
- sel.innerHTML=COLS.filter(c=>c.t==='s'||c.t==='n'||c.t==='date'||c.t==='atype').map(c=>`<option value="${c.k}">Sort: ${esc(c.l)}</option>`).join('');
+ // Grouped sort menu — EVERY field is sortable, organized under its category optgroup.
+ // Rebuildable so auto-discovered columns join the menu once data loads.
+ window.__buildSortSel=()=>{
+ const groups={};
+ COLS.forEach(c=>{ (groups[c.g||'Other']=groups[c.g||'Other']||[]).push(c); });
+ sel.innerHTML=Object.entries(groups).map(([g,cs])=>
+ `<optgroup label="${esc(g)}">`+cs.map(c=>`<option value="${c.k}">${esc(c.l)}</option>`).join('')+`</optgroup>`).join('');
+ if(Array.from(sel.options).some(o=>o.value===sortKey)) sel.value=sortKey;
+ };
+ window.__buildSortSel();
const sk=localStorage.getItem('brokSortKey'),sd=localStorage.getItem('brokSortDir');
if(sk&&COLS.some(c=>c.k===sk))sortKey=sk; if(sd==='1'||sd==='-1')sortDir=+sd;
if(!Array.from(sel.options).some(o=>o.value===sortKey))sortKey='listings';
@@ -278,7 +319,7 @@ $('#fNone').addEventListener('click',()=>{COLS.forEach(c=>VISCOL[c.k]=false);sav
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'); });
document.querySelectorAll('.rail .rsec').forEach(s=>s.classList.add('collapsed')); }
-fetch('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];autoCols();buildRail();render();})
+fetch('/api/brokers/all').then(r=>r.json()).then(d=>{DATA=d.brokers||[];autoCols();if(window.__buildSortSel)window.__buildSortSel();buildRail();render();})
.catch(()=>{$('#count').textContent='failed to load /api/brokers/all';});
</script>
</body>
← 9939cbd CRCP: default broker view lists ALL brokers again (Steve: ok
·
back to Commercialrealestate
·
CRCP: multi-metro gov-license ingest — gov_licensed_agent ta cdf2871 →