[object Object]

← 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

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 →