[object Object]

← back to Commercialrealestate

condos.html: add broker/agent contact field toggles (listing agent, phone, office, email, DRE#, responsible broker+address, buyer's side) — TK-10243

115cbb90eaa628734b3af4a8a092791bd8edc188 · 2026-08-05 12:00:41 -0700 · Steve Abrams

Master-parity contact toggles backed by real /api/condos overlay data. Cards show
both sides via brokerCell+buyerCell (always-on); List view gets sortable columns;
detail modal shows phones/email/responsible-broker/buyer-side. No dead toggles —
email fills from find-email enrichment, buyer's agent from the boughtWith scrape.

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

Files touched

Diff

commit 115cbb90eaa628734b3af4a8a092791bd8edc188
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 12:00:41 2026 -0700

    condos.html: add broker/agent contact field toggles (listing agent, phone, office, email, DRE#, responsible broker+address, buyer's side) — TK-10243
    
    Master-parity contact toggles backed by real /api/condos overlay data. Cards show
    both sides via brokerCell+buyerCell (always-on); List view gets sortable columns;
    detail modal shows phones/email/responsible-broker/buyer-side. No dead toggles —
    email fills from find-email enrichment, buyer's agent from the boughtWith scrape.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/condos.html | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 68 insertions(+), 4 deletions(-)

diff --git a/public/condos.html b/public/condos.html
index ff85e28..ac1b0de 100644
--- a/public/condos.html
+++ b/public/condos.html
@@ -184,9 +184,18 @@ const FIELDS=[
   {k:'year_built',  l:'Year built',            col:1},
   {k:'age',         l:'Age (yrs)',             col:1, calc:c=>c.year_built?(NOW_YEAR-c.year_built):null},
   {k:'zip',         l:'ZIP',                   col:1},
-  {k:'broker_name', l:'Broker',                col:1},
+  // ── Broker / agent CONTACT block (Steve 2026-08-05, TK-10243): every listing carries the full
+  // listing-side contact set + the sold-listing buyer side. All backed by /api/condos overlay data
+  // (data/condo-brokers.json) — no dead toggles. Email fills from the find-email enrichment; the
+  // buyer's agent fills only on SOLD listings from the boughtWith scrape. ──
+  {k:'broker_name', l:'Listing agent',         col:1},
   {k:'firm_name',   l:'Firm',                  col:1},
+  {k:'agent_phone', l:'Agent phone',           col:1},
+  {k:'office_phone',l:'Office phone',          col:1},
+  {k:'agent_email', l:'Agent email',           col:1},
   {k:'broker_dre',  l:'DRE#',                  col:1},
+  {k:'responsible_broker', l:'Responsible broker', col:1},
+  {k:'buyer_agent', l:'Buyer’s agent (sold)', col:1},
   {k:'warrant_source',l:'Warrant note',        col:0},
   {k:'created_at',  l:'Added',        date:1,  col:1},
 ];
@@ -244,6 +253,13 @@ function fcell(c,f){ const v=fval(c,f);
     if(!/^\d{6,8}$/.test(String(primary))) return esc(String(primary))+extra;   // defensive: only link a real DRE#
     const u=c.dre_url||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+primary);
     return `<a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">${esc(primary)} ↗</a>${extra}`; }
+  // ── contact-block cells (TK-10243) — resolve nested/flat, phone→tel, email→mailto, sortable text ──
+  if(f.k==='agent_phone'){ const p=v||(c.agents&&c.agents[0]&&c.agents[0].phone); return p?`<a href="${telHref(p)}">${esc(p)}</a>`:'—'; }
+  if(f.k==='office_phone'){ return v?`<a href="${telHref(v)}">${esc(v)}</a>`:'—'; }
+  if(f.k==='agent_email'){ const e=agentEmail(c); return e?`<a href="${mailHref(e)}" title="Email the listing agent">${esc(e)} ✉</a>`:'—'; }
+  if(f.k==='responsible_broker'){ const rb=respBroker(c); return rb?esc(rb):'—'; }
+  if(f.k==='buyer_agent'){ const ba=buyerAgents(c); if(!ba.length) return '—';
+    return ba.map(a=>{ const dre=a.dre?` · DRE# ${esc(a.dre)}`:''; return esc(a.name||'')+(a.firm?' · '+esc(a.firm):'')+dre; }).join('<br>'); }
   if(v==null||v==='') return '—';
   if(f.money) return '$'+(+v).toLocaleString()+(f.suf||'');
   if(f.num) return (+v).toLocaleString();
@@ -311,6 +327,16 @@ function buildFieldToggles(){ $('#fFields').innerHTML=orderedFields().map(f=>`<l
 // ---- per-listing broker (with DRE# hooked to the full CA DRE record) + notes ----
 let NOTES={};   // {id:{note,updated_at}} loaded once at boot from /api/condo-notes
 const telHref=p=>'tel:'+String(p||'').replace(/[^0-9]/g,'');
+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) || ''; }
+// 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
+// (buyer_agents[]) with a flat-field fallback. Empty array for active listings (no buyer side yet).
+function buyerAgents(c){ return (Array.isArray(c.buyer_agents)&&c.buyer_agents.length)?c.buyer_agents
+  : (c.buyer_agent_name?[{name:c.buyer_agent_name,firm:c.buyer_firm_name,dre:c.buyer_agent_dre,phone:c.buyer_agent_phone}]:[]); }
 function brokerCell(c){
   // Always render (Steve 2026-08-03: every listing must carry the broker/firm/contact) — not gated
   // on the field-visibility toggles the way the generic rows are.
@@ -325,7 +351,23 @@ function brokerCell(c){
   const agents=(Array.isArray(c.agents)&&c.agents.length)?c.agents:[{name:c.broker_name,firm:c.firm_name,dre:c.broker_dre,dre_record:(c.dre_url?{dre_url:c.dre_url}:null)}];
   const dreLink=(a)=>{ if(!a.dre) return ''; const u=(a.dre_record&&a.dre_record.dre_url)||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre); return ` · <a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)}</a>`; };
   const lines=agents.map((a,i)=>`🧑‍💼 ${esc(a.name||'')}${a.firm?(a.name?' · ':'')+esc(a.firm):''}${dreLink(a)}${i===0?aph+oph:'<span style="color:var(--mut);font-size:10px"> (co-agent)</span>'}`).join('<br>');
-  return `<div class="broker">${lines}</div>${exp}`;
+  // email line — always shown when present (Steve: every listing carries full contact); mailto-linked.
+  const em=agentEmail(c); const emLine=em?`<div class="broker" style="font-size:11px">✉ <a href="${mailHref(em)}">${esc(em)}</a></div>`:'';
+  return `<div class="broker">${lines}</div>${emLine}${exp}`;
+}
+// ── BUYER'S SIDE — the "other side" of a SOLD deal (TK-10243). Always shown on the card when the
+// boughtWith scrape captured it; empty (renders nothing) for active listings, which have no buyer
+// side yet. Kept visually distinct from the listing side with a 🤝 marker + a dashed divider. ──
+function buyerCell(c){
+  const ba=buyerAgents(c); if(!ba.length) return '';
+  const line=ba.map(a=>{
+    const u=a.dre?('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre):'';
+    const dre=a.dre?(u?` · <a href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)}</a>`:` · DRE# ${esc(a.dre)}`):'';
+    const ph=a.phone?` · <a href="${telHref(a.phone)}">📱 ${esc(a.phone)}</a>`:'';
+    const em=a.email?` · <a href="${mailHref(a.email)}">✉ ${esc(a.email)}</a>`:'';
+    return `🤝 ${esc(a.name||'')}${a.firm?' · '+esc(a.firm):''}${dre}${ph}${em}`;
+  }).join('<br>');
+  return `<div class="broker" style="border-top:1px dashed var(--line);margin-top:4px;padding-top:5px"><span style="color:var(--mut);font-size:10px;letter-spacing:.4px">BUYER'S SIDE · sold by</span><br>${line}</div>`;
 }
 function noteBox(c){
   const v=(NOTES[c.id]&&NOTES[c.id].note)||'';
@@ -340,7 +382,10 @@ function whenChip(c){
 }
 
 // ---- renderers ----
-const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warrant_source','created_at']);
+// Contact fields render via brokerCell/buyerCell (always-on) — skip them from the generic card rows
+// so a toggled-on column doesn't double-print on the card. They stay live as List columns + toggles.
+const CARD_SKIP=new Set(['warrantable_status','city','zip','project_name','warrant_source','created_at',
+  'broker_name','firm_name','agent_phone','office_phone','agent_email','broker_dre','responsible_broker','buyer_agent']);
 function dbCards(rows){
   const vis=orderedFields().filter(f=>fvis(f.k)&&!CARD_SKIP.has(f.k));
   return rows.map(c=>`<div class="condo">
@@ -350,6 +395,7 @@ function dbCards(rows){
     ${fvis('warrantable_status')?statusBadge(c.warrantable_status||c.warrant_signal):''}
     ${vis.map(f=>`<div class="row"><span class="k">${esc(f.l)}</span><span>${fcell(c,f)}</span></div>`).join('')}
     ${brokerCell(c)}
+    ${buyerCell(c)}
     ${fvis('warrant_source')&&c.warrant_source?`<div class="src">${esc(c.warrant_source)}</div>`:''}
     ${noteBox(c)}
   </div>`).join('');
@@ -517,7 +563,25 @@ function openDetail(id){
         const fm=a.firm?`${a.name?' · ':''}<a class="dl" href="/crcp.html?firm=${encodeURIComponent(a.firm)}">${esc(a.firm)}</a>`:'';
         const u=a.dre?((a.dre_record&&a.dre_record.dre_url)||('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre)):'';
         const dre=a.dre?` · <a class="dl" href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)} ↗</a>`:'';
-        return line(i===0?'Listed by':'Co-agent', nm+fm+dre);
+        // primary agent carries phone(s) + email inline; co-agents show name/firm/DRE
+        const ph=(i===0&&c.agent_phone)?` · <a class="dl" href="${telHref(c.agent_phone)}">📱 ${esc(c.agent_phone)}</a>`:'';
+        const op=(i===0&&c.office_phone&&c.office_phone!==c.agent_phone)?` · ☎ ${esc(c.office_phone)}`:'';
+        const emv=(i===0)?agentEmail(c):(a.email||''); const em=emv?` · <a class="dl" href="${mailHref(emv)}">✉ ${esc(emv)}</a>`:'';
+        return line(i===0?'Listed by':'Co-agent', nm+fm+dre+ph+op+em);
+      }).join('');
+    })()
+    +(function(){ const rb=respBroker(c); return rb?line('Responsible broker',esc(rb)):''; })()
+    +(function(){
+      // BUYER'S SIDE — the other party on a SOLD deal. Name→CRCP directory, DRE→full record.
+      const ba=buyerAgents(c); if(!ba.length) return '';
+      return ba.map((a,i)=>{
+        const nm=a.name?`<a class="dl" href="/crcp.html?brokerName=${encodeURIComponent(a.name)}">${esc(a.name)}</a>`:'';
+        const fm=a.firm?`${a.name?' · ':''}<a class="dl" href="/crcp.html?firm=${encodeURIComponent(a.firm)}">${esc(a.firm)}</a>`:'';
+        const u=a.dre?('https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id='+a.dre):'';
+        const dre=a.dre?` · <a class="dl" href="${esc(u)}" target="_blank" rel="noopener noreferrer" title="Full CA DRE license record">DRE# ${esc(a.dre)} ↗</a>`:'';
+        const ph=a.phone?` · <a class="dl" href="${telHref(a.phone)}">📱 ${esc(a.phone)}</a>`:'';
+        const em=a.email?` · <a class="dl" href="${mailHref(a.email)}">✉ ${esc(a.email)}</a>`:'';
+        return line(i===0?'Sold by (buyer’s side)':'Co-agent (buyer)', nm+fm+dre+ph+em);
       }).join('');
     })()
     +line('Added',dateTimeSpan(c.created_at))

← 74c643a auto-save: 2026-08-05T07:09:54 (1 files) — data/alerts-state  ·  back to Commercialrealestate  ·  condos both-sides: buyer-side scraper (Redfin buyingAgents) 2bee932 →