[object Object]

← back to Commercialrealestate

condos: add date-based quick-filter chips (Expiring ≤90d / Lapsed ≤180d)

c25ae4d3bcd8a791a9c9813c011f15086a142d96 · 2026-08-07 15:59:20 -0700 · Steve Abrams

Two one-click chips beside the FHA-approved/expired chips, filtering by the live
day-count via a shared daysToExp() helper (also backs the Days-to-expiry column):
- ⚠ Expiring ≤90d  — approved certs lapsing soon (renewal-outreach set)
- 🔴 Lapsed ≤180d  — certs that fell off in the last 180d (re-cert conversion set)
Counts shown on each chip; deep-linkable via ?status=; gold/red chip styling.

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

Files touched

Diff

commit c25ae4d3bcd8a791a9c9813c011f15086a142d96
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 15:59:20 2026 -0700

    condos: add date-based quick-filter chips (Expiring ≤90d / Lapsed ≤180d)
    
    Two one-click chips beside the FHA-approved/expired chips, filtering by the live
    day-count via a shared daysToExp() helper (also backs the Days-to-expiry column):
    - ⚠ Expiring ≤90d  — approved certs lapsing soon (renewal-outreach set)
    - 🔴 Lapsed ≤180d  — certs that fell off in the last 180d (re-cert conversion set)
    Counts shown on each chip; deep-linkable via ?status=; gold/red chip styling.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/condos.html | 43 ++++++++++++++++++++++++++++++-------------
 1 file changed, 30 insertions(+), 13 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index d0e7e0e..e91fe46 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -39,6 +39,8 @@
   .chip{background:var(--card);color:var(--mut);border:1px solid var(--line);border-radius:16px;padding:4px 10px;font-size:12px;cursor:pointer;user-select:none;}
   .chip:hover{color:var(--ink);} .chip.active{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,.08);}
   .chip.warr-unwarrantable.active{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.10);}
+  .chip.warr-expiring90.active{border-color:var(--gold);color:var(--gold);background:rgba(210,153,34,.10);}
+  .chip.warr-lapsed180.active{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.10);}
   .chip .ct{color:var(--mut);font-size:10px;margin-left:4px;}
   .row2{display:flex;gap:8px;} .fld{flex:1;} .fld label{display:block;font-size:10px;color:var(--mut);margin-bottom:3px;}
   .rail input[type=number]{width:100%;}
@@ -219,12 +221,7 @@ const FIELDS=[
   // Days until FHA approval expires, computed live from fha_expiration_date against today (so it's
   // never stale). Negative = already lapsed N days ago. Rendered as a color-coded badge in fcell
   // (red = expired, gold = ≤90d, green = further out). calc returns a Number so List-sort is numeric.
-  {k:'days_to_expiry', l:'Days to expiry', col:1, calc:c=>{
-    const m=String(c.fha_expiration_date||'').match(/(\d{2})\/(\d{2})\/(\d{4})/); if(!m) return null;
-    const exp=new Date(+m[3],+m[1]-1,+m[2]); exp.setHours(0,0,0,0);
-    const t=new Date(); t.setHours(0,0,0,0);
-    return Math.round((exp-t)/86400000);
-  }},
+  {k:'days_to_expiry', l:'Days to expiry', col:1, calc:c=>daysToExp(c)},
   {k:'city',        l:'City',                  col:1},
   {k:'hoa',         l:'HOA/mo',       money:1, col:1, suf:'/mo'},
   {k:'beds',        l:'Beds',                  col:1},
@@ -345,9 +342,25 @@ function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.le
 const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false,
   pmin:null,pmax:null, hoamin:null,hoamax:null, sqftMin:null,sqftMax:null, yearMin:null,yearMax:null, ppsfMin:null,ppsfMax:null };
 function rng(v,mn,mx){ if(mn!=null){ if(v==null||!(+v>=mn)) return false; } if(mx!=null){ if(v==null||!(+v<=mx)) return false; } return true; }
