[object Object]

← back to Commercialrealestate

CRCP firms: roster brokers now deep-link to their canonical /agent.html profile page (our own page, no Crexi) in a new tab, replacing the in-modal drill; firm → broker profile → their listings

84a87bc91d05b97649df2fd272a980d2be937ec6 · 2026-08-19 09:10:12 -0700 · Steve Abrams

Files touched

Diff

commit 84a87bc91d05b97649df2fd272a980d2be937ec6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 09:10:12 2026 -0700

    CRCP firms: roster brokers now deep-link to their canonical /agent.html profile page (our own page, no Crexi) in a new tab, replacing the in-modal drill; firm → broker profile → their listings
---
 public/firms.html | 37 +++++++------------------------------
 1 file changed, 7 insertions(+), 30 deletions(-)

diff --git a/public/firms.html b/public/firms.html
index 94362b9..638daf5 100644
--- a/public/firms.html
+++ b/public/firms.html
@@ -69,49 +69,26 @@ window.GRID_CONFIG = {
 };
 </script>
 <script>
-// firm → broker → property drill for the detail modal. buildFirm renders the roster (each broker
-// clickable) + firm listings; clicking a broker renders THEIR listings with a "← back to firm" link.
+// Firm detail modal: roster (each broker DEEP-LINKS to their canonical /agent.html profile page —
+// "our own page, no Crexi") + the firm's active listings. Names can collide, so the profile page
+// resolves the best-matching broker; a ↗ marks the external-tab nav so the firm roster stays open.
 window.__firmsUI = (function () {
   const esc = s => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
   const money = n => (n == null || n === '') ? '' : '$' + Number(n).toLocaleString();
-  const norm = u => /^https?:/.test(u) ? u : 'https://' + u;
-  let CURFIRM = null;
-  const listRow = l => `<div class="litem"><span class="m">${esc(l.address || '—')}${l.city ? ', ' + esc(l.city) : ''}</span><span>${l.price ? money(l.price) : (l.sold_price ? money(l.sold_price) + ' sold' : '')}${l.units ? ' · ' + l.units + 'u' : ''}${l.type ? ' · ' + esc(l.type) : ''}${l.sold_date ? ' · ' + esc(String(l.sold_date).slice(0, 10)) : ''}</span></div>`;
+  const listRow = l => `<div class="litem"><span class="m">${esc(l.address || '—')}${l.city ? ', ' + esc(l.city) : ''}</span><span>${l.price ? money(l.price) : (l.sold_price ? money(l.sold_price) + ' sold' : '')}${l.units ? ' · ' + l.units + 'u' : ''}${l.type ? ' · ' + esc(l.type) : ''}</span></div>`;
   async function buildFirm(name) {
     const d = await fetch('/api/firm?name=' + encodeURIComponent(name)).then(x => x.json());
     if (d.error) return `<h3>${esc(name)}</h3><div class="sec">Detail unavailable — ${esc(d.error)}</div>`;
     const agg = d.agg || {};
     const roster = (d.roster || []).map(b =>
-      `<div class="litem"><span class="m"><a class="dl brokerlink" data-bid="${esc(b.id)}">${esc(b.name)}</a>${b.agent_type ? ' · ' + esc(b.agent_type) : ''}</span><span>${b.phone ? '☎ <a class="dl" href="tel:' + esc(b.phone) + '">' + esc(b.phone) + '</a>  ' : ''}${b.email ? '✉ <a class="dl" href="mailto:' + esc(b.email) + '">' + esc(b.email) + '</a>  ' : ''}${b.listings && +b.listings ? '· ' + esc(b.listings) + ' listings' : ''}</span></div>`).join('');
+      `<div class="litem"><span class="m"><a class="dl brokerlink" href="/agent.html?name=${encodeURIComponent(b.name || '')}" target="_blank" rel="noopener noreferrer" title="Open ${esc(b.name)}'s profile">${esc(b.name)} ↗</a>${b.agent_type ? ' · ' + esc(b.agent_type) : ''}</span><span>${b.phone ? '☎ <a class="dl" href="tel:' + esc(b.phone) + '">' + esc(b.phone) + '</a>  ' : ''}${b.email ? '✉ <a class="dl" href="mailto:' + esc(b.email) + '">' + esc(b.email) + '</a>  ' : ''}${b.listings && +b.listings ? '· ' + esc(b.listings) + ' listings' : ''}</span></div>`).join('');
     const listings = (d.current || []).slice(0, 25).map(listRow).join('');
     return `<h3>${esc(name)}</h3>` +
       `<div class="mfirm">Brokerage firm · ${agg.brokers || 0} brokers (${agg.residential || 0} residential) · ${agg.phone || 0}☎ ${agg.email || 0}✉</div>` +
-      `<div class="sec"><h4>Roster (${(d.roster || []).length}) — click a broker for their listings</h4><div class="rosterscroll">${roster || '<div class="miss">—</div>'}</div></div>` +
+      `<div class="sec"><h4>Roster (${(d.roster || []).length}) — click a broker for their profile ↗</h4><div class="rosterscroll">${roster || '<div class="miss">—</div>'}</div></div>` +
       (listings ? `<div class="sec"><h4>Firm active listings (top ${Math.min(25, (d.current || []).length)} of ${(d.current || []).length})</h4>${listings}</div>` : '');
   }
-  async function buildBroker(id) {
-    const d = await fetch('/api/broker?id=' + encodeURIComponent(id)).then(x => x.json());
-    const back = CURFIRM ? `<div style="margin-bottom:8px"><a class="dl backfirm">← ${esc(CURFIRM)}</a></div>` : '';
-    if (d.error) return `${back}<div class="sec">Broker detail unavailable — ${esc(d.error)}</div>`;
-    const b = d.broker || {};
-    const contact = `${b.phone ? '☎ <a class="dl" href="tel:' + esc(b.phone) + '">' + esc(b.phone) + '</a>  ' : ''}${b.email ? '✉ <a class="dl" href="mailto:' + esc(b.email) + '">' + esc(b.email) + '</a>' : ''}` || '—';
-    const cur = (d.current || []).map(listRow).join('');
-    const closed = (d.closed || []).map(listRow).join('');
-    return back + `<h3>${esc(b.name || '—')}</h3>` +
-      `<div class="mfirm">${esc(b.title || b.agent_type || 'Broker')}${b.firm ? ' · ' + esc(b.firm) : ''}</div>` +
-      `<div class="sec"><div class="litem"><span class="m">Contact</span><span>${contact || '—'}</span></div>` +
-        (b.website ? `<div class="litem"><span class="m">Website</span><span><a class="dl" href="${esc(norm(b.website))}" target="_blank" rel="noopener noreferrer">${esc(b.website)}↗</a></span></div>` : '') +
-        (b.dre_license ? `<div class="litem"><span class="m">DRE #</span><span>${esc(b.dre_license)}</span></div>` : '') + `</div>` +
-      `<div class="sec"><h4>Active listings (${(d.current || []).length})</h4><div class="rosterscroll">${cur || '<div class="miss">—</div>'}</div></div>` +
-      (closed ? `<div class="sec"><h4>Closed (${(d.closed || []).length})</h4><div class="rosterscroll">${closed}</div></div>` : '');
-  }
-  document.addEventListener('click', async e => {
-    const bl = e.target.closest('#mbody .brokerlink');
-    if (bl) { e.preventDefault(); const mb = document.getElementById('mbody'); mb.innerHTML = '<div class="sec" style="opacity:.6">Loading broker…</div>'; mb.innerHTML = await buildBroker(bl.dataset.bid); return; }
-    const bf = e.target.closest('#mbody .backfirm');
-    if (bf && CURFIRM) { e.preventDefault(); const mb = document.getElementById('mbody'); mb.innerHTML = '<div class="sec" style="opacity:.6">Loading…</div>'; mb.innerHTML = await buildFirm(CURFIRM); return; }
-  });
-  return { openFirm: name => { CURFIRM = name; return buildFirm(name); } };
+  return { openFirm: name => buildFirm(name) };
 })();
 </script>
 <script src="/column-manager.js" defer></script>

← 7e3bea3 backfill: broker-website listing scraper (plain-fetch pilot  ·  back to Commercialrealestate  ·  broker-grid: agent name -> /agent.html?name= (our profile pa 83593fe →