[object Object]

← back to Commercialrealestate

Add MLS spreadsheet + grid view (sortable & searchable, all property data)

9c3c4e8dfde7c54f6dec56276fcfc32e9c8b9d57 · 2026-06-30 16:16:15 -0700 · Steve Abrams

public/mls.html: MLS-style view over ranked.json (1,407 properties) — Table (spreadsheet, sticky
header, 18 cols: address/city/zip/type/price/units/sqft/$per-unit/$per-sqft/cap/year/rent-control/
status/firm/finance/score/source) AND Grid toggle. Every column click-sorts (numeric-aware), global
search filters all fields. Header link added on index.html. Verified headless: sort/search/toggle, 0 errors.

Files touched

Diff

commit 9c3c4e8dfde7c54f6dec56276fcfc32e9c8b9d57
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 30 16:16:15 2026 -0700

    Add MLS spreadsheet + grid view (sortable & searchable, all property data)
    
    public/mls.html: MLS-style view over ranked.json (1,407 properties) — Table (spreadsheet, sticky
    header, 18 cols: address/city/zip/type/price/units/sqft/$per-unit/$per-sqft/cap/year/rent-control/
    status/firm/finance/score/source) AND Grid toggle. Every column click-sorts (numeric-aware), global
    search filters all fields. Header link added on index.html. Verified headless: sort/search/toggle, 0 errors.
---
 public/index.html |   1 +
 public/mls.html   | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 139 insertions(+)

diff --git a/public/index.html b/public/index.html
index 5f2f809..bddabb8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -125,6 +125,7 @@
   <div class="spacer"></div>
   <a href="/crcp.html" style="color:var(--warn);text-decoration:none;font-size:13px;margin-right:12px" title="Commercial Real Estate Control Panel (live)">🛰️ CRCP</a>
   <a href="/map.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="All properties on a map, colored by type, with overhead aerial photos">🗺️ Map</a>
+  <a href="/mls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="MLS-style spreadsheet + grid — all property data, sortable & searchable">📋 MLS</a>
   <a href="/deals.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Liquid-glass deal-intelligence charts">◆ Deals</a>
   <a href="/rent-rolls.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Rent-roll → DSCR underwriting">📑 Rent Rolls</a>
   <a href="/brokers.html" style="color:var(--blue);text-decoration:none;font-size:13px;margin-right:12px" title="Broker / agent relationship mind-map">🕸️ Broker map</a>
