[object Object]

← back to Commercialrealestate

CRCP: every agent opens its own page — link agent names in the explorer (list/card/agent-window) to /agent.html; agent page + /api/agent-profile now show CURRENT and PAST (closed/sold) listings with a track-record stat (Steve: 'errol should have its own page with current/past listings')

b422f1180dd17cb3ec264952c7c6480cedc404dc · 2026-08-19 09:19:54 -0700 · Steve Abrams

Files touched

Diff

commit b422f1180dd17cb3ec264952c7c6480cedc404dc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 19 09:19:54 2026 -0700

    CRCP: every agent opens its own page — link agent names in the explorer (list/card/agent-window) to /agent.html; agent page + /api/agent-profile now show CURRENT and PAST (closed/sold) listings with a track-record stat (Steve: 'errol should have its own page with current/past listings')
---
 public/agent.html | 26 +++++++++++++++++++++-----
 public/index.html |  7 ++++---
 scripts/serve.js  |  8 +++++++-
 3 files changed, 32 insertions(+), 9 deletions(-)

diff --git a/public/agent.html b/public/agent.html
index 2193923..57716ec 100644
--- a/public/agent.html
+++ b/public/agent.html
@@ -48,12 +48,16 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
 <div class="wrap">
   <div id="hero" class="hero"><div class="who"><h2 id="loading">Loading…</h2></div></div>
   <div class="toolbar">
-    <h3>Their listings <span class="count" id="count"></span></h3>
+    <h3>Current listings <span class="count" id="count"></span></h3>
     <span style="margin-left:auto"></span>
     <select id="sort"></select>
   </div>
   <div id="grid" class="grid"></div>
-  <div class="empty" id="empty" hidden>No listings on record for this agent in our data.</div>
+  <div class="empty" id="empty" hidden>No active listings on record for this agent in our data.</div>
+  <div id="pastwrap" hidden>
+    <div class="toolbar"><h3>Past · closed (sold) <span class="count" id="pastcount"></span></h3></div>
+    <div id="pastgrid" class="grid"></div>
+  </div>
   <div class="note" id="prov"></div>
 </div>
 <script>
@@ -77,11 +81,17 @@ function renderHero(a){
       `<div class="meta">${[type,a.license?'DRE '+esc(a.license):'',a.office?esc(a.office):''].filter(Boolean).join(' · ')||'&nbsp;'}</div>`+
       `<div class="contact">${phone}${email}${lin}</div></div>`+
     `<div class="stats">`+
-      `<div class="stat"><b>${(a.count||0).toLocaleString()}</b><span>listings</span></div>`+
-      `<div class="stat"><b class="g">$${((a.total_value||0)/1e6).toFixed(1)}M</b><span>total value</span></div>`+
+      `<div class="stat"><b>${(a.count||0).toLocaleString()}</b><span>current</span></div>`+
+      `<div class="stat"><b>${(a.closed_count||0).toLocaleString()}</b><span>closed (sold)</span></div>`+
+      `<div class="stat"><b class="g">$${((a.total_value||0)/1e6).toFixed(1)}M</b><span>active value</span></div>`+
       `<div class="stat"><b>${(a.cities||[]).length}</b><span>cities</span></div>`+
     `</div>`;
 }
+function closedCard(l){
+  return `<div class="lcard"><div class="addr">${esc(l.address||'—')}</div>`+
+    `<div class="sub">${esc(l.city||'')} · ${esc(l.type||'—')}${l.sold_date?' · sold '+esc(String(l.sold_date).slice(0,10)):''}</div>`+
+    `<div class="price">${money(l.sold_price)||'—'}<span class="k" style="font-weight:400;font-size:11px"> sold</span></div></div>`;
+}
 function cardHtml(l){
   const ppu=(l.price&&l.units)?Math.round(l.price/l.units):null;
   return `<div class="lcard"><div class="addr">${esc(l.address||'—')}</div>`+
@@ -110,7 +120,13 @@ else fetch('/api/agent-profile?name='+encodeURIComponent(NAME)).then(r=>r.json()
   renderHero(a);
   LISTINGS=a.listings||[];
   render();
-  $('#prov').innerHTML=`Profile built from Designer/CRCP's own broker graph — agent resolved by name, listings from our records. No aggregator (Crexi/LoopNet/etc.) data is used or shown.`;
+  const closed=a.closed||[];
+  if(closed.length){
+    $('#pastgrid').innerHTML=closed.map(closedCard).join('');
+    $('#pastcount').textContent=`(${closed.length})`;
+    $('#pastwrap').hidden=false;
+  }
+  $('#prov').innerHTML=`Profile built from Designer/CRCP's own broker graph — agent resolved by name; current + past (closed/sold) listings from our records. No aggregator (Crexi/LoopNet/etc.) data is used or shown.`;
 }).catch(()=>{ $('#hero').innerHTML=`<div class="who"><h2>${esc(NAME)}</h2><div class="meta">Could not load this agent.</div></div>`; });
 </script>
 <script src="/corner-nav.js" defer></script>
diff --git a/public/index.html b/public/index.html
index 988daa6..f7b9ff0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -295,6 +295,7 @@
   .composeH .flink { font-size:11px; font-weight:400; margin-left:8px; }
   /* Inline (no-outside-link) agent bits */
   .aname { font-weight:600; color:var(--ink); }
