[object Object]

← back to Commercialrealestate

graphics: point warrantability 'expiring' tiles at the new pre-filtered deep-links

4ea459daa9e2370bbccb77959408a3e1e4e6ccd1 · 2026-08-07 16:21:21 -0700 · Steve Abrams

The 'expiring within 12mo' card + stat now drill to condos.html?status=expiring365
(was ?status=fha_expired — the WRONG target: already-lapsed, not lapsing-soon),
and add &sort=expiry_soon so the landing is pre-filtered AND soonest-first. condos.html
boot now honors a ?sort= deep-link param (validated against the sort options).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 4ea459daa9e2370bbccb77959408a3e1e4e6ccd1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 16:21:21 2026 -0700

    graphics: point warrantability 'expiring' tiles at the new pre-filtered deep-links
    
    The 'expiring within 12mo' card + stat now drill to condos.html?status=expiring365
    (was ?status=fha_expired — the WRONG target: already-lapsed, not lapsing-soon),
    and add &sort=expiry_soon so the landing is pre-filtered AND soonest-first. condos.html
    boot now honors a ?sort= deep-link param (validated against the sort options).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/condos.html   | 54 +++++++++++++++++++++++++++++++++++++++++-----------
 public/graphics.html |  6 ++++--
 2 files changed, 47 insertions(+), 13 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index a3ea0f3..50d6eb7 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -1,7 +1,7 @@
 <!doctype html>
 <html lang="en">
 <head>
-<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
+<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='dark'?'dark':'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();</script>
 <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='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
@@ -46,6 +46,10 @@
   .rail input[type=number]{width:100%;}
   .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(88,166,255,.06);} .ftog input{accent-color:var(--blue);}
+  .ftog .flbl{flex:1;}
+  .ftog .fdrag{cursor:grab;color:var(--mut);font-size:12px;opacity:.5;user-select:none;line-height:1;} .ftog:hover .fdrag{opacity:1;}
+  .ftog.fdragging{opacity:.45;}
+  .ftog.fdropT{box-shadow:inset 0 3px 0 var(--gold);} .ftog.fdropB{box-shadow:inset 0 -3px 0 var(--gold);}
   .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(--blue);color:var(--ink);}
   main{flex:1 1 auto;min-width:0;padding:0 0 40px;}
   .topbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--line);position:sticky;top:53px;background:var(--headbg);backdrop-filter:blur(6px);z-index:20;}
@@ -141,12 +145,13 @@
   var root=document.documentElement;
   function apply(t){ root.setAttribute('data-theme',t);
     var ic=document.getElementById('themeIcon'), lb=document.getElementById('themeLbl');
-    if(ic) ic.textContent = t==='light'?'🌙':'☀️';
-    if(lb) lb.textContent = t==='light'?'Night':'Day';
+    // label shows the CURRENT mode (not the target) so it's never ambiguous
+    if(ic) ic.textContent = t==='light'?'☀️':'🌙';
+    if(lb) lb.textContent = t==='light'?'Day':'Night';
     try{ localStorage.setItem('crcp-theme',t); }catch(e){}
   }
   var cur; try{ cur=localStorage.getItem('crcp-theme'); }catch(e){}
-  apply(cur==='light'?'light':'dark');
+  apply(cur==='dark'?'dark':'light');
   var btn=document.getElementById('themeToggle');
   if(btn) btn.addEventListener('click',function(){ apply(root.getAttribute('data-theme')==='light'?'dark':'light'); });
 })();
@@ -155,6 +160,11 @@
 
 <div class="shell">
 <aside class="rail">
+  <div class="rsec" id="fieldsSec"><h4>Fields / columns — show / hide · reorder</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><button class="minibtn" id="fDef">Default</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 row by its ⠿ handle to reorder columns. In List view you can also drag a column header to reorder · its right edge to resize · click to sort.</div>
+  </div>
   <div class="rsec"><h4>Warrantability</h4><div class="chips" id="fWarr"></div></div>
   <div class="rsec" id="contactSec" style="display:none"><h4>Outreach</h4><div class="chips" id="fContact"></div></div>
   <div class="rsec"><h4>City</h4><div class="chips" id="fCity"></div></div>
