← 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 &mdash; Applications</h1>
  <a href="/">&larr; 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&hellip;</div></div>

<script>
const esc = s => (s||'').replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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>