← back to Commercialrealestate
public/firms.html
102 lines
<!doctype html>
<html lang="en">
<head>
<script>(function(){try{var t=localStorage.getItem('crcp-theme');document.documentElement.setAttribute('data-theme',t==='light'?'light':'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>
<!-- Self-heal: nav-agent grid controls can persist a "removed"/"collapsed" state that hides the
whole grid with no obvious way back. Clear it before nav-agent runs so a grid can never stay hidden. -->
<script>(function(){try{for(var i=localStorage.length-1;i>=0;i--){var k=localStorage.key(i);if(k&&k.indexOf('navagent:')===0){var v=localStorage.getItem(k);if(v&&v.indexOf('"removed":true')>=0){localStorage.removeItem(k);}}}}catch(e){}})();</script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='%230c0f15'/%3E%3Ctext x='8' y='12' font-size='9' text-anchor='middle' fill='%23C8A24B'%3E%F0%9F%8F%A2%3C/text%3E%3C/svg%3E">
<title>Firms — LA County CRE brokerages (broker rosters)</title>
<link rel="stylesheet" href="/lending-grid.css">
<link rel="stylesheet" href="/nav-agent/nav-agent.css"><!-- nav-agent -->
<link rel="stylesheet" href="/crcp-theme.css"><!-- CRCP day/night tokens: load LAST so html[data-theme] wins -->
</head>
<body>
<header>
<h1>🏢 <b>Firms</b> — brokerages</h1>
<div class="nav"><a href="/broker-grid.html">← Brokers</a><a href="/brokers.html">🕸️ Mind-map</a><a href="/crcp.html">🛰️ CRCP</a><a href="/gov-agents.html">🏛️ Gov Agents</a></div>
<span class="spacer"></span>
<input class="search" id="q" type="text" placeholder="search all fields — firm, broker count, listings… (space = AND)">
<div class="toggle" id="view"><button data-v="table" class="on">▦ List</button><button data-v="grid">▤ Grid</button></div>
<select id="sortsel" title="Sort"></select>
<label id="denswrap" title="Card density — grid view">⤢<input type="range" id="density" min="1" max="10" step="1"></label>
<button class="csvbtn" id="csv" title="Export current rows to CSV">⬇ CSV</button>
<span class="count" id="count">…</span>
</header>
<div class="shell">
<aside class="rail">
<div id="railFacets"></div>
<div class="rsec"><h4>Fields / columns — show / hide</h4>
<div class="fieldtoggles" id="fFields"></div>
<div style="margin-top:8px"><button class="minibtn" id="fAll">All</button><button class="minibtn" id="fNone">None</button><button class="minibtn" id="fReset" title="Reset columns to the tight default">↺ Reset</button></div>
<div class="mut" style="font-size:10.5px;margin-top:7px;line-height:1.35">Drag a column header to reorder · drag its right edge to resize · click to sort.</div>
</div>
</aside>
<div class="wrap">
<div class="databar"><span class="pubtag">Registry</span><span id="demoNote">usre broker / firm registry — every firm with a broker roster</span></div>
<div id="tableView" class="tblwrap"><table class="g" data-no-sort><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
<div id="gridView" class="grid hide"></div>
</div>
</div>
<div id="ov"><div id="modal"><button class="x" aria-label="Close" onclick="document.getElementById('ov').classList.remove('on')">✕</button><div id="mbody"></div></div></div>
<script>
window.GRID_CONFIG = {
ns: 'crcpFirms', noun: 'firms', nameKey: 'firm', idKey: 'firm',
title: 'Brokerage firm · LA County CRE',
api: '/api/firms', dataKey: 'firms',
sortKey: 'brokers', sortDir: -1,
defaultCols: ['firm', 'brokers', 'commercial', 'residential', 'listings', 'with_phone'],
cols: [
{ k: 'firm', l: 'Firm', t: 's', g: 'Identity', max: 48 },
{ k: 'brokers', l: 'Brokers', t: 'n', g: 'Roster' },
{ k: 'commercial', l: 'Commercial', t: 'n', g: 'Roster' },
{ k: 'residential', l: 'Residential', t: 'n', g: 'Roster' },
{ k: 'listings', l: 'Active listings', t: 'n', g: 'Activity' },
{ k: 'with_phone', l: 'Has phone', t: 'n', g: 'Contactable' },
{ k: 'with_email', l: 'Has email', t: 'n', g: 'Contactable' },
],
facets: [
{ k: 'size', l: 'Firm size', derive: r => r.brokers >= 20 ? '20+ brokers' : r.brokers >= 5 ? '5–19 brokers' : r.brokers >= 2 ? '2–4 brokers' : 'solo', limit: 5 },
{ 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 },
],
// #2 data hygiene — drop junk/placeholder firm names (a name with no alphanumeric char) before they clutter a sort.
rowOk: r => !!(r.firm && /[A-Za-z0-9]/.test(r.firm)),
// #1 firm → broker → property drill (handled by the __firmsUI module below so the "← back" nav works).
detail: firm => window.__firmsUI.openFirm(firm.firm),
};
</script>
<script>
// 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 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" href="/agent.html?id=${encodeURIComponent(b.id)}&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 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>` : '');
}
return { openFirm: name => buildFirm(name) };
})();
</script>
<script src="/column-manager.js" defer></script>
<script src="/lending-grid.js" defer></script>
<script src="/col-resize.js" defer></script>
<script src="/corner-nav.js" defer></script>
<script src="/nav-agent/nav-agent.js" defer></script><!-- nav-agent -->
<script src="/crcp-theme.js" defer></script><!-- CRCP day/night toggle (auto-injects into header) -->
</body>
</html>