← back to Trade Portal
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>Admin — Trade Portal Applications</title>
<style>
:root{--cols:3;--accent:#1a3a5c;--gold:#b8962e}
*{box-sizing:border-box}
body{margin:0;font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:#f0ede8}
.topbar{background:var(--accent);color:#fff;padding:10px 24px;display:flex;align-items:center;gap:14px}
.topbar h1{margin:0;font-size:14px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;flex:1}
.topbar a{color:#cce0ff;font-size:13px;text-decoration:none}
.controls{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:12px 24px;border-bottom:1px solid #ddd;background:#fff;position:sticky;top:0;z-index:10}
.controls h2{margin:0;font-size:13px;font-weight:600;color:#666;flex:1}
label{font-size:12px;color:#666;display:flex;gap:6px;align-items:center}
select,input[type=range]{font:inherit}
.count{font-size:12px;color:#999}
.grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:14px;padding:20px}
/* ── Application card ── */
.card{background:#fff;border:1px solid #e0dbd4;border-radius:8px;padding:14px;position:relative}
.card-header{display:flex;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:10px}
.card-company{font-size:14px;font-weight:700;color:var(--accent);margin:0}
.card-contact{font-size:12px;color:#555;margin:2px 0 0}
.status-badge{font-size:10px;font-weight:700;padding:2px 7px;border-radius:3px;text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;flex-shrink:0}
.status-pending{background:#fff3cd;color:#856404}
.status-approved{background:#d1e7dd;color:#0a3622}
.status-rejected{background:#f8d7da;color:#58151c}
/* ── HARD RULE: created date+time chip ── */
.when-chip{font-size:11px;color:#888;display:flex;align-items:center;gap:4px;margin-bottom:8px}
.when-chip svg{opacity:.5}
.divider{border:none;border-top:1px solid #f0ede8;margin:10px 0}
.field-row{display:flex;gap:6px;margin-bottom:5px;font-size:12px}
.field-label{color:#999;min-width:80px;flex-shrink:0}
.field-val{color:#333;word-break:break-word}
.field-val a{color:var(--accent);text-decoration:none}
.field-val a:hover{text-decoration:underline}
.card-notes{font-size:12px;color:#666;font-style:italic;margin-top:8px;padding:8px;background:#f9f8f7;border-radius:4px}
.card-id{font-size:10px;color:#ccc;margin-top:10px}
.empty{padding:60px;text-align:center;color:#999}
</style>
</head>
<body>
<div class="topbar">
<h1>Trade Portal — Applications</h1>
<a href="/">← Storefront</a>
</div>
<div class="controls">
<h2 id="page-heading">Trade Account Applications</h2>
<label>Sort
<select id="sort">
<option value="newest">Newest First</option>
<option value="oldest">Oldest First</option>
</select>
</label>
<label>Density <input id="density" type="range" min="1" max="4" value="3"></label>
<span class="count" id="count"></span>
</div>
<div class="grid" id="grid"><div class="empty">Loading…</div></div>
<script>
const esc = s => (s||'').replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const $id = id => document.getElementById(id);
// Persist sort + density (house rule)
const sortSel = $id('sort'), densInp = $id('density');
sortSel.value = localStorage.getItem('admin-sort') || 'newest';
densInp.value = localStorage.getItem('admin-cols') || '3';
document.documentElement.style.setProperty('--cols', densInp.value);
sortSel.onchange = () => { localStorage.setItem('admin-sort', sortSel.value); load(); };
densInp.oninput = () => {
document.documentElement.style.setProperty('--cols', densInp.value);
localStorage.setItem('admin-cols', densInp.value);
};
// HARD RULE: format date+time for admin cards
// Uses toLocaleString with full year/month/day + hour/minute in local timezone.
// Full ISO in title attribute for precision.
function fmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
}
function statusBadge(status) {
const map = { pending:'status-pending', approved:'status-approved', rejected:'status-rejected' };
const cls = map[status] || 'status-pending';
return `<span class="status-badge ${cls}">${esc(status||'pending')}</span>`;
}
function cardHTML(a) {
const createdIso = a.created_at || '';
const createdDisp = fmtDate(createdIso);
return `<div class="card">
<div class="card-header">
<div>
<div class="card-company">${esc(a.company_name||'—')}</div>
<div class="card-contact">${esc(a.contact_name||'')}</div>
</div>
${statusBadge(a.status)}
</div>
<!-- HARD RULE: created date+time visible on card; full ISO in title -->
<div class="when-chip" title="${esc(createdIso)}">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
</svg>
${esc(createdDisp)}
</div>
<hr class="divider">
<div class="field-row"><span class="field-label">Email</span>
<span class="field-val"><a href="mailto:${esc(a.email||'')}">${esc(a.email||'—')}</a></span></div>
<div class="field-row"><span class="field-label">Phone</span>
<span class="field-val">${esc(a.phone||'—')}</span></div>
<div class="field-row"><span class="field-label">Trade Cert</span>
<span class="field-val">${esc(a.trade_cert||'—')}</span></div>
<div class="field-row"><span class="field-label">Biz Type</span>
<span class="field-val">${esc(a.business_type||'—')}</span></div>
${a.website ? `<div class="field-row"><span class="field-label">Website</span>
<span class="field-val"><a href="${esc(a.website)}" target="_blank" rel="noopener">${esc(a.website)}</a></span></div>` : ''}
${a.how_heard ? `<div class="field-row"><span class="field-label">How heard</span>
<span class="field-val">${esc(a.how_heard)}</span></div>` : ''}
${a.notes ? `<div class="card-notes">${esc(a.notes)}</div>` : ''}
<div class="card-id">ID: ${esc(a.id||'—')}</div>
</div>`;
}
async function load() {
const r = await fetch('/api/admin/applications?sort=' + encodeURIComponent(sortSel.value));
if (r.status === 401) {
$id('grid').innerHTML = '<div class="empty">Not authorized. Reload and enter admin credentials.</div>';
return;
}
const d = await r.json();
$id('count').textContent = d.count + ' application' + (d.count !== 1 ? 's' : '');
$id('page-heading').textContent = 'Trade Account Applications (' + d.count + ')';
$id('grid').innerHTML = d.applications.length
? d.applications.map(cardHTML).join('')
: '<div class="empty">No applications yet. They appear here after someone submits the trade form.</div>';
}
load();
</script>
</body>
</html>