[object Object]

← back to Commercialrealestate

condos: renewal-outreach tooling — wider expiring buckets, CSV export, draft outreach

f19da0e7b693f085e7cafdfc55413474aa2125a6 · 2026-08-07 16:15:23 -0700 · Steve Abrams

- ⏳ Expiring ≤6mo / ≤12mo chips (cumulative windows) beside ≤90d, live counts
- ⬇ Export CSV of the current filtered+sorted set (whatever chips/search/sort active)
- 🎯 Draft outreach: queues one DRAFT email per FHA-matched listing in the current
  filter into the Solicitation Studio (draft-only, capped 100, nothing sends)
Captures the on-screen set as CURRENT; all three share it.

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

Files touched

Diff

commit f19da0e7b693f085e7cafdfc55413474aa2125a6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 16:15:23 2026 -0700

    condos: renewal-outreach tooling — wider expiring buckets, CSV export, draft outreach
    
    - ⏳ Expiring ≤6mo / ≤12mo chips (cumulative windows) beside ≤90d, live counts
    - ⬇ Export CSV of the current filtered+sorted set (whatever chips/search/sort active)
    - 🎯 Draft outreach: queues one DRAFT email per FHA-matched listing in the current
      filter into the Solicitation Studio (draft-only, capped 100, nothing sends)
    Captures the on-screen set as CURRENT; all three share it.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/condos.html | 60 +++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 53 insertions(+), 7 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index 7b6a0b1..a3ea0f3 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -39,7 +39,7 @@
   .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-expiring90.active,.chip.warr-expiring180.active,.chip.warr-expiring365.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;}
@@ -177,6 +177,8 @@
     <input type="text" id="q" placeholder="search all fields — project, city, zip, address, broker, firm, status… (space = AND)">
     <span class="viewseg" id="viewseg"><button data-view="grid" class="active">▦ Grid</button><button data-view="list">☰ List</button></span>
     <button id="shortbtn" title="Show only your shortlisted (starred) listings — shared with the Explorer & MLS">⭐ Shortlist (0)</button>
+    <button id="exportbtn" title="Download the current filtered + sorted set as a CSV">⬇ Export CSV</button>
+    <button id="draftbtn" title="Queue DRAFT outreach emails for the current FHA-matched set in the Solicitation Studio — nothing sends">🎯 Draft outreach</button>
     <select id="csort" title="Sort condos" style="background:var(--card,#141925);color:var(--ink,#E6E8EC);border:1px solid var(--line,#232a38);border-radius:7px;padding:5px 8px;font-size:12px">
       <option value="">Newest</option>
       <option value="price_desc">Price ↓</option>
@@ -203,7 +205,7 @@ const $$=s=>Array.from(document.querySelectorAll(s));
 const esc=s=>String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 const PROXY='FHA/VA-approval-based proxy, NOT lender-verified Fannie/Freddie warrantability.';
 const NOW_YEAR=new Date().getFullYear();
