[object Object]

← back to Commercialrealestate

deals-flow: 'look up any LA property' address box -> dossier (TK-10313 Cycle 4)

1d92fcb954074b0cdc708eaa05cba94ba2d94e51 · 2026-08-06 18:07:15 -0700 · Steve Abrams

Adds a lookup bar: type any LA address -> /api/property/by-address -> the same
public-record dossier modal. Refactored the renderer into renderDossier(d) reused
by both the by-AIN drill and by-address lookup; multi-parcel matches show a
pick-list (.pdpick -> openPropDetail). Enter + button wired; in-flight guard kept.
Extends enrichment to ANY property, not just grid deals. Verified via Chrome
(single hit, 8-candidate pick-list, pick->dossier). Use-code normalization
deferred (needs authoritative LA use-code legend to avoid mislabels). $0 local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 1d92fcb954074b0cdc708eaa05cba94ba2d94e51
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 6 18:07:15 2026 -0700

    deals-flow: 'look up any LA property' address box -> dossier (TK-10313 Cycle 4)
    
    Adds a lookup bar: type any LA address -> /api/property/by-address -> the same
    public-record dossier modal. Refactored the renderer into renderDossier(d) reused
    by both the by-AIN drill and by-address lookup; multi-parcel matches show a
    pick-list (.pdpick -> openPropDetail). Enter + button wired; in-flight guard kept.
    Extends enrichment to ANY property, not just grid deals. Verified via Chrome
    (single hit, 8-candidate pick-list, pick->dossier). Use-code normalization
    deferred (needs authoritative LA use-code legend to avoid mislabels). $0 local.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/deals-flow.html | 45 +++++++++++++++++++++++++++++++++++----------
 1 file changed, 35 insertions(+), 10 deletions(-)

