[object Object]

← back to Commercialrealestate

auto-save: 2026-06-28T08:43:43 (2 files) — public/crcp.html scripts/serve.js

43075b0a3e9689fac3fd2f6611a9e1639eeabedf · 2026-06-28 08:43:50 -0700 · Steve Abrams

Files touched

Diff

commit 43075b0a3e9689fac3fd2f6611a9e1639eeabedf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Jun 28 08:43:50 2026 -0700

    auto-save: 2026-06-28T08:43:43 (2 files) — public/crcp.html scripts/serve.js
---
 public/crcp.html | 20 ++++++++++++++++++--
 scripts/serve.js | 33 ++++++++++++++++++++++++++++++++-
 2 files changed, 50 insertions(+), 3 deletions(-)

diff --git a/public/crcp.html b/public/crcp.html
index 03bc95f..55108b8 100644
--- a/public/crcp.html
+++ b/public/crcp.html
@@ -110,6 +110,7 @@ async function openContact(name, id){
     </div>
     <div class="sec"><h4>Current listings (${d.current.length})</h4>${d.current.length?d.current.map(l=>`<div class="litem"><span class="a">${l.address}, ${l.city}</span><span class="m">${l.type} · ${money(l.price)}${l.cap_rate?' · '+l.cap_rate+'% cap':''}</span></div>`).join(''):'<div class="meta">none in our set</div>'}</div>
     <div class="sec"><h4>Closed listings — past 10 yrs (${d.closed.length})</h4>${d.closed.length?d.closed.map(l=>`<div class="litem"><span class="a">${l.address}, ${l.city}</span><span class="m">${money(l.sold_price)} · ${l.sold_date||''}</span></div>`).join(''):'<div class="meta">'+(d.closedNote||'none')+'</div>'}</div>
+    ${(d.comps&&d.comps.length)?`<div class="sec"><h4>Recent area comps (${d.comps.length})</h4>${d.comps.map(c=>`<div class="litem"><span class="a">${c.address}, ${c.city}</span><span class="m">${money(c.sold_price)} · ${(c.sold_date||'').toString().slice(0,10)}</span></div>`).join('')}<div class="meta">${d.compsNote||''}</div></div>`:''}
     <div class="sec"><h4>Arcstone pitch (non-warrantable condo financing)</h4>
       <button class="btn" onclick="genPitch('${name.replace(/'/g,"\\'")}','email',${b.id})">✉ Generate email pitch</button>
       <button class="btn alt" onclick="genPitch('${name.replace(/'/g,"\\'")}','call_script',${b.id})">☎ Generate call script</button>
@@ -117,6 +118,19 @@ async function openContact(name, id){
     </div>`;
 }
 function renderPitch(p){ return `<div class="pitch">${p.subject?`<div class="subj">${p.subject}</div>`:''}${(p.body||'').replace(/&/g,'&amp;').replace(/</g,'&lt;')}</div><div class="gate">⚑ DRAFT — sending is George-gated + Steve-approved; not sent from here.</div>`; }
+async function openFirm(name){
+  const ov=$('#ov'), mb=$('#mbody'); ov.classList.add('on'); mb.innerHTML='<div class="meta">loading '+name+'…</div>';
+  let d; try{ d=await (await fetch('/api/firm?name='+encodeURIComponent(name))).json(); }catch(e){ mb.innerHTML='error'; return; }
+  if(d.error){ mb.innerHTML='<div class="meta">'+d.error+'</div>'; return; }
+  const a=d.agg||{};
+  mb.innerHTML=`<h2>${d.firm.name}</h2><div class="mfirm">brokerage firm</div>
+    <div class="cinfo"><span>👥 ${a.brokers||0} brokers/agents</span><span>🏠 ${a.residential||0} residential</span><span>☎ ${a.phone||0}</span><span>✉ ${a.email||0}</span></div>
+    <div class="sec"><h4>Roster (${d.roster.length}) — click anyone for full contact + pitch</h4>
+      ${d.roster.map(b=>`<div class="litem brow2" data-id="${b.id}" data-name="${(b.name||'').replace(/"/g,'&quot;')}" style="cursor:pointer">
+        <span class="a">${b.name} ${atype(b.agent_type)}</span>
+        <span class="m">${b.listings} lists · ${b.phone?'☎':'·'} ${b.email?'✉':'·'}</span></div>`).join('')}</div>`;
+  document.querySelectorAll('.brow2').forEach(r=>r.onclick=()=>openContact(r.dataset.name, r.dataset.id));
+}
 async function genPitch(name,channel,id){
   const out=$('#pitchout'); out.innerHTML='<div class="meta">drafting…</div>';
   try{ const r=await (await fetch('/api/broker/pitch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,channel,id})})).json();
@@ -164,8 +178,10 @@ async function poll(){
   // warrantability
   $('#wa').innerHTML = bar('FHA-approved', s.fhaApproved||0, (s.fhaApproved||0)+(s.fhaExpired||0), 'var(--active)')
     + bar('FHA-expired (flag/review)', s.fhaExpired||0, (s.fhaApproved||0)+(s.fhaExpired||0), 'var(--staged)');
-  // firms
-  $('#fmTbl tbody').innerHTML=(s.topFirms||[]).map(f=>`<tr><td>${f.firm}</td><td class="n">${f.listings}</td><td class="n" style="color:var(--draft)">${f.brokers}</td></tr>`).join('');
+  // firms (clickable → roster)
+  $('#fmC').textContent='click a firm';
+  $('#fmTbl tbody').innerHTML=(s.topFirms||[]).map(f=>`<tr class="frow" data-firm="${(f.firm||'').replace(/"/g,'&quot;')}" style="cursor:pointer"><td>${f.firm}</td><td class="n">${f.listings}</td><td class="n" style="color:var(--draft)">${f.brokers}</td></tr>`).join('');
+  document.querySelectorAll('.frow').forEach(r=>r.onclick=()=>openFirm(r.dataset.firm));
   prev=s;
   // flash changed tiles
   document.querySelectorAll('.delta.show').forEach(d=>d.closest('.tile').classList.add('flash'));
