← 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, '&').replace(/</g, '<').replace(/"/g, '"');
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 →