← 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> · Permit Radar Admin
</footer>
<script>
'use strict';
const esc = s => (s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[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>