Built out the full restaurant owner admin dashboard: - Dashboard overview with stats, recent trades, and top meals - Meals management with card grid, add/edit/delete modals, search & filter - Orders page with status tabs, accept/ready/complete workflow - Trades activity page with search and stats overview - Shared admin-app.js module (sidebar, API client, toast, auth helpers) - All pages use existing design tokens, mobile-first responsive layout - Demo data fallback when API endpoints aren't ready yet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
311 lines
12 KiB
HTML
311 lines
12 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">Track and manage incoming meal orders from trades.</p>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs">
|
|
<button class="tab active" data-tab="all">All Orders</button>
|
|
<button class="tab" data-tab="pending">Pending</button>
|
|
<button class="tab" data-tab="preparing">Preparing</button>
|
|
<button class="tab" data-tab="ready">Ready</button>
|
|
<button class="tab" data-tab="completed">Completed</button>
|
|
</div>
|
|
|
|
<!-- Orders Table -->
|
|
<div class="card">
|
|
<div id="ordersContainer">
|
|
<div class="loading-overlay"><div class="spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Order Detail Modal -->
|
|
<div class="modal-overlay" id="orderModal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2>Order Details</h2>
|
|
<button class="modal-close" id="orderModalClose" aria-label="Close">×</button>
|
|
</div>
|
|
<div class="modal-body" id="orderModalBody">
|
|
</div>
|
|
<div class="modal-footer" id="orderModalFooter">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<script src="js/admin-app.js"></script>
|
|
<script>
|
|
if (!AdminApp.requireAuth()) throw new Error('Not authenticated');
|
|
const user = AdminApp.getUser();
|
|
|
|
if (user.name) {
|
|
document.getElementById('userName').textContent = user.name;
|
|
document.getElementById('userAvatar').textContent = user.name.split(' ').map(w => 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);
|
|
|
|
// --- State ---
|
|
let orders = [];
|
|
let currentTab = 'all';
|
|
|
|
const DEMO_ORDERS = [
|
|
{ id: 1001, user: 'Jamie L.', meal: 'Spicy Ramen Bowl', status: 'completed', time: '2026-03-27T15:10:00Z', notes: '' },
|
|
{ id: 1002, user: 'Alex K.', meal: 'Chicken Burrito', status: 'pending', time: '2026-03-27T15:15:00Z', notes: 'No onions please' },
|
|
{ id: 1003, user: 'Sam R.', meal: 'Veggie Power Bowl', status: 'completed', time: '2026-03-27T14:50:00Z', notes: '' },
|
|
{ id: 1004, user: 'Morgan T.', meal: 'BBQ Pulled Pork', status: 'preparing', time: '2026-03-27T14:35:00Z', notes: 'Extra sauce' },
|
|
{ id: 1005, user: 'Riley W.', meal: 'Greek Salad Wrap', status: 'ready', time: '2026-03-27T14:20:00Z', notes: '' },
|
|
{ id: 1006, user: 'Casey P.', meal: 'Spicy Ramen Bowl', status: 'pending', time: '2026-03-27T15:25:00Z', notes: 'Mild spice level' },
|
|
{ id: 1007, user: 'Jordan B.', meal: 'Chicken Burrito', status: 'preparing', time: '2026-03-27T14:55:00Z', notes: '' },
|
|
{ id: 1008, user: 'Taylor M.', meal: 'Veggie Power Bowl', status: 'completed', time: '2026-03-27T13:30:00Z', notes: 'Extra avocado' },
|
|
];
|
|
|
|
// --- Tabs ---
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
currentTab = tab.dataset.tab;
|
|
renderOrders();
|
|
});
|
|
});
|
|
|
|
// --- Load ---
|
|
async function loadOrders() {
|
|
try {
|
|
const data = await AdminApp.api('/admin/orders');
|
|
orders = data.orders || data;
|
|
} catch {
|
|
orders = [...DEMO_ORDERS];
|
|
}
|
|
renderOrders();
|
|
}
|
|
|
|
// --- Render ---
|
|
function renderOrders() {
|
|
const container = document.getElementById('ordersContainer');
|
|
let filtered = orders;
|
|
if (currentTab !== 'all') {
|
|
filtered = orders.filter(o => o.status === currentTab);
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="icon">📦</div>
|
|
<h3>No ${currentTab === 'all' ? '' : currentTab + ' '}orders</h3>
|
|
<p>Orders will appear here when users trade for your meals.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const statusBadge = (s) => {
|
|
const map = {
|
|
pending: 'badge-warning',
|
|
preparing: 'badge-info',
|
|
ready: 'badge-primary',
|
|
completed: 'badge-success',
|
|
cancelled: 'badge-error',
|
|
};
|
|
return `<span class="badge ${map[s] || 'badge-default'}">${s}</span>`;
|
|
};
|
|
|
|
container.innerHTML = `
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Order #</th>
|
|
<th>User</th>
|
|
<th>Meal</th>
|
|
<th>Status</th>
|
|
<th>Time</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filtered.map(o => `
|
|
<tr>
|
|
<td class="font-mono">#${o.id}</td>
|
|
<td>${AdminApp.escapeHtml(o.user)}</td>
|
|
<td>${AdminApp.escapeHtml(o.meal)}</td>
|
|
<td>${statusBadge(o.status)}</td>
|
|
<td class="text-muted">${AdminApp.timeAgo(o.time)}</td>
|
|
<td class="actions">
|
|
<button class="btn btn-ghost btn-sm" onclick="viewOrder(${o.id})">View</button>
|
|
${o.status === 'pending' ? `<button class="btn btn-sm btn-primary" onclick="updateStatus(${o.id}, 'preparing')">Accept</button>` : ''}
|
|
${o.status === 'preparing' ? `<button class="btn btn-sm btn-success" onclick="updateStatus(${o.id}, 'ready')">Ready</button>` : ''}
|
|
${o.status === 'ready' ? `<button class="btn btn-sm btn-success" onclick="updateStatus(${o.id}, 'completed')">Complete</button>` : ''}
|
|
</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// --- Update Status ---
|
|
async function updateStatus(id, newStatus) {
|
|
try {
|
|
await AdminApp.api(`/admin/orders/${id}/status`, {
|
|
method: 'PUT',
|
|
body: { status: newStatus },
|
|
});
|
|
} catch {
|
|
// Demo mode
|
|
}
|
|
|
|
const order = orders.find(o => o.id === id);
|
|
if (order) order.status = newStatus;
|
|
renderOrders();
|
|
|
|
const labels = { preparing: 'Order accepted', ready: 'Marked as ready', completed: 'Order completed' };
|
|
AdminApp.toast(labels[newStatus] || 'Status updated', 'success');
|
|
}
|
|
|
|
// --- View Order ---
|
|
function viewOrder(id) {
|
|
const order = orders.find(o => o.id === id);
|
|
if (!order) return;
|
|
|
|
const modal = document.getElementById('orderModal');
|
|
document.getElementById('orderModalBody').innerHTML = `
|
|
<div style="display:flex;flex-direction:column;gap:var(--gf-space-4)">
|
|
<div class="flex justify-between items-center">
|
|
<span class="font-mono text-muted">Order #${order.id}</span>
|
|
${(() => {
|
|
const map = { pending: 'badge-warning', preparing: 'badge-info', ready: 'badge-primary', completed: 'badge-success' };
|
|
return `<span class="badge ${map[order.status] || 'badge-default'}">${order.status}</span>`;
|
|
})()}
|
|
</div>
|
|
<div>
|
|
<div class="text-xs text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.25rem">Customer</div>
|
|
<strong>${AdminApp.escapeHtml(order.user)}</strong>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.25rem">Meal</div>
|
|
<strong>${AdminApp.escapeHtml(order.meal)}</strong>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.25rem">Time</div>
|
|
<span>${AdminApp.formatTime(order.time)} · ${AdminApp.formatDate(order.time)}</span>
|
|
</div>
|
|
${order.notes ? `
|
|
<div>
|
|
<div class="text-xs text-muted" style="text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.25rem">Notes</div>
|
|
<p>${AdminApp.escapeHtml(order.notes)}</p>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
// Footer buttons based on status
|
|
const footer = document.getElementById('orderModalFooter');
|
|
let btns = '<button class="btn btn-ghost" onclick="closeOrderModal()">Close</button>';
|
|
if (order.status === 'pending') btns += `<button class="btn btn-primary" onclick="updateStatus(${order.id}, 'preparing'); closeOrderModal()">Accept Order</button>`;
|
|
if (order.status === 'preparing') btns += `<button class="btn btn-success" onclick="updateStatus(${order.id}, 'ready'); closeOrderModal()">Mark Ready</button>`;
|
|
if (order.status === 'ready') btns += `<button class="btn btn-success" onclick="updateStatus(${order.id}, 'completed'); closeOrderModal()">Complete</button>`;
|
|
footer.innerHTML = btns;
|
|
|
|
modal.classList.add('visible');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeOrderModal() {
|
|
const modal = document.getElementById('orderModal');
|
|
modal.classList.remove('visible');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
document.getElementById('orderModalClose').addEventListener('click', closeOrderModal);
|
|
document.getElementById('orderModal').addEventListener('click', (e) => {
|
|
if (e.target.id === 'orderModal') closeOrderModal();
|
|
});
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeOrderModal();
|
|
});
|
|
|
|
loadOrders();
|
|
</script>
|
|
</body>
|
|
</html>
|