diff --git a/public/mls.html b/public/mls.html
new file mode 100644
index 0000000..c5ddfe5
--- /dev/null
+++ b/public/mls.html
@@ -0,0 +1,138 @@
+<!doctype html>
+<html lang="en">
+<head>
+<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='%230e1116'/%3E%3Ctext x='8' y='12' font-size='10' text-anchor='middle' fill='%233fb950'%3EMLS%3C/text%3E%3C/svg%3E">
+<title>LA County CRE — MLS Table</title>
+<style>
+  :root{ --bg:#0e1116; --card:#161b22; --line:#2a313c; --ink:#e6edf3; --mut:#8b949e; --acc:#3fb950; --warn:#d29922; --bad:#f85149; --blue:#58a6ff; }
+  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;}
+  header{display:flex;align-items:center;gap:12px;padding:11px 18px;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:40;background:rgba(14,17,22,.98);backdrop-filter:blur(8px);flex-wrap:wrap;}
+  header h1{font-size:16px;margin:0;} header h1 b{color:var(--acc);}
+  header .nav{display:flex;gap:12px;margin-left:6px;} header .nav a{color:var(--blue);text-decoration:none;font-size:13px;} header .nav a:hover{color:var(--warn);}
+  .spacer{flex:1;}
+  .search{background:var(--card);border:1px solid var(--line);color:var(--ink);border-radius:9px;padding:8px 12px;font-size:14px;min-width:220px;}
+  .toggle{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;}
+  .toggle button{background:var(--card);border:0;color:var(--mut);padding:8px 13px;font-size:13px;cursor:pointer;}
+  .toggle button.on{background:var(--blue);color:#0b0e14;font-weight:700;}
+  .count{color:var(--mut);font-size:12px;}
+  .wrap{padding:14px 18px 60px;}
+  /* TABLE (spreadsheet) */
+  .tblwrap{overflow:auto;max-height:calc(100vh - 130px);border:1px solid var(--line);border-radius:10px;}
+  table.mls{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px;white-space:nowrap;}
+  table.mls thead th{position:sticky;top:0;background:#1b2230;color:var(--mut);text-transform:uppercase;letter-spacing:.3px;font-size:10.5px;padding:8px 10px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none;z-index:2;}
+  table.mls thead th:hover{color:var(--ink);}
+  table.mls thead th.asc::after{content:' ▲';color:var(--acc);} table.mls thead th.desc::after{content:' ▼';color:var(--acc);}
+  table.mls tbody td{padding:7px 10px;border-bottom:1px solid #20262f;}
+  table.mls tbody tr:hover td{background:rgba(88,166,255,.07);}
+  table.mls td.n{text-align:right;font-variant-numeric:tabular-nums;}
+  table.mls a{color:var(--blue);text-decoration:none;} table.mls a:hover{text-decoration:underline;}
+  .pill{font-size:10px;padding:1px 6px;border-radius:8px;font-weight:600;}
+  .pill.active{background:rgba(63,185,80,.16);color:var(--acc);} .pill.other{background:rgba(210,153,34,.16);color:var(--warn);}
+  .mut{color:var(--mut);}
+  /* GRID */
+  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;}
+  .gc{background:var(--card);border:1px solid var(--line);border-radius:11px;padding:12px 14px;}
+  .gc .addr{font-weight:600;} .gc .sub{color:var(--mut);font-size:12px;margin-bottom:6px;}
+  .gc .row{display:flex;justify-content:space-between;font-size:12.5px;padding:2px 0;border-top:1px solid #20262f;}
+  .gc .row .k{color:var(--mut);} .gc .row .v{font-variant-numeric:tabular-nums;}
+  .hide{display:none;}
+</style>
+</head>
+<body>
+<header>
+  <h1><b>MLS</b> · LA County CRE</h1>
+  <div class="nav"><a href="/">← Explorer</a><a href="/map.html">🗺️ Map</a><a href="/crcp.html">🛰️ CRCP</a></div>
+  <span class="spacer"></span>
+  <input class="search" id="q" type="text" placeholder="search anything — address, city, firm, type, year…">
+  <div class="toggle" id="view"><button data-v="table" class="on">▦ Table</button><button data-v="grid">▤ Grid</button></div>
+  <span class="count" id="count">…</span>
+</header>
+<div class="wrap">
+  <div id="tableView" class="tblwrap"><table class="mls"><thead id="thead"></thead><tbody id="tbody"></tbody></table></div>
+  <div id="gridView" class="grid hide"></div>
+</div>
+<script>
+const $=s=>document.querySelector(s);
+const esc=s=>(s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/"/g,'&quot;');
+const money=n=>(n==null||n==='')?'—':'$'+Number(n).toLocaleString();
+const num=n=>(n==null||n==='')?'—':Number(n).toLocaleString();
+// Column spec: key, label, type (s=string,n=number,money,pct,link,status), accessor
+const COLS=[
+  {k:'rank',l:'#',t:'n'},
+  {k:'address',l:'Address',t:'s'},
+  {k:'city',l:'City',t:'s'},
+  {k:'zip',l:'ZIP',t:'s'},
+  {k:'type',l:'Type',t:'s'},
+  {k:'price',l:'Price',t:'money'},
+  {k:'units',l:'Units',t:'n'},
+  {k:'sqft',l:'SqFt',t:'n'},
+  {k:'ppu',l:'$/Unit',t:'money',calc:r=>r.units?Math.round(r.price/r.units):null},
+  {k:'pps',l:'$/SqFt',t:'money',calc:r=>r.sqft?Math.round(r.price/r.sqft):null},
+  {k:'cap_rate',l:'Cap %',t:'pct'},
+  {k:'year_built',l:'Year',t:'n'},
+  {k:'rent_control',l:'Rent Ctrl',t:'s'},
+  {k:'status',l:'Status',t:'status'},
+  {k:'firm',l:'Firm',t:'s'},
+  {k:'financeScore',l:'Finance',t:'n'},
+  {k:'composite',l:'Score',t:'n'},
+  {k:'source',l:'Source',t:'link'},
+];
+let DATA=[], view='table', sortKey='rank', sortDir=1, q='';
+const cval=(r,c)=>c.calc?c.calc(r):r[c.k];
+function cellHTML(r,c){
+  const v=cval(r,c);
+  if(c.t==='money') return money(v);
+  if(c.t==='pct') return v==null||v===''?'—':v+'%';
+  if(c.t==='n') return num(v);
+  if(c.t==='status'){ const a=String(v||'').startsWith('Active'); return `<span class="pill ${a?'active':'other'}">${esc(v||'—')}</span>`; }
+  if(c.t==='link') return v?`<a href="${esc(v)}" target="_blank" rel="noopener noreferrer">link↗</a>`:'—';
+  return esc(v==null||v===''?'—':v);
+}
+function filtered(){
+  let rows=DATA;
+  if(q){ const t=q.toLowerCase(); rows=rows.filter(r=>COLS.some(c=>{const v=cval(r,c);return v!=null&&String(v).toLowerCase().includes(t);})); }
+  const c=COLS.find(x=>x.k===sortKey)||COLS[0];
+  rows=rows.slice().sort((a,b)=>{
+    let x=cval(a,c), y=cval(b,c);
+    const xn=(x!=null&&x!==''&&!isNaN(+x)), yn=(y!=null&&y!==''&&!isNaN(+y));
+    if(xn&&yn){ x=+x; y=+y; } else { x=String(x==null?'':x).toLowerCase(); y=String(y==null?'':y).toLowerCase(); }
+    if(x<y) return -1*sortDir; if(x>y) return 1*sortDir; return 0;
+  });
+  return rows;
+}
+function render(){
+  const rows=filtered();
+  $('#count').textContent=`${rows.length} of ${DATA.length} properties`;
+  // table head
+  $('#thead').innerHTML='<tr>'+COLS.map(c=>`<th data-k="${c.k}" class="${sortKey===c.k?(sortDir>0?'asc':'desc'):''}">${c.l}</th>`).join('')+'</tr>';
+  if(view==='table'){
+    $('#tableView').classList.remove('hide'); $('#gridView').classList.add('hide');
+    $('#tbody').innerHTML=rows.map(r=>'<tr>'+COLS.map(c=>{
+      const cls=(c.t==='money'||c.t==='n'||c.t==='pct')?' class="n"':'';
+      return `<td${cls}>${cellHTML(r,c)}</td>`;
+    }).join('')+'</tr>').join('');
+  } else {
+    $('#tableView').classList.add('hide'); $('#gridView').classList.remove('hide');
+    $('#gridView').innerHTML=rows.map(r=>`<div class="gc">
+      <div class="addr">${esc(r.address||'—')}</div><div class="sub">${esc(r.city||'')} ${esc(r.zip||'')} · ${esc(r.type||'')}</div>
+      <div class="row"><span class="k">Price</span><span class="v">${money(r.price)}</span></div>
+      <div class="row"><span class="k">Units / SqFt</span><span class="v">${num(r.units)} / ${num(r.sqft)}</span></div>
+      <div class="row"><span class="k">Cap / Year</span><span class="v">${r.cap_rate!=null?r.cap_rate+'%':'—'} / ${num(r.year_built)}</span></div>
+      <div class="row"><span class="k">Status</span><span class="v"><span class="pill ${String(r.status||'').startsWith('Active')?'active':'other'}">${esc(r.status||'—')}</span></span></div>
+      <div class="row"><span class="k">Firm</span><span class="v mut">${esc(r.firm||'—')}</span></div>
+      ${r.source?`<div class="row"><span class="k">Listing</span><span class="v"><a href="${esc(r.source)}" target="_blank" rel="noopener noreferrer" style="color:var(--blue)">open↗</a></span></div>`:''}
+    </div>`).join('');
+  }
+}
+$('#thead').addEventListener('click',e=>{ const th=e.target.closest('th'); if(!th) return; const k=th.dataset.k;
+  if(sortKey===k) sortDir*=-1; else { sortKey=k; sortDir=(k==='address'||k==='city'||k==='firm'||k==='type')?1:-1; } render(); });
+$('#q').addEventListener('input',()=>{ q=$('#q').value.trim(); render(); });
+$('#view').addEventListener('click',e=>{ const b=e.target.closest('button'); if(!b) return;
+  document.querySelectorAll('#view button').forEach(x=>x.classList.toggle('on',x===b)); view=b.dataset.v; render(); });
+fetch('/data/ranked.json').then(r=>r.json()).then(d=>{ DATA=(d.ranked||d)||[]; render(); })
+  .catch(()=>{ $('#count').textContent='failed to load /data/ranked.json'; });
+</script>
+</body>
+</html>

← 22644c3 fix(condos): render listings — warrant_signals is an object,  ·  back to Commercialrealestate  ·  Broker card: current+expired+sales with dates/MLS#/url acros 03d9129 →