[object Object]

← back to Commercialrealestate

feat(index): click-to-sort on List column headers (Option C) — sort by ANY visible data point (price, $/SF, beds, sqft, HOA, year, assessed-derived, etc.) asc/desc with ▲▼ indicators; generic fieldVal resolver; sort dropdown clears column-sort. Contrarian-gated (grid clicks unaffected; colSort scoped to .listtbl th).

12bcd3056219122940e33d8b3e704d0944775690 · 2026-07-02 10:48:43 -0700 · Steve Abrams

Officer: vp-engineering
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 12bcd3056219122940e33d8b3e704d0944775690
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 2 10:48:43 2026 -0700

    feat(index): click-to-sort on List column headers (Option C) — sort by ANY visible data point (price, $/SF, beds, sqft, HOA, year, assessed-derived, etc.) asc/desc with ▲▼ indicators; generic fieldVal resolver; sort dropdown clears column-sort. Contrarian-gated (grid clicks unaffected; colSort scoped to .listtbl th).
    
    Officer: vp-engineering
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 public/index.html | 37 ++++++++++++++++++++++++++++++++-----
 1 file changed, 32 insertions(+), 5 deletions(-)

diff --git a/public/index.html b/public/index.html
index 2ac5835..e9108a5 100644
--- a/public/index.html
+++ b/public/index.html
@@ -152,6 +152,7 @@
   .listtbl th { position:sticky; top:0; background:#0b0e13; color:var(--mut); font-size:10px; text-transform:uppercase; letter-spacing:.5px; border-top:0; z-index:1; }
   .listtbl td.stick, .listtbl th.stick { text-align:left; position:sticky; left:0; background:var(--card); }
   .listtbl th.stick { background:#0b0e13; z-index:2; }
+  .listtbl th.sortable { cursor:pointer; } .listtbl th.sortable:hover { color:var(--ink); }
   .listtbl tr.lrow:hover td { background:rgba(88,166,255,.06); }
   .listtbl tr.lrow:hover td.stick { background:#1b222c; }
   .listtbl .la { font-weight:600; color:var(--ink); text-decoration:none; }
@@ -627,12 +628,38 @@ function colCell(k,p){ const f=p.finance||{};
 }
 function renderList(rows){
   const cols=listCols();
-  const head='<th class="stick">Property</th>'+cols.map(c=>`<th>${esc(c.label)}</th>`).join('');
+  const arrow=k=>colSort&&colSort.key===k?(colSort.dir>0?' ▲':' ▼'):'';
+  const head='<th class="stick sortable" data-sortk="addr">Property'+arrow('addr')+'</th>'+cols.map(c=>`<th class="sortable" data-sortk="${c.k}">${esc(c.label)}${arrow(c.k)}</th>`).join('');
   const body=rows.map(p=>`<tr class="lrow" data-id="${p.id}">`
     +`<td class="stick"><a class="la" href="${p.source}" target="_blank" rel="noopener noreferrer">${esc(p.address)}</a><div class="lc">${esc(p.city)}, CA ${p.zip||''}</div></td>`
     +cols.map(c=>`<td>${colCell(c.k,p)}</td>`).join('')+`</tr>`).join('');
   return `<div class="listwrap"><table class="listtbl"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
 }
+// raw (unformatted) value for a field, for generic column sorting
+function fieldVal(p,k){ const f=p.finance||{};
+  switch(k){
+    case 'addr': return p.address; case 'score': return p.composite; case 'deal': return p.deal&&p.deal.score;
+    case 'firm': return firmLabel(p); case 'type': return p.type; case 'units': return p.units; case 'price': return p.price;
+    case 'ppu': return ppu(p); case 'pricePerSf': return ppsf(p); case 'cap': return p.cap_rate; case 'yield': return f.grossYield;
+    case 'grm': return grm(p); case 'yearBuilt': return p.year_built; case 'dom': return daysOnMarket(p); case 'zip': return p.zip;
+    case 'status': return shortStatus(p); case 'afford': return f.affordable?1:0; case 'rec': return p.qwen&&p.qwen.recommendation;
+    case 'coc': return f.coc; case 'dscr': return f.dscr; case 'noi': return f.noi; case 'debt': return f.annualDebtService;
+    case 'cashNeeded': return f.cashNeeded; case 'cashFlow': return f.cashFlow;
+    case 'rent': { const r=rentEstimate(p); return r?r.std:null; }
+    case 'beds': return p.beds; case 'baths': return p.baths; case 'sqft': return p.sqft; case 'hoa': return p.hoa;
+    case 'warr': return p.warrantable_status; case 'broker': return p.broker_name;
+    default: return null;
+  }
+}
+function sortRows(r){
+  if(colSort){ const k=colSort.key, d=colSort.dir;
+    return r.sort((a,b)=>{ let x=fieldVal(a,k), y=fieldVal(b,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 -d; if(x>y) return d; return 0; });
+  }
+  return r.sort(SORTS[$('#sort').value]||SORTS.composite);
+}
 
 // ---- Unified Deal Score (0-100) — DealScore.compute() over the signals we already have. ----
 // Gathers: pro-forma gross yield (Census rent estimate), cap rate, in-place DSCR (finance.js),
@@ -943,7 +970,7 @@ function render(){
     saveState(); return;
   }
   let r = applyFilters(DATA.ranked.slice());
-  r.sort(SORTS[$('#sort').value]||SORTS.composite);
+  sortRows(r);
   if(!r.length){ g.className='grid'; g.innerHTML=`<div class="empty">No properties match these filters.<br><span class="small">Loosen a filter or hit “Reset all”.</span></div>`; }
   else if(view==='list'){ g.className='listhost'; g.innerHTML=renderList(r); }
   else { g.className='grid'; g.innerHTML=r.map(card).join(''); enrichAssessor(); }
@@ -1170,8 +1197,8 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   $('#scenario').onclick=e=>{ if(!e.target.dataset.s) return; setScenario(e.target.dataset.s); render(); };
 
   // sort (drawer + top bar mirror each other)
-  $('#sort').onchange=()=>{ $('#sortTop').value=$('#sort').value; render(); };
-  $('#sortTop').onchange=()=>{ $('#sort').value=$('#sortTop').value; render(); };
+  $('#sort').onchange=()=>{ colSort=null; $('#sortTop').value=$('#sort').value; render(); };
+  $('#sortTop').onchange=()=>{ colSort=null; $('#sort').value=$('#sortTop').value; render(); };
   $('#sortTop').value=$('#sort').value;
   // density
   const dv=localStorage.getItem('cre_density')||'3'; $('#density').value=dv; $('#densVal').textContent=dv; document.documentElement.style.setProperty('--cols',dv);
@@ -1191,7 +1218,7 @@ fetch('data/ranked.json').then(r=>r.json()).then(async d=>{
   $('#examples').innerHTML = EX.map(x=>`<button class="ex">${x}</button>`).join('');
   $('#examples').onclick=e=>{ if(e.target.classList.contains('ex')){ $('#q').value=e.target.textContent; $('#q').focus(); } };
   $('#chatform').onsubmit=e=>{ e.preventDefault(); const t=$('#q').value.trim(); if(!t) return; $('#q').value=''; ask(t); };
-  $('#grid').onclick=e=>{ const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h) fetchHistory(h); };
+  $('#grid').onclick=e=>{ const th=e.target.closest('.listtbl th[data-sortk]'); if(th){ const k=th.dataset.sortk; if(colSort&&colSort.key===k) colSort.dir*=-1; else colSort={key:k,dir:['type','firm','warr','broker','status','zip','addr','rec'].includes(k)?1:-1}; render(); return; } const b=e.target.closest('.lcbtn'); if(b){ fetchLiveComps(b.dataset.id); return; } const h=e.target.closest('.histbtn'); if(h) fetchHistory(h); };
 
   window.DATA_READY = true;
   recompute(); syncChips(); render();

← d036a01 auto-save: 2026-07-02T10:47:21 (1 files) — public/index.html  ·  back to Commercialrealestate  ·  feat(mls+broker-grid): fleet consistency (DTD Option D) — co acf3768 →