diff --git a/public/deals-flow.html b/public/deals-flow.html
index 10e3a85..1e3256c 100644
--- a/public/deals-flow.html
+++ b/public/deals-flow.html
@@ -123,6 +123,10 @@ select{background:var(--card);border:1px solid var(--line);color:var(--ink);bord
     <button id="watchBtn" title="Show only starred deals" style="background:var(--card);border:1px solid var(--line);color:var(--gold);border-radius:8px;padding:7px 10px;font-size:12px;cursor:pointer">☆ Watchlist</button>
     <span class="count" id="count"></span>
   </div>
+  <div class="toolbar" style="margin-top:-4px">
+    <input class="search" id="propLookup" placeholder="🔍 Look up ANY LA property by address — full public-record dossier" style="flex:1" title="Enter a street address; opens the public-record dossier (assessor + recent sales) for that parcel" aria-label="Look up any LA property by address">
+    <button id="propLookupBtn" style="background:var(--card);border:1px solid var(--blue);color:var(--blue);border-radius:8px;padding:7px 14px;font-size:12px;cursor:pointer">Look up →</button>
+  </div>
   <div class="grid" id="grid"></div>
 </div></div>
 
@@ -315,17 +319,12 @@ $('#fSaved').addEventListener('click',async e=>{const del=e.target.closest('[dat
 function syncRailActive(attr,set){const map={s:'#fSrc',u:'#fUse',y:'#fYear',c:'#fConf'},el=$(map[attr]);if(el)el.querySelectorAll('.chip').forEach(ch=>ch.classList.toggle('active',set.has(ch.dataset[attr])));}
 // ── property-detail drill: fetch the free public-record dossier + render inline ──
 let _pdSeq=0;   // guards against a stale fetch overwriting a newer one on rapid clicks (Cody Cycle-3)
-async function openPropDetail(ain){
-  const seq=++_pdSeq;
-  const m=$('#propModal'),body=$('#pmBody'),title=$('#pmTitle');
-  title.textContent='Property '+ain; body.innerHTML='<span style="color:var(--mut)">Loading public records…</span>'; m.classList.add('on');
-  let d; try{ d=await api('/api/property/'+encodeURIComponent(ain)); }catch(err){ if(seq===_pdSeq)body.innerHTML='<span style="color:var(--bad,#f85149)">Lookup failed: '+esc(err.message)+'</span>'; return; }
-  if(seq!==_pdSeq)return;   // a newer drill superseded this one — drop the stale render
-  if(!d||!d.found){ body.innerHTML='<span class="mut">No assessor record for AIN '+esc(ain)+'.</span>'; return; }
-  const p=d.parcel||{};
-  const row=(k,v)=>(v||v===0)?`<div style="display:flex;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)"><span style="color:var(--mut);min-width:140px;flex-shrink:0">${k}</span><span>${esc(String(v))}</span></div>`:'';
+// Render a dossier object (from /api/property/:ain or a resolved by-address hit) into the modal.
+function renderDossier(d,fallbackTitle){
+  const body=$('#pmBody'),title=$('#pmTitle'),p=d.parcel||{};
+  const row=(k,v)=>(v||v===0)?`<div style="display:flex;gap:8px;padding:3px 0;border-bottom:1px solid var(--line)"><span style="color:var(--mut);min-width:140px;flex-shrink:0">${esc(k)}</span><span>${esc(String(v))}</span></div>`:'';
   const dollar=v=>v?('$'+Number(v).toLocaleString()):'';
-  title.textContent=p.address||('AIN '+ain);
+  title.textContent=p.address||fallbackTitle||'Property';
   let html='<div style="font-size:12.5px">'
     +row('AIN',p.ain)+row('Address',p.address)+row('Use',p.use_desc||p.use_type)+row('Year built',p.year_built)
     +row('Sq ft',p.sqft)+row('Units',p.units)+row('Beds / baths',(p.bedrooms||p.bathrooms)?((p.bedrooms||'?')+' / '+(p.bathrooms||'?')):'')
@@ -340,6 +339,32 @@ async function openPropDetail(ain){
   html+=`<div class="mut" style="font-size:10.5px;margin-top:10px">Sources: ${esc((d.sources||[]).join(' · '))}</div></div>`;
   body.innerHTML=html;
 }
+async function openPropDetail(ain){
+  const seq=++_pdSeq,body=$('#pmBody'),title=$('#pmTitle');
+  title.textContent='Property '+ain; body.innerHTML='<span style="color:var(--mut)">Loading public records…</span>'; $('#propModal').classList.add('on');
+  let d; try{ d=await api('/api/property/'+encodeURIComponent(ain)); }catch(err){ if(seq===_pdSeq)body.innerHTML='<span style="color:var(--bad,#f85149)">Lookup failed: '+esc(err.message)+'</span>'; return; }
+  if(seq!==_pdSeq)return;
+  if(!d||!d.found){ body.innerHTML='<span class="mut">No assessor record for AIN '+esc(ain)+'.</span>'; return; }
+  renderDossier(d,'AIN '+ain);
+}
+// Look up ANY LA property by address → dossier (or a pick-list when multiple parcels match).
+async function openPropDetailByAddress(qstr){
+  const q=String(qstr||'').trim(); if(!q)return;
+  const seq=++_pdSeq,body=$('#pmBody'),title=$('#pmTitle');
+  title.textContent='Property lookup'; body.innerHTML='<span style="color:var(--mut)">Searching public records for “'+esc(q)+'”…</span>'; $('#propModal').classList.add('on');
+  let d; try{ d=await api('/api/property/by-address?q='+encodeURIComponent(q)); }catch(err){ if(seq===_pdSeq)body.innerHTML='<span style="color:var(--bad,#f85149)">Lookup failed: '+esc(err.message)+'</span>'; return; }
+  if(seq!==_pdSeq)return;
+  if(!d||!d.found){ body.innerHTML='<span class="mut">No LA County parcel matched “'+esc(q)+'”. Try a house number + street (e.g. 4150 Madison Ave).</span>'; return; }
+  if(d.multiple&&Array.isArray(d.candidates)){
+    title.textContent='Matches for “'+q+'”';
+    body.innerHTML='<div class="mut" style="margin-bottom:8px">'+d.candidates.length+' parcels matched — pick one:</div>'+d.candidates.map(c=>`<div class="pdpick" data-ain="${esc(String(c.ain||'').replace(/\D/g,''))}" style="cursor:pointer;padding:7px 9px;border:1px solid var(--line);border-radius:8px;margin-bottom:6px"><b>${esc(c.address||c.ain)}</b><div class="mut" style="font-size:11px">${esc(c.use_desc||c.use_type||'')} · AIN ${esc(c.ain)}</div></div>`).join('');
+    return;
+  }
+  renderDossier(d,q);
+}
+$('#pmBody').addEventListener('click',e=>{const pk=e.target.closest('.pdpick');if(pk)openPropDetail(pk.dataset.ain);});
+$('#propLookupBtn').addEventListener('click',()=>openPropDetailByAddress($('#propLookup').value));
+$('#propLookup').addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();openPropDetailByAddress(e.target.value);}});
 $('#propModal').addEventListener('click',e=>{if(e.target.id==='propModal')e.currentTarget.classList.remove('on');});
 $('#grid').addEventListener('click',async e=>{
   const pd=e.target.closest('.propdetail');if(pd){openPropDetail(pd.dataset.ain);return;}

← f6db22c CRCP mls: surface '🕓 Newest (first seen)' at top of sort dr  ·  back to Commercialrealestate  ·  auto-data-snapshot: 2026-08-06T18:06:44 (2 data files) — dat 899eba8 →