← back to Sample Box

public/admin.html

210 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sample Box Admin — Designer Wallcoverings</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--accent:#1a1a2e;--gold:#c9a84c;--bg:#f5f3ef;--card:#fff;--border:#e0dbd0;--muted:#888;--radius:10px;--cols:3}
body{font:14px/1.5 -apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;background:var(--bg);min-height:100vh}

.admin-header{background:var(--accent);color:#fff;padding:16px 28px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100}
.admin-header h1{font-size:18px;font-weight:700}
.admin-header .sub{font-size:12px;opacity:.65;margin-top:2px}
.badge{background:var(--gold);color:#fff;font-size:10px;font-weight:700;padding:2px 7px;border-radius:3px}
.badge-red{background:#e74c3c}

/* stats bar */
.stats{display:flex;gap:20px;padding:18px 28px;background:#fff;border-bottom:2px solid var(--border);flex-wrap:wrap}
.stat{text-align:center}
.stat .n{font-size:28px;font-weight:700;color:var(--accent)}
.stat .lbl{font-size:11px;color:var(--muted);margin-top:2px}

/* toolbar */
.toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:16px 28px;background:#fff;border-bottom:1px solid var(--border)}
.toolbar h2{font-size:15px;font-weight:700;flex:1}
label.ctrl{font-size:12px;color:var(--muted);display:flex;gap:6px;align-items:center}
select,input[type=range]{font:inherit}
.item-count{font-size:12px;color:var(--muted)}

/* order cards grid */
.orders-grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:16px;padding:24px 28px}
.order-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;display:flex;flex-direction:column;gap:10px;position:relative}
.order-card:hover{box-shadow:0 4px 14px rgba(0,0,0,.08)}

/* HARD RULE: every admin card shows created date+time */
.card-when{
  font-size:11px;color:var(--muted);
  display:flex;align-items:center;gap:5px;
}
.card-when .clock{opacity:.7}

.card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap}
.order-id{font-family:monospace;font-size:12px;font-weight:700;color:var(--accent)}
.status-badge{font-size:10px;font-weight:700;letter-spacing:.05em;padding:2px 8px;border-radius:3px;background:#fffbf0;color:var(--gold);border:1px solid var(--gold)}
.status-badge.paid{background:#f0faf4;color:#27ae60;border-color:#27ae60}

.card-vibe{font-size:12px;font-weight:600;background:var(--bg);padding:4px 10px;border-radius:20px;display:inline-block;color:var(--accent)}

.card-swatches{display:flex;gap:6px;flex-wrap:wrap}
.swatch-thumb{width:36px;height:36px;border-radius:5px;object-fit:cover;border:1px solid var(--border);background:#f0ede6}
.swatch-overflow{width:36px;height:36px;border-radius:5px;background:var(--bg);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--muted);border:1px solid var(--border)}

.card-addr{font-size:12px;color:#444;line-height:1.4}
.card-addr .name{font-weight:600;color:var(--accent)}
.card-email{font-size:11px;color:var(--muted)}

.card-price{font-size:14px;font-weight:700;color:var(--accent)}
.credit-line{font-size:11px;color:#27ae60;margin-top:2px}

.empty{padding:60px;text-align:center;color:var(--muted);font-size:14px;grid-column:1/-1}
.loading{padding:40px;text-align:center;color:var(--muted);grid-column:1/-1}

@media(max-width:900px){:root{--cols:2}}
@media(max-width:600px){:root{--cols:1}.orders-grid{padding:16px}}
</style>
</head>
<body>

<header class="admin-header">
  <div>
    <h1>Sample Box Admin</h1>
    <div class="sub">Fulfillment dashboard — order cards with sort + density</div>
  </div>
  <div style="display:flex;gap:8px;align-items:center">
    <span class="badge badge-red">TEST MODE</span>
    <a href="/" style="color:#fff;font-size:12px;opacity:.7;text-decoration:none">&#x2190; Storefront</a>
  </div>
</header>

<!-- Stats bar -->
<div class="stats" id="stats-bar">
  <div class="stat"><div class="n" id="stat-orders">—</div><div class="lbl">Total Orders</div></div>
  <div class="stat"><div class="n" id="stat-revenue">—</div><div class="lbl">Test Revenue</div></div>
  <div class="stat"><div class="n" id="stat-swatches">—</div><div class="lbl">Swatches Sent</div></div>
</div>

<!-- Toolbar — sort + density (house rule) -->
<div class="toolbar">
  <h2>Orders</h2>
  <label class="ctrl">Sort
    <select id="sort">
      <option value="newest">Newest</option>
      <option value="oldest">Oldest</option>
    </select>
  </label>
  <label class="ctrl">Density <input id="density" type="range" min="1" max="5" value="3"></label>
  <span class="item-count" id="item-count"></span>
</div>

<div class="orders-grid" id="orders-grid">
  <div class="loading">Loading orders…</div>
</div>

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

// sort + density — persist per house rule
const sortSel=$('#sort'),dens=$('#density');
sortSel.value=localStorage.getItem('admin-sort')||'newest';
dens.value=localStorage.getItem('admin-cols')||'3';
function applyCols(){
  document.documentElement.style.setProperty('--cols',dens.value);
  localStorage.setItem('admin-cols',dens.value);
}
applyCols();
dens.oninput=applyCols;
sortSel.onchange=()=>{localStorage.setItem('admin-sort',sortSel.value);loadOrders();};

// HARD RULE: format created date+time for every admin card
// toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'})
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'});
}

async function loadStats(){
  try{
    const r=await fetch('/api/admin/stats'),d=await r.json();
    $('#stat-orders').textContent=d.total_orders;
    $('#stat-revenue').textContent='$'+Number(d.total_revenue).toFixed(2);
    $('#stat-swatches').textContent=d.total_swatches;
  }catch(e){console.warn('stats failed',e);}
}

async function loadOrders(){
  const grid=$('#orders-grid');
  grid.innerHTML='<div class="loading">Loading…</div>';
  try{
    const r=await fetch(`/api/admin/orders?sort=${encodeURIComponent(sortSel.value)}`);
    const d=await r.json();
    $('#item-count').textContent=d.count+' order'+(d.count!==1?'s':'');
    if(!d.orders.length){grid.innerHTML='<div class="empty">No orders yet. Place a test order from the storefront.</div>';return;}
    grid.innerHTML=d.orders.map(o=>renderCard(o)).join('');
  }catch(e){grid.innerHTML=`<div class="empty">Error loading orders: ${esc(e.message)}</div>`;}
}

function renderCard(o){
  // HARD RULE: created date+time on every admin card
  const whenFmt=fmtDate(o.created_at);
  const whenISO=o.created_at||'';

  // swatch thumbnails (show up to 5, then overflow badge)
  const MAX_THUMBS=5;
  const swatches=o.swatches||[];
  const thumbs=swatches.slice(0,MAX_THUMBS).map(s=>
    `<img class="swatch-thumb" src="${esc(s.image||'')}" alt="${esc(s.title||'')}" loading="lazy"
         onerror="this.style.background='#ece8e1';this.removeAttribute('src')" title="${esc(s.title)}">`
  ).join('');
  const overflow=swatches.length>MAX_THUMBS
    ? `<div class="swatch-overflow">+${swatches.length-MAX_THUMBS}</div>` : '';

  const addr=o.shipping||{};
  const addrLine=`${esc(addr.address1||'')}${addr.address2?' '+esc(addr.address2):''}, ${esc(addr.city||'')}, ${esc(addr.state||'')} ${esc(addr.zip||'')}`;

  const vibeLabel={modern:'Modern & Minimal',traditional:'Traditional',floral:'Floral & Botanical',
    grasscloth:'Grasscloth & Natural','bold-color':'Bold Color',geometric:'Geometric'};

  return `<div class="order-card">
    <div class="card-header">
      <span class="order-id">${esc(o.id)}</span>
      <span class="status-badge paid">${esc(o.payment_status||'')}</span>
    </div>

    <!-- HARD RULE: created date + time, full ISO in title= -->
    <div class="card-when">
      <span class="clock">&#x1F553;</span>
      <time datetime="${esc(whenISO)}" title="${esc(whenISO)}">${esc(whenFmt)}</time>
    </div>

    <span class="card-vibe">${esc(vibeLabel[o.vibe]||o.vibe||'All')}</span>

    <div class="card-swatches" title="${swatches.length} swatches">
      ${thumbs}${overflow}
    </div>

    <div class="card-addr">
      <div class="name">${esc(addr.name||'')}</div>
      <div>${addrLine}</div>
      <div class="card-email">${esc(o.email||'')}</div>
    </div>

    <div>
      <div class="card-price">$${Number(o.box_price||0).toFixed(2)} — ${esc(o.payment_method||'')}</div>
      <div class="credit-line">&#x2705; ${esc(o.credit_note||'')}</div>
      ${o.shopify_permalink ? `<div style="margin-top:5px;font-size:11px"><a href="${esc(o.shopify_permalink)}" target="_blank" rel="noopener noreferrer" style="color:#27ae60">🔗 Shopify checkout link</a></div>` : ''}
    </div>
  </div>`;
}

loadStats();
loadOrders();
// auto-refresh every 30s while tab is visible
document.addEventListener('visibilitychange',()=>{if(!document.hidden){loadStats();loadOrders();}});
</script>
</body>
</html>