← back to Commercialrealestate
licensed: always show professional phone+email (TK-10703)
853e0863955c26e0a09e3c2d624fd2b241f5bfcb · 2026-08-18 18:26:01 -0700 · Steve Abrams
Files touched
Diff
commit 853e0863955c26e0a09e3c2d624fd2b241f5bfcb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 18 18:26:01 2026 -0700
licensed: always show professional phone+email (TK-10703)
---
public/licensed.html | 46 ++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 44 insertions(+), 2 deletions(-)
diff --git a/public/licensed.html b/public/licensed.html
index 59abc59..0beb703 100644
--- a/public/licensed.html
+++ b/public/licensed.html
@@ -38,6 +38,11 @@ input#q{min-width:230px}
.figc{font-size:11.5px;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.figm{font-size:10px;color:var(--mut);font-variant-numeric:tabular-nums}
.empty{color:var(--mut);padding:40px;text-align:center}
+/* contact strip — Steve HARD RULE: ALWAYS show phone + email, clickable, on every card */
+.contact{display:flex;flex-wrap:wrap;gap:5px 12px;margin-top:2px;font-size:12.5px}
+.contact a{display:inline-flex;align-items:center;gap:4px;color:var(--blue)}
+.contact a:hover{text-decoration:underline}
+.contact .none{color:var(--mut);display:inline-flex;align-items:center;gap:4px}
/* data-point show/hide toggles (Steve list-build rule 2026-07-31) */
.dpwrap{display:flex;align-items:center;gap:6px;position:relative}
.dpbtn{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:6px 10px;font-size:12.5px;cursor:pointer}
@@ -84,6 +89,33 @@ input#q{min-width:230px}
const esc=s=>String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
const $=s=>document.querySelector(s);
let DATA={agents:[],firms:[],title:[],escrow:[]}, FIRM_IG={}, FIRM_IDX={}, CONTACTS={};
+let BROKERS={}; // normalized firm/name -> {name,firm,phone,email,website} from /api/brokers/all (join for real contacts)
+// ── broker contact join (Steve HARD RULE: ALWAYS show phone + email) ──
+const clean=v=>{const s=(v==null?'':String(v)).trim();return(!s||/^null/i.test(s))?null:s;};
+const weburl=w=>{w=clean(w);return w?(/^https?:\/\//i.test(w)?w:'http://'+w):null;};
+const normBk=s=>String(s||'').toLowerCase().replace(/[^a-z0-9]+/g,'').replace(/(inc|llc|corp|corporation|company|co|realty|realestate|group|properties|partners|associates)$/,'');
+// Match a directory row to a broker record: agents by firm then name; firm/title/escrow by name.
+function brokerFor(r){
+ if(!r) return null;
+ const keys = r.kind==='agent' ? [r.firm, r.name] : [r.name];
+ for(const key of keys){ const k=normBk(key); if(k && BROKERS[k] && (BROKERS[k].phone||BROKERS[k].email)) return BROKERS[k]; }
+ return null;
+}
+// ALWAYS-visible phone + email. Real tel:/mailto: when we have a broker match; otherwise a
+// graceful fallback link to the authoritative source (CA-DRE license lookup, else a firm web search).
+function contactBlock(r){
+ const b=brokerFor(r);
+ const lic=clean(r.license_number);
+ const dreUrl=lic?`https://www2.dre.ca.gov/PublicASP/pplinfo.asp?License_id=${encodeURIComponent(lic)}`
+ :`https://www.google.com/search?q=${encodeURIComponent((r.name||'')+' '+((r.city||'')+' ')+'real estate contact')}`;
+ const ph = b&&b.phone
+ ? `<a href="tel:${esc(b.phone.replace(/[^0-9+]/g,''))}" onclick="event.stopPropagation()" title="${esc(b.phone)}">📞 ${esc(b.phone)}</a>`
+ : `<a href="${esc(dreUrl)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="No phone on file — look it up">📞 <span class="none">look up ↗</span></a>`;
+ const em = b&&b.email
+ ? `<a href="mailto:${esc(b.email)}" onclick="event.stopPropagation()" title="${esc(b.email)}">✉️ ${esc(b.email)}</a>`
+ : `<a href="${esc(dreUrl)}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()" title="No email on file — look it up">✉️ <span class="none">look up ↗</span></a>`;
+ return `<div class="contact">${ph}${em}</div>`;
+}
let tab='firms', sort=localStorage.getItem('ld_sort')||'name';
// ── Data-point show/hide (Steve list-build rule 2026-07-31) ──
// Cards render as key·value chips per enabled field. START TIGHT: only the essentials
@@ -119,14 +151,22 @@ function igStrip(firm){const h=firmHandle(firm);if(!h)return '';const c=FIRM_IG[
return `<div class="figwrap"><div class="fighd">📷 @${esc(h)} · firm’s latest 3</div>${items}</div>`;}
async function boot(){
- const [ld,cache,map,contacts]=await Promise.all([
+ const [ld,cache,map,contacts,brk]=await Promise.all([
fetch('data/license-directory.json').then(r=>r.json()),
fetch('data/firm-ig-cache.json').then(r=>r.json()).catch(()=>({})),
fetch('data/firm-ig-map.json').then(r=>r.json()).catch(()=>({handles:{}})),
fetch('/api/contacts').then(r=>r.json()).then(j=>j.contacts||{}).catch(()=>({})),
+ fetch('/api/brokers/all').then(r=>r.json()).then(j=>j.brokers||j.results||[]).catch(()=>[]),
]);
DATA={agents:ld.agents||[],firms:ld.firms||[],title:ld.title||[],escrow:ld.escrow||[]};
FIRM_IG=cache; CONTACTS=contacts;
+ // index brokers by normalized firm AND name so directory rows can join for real phone/email
+ (Array.isArray(brk)?brk:[]).forEach(b=>{
+ const ph=clean(b.phone),em=clean(b.email);
+ [b.firm,b.name].forEach(key=>{const k=normBk(key);if(!k)return;
+ if(!BROKERS[k] || ((ph||em)&&!(BROKERS[k].phone||BROKERS[k].email)))
+ BROKERS[k]={name:clean(b.name),firm:b.firm,phone:ph,email:em,website:weburl(b.website)};});
+ });
for(const[h,al]of Object.entries(map.handles||{}))for(const a of al)FIRM_IDX[a]=h;
$('#scope').textContent=`· ${(ld.scope&&ld.scope.state)||'CA'} · ${(ld.scope&&ld.scope.metros||[]).join(', ')}`;
const T=[['firms','Firms',DATA.firms.length],['title','Title',DATA.title.length],['escrow','Escrow',DATA.escrow.length],['agents','Agents',DATA.agents.length]];
@@ -178,6 +218,7 @@ function card(r){
return `<div class="card" data-id="${esc(id)}">
<div class="ctop"><div class="nm">${esc(r.name)}</div><span class="kind ${r.kind}">${r.kind}</span></div>
<div class="meta">${metaBits.map(esc).join(' · ')}</div>
+ ${contactBlock(r)}
${igStrip(firmForIg)}
<div class="actions"><button class="btn mark" data-id="${esc(id)}">${c.contacted_at?'↻ Update':'✓ Mark contacted'}</button>${contacted}</div>
</div>`;
@@ -198,7 +239,8 @@ function openDetail(id){
}).filter(Boolean).join('');
const c=CONTACTS[id]||{};
const contacted=c.contacted_at?`<div class="mrow"><span class="mk">contacted</span><span class="mv">${esc(String(c.contacted_at).slice(0,10))}</span></div>`:'';
- mb.innerHTML=`<h3>${esc(r.name)} <span class="kind ${esc(r.kind)}">${esc(r.kind)}</span></h3>${rowsHtml}${contacted}`;
+ const contactRow=`<div class="mrow"><span class="mk">contact</span><span class="mv">${contactBlock(r)}</span></div>`;
+ mb.innerHTML=`<h3>${esc(r.name)} <span class="kind ${esc(r.kind)}">${esc(r.kind)}</span></h3>${contactRow}${rowsHtml}${contacted}`;
ov.classList.add('on');
}
document.addEventListener('click',async e=>{
← f97fd3d CRCP filters: responsive hybrid (collapsed rail desktop / ⚙
·
back to Commercialrealestate
·
licensed-agents: always show agent phone+email (TK-10703) 352846c →