[object Object]

← back to Commercialrealestate

condos: add Contact-ready outreach chip + 'FHA expires soonest' sort preset

e8f05ab09420cc8bf86dc22f743bfc3436921230 · 2026-08-07 16:02:49 -0700 · Steve Abrams

- 📇 Contact-ready toggle (own rail section, auto-hidden when no listing has a
  contact): filters to listings with a reachable agent email/phone (152 live).
  Independent of warrantability so it stacks with the Expiring/Lapsed chips.
- New 'FHA expires ↑ (soonest)' option in the sort dropdown: upcoming certs by
  nearness, then recently-lapsed, then unmatched last. Shares daysToExp().

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

Files touched

Diff

commit e8f05ab09420cc8bf86dc22f743bfc3436921230
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 16:02:49 2026 -0700

    condos: add Contact-ready outreach chip + 'FHA expires soonest' sort preset
    
    - 📇 Contact-ready toggle (own rail section, auto-hidden when no listing has a
      contact): filters to listings with a reachable agent email/phone (152 live).
      Independent of warrantability so it stacks with the Expiring/Lapsed chips.
    - New 'FHA expires ↑ (soonest)' option in the sort dropdown: upcoming certs by
      nearness, then recently-lapsed, then unmatched last. Shares daysToExp().
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/condos.html | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/public/condos.html b/public/condos.html
index e91fe46..7b6a0b1 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -156,6 +156,7 @@
 <div class="shell">
 <aside class="rail">
   <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>
   <div class="rsec" id="firmSec"><h4>Listing firm</h4><div class="chips" id="fFirm"></div></div>
   <div class="rsec" id="bedSec"><h4>Beds (min)</h4><div class="chips" id="fBeds"></div></div>
@@ -185,6 +186,7 @@
       <option value="hoa_asc">HOA ↑</option>
       <option value="proj_az">Project A→Z</option>
       <option value="city_az">City A→Z</option>
+      <option value="expiry_soon">FHA expires ↑ (soonest)</option>
     </select>
     <label class="dens">Density <input type="range" id="cols" min="1" max="20" step="1" value="3"></label>
     <span class="count" id="count">…</span>
@@ -339,7 +341,7 @@ function renderStats(rows){ const sb=$('#statsbar'); if(!sb) return; if(!rows.le
   const pill=(l,v)=>v==null?'':`<span class="statpill"><span class="sl">${l}</span> <b>${v}</b></span>`;
   sb.innerHTML=pill('Count',rows.length)+pill('Median price',prices.length?'$'+Math.round(median(prices)).toLocaleString():null)+pill('Median $/SF',pps.length?'$'+Math.round(median(pps)).toLocaleString():null)+pill('Median HOA',hoas.length?'$'+Math.round(median(hoas)).toLocaleString()+'/mo':null)+pill('Median year',yrs.length?Math.round(median(yrs)):null);
 }
-const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false,
+const F={ warr:'all', cities:new Set(), firms:new Set(), bedsMin:0, bathsMin:0, shortlist:false, contactReady: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
@@ -371,6 +373,7 @@ function passFacets(c){
   if(!rng(c.year_built,F.yearMin,F.yearMax)) return false;
   if(!rng(c.sqft?c.price/c.sqft:null,F.ppsfMin,F.ppsfMax)) return false;
   if(F.shortlist && !SHORT.has(c.id)) return false;
+  if(F.contactReady && !hasContact(c)) return false;
   return true;
 }
 
@@ -387,6 +390,11 @@ function buildRail(){
     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('');
+  // 📇 Contact-ready toggle — independent of warrantability; auto-hidden when no listing carries a
+  // contact (e.g. the FHA-directory fallback mode, whose rows have no broker overlay).
+  const cn=DATA.filter(hasContact).length; const cs=$('#contactSec');
+  if(cs) cs.style.display = cn ? '' : 'none';
+  if(cn) $('#fContact').innerHTML=`<span class="chip ${F.contactReady?'active':''}" data-contact="1" title="Only listings with a reachable agent email or phone — the outreach-ready set">📇 Contact-ready<span class="ct">${cn}</span></span>`;
   $('#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('');
   const firms=Object.entries(fc).sort((a,b)=>b[1]-a[1]).slice(0,14);
   $('#firmSec').style.display=firms.length?'':'none';
@@ -406,6 +414,9 @@ const mailHref=e=>'mailto:'+String(e||'').trim();
 // ── contact resolvers (TK-10243): each falls back through the flat field → primary agent record so
 // the List column matches whatever the card shows, and returns '' when genuinely absent (no fake '—'). ──
 function agentEmail(c){ return c.agent_email || (c.agents&&c.agents[0]&&c.agents[0].email) || ''; }
+// Contact-ready = we have a reachable channel for the listing agent (email or any phone) — the set
+// worth filtering to for renewal / re-cert outreach. Powers the 📇 Contact-ready toggle chip.
+function hasContact(c){ return !!(agentEmail(c) || c.agent_phone || c.office_phone || (c.agents&&c.agents[0]&&(c.agents[0].phone||c.agents[0].email))); }
 // responsible broker carries the brokerage's office mailing ADDRESS (from the CA DRE record)
 function respBroker(c){ return c.responsible_broker || (c.agents&&c.agents[0]&&c.agents[0].dre_record&&c.agents[0].dre_record.responsible_broker) || ''; }
 // buyer-SIDE agents — SOLD listings only; mirrors agents[]. Filled by the boughtWith scrape
@@ -575,6 +586,7 @@ $('#grid').addEventListener('input',e=>{
   },600);
 });
 $('#fWarr').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.warr=c.dataset.warr;$$('#fWarr .chip').forEach(x=>x.classList.toggle('active',x.dataset.warr===F.warr));apply();});
+$('#fContact').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.contactReady=!F.contactReady;c.classList.toggle('active',F.contactReady);apply();});
 $('#fCity').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.cities,c.dataset.cy);c.classList.toggle('active');apply();});
 $('#fFirm').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;tog(F.firms,c.dataset.fm);c.classList.toggle('active');apply();});
 $('#fBeds').addEventListener('click',e=>{const c=e.target.closest('.chip');if(!c)return;F.bedsMin=+c.dataset.beds;$$('#fBeds .chip').forEach(x=>x.classList.toggle('active',+x.dataset.beds===F.bedsMin));apply();});
@@ -599,6 +611,9 @@ function sortRows(rows){
     hoa_asc:(a,b)=>(num(a.hoa)??Infinity)-(num(b.hoa)??Infinity),
     proj_az:(a,b)=>String(a.project_name||a.address||'').localeCompare(String(b.project_name||b.address||'')),
     city_az:(a,b)=>String(a.city||'').localeCompare(String(b.city||'')),
+    // FHA expires ↑ (soonest actionable first): upcoming certs by nearness (0,1,2…), THEN already-
+    // lapsed (most-recent first), THEN unmatched (no FHA date) last. Ranks via daysToExp().
+    expiry_soon:(a,b)=>{const k=c=>{const d=daysToExp(c);return d==null?Infinity:(d>=0?d:1e6-d);};return k(a)-k(b);},
   }[SORTKEY];
   return cmp?rows.slice().sort(cmp):rows;
 }

← c25ae4d condos: add date-based quick-filter chips (Expiring ≤90d / L  ·  back to Commercialrealestate  ·  CRE marketing-exec leads: seed + master contact list (name+p 56e5e81 →