← back to Re Flyer Aggregator

scripts/render-asset-index.mjs

73 lines

#!/usr/bin/env node
// TK-10708  Consumer demo: renders the aggregated marketing assets per deal as a static
// HTML index (what CRCP/RENTV would show next to each deal). Joins deal facts from usre
// with the deal_assets_v view in the reflyers staging DB. No server; open the HTML.
//
// Usage: node scripts/render-asset-index.mjs

import { execFileSync } from 'node:child_process';
import { writeFileSync, mkdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const OUT = join(ROOT, 'out'); mkdirSync(OUT, { recursive: true });
const jrows = (db, sql) => execFileSync('psql', [db, '-t', '-A', '-c', `SELECT row_to_json(t) FROM (${sql}) t`], { encoding: 'utf8' })
  .trim().split('\n').filter(Boolean).map(l => JSON.parse(l));

// deal facts (usre) for the deals that have assets
const docs = jrows('reflyers', `SELECT DISTINCT doc_number FROM deal_assets_v`).map(r => r.doc_number);
if (!docs.length) { console.error('No aggregated assets yet.'); process.exit(1); }
const inList = docs.map(d => `'${d.replace(/'/g, "''")}'`).join(',');
const deals = jrows('usre', `SELECT doc_number,address,city,county_name,sale_price::bigint AS sale_price,sale_date,ctype FROM recent_commercial_deals WHERE doc_number IN (${inList})`);
const assets = jrows('reflyers', `SELECT doc_number,asset_type,tier,rights_basis,title,source_name,source_landing_url,local_path,match_confidence FROM deal_assets_v`);

const byDoc = {};
for (const a of assets) (byDoc[a.doc_number] ||= []).push(a);
const dealList = [...new Map(deals.map(d => [d.doc_number, d])).values()].sort((a, b) => b.sale_price - a.sale_price);

const money = n => '$' + (+n).toLocaleString('en-US');
const tc = s => (s || '').toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
const badge = a => {
  const map = { self_generated: ['#16357A', 'RECAP (ours)'], broker_owned: ['#0E7C4A', 'BROKER'], first_party: ['#8A5A00', 'FIRST-PARTY'] };
  const [bg, lbl] = map[a.rights_basis] || ['#6B7280', (a.rights_basis || '').toUpperCase()];
  return `<span class="b" style="background:${bg}">${lbl}</span>`;
};
const assetRow = a => {
  const href = a.source_landing_url || (a.local_path ? a.local_path.replace(/^out\//, '') : '#');
  return `<li>${badge(a)} <a href="${href}" target="_blank">${a.title || a.source_name}</a>
    <span class="meta">· ${a.asset_type} · ${a.source_name}${a.match_confidence != null ? ' · conf ' + a.match_confidence : ''}</span></li>`;
};

const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Deal marketing assets (aggregated)</title>
<style>
 body{font-family:Inter,Helvetica,Arial,sans-serif;margin:0;background:#f4f6fb;color:#141414}
 header{background:#0E2350;color:#fff;padding:22px 32px}
 header h1{margin:0;font-size:22px} header .s{color:#E8A81C;font-size:13px;margin-top:4px}
 .wrap{max-width:1000px;margin:24px auto;padding:0 20px}
 .card{background:#fff;border-radius:10px;padding:18px 22px;margin-bottom:16px;box-shadow:0 2px 10px rgba(0,0,0,.06)}
 .card h2{margin:0 0 2px;font-size:19px;color:#16357A}
 .card .sub{color:#6B7280;font-size:13px;margin-bottom:12px}
 .price{float:right;font-weight:800;color:#0E7C4A;font-size:20px}
 ul{list-style:none;padding:0;margin:0} li{padding:6px 0;border-top:1px solid #eef0f6;font-size:14px}
 a{color:#16357A;text-decoration:none} a:hover{text-decoration:underline}
 .b{color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:4px;letter-spacing:.5px;margin-right:6px}
 .meta{color:#6B7280;font-size:12px}
 .note{color:#6B7280;font-size:12px;margin:6px 0 20px}
</style></head><body>
<header><h1>Deal marketing assets — aggregated (TK-10708)</h1>
<div class="s">reflyers staging · ${dealList.length} deals · ${assets.length} consumer-visible assets (GATED marketplace assets hidden)</div></header>
<div class="wrap">
<div class="note">Consumer preview of what CRCP/RENTV would render beside each deal. RECAP = our own Tier-2 one-sheet; FIRST-PARTY = owner/developer/press link; BROKER = broker-owned page.</div>
${dealList.map(d => `<div class="card">
  <span class="price">${money(d.sale_price)}</span>
  <h2>${tc(d.address)}</h2>
  <div class="sub">${tc(d.city)}${d.county_name ? ', ' + tc(d.county_name) + ' County' : ''} · ${tc(d.ctype || '')} · sold ${d.sale_date} · doc ${d.doc_number}</div>
  <ul>${(byDoc[d.doc_number] || []).map(assetRow).join('')}</ul>
</div>`).join('\n')}
</div></body></html>`;

const path = join(OUT, 'deal-assets.html');
writeFileSync(path, html);
console.log(`Wrote ${path} — ${dealList.length} deals, ${assets.length} consumer assets.`);