415 lines
21 KiB
HTML
415 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
|
|
<title>Vela — Order Management</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
:root{--bg:#0a0a0b;--bg-card:#121214;--bg-card-hover:#1a1a1e;--border:#1e1e22;--border-hover:#2a2a30;--text:#e8e8ed;--text-dim:#888891;--text-muted:#5c5c66;--accent:#6c5ce7;--green:#00c951;--orange:#ff922b;--red:#ff4757;--overlay:rgba(0,0,0,.7)}
|
|
.light{--bg:#f5f5f7;--bg-card:#ffffff;--bg-card-hover:#f0f0f2;--border:#d2d2d7;--border-hover:#c0c0c5;--text:#1d1d1f;--text-dim:#6e6e73;--text-muted:#aeaeb2;--accent:#5e5ce6;--green:#34c759;--orange:#ff9500;--red:#ff3b30;--overlay:rgba(0,0,0,.35)}
|
|
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);padding:1.5rem;-webkit-font-smoothing:antialiased}
|
|
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;padding-bottom:.75rem;border-bottom:1px solid var(--border)}
|
|
.header h1{font-size:1.4rem;font-weight:700}
|
|
.header h1 span{color:var(--accent)}
|
|
.logout{font-size:.8rem;color:var(--text-dim);cursor:pointer;padding:.4rem .8rem;border:1px solid var(--border);border-radius:6px;background:transparent;font-family:inherit;transition:all .2s}
|
|
.logout:hover,.logout:active{border-color:var(--red);color:var(--red)}
|
|
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-bottom:1.5rem}
|
|
.stat{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.8rem;text-align:center}
|
|
.stat .num{font-size:1.6rem;font-weight:800}
|
|
.stat .lbl{font-size:.72rem;color:var(--text-dim);margin-top:.15rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
.tabs{display:flex;gap:.35rem;margin-bottom:1rem;overflow-x:auto;padding-bottom:.25rem;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
|
.tabs::-webkit-scrollbar{display:none}
|
|
.tab{padding:.45rem .85rem;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-dim);cursor:pointer;font-family:inherit;font-size:.78rem;white-space:nowrap;transition:all .2s;flex-shrink:0;touch-action:manipulation}
|
|
.tab:active{transform:scale(.96)}
|
|
.tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
|
|
.order-summary{font-size:.75rem;color:var(--text-dim);margin-bottom:.75rem}
|
|
/* ── Desktop table ── */
|
|
.orders-table{width:100%;border-collapse:collapse;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;overflow:hidden;display:none}
|
|
.orders-table th{text-align:left;padding:.65rem .85rem;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em;color:var(--text-dim);border-bottom:1px solid var(--border);font-weight:600}
|
|
.orders-table td{padding:.65rem .85rem;font-size:.82rem;border-bottom:1px solid var(--border)}
|
|
.orders-table tr:last-child td{border-bottom:none}
|
|
.orders-table tr{cursor:pointer;transition:background .15s}
|
|
.orders-table tr:hover{background:var(--bg-card-hover)}
|
|
/* ── Mobile cards ── */
|
|
.order-cards{display:flex;flex-direction:column;gap:.6rem}
|
|
.order-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:.85rem;cursor:pointer;transition:all .15s;position:relative;touch-action:manipulation}
|
|
.order-card:active{transform:scale(.98);border-color:var(--border-hover)}
|
|
.order-card .top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.5rem}
|
|
.order-card .top .id{font-weight:700;font-size:.9rem}
|
|
.order-card .top .date{font-size:.72rem;color:var(--text-muted)}
|
|
.order-card .fields{display:grid;grid-template-columns:1fr 1fr;gap:.3rem .6rem;font-size:.82rem}
|
|
.order-card .fields .lbl{color:var(--text-dim);font-size:.72rem}
|
|
.order-card .fields .val{color:var(--text)}
|
|
.order-card .fields .val.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.badge{display:inline-block;padding:.25rem .65rem;border-radius:100px;font-size:.72rem;font-weight:600}
|
|
.badge-received{background:rgba(108,92,231,.15);color:var(--accent)}
|
|
.badge-confirmed{background:rgba(0,201,81,.15);color:var(--green)}
|
|
.badge-hardware{background:rgba(255,146,43,.15);color:var(--orange)}
|
|
.badge-installing{background:rgba(108,92,231,.15);color:var(--accent)}
|
|
.badge-completed{background:rgba(0,201,81,.15);color:var(--green)}
|
|
.badge-cancelled{background:rgba(255,71,87,.15);color:var(--red)}
|
|
.status-select{padding:.35rem .5rem;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:inherit;font-size:.82rem;cursor:pointer}
|
|
.modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:var(--overlay);z-index:200;align-items:flex-end;justify-content:center;padding:env(safe-area-inset-bottom) 0 0}
|
|
.modal.open{display:flex}
|
|
.modal.open .modal-inner{animation:slideUp .25s cubic-bezier(.4,0,.2,1)}
|
|
@keyframes slideUp{from{transform:translateY(100%)}to{transform:translateY(0)}}
|
|
.modal-inner{background:var(--bg-card);border:1px solid var(--border);border-radius:16px 16px 0 0;padding:1.5rem;max-width:600px;width:100%;max-height:85vh;overflow-y:auto}
|
|
.modal-handle{width:36px;height:4px;border-radius:4px;background:var(--text-muted);margin:0 auto 1.25rem}
|
|
.modal-inner h2{font-size:1.2rem;margin-bottom:1rem}
|
|
.modal-inner .row{display:flex;gap:.5rem;margin-bottom:.5rem;font-size:.9rem;align-items:center}
|
|
.modal-inner .row .lbl{color:var(--text-dim);min-width:70px;flex-shrink:0}
|
|
.modal-inner textarea{width:100%;padding:.6rem;border-radius:8px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:inherit;font-size:.85rem;resize:vertical;min-height:60px;margin-top:.5rem}
|
|
.modal-inner .btn-row{display:flex;gap:.5rem;margin-top:1rem}
|
|
.modal-inner .btn{padding:.55rem 1.2rem;border-radius:8px;border:none;font-family:inherit;font-size:.85rem;cursor:pointer;transition:all .15s;touch-action:manipulation}
|
|
.modal-inner .btn:active{transform:scale(.96)}
|
|
.modal-inner .btn-primary{background:var(--accent);color:#fff}
|
|
.modal-inner .btn-ghost{background:transparent;border:1px solid var(--border);color:var(--text-dim)}
|
|
.modal-inner .btn-ghost:active{background:var(--bg-card-hover)}
|
|
.notes{margin-top:1rem}
|
|
.note-item{font-size:.82rem;color:var(--text-dim);padding:.5rem 0;border-bottom:1px solid var(--border)}
|
|
.note-item:last-child{border-bottom:none}
|
|
.note-item .date{font-size:.7rem;color:var(--text-muted);margin-top:.15rem}
|
|
.login{display:flex;align-items:center;justify-content:center;min-height:80vh;padding:1.5rem}
|
|
.login-card{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;padding:2.5rem 2rem;width:100%;max-width:360px;text-align:center}
|
|
.login-card h1{font-size:1.8rem;font-weight:800;margin-bottom:.35rem}
|
|
.login-card p{color:var(--text-dim);font-size:.9rem;margin-bottom:2rem}
|
|
.login-card input{width:100%;padding:.85rem;border-radius:10px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-family:inherit;font-size:1.05rem;margin-bottom:1rem;outline:none;transition:border-color .2s}
|
|
.login-card input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(108,92,231,.18)}
|
|
.login-card .btn{width:100%;padding:.85rem;border-radius:10px;border:none;background:var(--accent);color:#fff;font-family:inherit;font-size:1.05rem;font-weight:600;cursor:pointer;transition:background .2s}
|
|
.login-card .btn:active{background:var(--accent-hover)}
|
|
.login-error{color:var(--red);font-size:.85rem;margin-top:.75rem;display:none}
|
|
.empty{text-align:center;padding:3rem 1rem;color:var(--text-dim);font-size:.9rem}
|
|
.empty .icon{font-size:2.5rem;margin-bottom:1rem;opacity:.5}
|
|
@media(min-width:769px){
|
|
body{padding:2rem}
|
|
.orders-table{display:table}
|
|
.order-cards{display:none}
|
|
.modal.open .modal-inner{animation:none}
|
|
.modal{align-items:center}
|
|
.modal-inner{border-radius:12px;padding:2rem}
|
|
.modal-handle{display:none}
|
|
}
|
|
@media(max-width:768px){
|
|
body{padding:.75rem}
|
|
.orders-table{display:none}
|
|
.order-cards{display:flex}
|
|
.header h1{font-size:1.2rem}
|
|
.stats{grid-template-columns:repeat(auto-fit,minmax(85px,1fr));gap:.5rem}
|
|
.stat{padding:.55rem}
|
|
.stat .num{font-size:1.2rem}
|
|
.stat .lbl{font-size:.65rem}
|
|
.tabs{gap:.25rem;margin-left:-.75rem;margin-right:-.75rem;padding:0 .75rem .25rem}
|
|
.tab{padding:.4rem .7rem;font-size:.72rem}
|
|
.login-card{padding:2rem 1.5rem;border-radius:12px}
|
|
}
|
|
.theme-btn{font-size:1.1rem;background:transparent;border:none;cursor:pointer;padding:.3rem;border-radius:6px;color:var(--text-dim);transition:color .2s;line-height:1;touch-action:manipulation}
|
|
.theme-btn:hover,.theme-btn:active{color:var(--text)}
|
|
</style>
|
|
<script>(function(){var t=localStorage.getItem('vela_theme');if(t==='light'||(!t&&window.matchMedia('(prefers-color-scheme:light)').matches))document.documentElement.classList.add('light')})();</script>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="loginView" class="login">
|
|
<div class="login-card">
|
|
<h1>VELA <span style="color:var(--accent)">Admin</span></h1>
|
|
<p>Order management dashboard</p>
|
|
<input type="password" id="loginPassword" placeholder="Admin password" autocomplete="current-password" onkeydown="if(event.key==='Enter')login()">
|
|
<button class="btn" onclick="login()">Sign In</button>
|
|
<div class="login-error" id="loginError">Wrong password</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="dashboardView" style="display:none">
|
|
<div class="header">
|
|
<h1>VELA <span>Admin</span></h1>
|
|
<div style="display:flex;gap:.5rem;align-items:center">
|
|
<button class="theme-btn" id="themeToggle" onclick="toggleTheme()" title="Toggle theme"></button>
|
|
<button class="logout" onclick="logout()">Logout</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats" id="statsRow"></div>
|
|
|
|
<div class="tabs" id="tabBar">
|
|
<button class="tab active" data-filter="all" onclick="setFilter('all')">All</button>
|
|
<button class="tab" data-filter="received" onclick="setFilter('received')">Received</button>
|
|
<button class="tab" data-filter="confirmed" onclick="setFilter('confirmed')">Confirmed</button>
|
|
<button class="tab" data-filter="hardware ready" onclick="setFilter('hardware ready')">Ready</button>
|
|
<button class="tab" data-filter="installing" onclick="setFilter('installing')">Installing</button>
|
|
<button class="tab" data-filter="completed" onclick="setFilter('completed')">Completed</button>
|
|
<button class="tab" data-filter="cancelled" onclick="setFilter('cancelled')">Cancelled</button>
|
|
</div>
|
|
|
|
<table class="orders-table">
|
|
<thead>
|
|
<tr><th>ID</th><th>Name</th><th>Email</th><th>Phone</th><th>Status</th><th>Date</th><th></th></tr>
|
|
</thead>
|
|
<tbody id="ordersBody"></tbody>
|
|
</table>
|
|
|
|
<div class="order-cards" id="ordersCards"></div>
|
|
</div>
|
|
|
|
<div class="modal" id="detailModal">
|
|
<div class="modal-inner">
|
|
<div class="modal-handle"></div>
|
|
<h2>Order #<span id="detailId"></span></h2>
|
|
<div class="row"><span class="lbl">Name:</span><span id="detailName"></span></div>
|
|
<div class="row"><span class="lbl">Email:</span><span id="detailEmail"></span></div>
|
|
<div class="row"><span class="lbl">Phone:</span><span id="detailPhone"></span></div>
|
|
<div class="row"><span class="lbl">Date:</span><span id="detailDate"></span></div>
|
|
<div class="row"><span class="lbl">Status:</span>
|
|
<select class="status-select" id="detailStatus" onchange="updateStatus()">
|
|
<option value="received">Received</option>
|
|
<option value="confirmed">Confirmed</option>
|
|
<option value="hardware ready">Hardware Ready</option>
|
|
<option value="installing">Installing</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
<div style="margin-top:1rem">
|
|
<label style="font-size:.85rem;color:var(--text-dim)">Internal Note</label>
|
|
<textarea id="noteInput" placeholder="Add a note about this order..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();addNote()}"></textarea>
|
|
<button class="btn btn-primary" onclick="addNote()" style="margin-top:.5rem">Add Note</button>
|
|
</div>
|
|
<div class="notes" id="notesList"></div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API = 'http://192.168.1.126:8581';
|
|
let token = localStorage.getItem('vela_admin_token') || '';
|
|
let orders = [];
|
|
let currentFilter = 'all';
|
|
let currentOrderId = null;
|
|
|
|
// ── Auth ──
|
|
|
|
async function login() {
|
|
const pw = document.getElementById('loginPassword').value;
|
|
try {
|
|
const r = await fetch(API + '/admin/login', {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});
|
|
if (!r.ok) { document.getElementById('loginError').style.display='block'; return; }
|
|
const d = await r.json();
|
|
token = d.token;
|
|
localStorage.setItem('vela_admin_token', token);
|
|
showDashboard();
|
|
} catch(e) { document.getElementById('loginError').style.display='block'; }
|
|
}
|
|
|
|
function logout() {
|
|
token = ''; localStorage.removeItem('vela_admin_token');
|
|
document.getElementById('dashboardView').style.display='none';
|
|
document.getElementById('loginView').style.display='flex';
|
|
}
|
|
|
|
function showDashboard() {
|
|
document.getElementById('loginView').style.display='none';
|
|
document.getElementById('dashboardView').style.display='block';
|
|
loadOrders();
|
|
}
|
|
|
|
// ── Data loading ──
|
|
|
|
async function loadOrders() {
|
|
try {
|
|
const r = await fetch(API + '/admin/submissions?per_page=200', {headers:{'Authorization':'Bearer '+token}});
|
|
if (r.status === 401) { logout(); return; }
|
|
if (!r.ok) return;
|
|
const d = await r.json();
|
|
orders = d.submissions || [];
|
|
renderStats(d); renderOrders();
|
|
} catch(e) {}
|
|
}
|
|
|
|
// ── Stats ──
|
|
|
|
function renderStats(d) {
|
|
const total = d.total || 0;
|
|
const byStatus = {};
|
|
orders.forEach(o => { const s = o.status || 'received'; byStatus[s] = (byStatus[s]||0)+1; });
|
|
const labels = {received:'Received',confirmed:'Confirmed','hardware ready':'Ready',installing:'Installing',completed:'Completed',cancelled:'Cancelled'};
|
|
let html = '<div class="stat"><div class="num">'+total+'</div><div class="lbl">Total</div></div>';
|
|
for (const [k,v] of Object.entries(byStatus))
|
|
html += '<div class="stat"><div class="num">'+v+'</div><div class="lbl">'+(labels[k]||k)+'</div></div>';
|
|
document.getElementById('statsRow').innerHTML = html;
|
|
}
|
|
|
|
function orderData(o) {
|
|
const data = {}; (o.data||[]).forEach(d => data[d.field_label] = d.field_value);
|
|
return data;
|
|
}
|
|
|
|
function statusLabel(s) {
|
|
return {received:'Received',confirmed:'Confirmed','hardware ready':'Hardware Ready',installing:'Installing',completed:'Completed',cancelled:'Cancelled'}[s]||s;
|
|
}
|
|
|
|
function statusBadge(s) {
|
|
return '<span class="badge badge-'+s.replace(' ','')+'">'+statusLabel(s)+'</span>';
|
|
}
|
|
|
|
// ── Render ──
|
|
|
|
function renderOrders() {
|
|
const filtered = currentFilter === 'all' ? orders : orders.filter(o => (o.status||'received') === currentFilter);
|
|
const summary = document.getElementById('orderSummary') || document.createElement('div');
|
|
if (!summary.id) { summary.id = 'orderSummary'; summary.className = 'order-summary'; document.getElementById('tabBar').after(summary); }
|
|
const isMobile = window.innerWidth < 769;
|
|
summary.textContent = filtered.length + ' order' + (filtered.length !== 1 ? 's' : '');
|
|
|
|
// Table rows (desktop)
|
|
let tHtml = '';
|
|
let cHtml = '';
|
|
filtered.forEach(o => {
|
|
const data = orderData(o);
|
|
const status = o.status || 'received';
|
|
const name = data.Name || data.name || data.email || '-';
|
|
const email = data.Email || data.email || '-';
|
|
const phone = data.Phone || data.phone || '-';
|
|
const date = (o.created_at||'').slice(0,10);
|
|
|
|
tHtml += '<tr onclick="openModal('+o.id+')">'
|
|
+ '<td style="font-weight:600">#'+o.id+'</td>'
|
|
+ '<td>'+esc(name)+'</td>'
|
|
+ '<td>'+esc(email)+'</td>'
|
|
+ '<td>'+esc(phone)+'</td>'
|
|
+ '<td>'+statusBadge(status)+'</td>'
|
|
+ '<td style="font-size:.78rem;color:var(--text-dim)">'+date+'</td>'
|
|
+ '<td style="color:var(--text-dim);font-size:.82rem">→</td>'
|
|
+ '</tr>';
|
|
|
|
cHtml += '<div class="order-card" onclick="openModal('+o.id+')">'
|
|
+ '<div class="top">'
|
|
+ '<div><span class="id">#'+o.id+'</span> '+statusBadge(status)+'</div>'
|
|
+ '<span class="date">'+date+'</span>'
|
|
+ '</div>'
|
|
+ '<div class="fields">'
|
|
+ '<span class="lbl">Name</span><span class="val truncate">'+esc(name)+'</span>'
|
|
+ '<span class="lbl">Email</span><span class="val truncate">'+esc(email)+'</span>'
|
|
+ '<span class="lbl">Phone</span><span class="val">'+esc(phone)+'</span>'
|
|
+ '</div>'
|
|
+ '</div>';
|
|
});
|
|
|
|
if (!filtered.length) {
|
|
tHtml = '<tr><td colspan="7"><div class="empty"><div class="icon">📋</div>'+(currentFilter==='all'?'No orders yet':'No orders with this status')+'</div></td></tr>';
|
|
cHtml = '<div class="empty"><div class="icon">📋</div>'+(currentFilter==='all'?'No orders yet':'No orders with this status')+'</div>';
|
|
}
|
|
|
|
document.getElementById('ordersBody').innerHTML = tHtml;
|
|
document.getElementById('ordersCards').innerHTML = cHtml;
|
|
}
|
|
|
|
function esc(s) {
|
|
const div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(s));
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// ── Filter ──
|
|
|
|
function setFilter(f) {
|
|
currentFilter = f;
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.filter===f));
|
|
// scroll active tab into view (mobile)
|
|
const active = document.querySelector('.tab.active');
|
|
if (active) active.scrollIntoView({behavior:'smooth',inline:'center',block:'nearest'});
|
|
renderOrders();
|
|
}
|
|
|
|
// ── Modal ──
|
|
|
|
async function openModal(id) {
|
|
currentOrderId = id;
|
|
const o = orders.find(x => x.id === id); if (!o) return;
|
|
const data = orderData(o);
|
|
document.getElementById('detailId').textContent = id;
|
|
document.getElementById('detailName').textContent = data.Name||data.name||'-';
|
|
document.getElementById('detailEmail').textContent = data.Email||data.email||'-';
|
|
document.getElementById('detailPhone').textContent = data.Phone||data.phone||'-';
|
|
document.getElementById('detailDate').textContent = o.created_at||'-';
|
|
document.getElementById('detailStatus').value = o.status||'received';
|
|
document.getElementById('noteInput').value = '';
|
|
document.getElementById('notesList').innerHTML = '<div style="color:var(--text-dim);font-size:.85rem">Loading...</div>';
|
|
document.getElementById('detailModal').classList.add('open');
|
|
document.body.style.overflow = 'hidden';
|
|
loadNotes(id);
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('detailModal').classList.remove('open');
|
|
document.body.style.overflow = '';
|
|
currentOrderId = null;
|
|
}
|
|
|
|
// Close modal on backdrop tap
|
|
document.getElementById('detailModal').addEventListener('click', function(e) {
|
|
if (e.target === this) closeModal();
|
|
});
|
|
|
|
async function updateStatus() {
|
|
const status = document.getElementById('detailStatus').value;
|
|
await fetch(API + '/admin/status', {method:'POST',headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({submission_id:currentOrderId,status})});
|
|
loadOrders();
|
|
}
|
|
|
|
async function addNote() {
|
|
const note = document.getElementById('noteInput').value.trim();
|
|
if (!note) return;
|
|
await fetch(API + '/admin/notes', {method:'POST',headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json'},body:JSON.stringify({submission_id:currentOrderId,note})});
|
|
document.getElementById('noteInput').value = '';
|
|
loadNotes(currentOrderId);
|
|
}
|
|
|
|
async function loadNotes(id) {
|
|
const r = await fetch(API + '/admin/notes/'+id, {headers:{'Authorization':'Bearer '+token}});
|
|
if (!r.ok) return;
|
|
const notes = await r.json();
|
|
let html = notes.length ? '<div style="font-size:.85rem;font-weight:600;margin-bottom:.5rem">Notes</div>' : '<div style="color:var(--text-dim);font-size:.85rem">No notes yet.</div>';
|
|
notes.forEach(n => {
|
|
html += '<div class="note-item">'+esc(n.note||'')+'<div class="date">'+((n.created_at||'').slice(0,16))+'</div></div>';
|
|
});
|
|
document.getElementById('notesList').innerHTML = html;
|
|
}
|
|
|
|
// ── Theme ──
|
|
|
|
function toggleTheme() {
|
|
const html = document.documentElement;
|
|
html.classList.toggle('light');
|
|
const isLight = html.classList.contains('light');
|
|
localStorage.setItem('vela_theme', isLight ? 'light' : 'dark');
|
|
document.getElementById('themeToggle').textContent = isLight ? '🌙' : '☀️';
|
|
}
|
|
|
|
function initThemeIcon() {
|
|
const isLight = document.documentElement.classList.contains('light');
|
|
document.getElementById('themeToggle').textContent = isLight ? '🌙' : '☀️';
|
|
}
|
|
|
|
// ── Init ──
|
|
|
|
if (token) {
|
|
fetch(API + '/admin/stats', {headers:{'Authorization':'Bearer '+token}}).then(r => {
|
|
if (r.ok) showDashboard();
|
|
}).catch(()=>{});
|
|
}
|
|
|
|
initThemeIcon();
|
|
|
|
// Re-render on resize (table ↔ cards)
|
|
let resizeTimer;
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(renderOrders, 150);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|