← back to Commercialrealestate
CRCP refine: add optional async CFG.detail hook to lending-grid engine (backward-compatible); firms drill a firm → full broker roster + active listings via /api/firm (click-to-call/email), turning the directory navigable
6e1d7c57b3cace91b4a2300bee8174e57efc4728 · 2026-08-19 08:57:10 -0700 · Steve Abrams
Files touched
M public/firms.htmlM public/lending-grid.js
Diff
commit 6e1d7c57b3cace91b4a2300bee8174e57efc4728
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 19 08:57:10 2026 -0700
CRCP refine: add optional async CFG.detail hook to lending-grid engine (backward-compatible); firms drill a firm → full broker roster + active listings via /api/firm (click-to-call/email), turning the directory navigable
---
public/firms.html | 16 ++++++++++++++++
public/lending-grid.js | 10 ++++++++++
2 files changed, 26 insertions(+)
diff --git a/public/firms.html b/public/firms.html
index 6f73f47..7587a21 100644
--- a/public/firms.html
+++ b/public/firms.html
@@ -62,6 +62,22 @@ window.GRID_CONFIG = {
{ k: 'activity', l: 'Listing activity', derive: r => r.listings >= 10 ? '10+ listings' : r.listings >= 1 ? '1–9 listings' : 'no active listings', limit: 4 },
{ k: 'mix', l: 'Roster mix', derive: r => r.commercial > 0 && r.residential > 0 ? 'commercial + residential' : r.commercial > 0 ? 'commercial-only' : 'residential-only', limit: 4 },
],
+ // Click a firm → drill into its full broker roster + active listings (via /api/firm),
+ // instead of the generic aggregate-field dump. Turns the directory into a navigable one.
+ detail: async (firm, h) => {
+ const esc = h.esc, money = h.money;
+ const d = await fetch('/api/firm?name=' + encodeURIComponent(firm.firm)).then(x => x.json());
+ if (d.error) throw new Error(d.error);
+ const agg = d.agg || {};
+ const roster = (d.roster || []).map(b =>
+ `<div class="litem"><span class="m">${esc(b.name)}${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(l =>
+ `<div class="litem"><span class="m">${esc(l.address || '—')}${l.city ? ', ' + esc(l.city) : ''}</span><span>${l.price ? money(l.price) : ''}${l.units ? ' · ' + l.units + 'u' : ''}${l.type ? ' · ' + esc(l.type) : ''}</span></div>`).join('');
+ return `<h3>${esc(firm.firm)}</h3>` +
+ `<div class="mfirm">Brokerage firm · ${agg.brokers || firm.brokers || 0} brokers (${agg.residential || 0} residential) · ${agg.phone || 0}☎ ${agg.email || 0}✉</div>` +
+ `<div class="sec"><h4>Roster (${(d.roster || []).length})</h4>${roster || '<div class="miss">—</div>'}</div>` +
+ (listings ? `<div class="sec"><h4>Active listings (top ${Math.min(25, (d.current || []).length)} of ${(d.current || []).length})</h4>${listings}</div>` : '');
+ },
};
</script>
<script src="/column-manager.js" defer></script>
diff --git a/public/lending-grid.js b/public/lending-grid.js
index 3177ea0..b95a177 100644
--- a/public/lending-grid.js
+++ b/public/lending-grid.js
@@ -184,6 +184,16 @@
const r = DATA.find(x => String(x[idKey]) === String(id)); if (!r) return;
const ov = $('#ov'), mb = $('#mbody'); ov.classList.add('on');
const nameKey = CFG.nameKey || COLS[0].k;
+ // Optional per-page rich detail (e.g. firms.html drills a firm → its broker roster + listings
+ // via /api/firm). Async-safe + backward-compatible: pages without CFG.detail keep the generic
+ // field/evidence dump below. Helpers are passed so the page needn't re-implement formatters.
+ if (typeof CFG.detail === 'function') {
+ mb.innerHTML = `<h3>${esc(r[nameKey])}</h3><div class="mfirm">${esc(CFG.title || '')}</div><div class="sec" style="opacity:.6">Loading…</div>`;
+ Promise.resolve(CFG.detail(r, { esc, money, money000, num, pct, dt, link, siteName })).then(html => {
+ if (typeof html === 'string' && ov.classList.contains('on')) mb.innerHTML = html;
+ }).catch(err => { mb.innerHTML = `<h3>${esc(r[nameKey])}</h3><div class="sec">Detail unavailable — ${esc(String((err && err.message) || err))}</div>`; });
+ return;
+ }
const rows = COLS.filter(c => r[c.k] != null && r[c.k] !== '' && c.k !== nameKey)
.map(c => `<div class="litem"><span class="m">${esc(c.l)}</span><span>${fmt(r, c)}</span></div>`).join('');
const ev = r.source_url
← 3a8c168 crawl-broker-listings: read sale[]/lease[] payloads + honor
·
back to Commercialrealestate
·
CRCP: paginate Showcase (16 -> 440 LA for-sale listings, ~27 2f6ff29 →