← back to Permit Radar

public/admin.html

242 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin — Permit Radar</title>
<style>
:root { --cols: 3; --accent: #1d4ed8; }
*, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font: 14px/1.5 -apple-system, Segoe UI, Roboto, sans-serif; color: #1a1a1a; background: #f1f5f9; }

nav { background: #0f172a; color: #fff; padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
.logo { font-size: 17px; font-weight: 800; color: #fff; text-decoration: none; }
.logo span { color: #60a5fa; }
.admin-badge { background: #ef4444; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
.nav-links { margin-left: auto; display: flex; gap: 16px; }
.nav-links a { color: #94a3b8; font-size: 13px; text-decoration: none; }
.nav-links a:hover { color: #fff; }

/* ── TABS ── */
.tabs { display: flex; gap: 0; border-bottom: 2px solid #e2e8f0; background: #fff; padding: 0 20px; }
.tab {
  padding: 12px 20px; font-size: 14px; font-weight: 600; color: #64748b;
  cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px;
}
.tab.active { color: var(--accent); border-color: var(--accent); }
.tab-badge { background: #e2e8f0; color: #475569; font-size: 11px; font-weight: 700; padding: 1px 7px; border-radius: 99px; margin-left: 6px; }
.tab-badge.new { background: #dbeafe; color: var(--accent); }

/* ── CONTROLS ── */
.controls {
  display: flex; gap: 12px; align-items: center; flex-wrap: wrap;
  padding: 12px 20px; background: #fff; border-bottom: 1px solid #e2e8f0;
  position: sticky; top: 0; z-index: 5;
}
.controls select { padding: 7px 10px; border: 1px solid #d1d5db; border-radius: 6px; font: inherit; font-size: 13px; }
.ctrl-label { font-size: 12px; color: #64748b; display: flex; gap: 5px; align-items: center; }
.count-badge { margin-left: auto; background: #f1f5f9; color: #475569; font-size: 12px; font-weight: 600; padding: 4px 10px; border-radius: 99px; }

/* ── GRID ── */
.grid-wrap { padding: 20px; }
.grid { display: grid; grid-template-columns: repeat(var(--cols), minmax(0, 1fr)); gap: 14px; }
.empty { padding: 60px; text-align: center; color: #94a3b8; }

/* ── ADMIN CARD ── */
/* House rule: every admin card MUST show created date + time */
.card {
  background: #fff; border: 1px solid #e2e8f0; border-radius: 10px;
  overflow: hidden; display: flex; flex-direction: column;
}
.card-header { padding: 14px 14px 10px; border-bottom: 1px solid #f1f5f9; }
.card-id { font-size: 11px; color: #94a3b8; font-weight: 600; letter-spacing: .4px; margin-bottom: 4px; }
.card-title { font-size: 15px; font-weight: 700; color: #1e293b; margin: 0 0 2px; }
.card-subtitle { font-size: 12px; color: #64748b; }
.card-body { padding: 12px 14px; flex: 1; }
.field { margin-bottom: 6px; font-size: 13px; color: #374151; }
.field .lbl { font-size: 11px; color: #94a3b8; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; display: block; margin-bottom: 1px; }
/* HARD RULE: created date+time chip — visible on every admin card */
.when-chip {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 11px; color: #64748b; background: #f8fafc;
  border: 1px solid #e2e8f0; border-radius: 6px; padding: 3px 8px;
  margin-top: 10px; font-weight: 500;
}
.when-chip .icon { color: #94a3b8; }
.type-chip {
  display: inline-block; font-size: 11px; font-weight: 600; padding: 2px 8px;
  border-radius: 4px; background: #f1f5f9; color: #475569; margin-right: 4px;
}
.plan-chip { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; }
.plan-basic { background: #f1f5f9; color: #475569; }
.plan-pro   { background: #dbeafe; color: #1d4ed8; }
.plan-team  { background: #d1fae5; color: #065f46; }
.val-chip { font-size: 13px; font-weight: 700; color: #15803d; }

footer { background: #0f172a; color: #475569; padding: 20px; text-align: center; font-size: 13px; }
footer a { color: #64748b; text-decoration: none; }
</style>
</head>
<body>

<nav>
  <a class="logo" href="/"><span>📡</span> Permit Radar</a>
  <span class="admin-badge">ADMIN</span>
  <div class="nav-links">
    <a href="/">Directory</a>
    <a href="/#get-alerts">Get Alerts</a>
  </div>
</nav>

<div class="tabs">
  <div class="tab active" id="tab-subs" onclick="showTab('subs')">
    Subscribers <span class="tab-badge new" id="badge-subs">…</span>
  </div>
  <div class="tab" id="tab-leads" onclick="showTab('leads')">
    Lead Requests <span class="tab-badge" id="badge-leads">…</span>
  </div>
</div>

<!-- CONTROLS — sort + density (house rule: admin grid also gets both) -->
<div class="controls">
  <label>
    <select id="sort">
      <option value="newest">Sort: Newest First</option>
      <option value="oldest">Oldest First</option>
    </select>
  </label>
  <span class="ctrl-label">
    Density <input id="density" type="range" min="1" max="6" value="3" style="width:70px">
  </span>
  <span class="count-badge" id="count">…</span>
</div>

<!-- PANELS -->
<div class="grid-wrap" id="panel-subs">
  <div class="grid" id="grid-subs"><div class="empty">Loading subscribers…</div></div>
</div>
<div class="grid-wrap" id="panel-leads" style="display:none">
  <div class="grid" id="grid-leads"><div class="empty">Loading lead requests…</div></div>
</div>

<footer>
  <a href="/">← Back to Directory</a> &nbsp;·&nbsp; Permit Radar Admin
</footer>

<script>
'use strict';
const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));

// House rule: created date+time on every admin card
// Must show BOTH date and time via toLocaleString; full ISO in title= attribute
function fmtDateTime(iso) {
  if (!iso) return '—';
  const d = new Date(iso);
  return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
}

// ── State ─────────────────────────────────────────────────────────────────────
let activeTab = 'subs';
const sortSel = document.getElementById('sort');
const dens    = document.getElementById('density');

sortSel.value = localStorage.getItem('adm_sort') || 'newest';
dens.value    = localStorage.getItem('adm_cols') || '3';
function applyCols() {
  document.documentElement.style.setProperty('--cols', dens.value);
  localStorage.setItem('adm_cols', dens.value);
}
applyCols();

sortSel.onchange = () => { localStorage.setItem('adm_sort', sortSel.value); loadBoth(); };
dens.oninput = applyCols;

// ── Tab switching ─────────────────────────────────────────────────────────────
function showTab(tab) {
  activeTab = tab;
  document.getElementById('tab-subs').classList.toggle('active', tab === 'subs');
  document.getElementById('tab-leads').classList.toggle('active', tab === 'leads');
  document.getElementById('panel-subs').style.display  = tab === 'subs'   ? '' : 'none';
  document.getElementById('panel-leads').style.display = tab === 'leads'  ? '' : 'none';
  updateCount();
}

let _subCount = 0, _leadCount = 0;
function updateCount() {
  document.getElementById('count').textContent =
    (activeTab === 'subs' ? _subCount : _leadCount) + ' records';
}

// ── Subscribers ───────────────────────────────────────────────────────────────
async function loadSubs() {
  const r = await fetch('/api/admin/subscribers?sort=' + sortSel.value);
  if (r.status === 401) { document.getElementById('grid-subs').innerHTML = '<div class="empty">Auth required — reload with credentials.</div>'; return; }
  const d = await r.json();
  _subCount = d.count;
  document.getElementById('badge-subs').textContent = d.count;
  updateCount();
  const grid = document.getElementById('grid-subs');
  if (!d.subscribers.length) { grid.innerHTML = '<div class="empty">No subscribers yet.</div>'; return; }
  grid.innerHTML = d.subscribers.map(s => {
    const planClass = 'plan-' + (s.plan || 'basic');
    const isoCreated = s.created_at || '';
    return `
<div class="card">
  <div class="card-header">
    <div class="card-id">${esc(s.id)}</div>
    <div class="card-title">${esc(s.email)}</div>
    <div class="card-subtitle"><span class="type-chip">${esc(s.trade_type || '—')}</span><span class="plan-chip ${planClass}">${esc(s.plan || 'basic')}</span></div>
  </div>
  <div class="card-body">
    <div class="field"><span class="lbl">Service Area</span>${esc(s.service_area || '—')}</div>
    ${s.jurisdictions && s.jurisdictions.length ? `<div class="field"><span class="lbl">Jurisdictions</span>${esc(s.jurisdictions.join(', '))}</div>` : ''}
    ${s.permit_types && s.permit_types.length ? `<div class="field"><span class="lbl">Permit Types</span>${esc(s.permit_types.join(', '))}</div>` : ''}
    <!-- House rule: created date + time chip — BOTH date and time, always visible -->
    <div class="when-chip" title="${esc(isoCreated)}">
      <span class="icon">🕓</span> ${esc(fmtDateTime(isoCreated))}
    </div>
  </div>
</div>`;
  }).join('');
}

// ── Lead Requests ─────────────────────────────────────────────────────────────
async function loadLeads() {
  const r = await fetch('/api/admin/lead-requests?sort=' + sortSel.value);
  if (r.status === 401) { document.getElementById('grid-leads').innerHTML = '<div class="empty">Auth required.</div>'; return; }
  const d = await r.json();
  _leadCount = d.count;
  document.getElementById('badge-leads').textContent = d.count;
  updateCount();
  const grid = document.getElementById('grid-leads');
  if (!d.lead_requests.length) { grid.innerHTML = '<div class="empty">No lead requests yet.</div>'; return; }
  grid.innerHTML = d.lead_requests.map(lr => {
    const isoCreated = lr.created_at || '';
    return `
<div class="card">
  <div class="card-header">
    <div class="card-id">${esc(lr.id)}</div>
    <div class="card-title">${esc(lr.name || lr.email)}</div>
    <div class="card-subtitle">${esc(lr.email)}</div>
  </div>
  <div class="card-body">
    <div class="field"><span class="lbl">Permit</span>${esc(lr.permit_id)} — ${esc(lr.permit_address || '')}</div>
    <div class="field"><span class="lbl">Valuation</span><span class="val-chip">${lr.permit_valuation ? '$' + Number(lr.permit_valuation).toLocaleString() : '—'}</span></div>
    <div class="field"><span class="lbl">Trade Type</span><span class="type-chip">${esc(lr.trade_type || '—')}</span></div>
    ${lr.phone ? `<div class="field"><span class="lbl">Phone</span>${esc(lr.phone)}</div>` : ''}
    ${lr.message ? `<div class="field"><span class="lbl">Message</span>${esc(lr.message)}</div>` : ''}
    <!-- House rule: created date + time chip — BOTH date and time, always visible -->
    <div class="when-chip" title="${esc(isoCreated)}">
      <span class="icon">🕓</span> ${esc(fmtDateTime(isoCreated))}
    </div>
  </div>
</div>`;
  }).join('');
}

async function loadBoth() { await Promise.all([loadSubs(), loadLeads()]); }
loadBoth();
</script>
</body>
</html>