+// Days until the matched FHA cert expires (negative = lapsed N days ago), parsed from the clean
+// fha_expiration_date the server surfaces. SINGLE source of truth for the 'Days to expiry' column
+// AND the date-based quick-filter chips (⚠ Expiring ≤90d / 🔴 Lapsed ≤180d). null = no FHA match.
+function daysToExp(c){
+  const m=String(c.fha_expiration_date||'').match(/(\d{2})\/(\d{2})\/(\d{4})/); if(!m) return null;
+  const exp=new Date(+m[3],+m[1]-1,+m[2]); exp.setHours(0,0,0,0);
+  const t=new Date(); t.setHours(0,0,0,0);
+  return Math.round((exp-t)/86400000);
+}
+// The two computed chips ('expiring90','lapsed180') are subsets of approved/expired keyed off the
+// live day-count, so they filter by date rather than by the frozen warrantable_status string.
+function passWarrChip(c,ws){
+  if(F.warr==='expiring90'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=90; }
+  if(F.warr==='lapsed180'){ const d=daysToExp(c); return ws==='fha_expired'&&d!=null&&d<0&&d>=-180; }
+  return F.warr==='all'?true:F.warr==='unwarrantable'?ws!=='fha_approved':ws===F.warr;
+}
 function passFacets(c){
   const ws=c.warrantable_status||c.warrant_signal;
-  if(!(F.warr==='all'?true:F.warr==='unwarrantable'?ws!=='fha_approved':ws===F.warr)) return false;
+  if(!passWarrChip(c,ws)) return false;
   if(F.cities.size&&!F.cities.has(c.city)) return false;
   if(F.firms.size&&!F.firms.has(c.firm_name)) return false;
   if(F.bedsMin&&!(+c.beds>=F.bedsMin)) return false;
@@ -363,11 +376,15 @@ function passFacets(c){
 
 // ---- rail builder ----
 function buildRail(){
-  const wc={},cc={},fc={};
-  DATA.forEach(c=>{ const s=c.warrantable_status||c.warrant_signal||'?'; wc[s]=(wc[s]||0)+1; if(c.city)cc[c.city]=(cc[c.city]||0)+1; if(c.firm_name)fc[c.firm_name]=(fc[c.firm_name]||0)+1; });
-  const order=[['all','All'],['fha_approved','FHA-approved'],['fha_expired','FHA-expired'],['not_listed','Not-listed'],['heuristic_flag','Flagged'],['unwarrantable','⚑ Unwarrantable · non-FHA']];
-  $('#fWarr').innerHTML=order.filter(([v])=>v==='all'||v==='unwarrantable'||wc[v]).map(([v,l])=>{
-    const n=v==='all'?DATA.length:v==='unwarrantable'?(DATA.length-(wc.fha_approved||0)):wc[v];
+  const wc={},cc={},fc={}; let ec=0,lc=0;   // ec=approved expiring ≤90d, lc=expired within last 180d
+  DATA.forEach(c=>{ const s=c.warrantable_status||c.warrant_signal||'?'; wc[s]=(wc[s]||0)+1; if(c.city)cc[c.city]=(cc[c.city]||0)+1; if(c.firm_name)fc[c.firm_name]=(fc[c.firm_name]||0)+1;
+    const d=daysToExp(c);
+    if(s==='fha_approved'&&d!=null&&d>=0&&d<=90) ec++;
+    if(s==='fha_expired'&&d!=null&&d<0&&d>=-180) lc++; });
+  const scount={expiring90:ec, lapsed180:lc};   // computed-chip counts (not keyed by warrantable_status)
+  const order=[['all','All'],['fha_approved','FHA-approved'],['expiring90','⚠ Expiring ≤90d'],['fha_expired','FHA-expired'],['lapsed180','🔴 Lapsed ≤180d'],['not_listed','Not-listed'],['heuristic_flag','Flagged'],['unwarrantable','⚑ Unwarrantable · non-FHA']];
+  $('#fWarr').innerHTML=order.filter(([v])=>v==='all'||v==='unwarrantable'||(v in scount?scount[v]:wc[v])).map(([v,l])=>{
+    const n=v==='all'?DATA.length:v==='unwarrantable'?(DATA.length-(wc.fha_approved||0)):(v in scount?scount[v]:wc[v]);
     return `<span class="chip warr-${v} ${F.warr===v?'active':''}" data-warr="${v}">${l}<span class="ct">${n}</span></span>`;
   }).join('');
   $('#fCity').innerHTML=Object.entries(cc).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,c])=>`<span class="chip ${F.cities.has(k)?'active':''}" data-cy="${esc(k)}">${esc(k)}<span class="ct">${c}</span></span>`).join('');
@@ -528,7 +545,7 @@ async function boot(){
   // deep-link (drill-through from the graphics dashboard): ?status=…&city=…&q=…
   const P=new URLSearchParams(location.search);
   const qs=P.get('status');
-  if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable'].includes(qs)) F.warr=qs;
+  if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable','expiring90','lapsed180'].includes(qs)) F.warr=qs;
   const cityParam=P.get('city');
   if(cityParam){
     // FHA-list cities are UPPERCASE, scraped-listing cities are Title Case → resolve case-insensitively

← 0f76d8b auto-data-snapshot: 2026-08-07T15:54:54 (13 data files) — da  ·  back to Commercialrealestate  ·  condos: add Contact-ready outreach chip + 'FHA expires soone e8f05ab →