[object Object]

← back to Commercialrealestate

licensed-agents: always show agent phone+email (TK-10703)

352846cdbb1107ee964709c338b6ac4a2711c45e · 2026-08-18 18:26:11 -0700 · Steve Abrams

Files touched

Diff

commit 352846cdbb1107ee964709c338b6ac4a2711c45e
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 18 18:26:11 2026 -0700

    licensed-agents: always show agent phone+email (TK-10703)
---
 public/licensed-agents.html | 55 +++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 55 insertions(+)

diff --git a/public/licensed-agents.html b/public/licensed-agents.html
index e2b709e..9d27a2a 100644
--- a/public/licensed-agents.html
+++ b/public/licensed-agents.html
@@ -40,6 +40,11 @@
   .kv .v a{color:var(--blue);text-decoration:none;} .kv .v a:hover{text-decoration:underline;}
   .conf{font-size:10px;color:var(--mut);margin-top:7px;}
   .verified{color:var(--acc);font-weight:700;}
+  /* ALWAYS-VISIBLE agent contact (Steve HARD RULE 2026-08-18): phone + email on every card */
+  .contact{display:flex;flex-wrap:wrap;gap:6px 12px;margin-top:9px;padding-top:8px;border-top:1px solid var(--line);font-size:12px;}
+  .contact a{color:var(--blue);text-decoration:none;} .contact a:hover{text-decoration:underline;}
+  .contact .none{color:var(--mut);}
+  #modal .contact{font-size:13px;}
   .modes{display:inline-flex;border:1px solid var(--line);border-radius:8px;overflow:hidden;}
   .modes button{background:var(--card);color:var(--mut);border:0;padding:6px 12px;font-size:12px;cursor:pointer;}
   .modes button.on{background:var(--blue);color:#0e1116;font-weight:700;}
@@ -95,6 +100,53 @@ const $=s=>document.querySelector(s);
 let DATA=[], META={}, filt='all', q='', sortKey='name';
 const esc=s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 
+// ── Agent CONTACT join (Steve HARD RULE 2026-08-18): the /api/agents/licensed feed carries NO
+//    phone/email, so join client-side against /api/brokers/all ({name,firm,phone,email,website})
+//    by agent NAME (precise — that's the set these agents were derived from), firm as fallback.
+//    Mirrors just-listed.html's brokerFor() + clean()/weburl() helpers. Never fabricates contact.
+const clean=v=>{const s=(v==null?'':String(v)).trim();return(!s||/^null/i.test(s))?null:s;}; // drop stringified "null"
+const weburl=w=>{w=clean(w);return w?(/^https?:\/\//i.test(w)?w:'http://'+w):null;};
+const nn=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'');                                   // name key
+const normFirm=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'').replace(/(inc|llc|corp|corporation|company|co|realty|realestate|group|properties|partners|associates)$/,'');
+let NAMEIDX={}, FIRMIDX={}, contactsLoaded=false;
+async function loadContacts(){
+  if(contactsLoaded) return; contactsLoaded=true;
+  try{
+    const r=await (await fetch('/api/brokers/all')).json();
+    const arr=(r&&(r.brokers||r.results||r))||[];
+    (Array.isArray(arr)?arr:[]).forEach(b=>{
+      const ph=clean(b.phone), em=clean(b.email);
+      const rec={name:clean(b.name),firm:clean(b.firm),phone:ph,email:em,website:weburl(b.website)};
+      const has=ph||em;
+      const nk=nn(b.name);
+      if(nk && (!NAMEIDX[nk] || (has && !(NAMEIDX[nk].phone||NAMEIDX[nk].email)))) NAMEIDX[nk]=rec;
+      const fk=normFirm(b.firm||b.name);
+      if(fk && (!FIRMIDX[fk] || (has && !(FIRMIDX[fk].phone||FIRMIDX[fk].email)))) FIRMIDX[fk]=rec;
+    });
+  }catch(e){}
+}
+// Return the best contact record for an agent (prefer a name match that actually has contact).
+function contactFor(a){
+  if(!a) return null;
+  const bn=NAMEIDX[nn(a.agent)];
+  if(bn && (bn.phone||bn.email)) return bn;
+  const bf=FIRMIDX[normFirm(a.brokerage)];
+  if(bf && (bf.phone||bf.email)) return bf;
+  return bn||bf||null;
+}
+// Always-visible phone + email markup. Falls back to the agent's DRE profile / directory when no contact exists.
+function contactHtml(a){
+  const b=contactFor(a);
+  const ph=b&&b.phone
+    ? `<a href="tel:${esc(b.phone.replace(/[^0-9+]/g,''))}" onclick="event.stopPropagation()">📞 ${esc(b.phone)}</a>`
+    : (a.dre_url?`<a href="${esc(a.dre_url)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">📞 via DRE ↗</a>`:'<span class="none">📞 contact via directory</span>');
+  const em=b&&b.email
+    ? `<a href="mailto:${esc(b.email)}" onclick="event.stopPropagation()">✉️ ${esc(b.email)}</a>`
+    : (a.dre_url?`<a href="${esc(a.dre_url)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">✉️ via DRE ↗</a>`:'<span class="none">✉️ contact via directory</span>');
+  const site=b&&b.website?` <a href="${esc(b.website)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">🌐 site</a>`:'';
+  return `<div class="contact">${ph}${em}${site}</div>`;
+}
+
 // ── Card data-point fields (Steve rule 2026-07-31: show/hide toggles, START TIGHT, expand for detail) ──
 // Every value the /api/agents/licensed feed carries per agent. `on` = the tight default set.
 const AGENT_FIELDS=[
@@ -131,6 +183,7 @@ function openAgent(a){
   const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on');
   mb.innerHTML='<h3>'+esc(a.agent)+(a.dre_verified?' <span class="verified" title="Verified against CA DRE public license record">✔</span>':'')+'</h3>'
     +'<div class="msub">'+esc(a.brokerage||'—')+' · <span class="badge '+statusClass(a)+'">'+esc(statusText(a))+'</span></div>'
+    +contactHtml(a)
     +'<div class="kv">'+AGENT_FIELDS.map(f=>'<span class="k">'+esc(f.l)+'</span><span class="v">'+fieldVal(a,f.k)+'</span>').join('')+'</div>';
 }
 
@@ -186,6 +239,7 @@ function cardHtml(a,idx){
       <span class="badge ${statusClass(a)}">${esc(statusText(a))}</span>
     </div>
     ${fields.length?`<div class="kv">`+fields.map(f=>`<span class="k">${esc(f.l)}</span><span class="v">${fieldVal(a,f.k)}</span>`).join('')+`</div>`:''}
+    ${contactHtml(a)}
   </div>`;
 }
 function pills(){
@@ -203,6 +257,7 @@ function pills(){
 }
 let aLoaded=false, AGENT_NOTE='';
 async function load(){
+  await loadContacts();   // ensure phone/email join is ready before first paint (Steve HARD RULE: always show contact)
   if(!aLoaded){
     let d; try{ d=await (await fetch('/api/agents/licensed')).json(); }catch(e){ $('#note').textContent='Failed to load /api/agents/licensed'; return; }
     DATA=d.agents||[]; META=d.meta||{};

← 853e086 licensed: always show professional phone+email (TK-10703)  ·  back to Commercialrealestate  ·  loan-officers: always show phone+email (TK-10703) a7b1904 →