+  a.aname { text-decoration:none; cursor:pointer; } a.aname:hover { color:var(--blue); text-decoration:underline; }
   .flink { cursor:pointer; }
   .listtbl td .flink { border-bottom:1px dotted #3a4453; }
   .listtbl td .flink:hover { color:var(--blue); border-bottom-color:var(--blue); }
@@ -981,7 +982,7 @@ function expandBody(p){
   if(firm&&firm!=='Unknown') deep.push(`<button data-firmfilter="${esc(firm).replace(/"/g,'&quot;')}" title="Show only ${esc(firm)} listings">▦ More from ${esc(firm)}</button>`);
   // Agent summary + notes preview. Full editing (info, notes, letters, draft) lives in the inline
   // agent window that ✎ Open launches — no external search, no new tabs.
-  const summary=`<div class="crm-summary">${a.name?`<span class="aname">${esc(a.name)}</span> `:'<span class="amut">no agent on file — add via ✎ Open</span> '}${contactBits(a)}`
+  const summary=`<div class="crm-summary">${a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a> `:'<span class="amut">no agent on file — add via ✎ Open</span> '}${contactBits(a)}`
     +`${a.contacted_at?`<span class="achip done">contacted ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip">✎ ${a.letters.length} saved</span>`:''}</div>`
     +(a.notes?`<div class="crm-note">🗒 ${esc(a.notes)}</div>`:'');
   return `<div class="src">${deep.join(' ')}</div>`
@@ -1233,7 +1234,7 @@ function contactBits(a){
 // The Agent · contact column cell: name + copyable phone/email + Open/Mark buttons + chips.
 function agentContactCell(p){
   const a=agentInfo(p);
-  const nm=a.name?`<span class="aname">${esc(a.name)}</span>`:`<span class="amut">no agent on file</span>`;
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:`<span class="amut">no agent on file</span>`;
   const contacted=a.contacted_at?`<span class="achip done" title="Marked contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:'';
   const lc=a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:'';
   const nt=a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:'';
@@ -1336,7 +1337,7 @@ function card(p){
 // contact modal + mark-contacted as the list rows. Compact: name/phone/email + chips + buttons.
 function cardAgentBlock(p){
   const a=agentInfo(p), id=p.id;
-  const nm=a.name?`<span class="aname">${esc(a.name)}</span>`:`<span class="amut">no agent on file</span>`;
+  const nm=a.name?`<a class="aname" href="/agent.html?name=${encodeURIComponent(a.name)}" target="_blank" rel="noopener noreferrer" title="Open this agent's own page — current & past listings">${esc(a.name)} ↗</a>`:`<span class="amut">no agent on file</span>`;
   const chips=`${a.contacted_at?`<span class="achip done" title="Contacted ${esc(a.contacted_at)}">✓ ${fmtWhen(a.contacted_at)}</span>`:''}${a.letters.length?`<span class="achip" title="${a.letters.length} saved letter(s)">✎ ${a.letters.length}</span>`:''}${a.notes?`<span class="achip" title="Has notes">🗒 notes</span>`:''}`;
   return `<div class="cardagent" id="cardagent-${id}">`
     +`<div class="ca-top">🧑‍💼 ${nm} ${chips}</div>`
diff --git a/scripts/serve.js b/scripts/serve.js
index d910441..3ceb5f1 100644
--- a/scripts/serve.js
+++ b/scripts/serve.js
@@ -355,12 +355,18 @@ app.get('/api/agent-profile', async (req, res) => {
       `SELECT l.address, l.city, l.zip, l.type, l.price, l.units, l.cap_rate
          FROM broker_listing bl JOIN listing l ON l.id = bl.listing_id
         WHERE bl.broker_id = $1 ORDER BY l.price DESC NULLS LAST`, [b.id])).rows;
+    // Past / closed (sold) listings — the agent's track record, mirrors /api/broker.
+    const closed = (await brokerdb.pool.query(
+      `SELECT address, city, sold_price, sold_date, type, source
+         FROM broker_closed_listing WHERE broker_id = $1 ORDER BY sold_date DESC NULLS LAST`, [b.id]).catch(() => ({ rows: [] }))).rows;
     res.json({
       found: true, id: b.id, name: b.name, firm: b.firm, phone: b.phone, email: b.email,
       title: b.title, agent_type: b.agent_type, license: b.license, office: b.office_addr, linkedin: b.linkedin,
       listings, count: listings.length,
+      closed, closed_count: closed.length,
       total_value: listings.reduce((s, l) => s + Number(l.price || 0), 0),
-      cities: [...new Set(listings.map(l => l.city).filter(Boolean))],
+      closed_value: closed.reduce((s, l) => s + Number(l.sold_price || 0), 0),
+      cities: [...new Set(listings.map(l => l.city).concat(closed.map(l => l.city)).filter(Boolean))],
     });
   } catch (e) { res.json({ name, firm: null, listings: [], found: false, error: String(e.message).split('\n')[0] }); }
 });

← 83593fe broker-grid: agent name -> /agent.html?name= (our profile pa  ·  back to Commercialrealestate  ·  backfill: garbage-address filter + unique-domain-only attrib 48a8e85 →