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>
233 lines
9 KiB
HTML
233 lines
9 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 — Trades</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">
|
|
<span class="icon">📦</span> Orders
|
|
</a>
|
|
<a href="trades.html" class="sidebar-link active">
|
|
<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">Trades</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>Trade Activity</h1>
|
|
<p class="subtitle">See how your meals are being traded on Grubflip.</p>
|
|
</div>
|
|
|
|
<!-- Stats row -->
|
|
<div class="stats-grid" style="margin-bottom: var(--gf-space-6);">
|
|
<div class="stat-card">
|
|
<div class="stat-icon green">🔄</div>
|
|
<div class="stat-info">
|
|
<h3>Total Trades</h3>
|
|
<div class="stat-value" id="totalTrades">159</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon orange">🔥</div>
|
|
<div class="stat-info">
|
|
<h3>This Week</h3>
|
|
<div class="stat-value" id="weekTrades">47</div>
|
|
<div class="stat-change up">+23% vs last week</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon yellow">⭐</div>
|
|
<div class="stat-info">
|
|
<h3>Most Traded</h3>
|
|
<div class="stat-value" id="topMeal" style="font-size:var(--gf-text-lg)">Spicy Ramen</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon blue">📈</div>
|
|
<div class="stat-info">
|
|
<h3>Trade Rate</h3>
|
|
<div class="stat-value" id="tradeRate">87%</div>
|
|
<div class="stat-change up">Acceptance rate</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trades Table -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h2>Recent Trades</h2>
|
|
<div class="search-box" style="max-width: 14rem;">
|
|
<span class="search-icon">🔍</span>
|
|
<input type="text" class="form-input" id="searchTrades" placeholder="Search trades...">
|
|
</div>
|
|
</div>
|
|
<div id="tradesContainer">
|
|
<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');
|
|
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);
|
|
|
|
const DEMO_TRADES = [
|
|
{ id: 501, from_user: 'Jamie L.', to_user: 'Pat D.', meal: 'Spicy Ramen Bowl', offered: 'Pad Thai', status: 'completed', time: '2026-03-27T15:10:00Z' },
|
|
{ id: 502, from_user: 'Alex K.', to_user: 'Sam R.', meal: 'Chicken Burrito', offered: 'Falafel Wrap', status: 'pending', time: '2026-03-27T15:15:00Z' },
|
|
{ id: 503, from_user: 'Morgan T.', to_user: 'Riley W.', meal: 'BBQ Pulled Pork', offered: 'Fish Tacos', status: 'completed', time: '2026-03-27T14:35:00Z' },
|
|
{ id: 504, from_user: 'Casey P.', to_user: 'Jordan B.', meal: 'Veggie Power Bowl', offered: 'Caesar Salad', status: 'completed', time: '2026-03-27T13:20:00Z' },
|
|
{ id: 505, from_user: 'Taylor M.', to_user: 'Jamie L.', meal: 'Greek Salad Wrap', offered: 'Spicy Ramen Bowl', status: 'declined', time: '2026-03-27T12:45:00Z' },
|
|
{ id: 506, from_user: 'Pat D.', to_user: 'Alex K.', meal: 'Spicy Ramen Bowl', offered: 'Poke Bowl', status: 'pending', time: '2026-03-27T15:30:00Z' },
|
|
{ id: 507, from_user: 'Riley W.', to_user: 'Casey P.', meal: 'Chicken Burrito', offered: 'BBQ Pulled Pork', status: 'completed', time: '2026-03-27T11:00:00Z' },
|
|
{ id: 508, from_user: 'Jordan B.', to_user: 'Taylor M.', meal: 'Veggie Power Bowl', offered: 'Grilled Cheese', status: 'completed', time: '2026-03-26T16:30:00Z' },
|
|
];
|
|
|
|
let trades = [];
|
|
|
|
async function loadTrades() {
|
|
try {
|
|
const data = await AdminApp.api('/admin/trades');
|
|
trades = data.trades || data;
|
|
} catch {
|
|
trades = [...DEMO_TRADES];
|
|
}
|
|
renderTrades();
|
|
}
|
|
|
|
function renderTrades() {
|
|
const container = document.getElementById('tradesContainer');
|
|
const search = document.getElementById('searchTrades').value.toLowerCase();
|
|
|
|
let filtered = trades;
|
|
if (search) {
|
|
filtered = trades.filter(t =>
|
|
t.from_user.toLowerCase().includes(search) ||
|
|
t.to_user.toLowerCase().includes(search) ||
|
|
t.meal.toLowerCase().includes(search) ||
|
|
t.offered.toLowerCase().includes(search)
|
|
);
|
|
}
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="icon">🔄</div>
|
|
<h3>No trades found</h3>
|
|
<p>Trades involving your meals will appear here.</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const statusBadge = (s) => {
|
|
const map = { pending: 'badge-warning', completed: 'badge-success', declined: 'badge-error', cancelled: 'badge-default' };
|
|
return `<span class="badge ${map[s] || 'badge-default'}">${s}</span>`;
|
|
};
|
|
|
|
container.innerHTML = `
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Trade #</th>
|
|
<th>From</th>
|
|
<th>To</th>
|
|
<th>Meal Wanted</th>
|
|
<th>Offered</th>
|
|
<th>Status</th>
|
|
<th>Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filtered.map(t => `
|
|
<tr>
|
|
<td class="font-mono">#${t.id}</td>
|
|
<td>${AdminApp.escapeHtml(t.from_user)}</td>
|
|
<td>${AdminApp.escapeHtml(t.to_user)}</td>
|
|
<td><strong>${AdminApp.escapeHtml(t.meal)}</strong></td>
|
|
<td>${AdminApp.escapeHtml(t.offered)}</td>
|
|
<td>${statusBadge(t.status)}</td>
|
|
<td class="text-muted">${AdminApp.timeAgo(t.time)}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
document.getElementById('searchTrades').addEventListener('input', renderTrades);
|
|
|
|
loadTrades();
|
|
</script>
|
|
</body>
|
|
</html>
|