← back to Estimate Instant

public/admin.html

148 lines

<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>estimate-instant — captured quotes (admin)</title>
<style>
  :root{--ink:#1a1a1a;--line:#e6e6e6;--accent:#2a3a4a;--gold:#a98b4a;--bg:#f7f5f2}
  *{box-sizing:border-box}
  body{margin:0;font:15px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:var(--ink);background:var(--bg)}
  header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:14px 20px;background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:10}
  h1{font-size:17px;margin:0;white-space:nowrap}
  .count{font-size:12px;color:#999;white-space:nowrap}
  .spacer{flex:1}
  a{color:#888;font-size:13px}
  /* Sort + Density — house rule: every admin grid gets these controls */
  .controls{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:12px 20px;background:#fff;border-bottom:1px solid var(--line)}
  .controls label{font-size:12px;color:#888;display:flex;align-items:center;gap:6px;white-space:nowrap}
  select{font:inherit;font-size:13px;padding:5px 8px;border:1px solid #d8d4cd;border-radius:7px;background:#fff;cursor:pointer}
  input[type=range]{width:100px;accent-color:var(--accent);cursor:pointer}
  .density-val{font-size:12px;color:#888;min-width:26px}
  /* Grid */
  .grid{display:grid;gap:14px;padding:18px 20px}
  /* Card — admin card MUST show created date+time (house rule) */
  .card{background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px;display:flex;flex-direction:column;gap:4px}
  .email{font-weight:700;font-size:15px;word-break:break-all}
  /* 🕓 created date+time chip — house rule, ALWAYS visible on the card */
  .when{display:inline-flex;align-items:center;gap:4px;margin-top:4px;font-size:12px;color:#777;background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:2px 7px;width:fit-content}
  .q{margin-top:8px;font-size:13px;color:#555;border-top:1px solid var(--line);padding-top:8px}
  .q b{color:var(--ink)}
  .q .roll{color:#888;font-size:12px;margin-top:2px}
  .price-tag{display:inline-block;background:var(--accent);color:#fff;border-radius:5px;padding:1px 7px;font-size:13px;font-weight:700;margin-left:4px}
  .empty{padding:50px;text-align:center;color:#999;grid-column:1/-1}
  .no-leads{text-align:center;padding:50px 20px;color:#999}
</style></head><body>

<header>
  <h1>Captured quotes</h1>
  <span class="count" id="count"></span>
  <span class="spacer"></span>
  <a href="/">← calculator</a>
</header>

<!-- Sort + Density — house rule: every admin grid gets these controls, persisted to localStorage -->
<div class="controls">
  <label>Sort
    <select id="sortSel">
      <option value="newest">Newest first</option>
      <option value="oldest">Oldest first</option>
      <option value="email">Email A→Z</option>
      <option value="price-desc">Price ↓</option>
      <option value="price-asc">Price ↑</option>
    </select>
  </label>
  <label>Density
    <input type="range" id="density" min="200" max="480" step="20" value="280">
    <span class="density-val" id="dval">280</span>
  </label>
</div>

<div class="grid" id="grid"></div>

<script>
const $ = id => document.getElementById(id);
let allLeads = [];

// House rule: format date as "🕓 Jul 15, 2026, 3:17 AM" with full ISO in title=
function fmt(iso) {
  try {
    return new Date(iso).toLocaleString(undefined, {
      year: 'numeric', month: 'short', day: 'numeric',
      hour: 'numeric', minute: '2-digit'
    });
  } catch { return iso; }
}

function sortLeads(leads, mode) {
  const a = leads.slice();
  switch (mode) {
    case 'oldest':    return a.sort((x,y) => x.created_at.localeCompare(y.created_at));
    case 'email':     return a.sort((x,y) => x.email.localeCompare(y.email));
    case 'price-desc':return a.sort((x,y) => (y.quote?.price||0) - (x.quote?.price||0));
    case 'price-asc': return a.sort((x,y) => (x.quote?.price||0) - (y.quote?.price||0));
    default:          return a.sort((x,y) => y.created_at.localeCompare(x.created_at)); // newest
  }
}

function render() {
  const sort = $('sortSel').value;
  const density = parseInt($('density').value, 10);
  const leads = sortLeads(allLeads, sort);

  $('grid').style.gridTemplateColumns = `repeat(auto-fill,minmax(${density}px,1fr))`;
  $('count').textContent = leads.length + ' lead' + (leads.length === 1 ? '' : 's');

  if (!leads.length) {
    $('grid').innerHTML = '<div class="no-leads">No quotes captured yet.<br><a href="/">← Go calculate one</a></div>';
    return;
  }

  $('grid').innerHTML = leads.map(l => {
    const q = l.quote;
    const quoteHtml = q && q.ok !== false
      ? `<div class="q">
           <div>${q.pattern || '—'} · <b>${q.rollsNeeded || '?'} rolls</b><span class="price-tag">$${(q.price||0).toLocaleString()}</span></div>
           <div class="roll">${q.sku || ''} · $${q.pricePerRoll || '?'}/roll · ${q.totalStrips || '?'} strips · ${q.wastePct || 0}% waste</div>
         </div>`
      : '';
    // HARD RULE: 🕓 created date+time ALWAYS visible on card; ISO timestamp in title=
    return `<div class="card">
      <div class="email">${escHtml(l.email)}</div>
      <span class="when" title="${escHtml(l.created_at)}">🕓 ${fmt(l.created_at)}</span>
      ${quoteHtml}
    </div>`;
  }).join('');
}

function escHtml(s) {
  return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

// Persist sort + density to localStorage
function persist() {
  try { localStorage.setItem('ei-admin-sort', $('sortSel').value); } catch {}
  try { localStorage.setItem('ei-admin-density', $('density').value); } catch {}
}
function restore() {
  try { const v = localStorage.getItem('ei-admin-sort'); if (v) $('sortSel').value = v; } catch {}
  try { const v = localStorage.getItem('ei-admin-density'); if (v) { $('density').value = v; $('dval').textContent = v; } } catch {}
}

$('sortSel').addEventListener('change', () => { persist(); render(); });
$('density').addEventListener('input', () => {
  $('dval').textContent = $('density').value;
  persist(); render();
});

restore();

fetch('/api/leads')
  .then(r => r.json())
  .then(({ leads }) => {
    allLeads = leads || [];
    render();
  })
  .catch(() => {
    $('grid').innerHTML = '<div class="no-leads">Failed to load leads.</div>';
  });
</script>
</body></html>