← 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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
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 →