diff --git a/scripts/serve.js b/scripts/serve.js
index cec60dd..9d625d9 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -277,7 +277,38 @@ app.get('/api/broker', async (req, res) => {
       ORDER BY price DESC NULLS LAST`, [broker.id]);
     const closed = await q(`SELECT address, city, sold_price, sold_date, type, source FROM broker_closed_listing WHERE broker_id=$1 ORDER BY sold_date DESC NULLS LAST`, [broker.id]).catch(() => []);
     const pitches = await q(`SELECT id, channel, subject, body, status, created_at FROM broker_pitch WHERE broker_id=$1 ORDER BY created_at DESC`, [broker.id]).catch(() => []);
-    res.json({ broker, current, closed, pitches, closedNote: closed.length ? null : 'Public 10-yr closed-sales enrichment not yet run (gated public-records pull).' });
+    // Market comps: recent closed_sale (Redfin sold, ~last 5yr) in the cities this broker lists in.
+    // Honest area context — NOT attributed to this broker (the sold feed carries no agent).
+    const cities = [...new Set(current.map(c => c.city).filter(Boolean))].slice(0, 6);
+    let comps = [];
+    if (cities.length) comps = await q(
+      `SELECT address, city, sold_price, sold_date FROM closed_sale WHERE city = ANY($1) AND sold_price>0
+       ORDER BY sold_date DESC NULLS LAST LIMIT 8`, [cities]).catch(() => []);
+    res.json({ broker, current, closed, pitches, comps,
+      compsNote: comps.length ? 'Recent sold comps in this broker’s areas (Redfin, ~5yr) — area context, not this broker’s own closings.' : null,
+      closedNote: closed.length ? null : 'Per-broker closed history needs a gated detail-scrape; recent area comps shown instead.' });
+  } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0] }); }
+});
+
+// Firm detail — the roster (brokers/agents at the firm) + aggregate listings. Powers the click-through
+// on a firm row. Read-only.
+app.get('/api/firm', async (req, res) => {
+  if (!brokerdb) return res.status(503).json({ error: 'broker DB unavailable' });
+  const name = String(req.query.name || '').trim();
+  if (!name) return res.status(400).json({ error: 'name required' });
+  try {
+    const q = (s, a) => brokerdb.pool.query(s, a).then(r => r.rows);
+    const firm = (await q(`SELECT id, name FROM firm WHERE name ILIKE $1 LIMIT 1`, [name]))[0];
+    if (!firm) return res.status(404).json({ error: 'firm not found' });
+    const roster = await q(
+      `SELECT b.id, b.name, b.agent_type, b.phone, b.email, b.website,
+              (SELECT count(*) FROM broker_listing bl WHERE bl.broker_id=b.id)
+              + (SELECT count(*) FROM broker_condo bc WHERE bc.broker_id=b.id) AS listings
+         FROM broker b WHERE b.firm_id=$1 ORDER BY listings DESC, b.name LIMIT 200`, [firm.id]);
+    const agg = (await q(`SELECT count(*)::int brokers,
+        count(*) FILTER (WHERE agent_type='residential')::int residential,
+        count(phone)::int phone, count(email)::int email FROM broker WHERE firm_id=$1`, [firm.id]))[0];
+    res.json({ firm, roster, agg });
   } catch (e) { res.status(502).json({ error: String(e.message).split('\n')[0] }); }
 });
 

← 8e18085 auto-save: 2026-06-28T08:13:35 (2 files) — scripts/fetch-clo  ·  back to Commercialrealestate  ·  CRE residential agents: final capture artifact — 637 agents ed38493 →