← back to Commercialrealestate

public/sales.html

101 lines

<!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='%230a0d13'/%3E%3Ctext x='8' y='12' font-size='11' text-anchor='middle' fill='%23c8a24b'%3E%24%3C/text%3E%3C/svg%3E">
<title>Closed Sales — LA Condo Comps</title>
<link rel="stylesheet" href="/crcp-grid.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="/crcp-grid.js"></script>
<style>
  :root{ --bg:#0c0f15; --panel:#141925; --fg:#E6E8EC; --muted:#8A93A2; --gold:#C8A24B; --active:#70AD47; --draft:#5B9BD5; --line:#232a38;
    /* crcp-grid engine theme mapping (gold/panel) */
    --cg-line:#232a38; --cg-mut:#8A93A2; --cg-ink:#E6E8EC; --cg-card:#141925;
    --cg-acc:#C8A24B; --cg-accbg:rgba(200,162,75,.12); --cg-railbg:#0b0e13; --cg-topbg:rgba(12,15,21,.95); --cg-red:#f85149; }
  *{box-sizing:border-box;} body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;}
  header{display:flex;align-items:center;gap:14px;padding:14px 22px;border-bottom:1px solid var(--line);position:sticky;top:0;background:var(--bg);z-index:20;}
  header h1{margin:0;font-size:17px;} header h1 b{color:var(--gold);} header a{color:var(--draft);text-decoration:none;font-size:13px;}
  header .nav{margin-left:auto;display:flex;gap:14px;}
  .stat{color:var(--muted);font-size:13px;padding:12px 22px 4px;} .stat b{color:var(--fg);}
  #depthNote{font-size:12px;color:var(--muted);margin:2px 22px 8px;padding:6px 10px;border-left:2px solid var(--gold);background:rgba(200,162,75,.06);}
  .charts{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:18px;} @media(max-width:880px){.charts{grid-template-columns:1fr;}}
  .panel{background:var(--panel);border:1px solid var(--line);border-radius:13px;padding:16px;}
  .panel h3{margin:0 0 12px;font-size:12px;text-transform:uppercase;letter-spacing:.6px;color:var(--gold);}
  canvas{max-height:240px;}
  .price{color:var(--active);font-weight:600;}
</style>
</head>
<body>
<header>
  <h1><b>Closed Sales</b> · LA condo comps</h1>
  <nav class="nav"><a href="/crcp.html">← CRCP</a><a href="/condos.html">Condos</a><a href="/graphics.html">Graphics</a><a href="/fha-loans.html">FHA Loans</a></nav>
</header>
<div class="stat" id="stat">loading…</div>
<div id="depthNote"></div>

<div class="cg-shell">
  <aside class="cg-rail" id="cgRail"></aside>
  <div class="cg-main" id="cgMain"><div class="cg-empty" style="padding:50px">loading sold comps…</div></div>
</div>

<div class="charts">
  <div class="panel"><h3>By city (avg price)</h3><canvas id="cityChart"></canvas></div>
  <div class="panel"><h3>Sales by year</h3><canvas id="yearChart"></canvas></div>
</div>

<script>
const $=s=>document.querySelector(s);
const money=n=>n?'$'+Number(n).toLocaleString():'—';
let cityChart, yearChart;

// Every data point — each toggleable (cards + list columns).
const FIELDS=[
  {k:'sold_price',l:'Sold',      money:1, col:1, def:1},
  {k:'sold_date', l:'Sold date',          col:1, def:1, calc:r=>String(r.sold_date||'').slice(0,10)},
  {k:'beds',      l:'Beds',      num:1,   col:1, def:1},
  {k:'baths',     l:'Baths',     num:1,   col:1, def:1},
  {k:'sqft',      l:'Sq Ft',     num:1,   col:1, def:1},
  {k:'ppsf',      l:'$/Sq Ft',   money:1, col:1, def:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
  {k:'city',      l:'City',               col:1, def:1},
  {k:'zip',       l:'ZIP',                col:1, def:0},
];
const FACETS=[
  {k:'city',      l:'City',       type:'chip', max:20},
  {k:'year',      l:'Sold year',  type:'chip', calc:r=>String(r.sold_date||'').slice(0,4), max:12},
  {k:'beds',      l:'Beds',       type:'chip', calc:r=>String(r.beds||'—')},
  {k:'sold_price',l:'Sold price $',type:'range', money:1},
  {k:'sqft',      l:'Sq Ft',      type:'range'},
  {k:'ppsf',      l:'$/Sq Ft',    type:'range', money:1, calc:r=>r.sqft?Math.round(r.sold_price/r.sqft):null},
];

async function load(){
  let d;
  try{ d=await (await fetch('/api/closed-sales?limit=5000')).json(); }
  catch(e){ $('#cgMain').innerHTML='<div class="cg-empty" style="padding:50px">sold-comps load error.</div>'; return; }
  const t=d.total||{};
  $('#stat').innerHTML=`<b>${(t.n||0).toLocaleString()}</b> sold condos · avg <b>${money(t.avg_price)}</b> · ${(t.oldest||'').toString().slice(0,10)} → ${(t.newest||'').toString().slice(0,10)} · <span style="color:var(--muted)">${d.source||''}</span>`;
  if(d.depthNote) $('#depthNote').textContent='ℹ '+d.depthNote;

  CrcpGrid.mount({
    data:d.rows||[], fields:FIELDS, facets:FACETS,
    mount:'#cgMain', rail:'#cgRail', view:'list', cols:3,
    storageKey:'sales', titleCol:'Address',
    title:r=>({ name:r.address||'—', sub:[r.city,r.zip].filter(Boolean).join(' '), href:r.url||'' }),
    search:r=>((r.address||'')+' '+(r.city||'')+' '+(r.zip||'')),
  });

  // charts (over the loaded window)
  const bc=d.byCity||[];
  if(cityChart) cityChart.destroy();
  cityChart=new Chart($('#cityChart'),{type:'bar',data:{labels:bc.map(c=>c.city),datasets:[{label:'avg $',data:bc.map(c=>c.avg_price),backgroundColor:'#C8A24B'}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2',font:{size:9}}},y:{ticks:{color:'#8A93A2'}}}}});
  const by=d.byYear||[];
  if(yearChart) yearChart.destroy();
  yearChart=new Chart($('#yearChart'),{type:'line',data:{labels:by.map(y=>y.yr),datasets:[{label:'sales',data:by.map(y=>y.n),borderColor:'#70AD47',backgroundColor:'rgba(112,173,71,.15)',fill:true,tension:.3}]},options:{plugins:{legend:{display:false}},scales:{x:{ticks:{color:'#8A93A2'}},y:{ticks:{color:'#8A93A2'}}}}});
}
load();
</script>
  <script src="/col-resize.js" defer></script>
</body>
</html>