← 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 →