-let DATA=[], MODE='db', VIEW=localStorage.getItem('condoView')||'grid', bannerDefault='';
+let DATA=[], CURRENT=[], MODE='db', VIEW=localStorage.getItem('condoView')||'grid', bannerDefault='';
 let SORTKEY=localStorage.getItem('condoSort')||'';   // hoisted so early apply() during boot never hits TDZ
 
 function statusBadge(s){
@@ -357,6 +359,8 @@ function daysToExp(c){
 // 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==='expiring180'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=180; }
+  if(F.warr==='expiring365'){ const d=daysToExp(c); return ws==='fha_approved'&&d!=null&&d>=0&&d<=365; }
   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;
 }
@@ -379,13 +383,13 @@ function passFacets(c){
 
 // ---- rail builder ----
 function buildRail(){
-  const wc={},cc={},fc={}; let ec=0,lc=0;   // ec=approved expiring ≤90d, lc=expired within last 180d
+  const wc={},cc={},fc={}; let ec=0,e180=0,e365=0,lc=0;   // approved expiring ≤90/≤180/≤365d (cumulative); 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_approved'&&d!=null&&d>=0){ if(d<=90)ec++; if(d<=180)e180++; if(d<=365)e365++; }
     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']];
+  const scount={expiring90:ec, expiring180:e180, expiring365:e365, lapsed180:lc};   // computed-chip counts (not keyed by warrantable_status)
+  const order=[['all','All'],['fha_approved','FHA-approved'],['expiring90','⚠ Expiring ≤90d'],['expiring180','Expiring ≤6mo'],['expiring365','Expiring ≤12mo'],['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>`;
@@ -535,6 +539,7 @@ function apply(){
   }
   rows=sortRows(rows);
   rows=listSort(rows);
+  CURRENT=rows;   // the exact filtered+sorted set on screen — powers CSV export + the draft-outreach action
   renderStats(rows); updateShortBtn();
   const unw=F.warr==='unwarrantable';
   $('#count').innerHTML=`<b>${rows.length}</b> of ${DATA.length} ${unw?'unwarrantable (non-FHA) condos':(MODE==='fha'?'FHA-list projects':'condos')}`;
@@ -556,7 +561,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','expiring90','lapsed180'].includes(qs)) F.warr=qs;
+  if(qs && ['all','fha_approved','fha_expired','not_listed','heuristic_flag','unwarrantable','expiring90','expiring180','expiring365','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
@@ -587,6 +592,47 @@ $('#grid').addEventListener('input',e=>{
 });
 $('#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();});
+
+// ── #4 CSV export of the CURRENT filter (whatever chips/search/sort are active) ──
+function exportCSV(){
+  const rows=CURRENT; if(!rows.length){ alert('Nothing to export — the current filter matches 0 listings.'); return; }
+  const cols=[['project_name','Project'],['address','Address'],['city','City'],['zip','ZIP'],
+    ['warrantable_status','Warrantability'],['fha_expiration_date','FHA expires'],['days_to_expiry','Days to expiry'],
+    ['price','Price'],['hoa','HOA/mo'],['beds','Beds'],['baths','Baths'],
+    ['broker_name','Listing agent'],['firm_name','Firm'],['agent_email','Agent email'],['agent_phone','Agent phone']];
+  const dtf=FIELDS.find(f=>f.k==='days_to_expiry');
+  const val=(c,k)=> k==='days_to_expiry' ? (dtf.calc(c)==null?'':dtf.calc(c)) : k==='agent_email' ? agentEmail(c) : (c[k]==null?'':c[k]);
+  const q=s=>{s=String(s); return /[",\n]/.test(s)?'"'+s.replace(/"/g,'""')+'"':s;};
+  const csv=[cols.map(c=>q(c[1])).join(',')].concat(rows.map(r=>cols.map(c=>q(val(r,c[0]))).join(','))).join('\n');
+  const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
+  a.download=`crcp-condos-${F.warr}${F.contactReady?'-contact':''}-${new Date().toISOString().slice(0,10)}.csv`;
+  document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href);
+}
+$('#exportbtn') && $('#exportbtn').addEventListener('click',exportCSV);
+
+// ── #2 Renewal targets → Solicitation Studio DRAFTS (draft-only; nothing sends) ──
+async function draftOutreach(){
+  const rows=CURRENT.filter(c=>daysToExp(c)!=null);   // only FHA-matched listings have a renewal angle
+  if(!rows.length){ alert('No FHA-matched listings in the current filter to draft outreach for.\nTry the ⚠ Expiring or 🔴 Lapsed chip first.'); return; }
+  const CAP=100, set=rows.slice(0,CAP);
+  if(!confirm(`Queue ${set.length} DRAFT outreach email(s) in the Solicitation Studio?\n\nDrafts only — nothing sends. ($0, local)`)) return;
+  let ok=0,fail=0;
+  for(const c of set){
+    const d=daysToExp(c), proj=c.project_name||c.address||'this project';
+    const when=d<0?`expired ${(-d).toLocaleString()} days ago`:`expires in ${d.toLocaleString()} days (${c.fha_expiration_date})`;
+    const subject=`FHA approval ${d<0?'has lapsed':'is expiring'} — ${proj}`;
+    const body=`Hello,\n\nOur records show the FHA project approval for ${proj}${c.city?` in ${c.city}`:''} ${when}. `+
+      `${d<0?'Recertifying restores FHA/VA financing eligibility for buyers in the community.':'Recertifying before it lapses keeps FHA/VA buyers eligible and protects unit values.'}\n\n`+
+      `We help HOAs navigate the FHA recertification process — happy to walk you through it.\n\n— Designer / CRCP`;
+    try{ const r=await fetch('/api/solicitation/queue',{method:'POST',headers:{'Content-Type':'application/json'},
+        body:JSON.stringify({tone:'professional',type:'email',audience:'fha-renewal',subject,body})}); r.ok?ok++:fail++; }
+    catch(_){ fail++; }
+  }
+  alert(`Drafted ${ok} outreach email(s) to the Solicitation Studio queue${fail?` (${fail} failed)`:''}.`+
+    `${rows.length>CAP?`\n\nCapped at ${CAP}; ${(rows.length-CAP).toLocaleString()} more matched the filter.`:''}`+
+    `\n\nAll DRAFT — review + send from the Solicitation Studio. Nothing was sent.`);
+}
+$('#draftbtn') && $('#draftbtn').addEventListener('click',draftOutreach);
 $('#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();});

← e288be3 CRE leads: enrich master with 9 direct-dial/email contacts +  ·  back to Commercialrealestate  ·  graphics: point warrantability 'expiring' tiles at the new p 4ea459d →