Complete admin dashboard with all pages: profile editor (cover photo, business hours, social links), settings (account, notifications, security), enhanced trades view with detail modal, and orders with status workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
220 lines
10 KiB
HTML
220 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Grubflip Admin — Orders</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Plus+Jakarta+Sans:wght@600;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/tokens.css">
|
|
<link rel="stylesheet" href="css/admin.css">
|
|
</head>
|
|
<body>
|
|
<div class="admin-layout">
|
|
<div class="sidebar-overlay" id="sidebarOverlay"></div>
|
|
|
|
<aside class="sidebar" id="sidebar" role="navigation" aria-label="Admin navigation">
|
|
<div class="sidebar-brand">
|
|
<div class="logo-icon">GF</div>
|
|
<span>Grubflip</span>
|
|
</div>
|
|
<nav class="sidebar-nav">
|
|
<div class="sidebar-section-title">Main</div>
|
|
<a href="dashboard.html" class="sidebar-link"><span class="icon">📊</span> Dashboard</a>
|
|
<a href="meals.html" class="sidebar-link"><span class="icon">🍔</span> Meals</a>
|
|
<a href="orders.html" class="sidebar-link active"><span class="icon">📦</span> Orders</a>
|
|
<a href="trades.html" class="sidebar-link"><span class="icon">🔄</span> Trades</a>
|
|
<div class="sidebar-section-title">Settings</div>
|
|
<a href="profile.html" class="sidebar-link"><span class="icon">🏪</span> Restaurant Profile</a>
|
|
<a href="settings.html" class="sidebar-link"><span class="icon">⚙️</span> Settings</a>
|
|
</nav>
|
|
<div class="sidebar-footer">
|
|
<div class="sidebar-user">
|
|
<div class="avatar" id="userAvatar">DR</div>
|
|
<div class="user-info">
|
|
<div class="user-name" id="userName">Demo Restaurant</div>
|
|
<div class="user-role" id="userRole">Owner</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="main-content">
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<button class="menu-toggle" id="menuToggle" aria-label="Toggle menu">☰</button>
|
|
<h1 class="topbar-title">Orders</h1>
|
|
</div>
|
|
<div class="topbar-right">
|
|
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">⏻</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="page-content">
|
|
<div class="page-header">
|
|
<h1>Orders</h1>
|
|
<p class="subtitle">Manage incoming meal orders from customers.</p>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-grid" style="margin-bottom:var(--gf-space-6)">
|
|
<div class="stat-card">
|
|
<div class="stat-icon orange">📦</div>
|
|
<div class="stat-info">
|
|
<h3>Total Orders</h3>
|
|
<div class="stat-value" id="statTotal">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon yellow">🍳</div>
|
|
<div class="stat-info">
|
|
<h3>Preparing</h3>
|
|
<div class="stat-value" id="statPreparing">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon green">✅</div>
|
|
<div class="stat-info">
|
|
<h3>Fulfilled</h3>
|
|
<div class="stat-value" id="statFulfilled">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon blue">💰</div>
|
|
<div class="stat-info">
|
|
<h3>Revenue Today</h3>
|
|
<div class="stat-value" id="statRevenue">--</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="meals-toolbar">
|
|
<div class="search-box">
|
|
<span class="search-icon">🔍</span>
|
|
<input type="text" class="form-input" id="searchInput" placeholder="Search orders...">
|
|
</div>
|
|
<select class="form-select" id="filterStatus" style="width:auto;min-width:8rem">
|
|
<option value="">All Status</option>
|
|
<option value="new">New</option>
|
|
<option value="preparing">Preparing</option>
|
|
<option value="ready">Ready</option>
|
|
<option value="fulfilled">Fulfilled</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Orders Table -->
|
|
<div class="card">
|
|
<div id="ordersContent">
|
|
<div class="loading-overlay"><div class="spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<script src="js/admin-app.js"></script>
|
|
<script>
|
|
if (!AdminApp.requireAuth()) throw new Error('Not authenticated');
|
|
var user = AdminApp.getUser();
|
|
if (user.name) {
|
|
document.getElementById('userName').textContent = user.name;
|
|
document.getElementById('userAvatar').textContent = user.name.split(' ').map(function(w){ return w[0]; }).join('').slice(0,2).toUpperCase();
|
|
}
|
|
if (user.role) document.getElementById('userRole').textContent = user.role.charAt(0).toUpperCase() + user.role.slice(1);
|
|
|
|
AdminApp.initSidebar();
|
|
document.getElementById('logoutBtn').addEventListener('click', AdminApp.logout);
|
|
|
|
var orders = [];
|
|
|
|
var DEMO_ORDERS = [
|
|
{ id: 'ORD-001', customer: 'Jamie L.', meal: 'Spicy Ramen Bowl', qty: 1, total: 14.99, status: 'new', created_at: '2026-03-27T15:30:00Z' },
|
|
{ id: 'ORD-002', customer: 'Alex K.', meal: 'Chicken Burrito', qty: 2, total: 25.98, status: 'preparing', created_at: '2026-03-27T15:15:00Z' },
|
|
{ id: 'ORD-003', customer: 'Sam R.', meal: 'Veggie Power Bowl', qty: 1, total: 13.49, status: 'ready', created_at: '2026-03-27T14:45:00Z' },
|
|
{ id: 'ORD-004', customer: 'Morgan T.', meal: 'BBQ Pulled Pork', qty: 1, total: 11.99, status: 'fulfilled', created_at: '2026-03-27T13:20:00Z' },
|
|
{ id: 'ORD-005', customer: 'Riley W.', meal: 'Greek Salad Wrap', qty: 3, total: 31.47, status: 'fulfilled', created_at: '2026-03-27T12:00:00Z' },
|
|
{ id: 'ORD-006', customer: 'Casey H.', meal: 'Spicy Ramen Bowl', qty: 1, total: 14.99, status: 'fulfilled', created_at: '2026-03-27T11:30:00Z' },
|
|
{ id: 'ORD-007', customer: 'Drew N.', meal: 'Chicken Burrito', qty: 1, total: 12.99, status: 'cancelled', created_at: '2026-03-27T10:45:00Z' },
|
|
{ id: 'ORD-008', customer: 'Taylor P.', meal: 'Veggie Power Bowl', qty: 2, total: 26.98, status: 'fulfilled', created_at: '2026-03-26T16:00:00Z' },
|
|
];
|
|
|
|
async function loadOrders() {
|
|
try {
|
|
var data = await AdminApp.api('/admin/orders');
|
|
orders = data.orders || data;
|
|
} catch(e) {
|
|
orders = DEMO_ORDERS.slice();
|
|
}
|
|
updateStats();
|
|
renderOrders();
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('statTotal').textContent = orders.length;
|
|
document.getElementById('statPreparing').textContent = orders.filter(function(o){ return o.status === 'preparing' || o.status === 'new'; }).length;
|
|
document.getElementById('statFulfilled').textContent = orders.filter(function(o){ return o.status === 'fulfilled'; }).length;
|
|
var todayStr = new Date().toDateString();
|
|
var revenue = orders.filter(function(o){ return o.status === 'fulfilled' && new Date(o.created_at).toDateString() === todayStr; })
|
|
.reduce(function(sum, o){ return sum + o.total; }, 0);
|
|
document.getElementById('statRevenue').textContent = '$' + revenue.toFixed(2);
|
|
}
|
|
|
|
function renderOrders() {
|
|
var container = document.getElementById('ordersContent');
|
|
var search = document.getElementById('searchInput').value.toLowerCase();
|
|
var statusFilter = document.getElementById('filterStatus').value;
|
|
|
|
var filtered = orders.filter(function(o) {
|
|
if (search && (o.customer + ' ' + o.meal + ' ' + o.id).toLowerCase().indexOf(search) === -1) return false;
|
|
if (statusFilter && o.status !== statusFilter) return false;
|
|
return true;
|
|
});
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="icon">📦</div><h3>No orders found</h3><p>Try adjusting your filters.</p></div>';
|
|
return;
|
|
}
|
|
|
|
var statusMap = { 'new': 'badge-info', preparing: 'badge-warning', ready: 'badge-primary', fulfilled: 'badge-success', cancelled: 'badge-default' };
|
|
|
|
container.innerHTML = '<div class="table-wrapper"><table class="data-table"><thead><tr><th>Order</th><th>Customer</th><th>Meal</th><th>Qty</th><th>Total</th><th>Status</th><th>Time</th><th></th></tr></thead><tbody>' +
|
|
filtered.map(function(o) {
|
|
var canUpdate = o.status !== 'fulfilled' && o.status !== 'cancelled';
|
|
return '<tr>' +
|
|
'<td class="font-mono text-muted">' + o.id + '</td>' +
|
|
'<td>' + AdminApp.escapeHtml(o.customer) + '</td>' +
|
|
'<td>' + AdminApp.escapeHtml(o.meal) + '</td>' +
|
|
'<td>' + o.qty + '</td>' +
|
|
'<td class="font-mono">$' + o.total.toFixed(2) + '</td>' +
|
|
'<td><span class="badge ' + (statusMap[o.status] || 'badge-default') + '">' + o.status + '</span></td>' +
|
|
'<td class="text-muted">' + AdminApp.timeAgo(o.created_at) + '</td>' +
|
|
'<td>' + (canUpdate ? '<button class="btn btn-ghost btn-sm" onclick="advanceOrder(\'' + o.id + '\')">Advance</button>' : '') + '</td>' +
|
|
'</tr>';
|
|
}).join('') +
|
|
'</tbody></table></div>';
|
|
}
|
|
|
|
function advanceOrder(id) {
|
|
var order = orders.find(function(o){ return o.id === id; });
|
|
if (!order) return;
|
|
var flow = { 'new': 'preparing', 'preparing': 'ready', 'ready': 'fulfilled' };
|
|
var next = flow[order.status];
|
|
if (!next) return;
|
|
order.status = next;
|
|
updateStats();
|
|
renderOrders();
|
|
AdminApp.toast('Order ' + id + ' → ' + next, 'success');
|
|
}
|
|
|
|
document.getElementById('searchInput').addEventListener('input', renderOrders);
|
|
document.getElementById('filterStatus').addEventListener('change', renderOrders);
|
|
|
|
loadOrders();
|
|
</script>
|
|
</body>
|
|
</html>
|