@@ -166,11 +176,6 @@
   <div class="rsec"><h4>Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="sqMin" step="100" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="sqMax" step="100" placeholder="any"></div></div></div>
   <div class="rsec"><h4>$ / Sq Ft</h4><div class="row2"><div class="fld"><label>Min</label><input type="number" id="psfMin" step="25" placeholder="any"></div><div class="fld"><label>Max</label><input type="number" id="psfMax" step="25" placeholder="any"></div></div></div>
   <div class="rsec"><h4>Year built</h4><div class="row2"><div class="fld"><label>After</label><input type="number" id="yrMin" step="1" placeholder="any"></div><div class="fld"><label>Before</label><input type="number" id="yrMax" step="1" placeholder="any"></div></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><button class="minibtn" id="fDef">Default</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">In List view — drag a column header to reorder · drag its right edge to resize · click to sort.</div>
-  </div>
 </aside>
 <main>
   <div class="topbar">
@@ -409,7 +414,7 @@ function buildRail(){
   $('#fBaths').innerHTML=[0,1,2,3].map(n=>`<span class="chip ${F.bathsMin===n?'active':''}" data-baths="${n}">${n===0?'Any':n+'+'}</span>`).join('');
   buildFieldToggles();
 }
-function buildFieldToggles(){ $('#fFields').innerHTML=orderedFields().map(f=>`<label class="ftog"><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span>${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }
+function buildFieldToggles(){ $('#fFields').innerHTML=orderedFields().map(f=>`<label class="ftog" draggable="true" data-fk="${f.k}"><span class="fdrag" title="Drag to reorder column">⠿</span><input type="checkbox" data-fk="${f.k}" ${fvis(f.k)?'checked':''}><span class="flbl">${esc(f.l)}</span>${f.col?'<span class="ct" title="Also a List column">▤</span>':''}</label>`).join(''); }
 
 // ---- per-listing broker (with DRE# hooked to the full CA DRE record) + notes ----
 let NOTES={};   // {id:{note,updated_at}} loaded once at boot from /api/condo-notes
@@ -571,6 +576,11 @@ async function boot(){
   }
   const qParam=P.get('q');
   if(qParam){ const box=$('#q'); if(box) box.value=qParam; }
+  // deep-link sort (e.g. graphics "expiring" tiles → &sort=expiry_soon lands soonest-first)
+  const sortParam=P.get('sort');
+  if(sortParam && ['price_desc','price_asc','pps_desc','year_desc','hoa_asc','proj_az','city_az','expiry_soon'].includes(sortParam)){
+    SORTKEY=sortParam; const sel=$('#csort'); if(sel) sel.value=SORTKEY; try{localStorage.setItem('condoSort',SORTKEY);}catch(_){}
+  }
   buildRail(); apply();
 }
 
@@ -642,6 +652,28 @@ rangeWire('pMin','pmin');rangeWire('pMax','pmax');rangeWire('hoaMin','hoamin');r
 rangeWire('sqMin','sqftMin');rangeWire('sqMax','sqftMax');rangeWire('psfMin','ppsfMin');rangeWire('psfMax','ppsfMax');
 rangeWire('yrMin','yearMin');rangeWire('yrMax','yearMax');
 $('#fFields').addEventListener('change',e=>{const cb=e.target.closest('input[data-fk]');if(!cb)return;VIS[cb.dataset.fk]=cb.checked;saveVis();apply();});
+// ── panel drag-to-reorder columns (Steve rule: resort order here in the panel too) ──
+let pDragKey=null;
+$('#fFields').addEventListener('dragstart',e=>{ const row=e.target.closest('.ftog'); if(!row) return;
+  if(!e.target.closest('.fdrag')){ e.preventDefault(); return; }   // only the ⠿ handle starts a move (so checkbox clicks still work)
+  pDragKey=row.dataset.fk; e.dataTransfer.effectAllowed='move'; try{e.dataTransfer.setData('text/plain',pDragKey);}catch(_){}
+  row.classList.add('fdragging'); });
+$('#fFields').addEventListener('dragover',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row||row.dataset.fk===pDragKey) return;
+  e.preventDefault(); e.dataTransfer.dropEffect='move';
+  const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+  $('#fFields').querySelectorAll('.ftog').forEach(x=>x.classList.remove('fdropT','fdropB'));
+  row.classList.toggle('fdropB',after); row.classList.toggle('fdropT',!after); });
+$('#fFields').addEventListener('dragleave',e=>{ const row=e.target.closest('.ftog'); if(row) row.classList.remove('fdropT','fdropB'); });
+$('#fFields').addEventListener('drop',e=>{ if(!pDragKey) return; const row=e.target.closest('.ftog'); if(!row){ pDragKey=null; return; } e.preventDefault();
+  const tgt=row.dataset.fk;
+  if(tgt!==pDragKey){
+    const r=row.getBoundingClientRect(), after=(e.clientY-r.top)>r.height/2;
+    syncColOrder(); const from=COLORDER.indexOf(pDragKey);
+    if(from>=0){ COLORDER.splice(from,1); let to=COLORDER.indexOf(tgt); if(after) to+=1; COLORDER.splice(to,0,pDragKey);
+      saveColOrder(); lastColSig=''; buildFieldToggles(); apply(); }
+  }
+  pDragKey=null; });
+$('#fFields').addEventListener('dragend',()=>{ $('#fFields').querySelectorAll('.fdragging,.fdropT,.fdropB').forEach(x=>x.classList.remove('fdragging','fdropT','fdropB')); pDragKey=null; });
 $('#fAll').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=true);saveVis();buildFieldToggles();apply();});
 $('#fNone').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=false);saveVis();buildFieldToggles();apply();});
 $('#fDef').addEventListener('click',()=>{FIELDS.forEach(f=>VIS[f.k]=!!f.def);saveVis();buildFieldToggles();apply();});
@@ -776,7 +808,7 @@ $('#fReset').addEventListener('click',()=>{
 // 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'); });
-  document.querySelectorAll('.rail .rsec').forEach(s=>s.classList.add('collapsed')); }
+  document.querySelectorAll('.rail .rsec').forEach(s=>{ if(s.id!=='fieldsSec') s.classList.add('collapsed'); }); }
 boot();
 </script>
   <script src="/col-resize.js" defer></script>
diff --git a/public/graphics.html b/public/graphics.html
index 2c39a58..a33fe9f 100644
--- a/public/graphics.html
+++ b/public/graphics.html
@@ -187,7 +187,9 @@ const HOVER=(evt,els)=>{ const t=evt&&evt.native&&evt.native.target; if(t) t.sty
       plugins:{ legend:{display:false} }, scales:{ x:{grid:{color:GH.line}}, y:{grid:{display:false}} } } });
 
   // --- expiring table ---
-  const EXP='/condos.html?status=fha_expired';
+  // This card = APPROVED projects lapsing within 12mo, so drill to the matching pre-filter
+  // (?status=expiring365), NOT fha_expired (already-lapsed) which was the wrong prior target.
+  const EXP='/condos.html?status=expiring365&sort=expiry_soon';
   const A=(href,txt)=>`<a class="dl" href="${href}">${txt}</a>`;
   $('#expTable tbody').innerHTML = (w.expiring||[]).map(e=>`
     <tr><td>${A(EXP+'&q='+enc(e.project||''), esc(e.project))}</td>
@@ -206,7 +208,7 @@ const HOVER=(evt,els)=>{ const t=evt&&evt.native&&evt.native.target; if(t) t.sty
     `<a class="statlink" href="/condos.html?status=fha_approved"><b>${cs.byStatus&&cs.byStatus.fha_approved||0}</b> listings FHA-approved (proxy)</a>`+
     `<a class="statlink" href="/condos.html?status=fha_approved"><b>${approved}</b> FHA-approved condo projects</a>`+
     `<a class="statlink" href="/condos.html?status=fha_expired"><b>${expired}</b> FHA-expired</a>`+
-    `<a class="statlink" href="#expCard"><b>${(w.expiring||[]).length}</b> approvals lapsing &lt;12mo</a>`+
+    `<a class="statlink" href="/condos.html?status=expiring365&sort=expiry_soon"><b>${(w.expiring||[]).length}</b> approvals lapsing &lt;12mo</a>`+
     `<span style="color:var(--mut)">source: HUD FHA list · cre broker graph · <em>click any bar, slice, row or number to drill in</em></span>`;
 })().catch(e=>{ $('#stats').innerHTML='<span style="color:var(--red)">load error: '+e.message+'</span>'; });
 </script>

← f19da0e condos: renewal-outreach tooling — wider expiring buckets, C  ·  back to Commercialrealestate  ·  CRE leads: full email enrichment (free web patterns + local 7553ed1 →