← back to Commercialrealestate
licensed-agents: always show agent phone+email (TK-10703)
352846cdbb1107ee964709c338b6ac4a2711c45e · 2026-08-18 18:26:11 -0700 · Steve Abrams
Files touched
M public/licensed-agents.html
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=>({'&':'&','<':'<','>':'>','"':'"'}[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 →