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>
343 lines
16 KiB
HTML
343 lines
16 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 History</h1>
|
|
<p class="subtitle">Track all meal trades happening at your restaurant.</p>
|
|
</div>
|
|
|
|
<!-- Stats Row -->
|
|
<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 Trades</h3>
|
|
<div class="stat-value" id="statTotal">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon green">✅</div>
|
|
<div class="stat-info">
|
|
<h3>Completed</h3>
|
|
<div class="stat-value" id="statCompleted">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon yellow">⏳</div>
|
|
<div class="stat-info">
|
|
<h3>Pending</h3>
|
|
<div class="stat-value" id="statPending">--</div>
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon blue">📈</div>
|
|
<div class="stat-info">
|
|
<h3>Today</h3>
|
|
<div class="stat-value" id="statToday">--</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 by user or meal...">
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<select class="form-select" id="filterStatus" style="width:auto;min-width:8rem">
|
|
<option value="">All Status</option>
|
|
<option value="completed">Completed</option>
|
|
<option value="pending">Pending</option>
|
|
<option value="cancelled">Cancelled</option>
|
|
<option value="disputed">Disputed</option>
|
|
</select>
|
|
<select class="form-select" id="filterDate" style="width:auto;min-width:8rem">
|
|
<option value="">All Time</option>
|
|
<option value="today">Today</option>
|
|
<option value="week">This Week</option>
|
|
<option value="month">This Month</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Trades Table -->
|
|
<div class="card">
|
|
<div id="tradesContent">
|
|
<div class="loading-overlay"><div class="spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Trade Detail Modal -->
|
|
<div class="modal-overlay" id="tradeModal">
|
|
<div class="modal" role="dialog" aria-modal="true">
|
|
<div class="modal-header">
|
|
<h2>Trade Details</h2>
|
|
<button class="modal-close" id="tradeModalClose" aria-label="Close">×</button>
|
|
</div>
|
|
<div class="modal-body" id="tradeDetailBody"></div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-ghost" id="tradeModalDone">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
|
|
<style>
|
|
.trade-detail-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: var(--gf-space-4);
|
|
}
|
|
@media (min-width: 640px) {
|
|
.trade-detail-grid { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
.trade-party {
|
|
padding: var(--gf-space-4);
|
|
border-radius: var(--gf-radius-lg);
|
|
background: var(--gf-bg-muted);
|
|
}
|
|
.trade-party h4 {
|
|
font-size: var(--gf-text-xs);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--gf-neutral-500);
|
|
margin-bottom: var(--gf-space-2);
|
|
}
|
|
.trade-party .user-name {
|
|
font-weight: var(--gf-weight-semibold);
|
|
font-size: var(--gf-text-base);
|
|
color: var(--gf-neutral-900);
|
|
}
|
|
.trade-party .meal-offered {
|
|
font-size: var(--gf-text-sm);
|
|
color: var(--gf-neutral-600);
|
|
margin-top: var(--gf-space-1);
|
|
}
|
|
.trade-meta-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--gf-space-2) 0;
|
|
border-bottom: 1px solid var(--gf-neutral-100);
|
|
font-size: var(--gf-text-sm);
|
|
}
|
|
.trade-meta-row:last-child { border-bottom: none; }
|
|
.trade-meta-label { color: var(--gf-neutral-500); font-weight: var(--gf-weight-medium); }
|
|
.trade-meta-value { color: var(--gf-neutral-800); font-weight: var(--gf-weight-semibold); }
|
|
</style>
|
|
|
|
<script src="js/admin-app.js"></script>
|
|
<script>
|
|
// --- Auth ---
|
|
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 trades = [];
|
|
|
|
const DEMO_TRADES = [
|
|
{ id: 101, from_user: 'Jamie L.', to_user: 'Alex K.', from_meal: 'Spicy Ramen Bowl', to_meal: 'Chicken Burrito', status: 'completed', created_at: '2026-03-27T15:20:00Z', completed_at: '2026-03-27T15:35:00Z' },
|
|
{ id: 102, from_user: 'Alex K.', to_user: 'Sam R.', from_meal: 'Chicken Burrito', to_meal: 'Veggie Power Bowl', status: 'pending', created_at: '2026-03-27T14:45:00Z', completed_at: null },
|
|
{ id: 103, from_user: 'Sam R.', to_user: 'Morgan T.', from_meal: 'Veggie Power Bowl', to_meal: 'BBQ Pulled Pork', status: 'completed', created_at: '2026-03-27T13:10:00Z', completed_at: '2026-03-27T13:25:00Z' },
|
|
{ id: 104, from_user: 'Morgan T.', to_user: 'Riley W.', from_meal: 'BBQ Pulled Pork', to_meal: 'Greek Salad Wrap', status: 'completed', created_at: '2026-03-27T12:30:00Z', completed_at: '2026-03-27T12:50:00Z' },
|
|
{ id: 105, from_user: 'Riley W.', to_user: 'Casey H.', from_meal: 'Greek Salad Wrap', to_meal: 'Spicy Ramen Bowl', status: 'completed', created_at: '2026-03-27T11:15:00Z', completed_at: '2026-03-27T11:30:00Z' },
|
|
{ id: 106, from_user: 'Drew N.', to_user: 'Jamie L.', from_meal: 'Spicy Ramen Bowl', to_meal: 'Veggie Power Bowl', status: 'cancelled', created_at: '2026-03-27T10:00:00Z', completed_at: null },
|
|
{ id: 107, from_user: 'Casey H.', to_user: 'Alex K.', from_meal: 'BBQ Pulled Pork', to_meal: 'Chicken Burrito', status: 'completed', created_at: '2026-03-26T16:30:00Z', completed_at: '2026-03-26T16:45:00Z' },
|
|
{ id: 108, from_user: 'Taylor P.', to_user: 'Morgan T.', from_meal: 'Spicy Ramen Bowl', to_meal: 'Greek Salad Wrap', status: 'completed', created_at: '2026-03-26T14:00:00Z', completed_at: '2026-03-26T14:20:00Z' },
|
|
{ id: 109, from_user: 'Sam R.', to_user: 'Drew N.', from_meal: 'Chicken Burrito', to_meal: 'Veggie Power Bowl', status: 'disputed', created_at: '2026-03-26T11:30:00Z', completed_at: null },
|
|
{ id: 110, from_user: 'Jamie L.', to_user: 'Riley W.', from_meal: 'Veggie Power Bowl', to_meal: 'BBQ Pulled Pork', status: 'completed', created_at: '2026-03-25T15:00:00Z', completed_at: '2026-03-25T15:15:00Z' },
|
|
];
|
|
|
|
// --- Load ---
|
|
async function loadTrades() {
|
|
try {
|
|
const data = await AdminApp.api('/admin/trades');
|
|
trades = data.trades || data;
|
|
} catch {
|
|
trades = [...DEMO_TRADES];
|
|
}
|
|
updateStats();
|
|
renderTrades();
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('statTotal').textContent = trades.length;
|
|
document.getElementById('statCompleted').textContent = trades.filter(t => t.status === 'completed').length;
|
|
document.getElementById('statPending').textContent = trades.filter(t => t.status === 'pending').length;
|
|
const today = new Date().toDateString();
|
|
document.getElementById('statToday').textContent = trades.filter(t => new Date(t.created_at).toDateString() === today).length;
|
|
}
|
|
|
|
function renderTrades() {
|
|
const container = document.getElementById('tradesContent');
|
|
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
const statusFilter = document.getElementById('filterStatus').value;
|
|
const dateFilter = document.getElementById('filterDate').value;
|
|
|
|
let filtered = trades.filter(t => {
|
|
if (search) {
|
|
const haystack = (t.from_user + ' ' + t.to_user + ' ' + t.from_meal + ' ' + t.to_meal).toLowerCase();
|
|
if (!haystack.includes(search)) return false;
|
|
}
|
|
if (statusFilter && t.status !== statusFilter) return false;
|
|
if (dateFilter) {
|
|
const d = new Date(t.created_at);
|
|
const now = new Date();
|
|
if (dateFilter === 'today' && d.toDateString() !== now.toDateString()) return false;
|
|
if (dateFilter === 'week' && d < new Date(now.getTime() - 7 * 86400000)) return false;
|
|
if (dateFilter === 'month' && (d.getMonth() !== now.getMonth() || d.getFullYear() !== now.getFullYear())) return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<div class="empty-state"><div class="icon">🔄</div><h3>No trades found</h3><p>Try adjusting your search or filters.</p></div>';
|
|
return;
|
|
}
|
|
|
|
const statusBadge = (status) => {
|
|
const map = { completed: 'badge-success', pending: 'badge-warning', cancelled: 'badge-default', disputed: 'badge-error' };
|
|
return '<span class="badge ' + (map[status] || 'badge-default') + '">' + status + '</span>';
|
|
};
|
|
|
|
container.innerHTML = '<div class="table-wrapper"><table class="data-table"><thead><tr><th>ID</th><th>From</th><th>To</th><th>Meals</th><th>Status</th><th>Date</th><th></th></tr></thead><tbody>' +
|
|
filtered.map(t =>
|
|
'<tr>' +
|
|
'<td class="font-mono text-muted">#' + t.id + '</td>' +
|
|
'<td>' + AdminApp.escapeHtml(t.from_user) + '</td>' +
|
|
'<td>' + AdminApp.escapeHtml(t.to_user) + '</td>' +
|
|
'<td><span class="text-sm">' + AdminApp.escapeHtml(t.from_meal) + '</span> <span class="text-muted">⇄</span> <span class="text-sm">' + AdminApp.escapeHtml(t.to_meal) + '</span></td>' +
|
|
'<td>' + statusBadge(t.status) + '</td>' +
|
|
'<td class="text-muted">' + AdminApp.timeAgo(t.created_at) + '</td>' +
|
|
'<td><button class="btn btn-ghost btn-sm" onclick="viewTrade(' + t.id + ')">View</button></td>' +
|
|
'</tr>'
|
|
).join('') +
|
|
'</tbody></table></div>';
|
|
}
|
|
|
|
// --- Search & Filter ---
|
|
document.getElementById('searchInput').addEventListener('input', renderTrades);
|
|
document.getElementById('filterStatus').addEventListener('change', renderTrades);
|
|
document.getElementById('filterDate').addEventListener('change', renderTrades);
|
|
|
|
// --- Trade Detail Modal ---
|
|
const tradeModal = document.getElementById('tradeModal');
|
|
|
|
function viewTrade(id) {
|
|
const trade = trades.find(t => t.id === id);
|
|
if (!trade) return;
|
|
|
|
const statusBadge = (status) => {
|
|
const map = { completed: 'badge-success', pending: 'badge-warning', cancelled: 'badge-default', disputed: 'badge-error' };
|
|
return '<span class="badge ' + (map[status] || 'badge-default') + '">' + status + '</span>';
|
|
};
|
|
|
|
document.getElementById('tradeDetailBody').innerHTML =
|
|
'<div class="trade-detail-grid">' +
|
|
'<div class="trade-party"><h4>Offered By</h4><div class="user-name">' + AdminApp.escapeHtml(trade.from_user) + '</div><div class="meal-offered">🍽 ' + AdminApp.escapeHtml(trade.from_meal) + '</div></div>' +
|
|
'<div class="trade-party"><h4>Traded With</h4><div class="user-name">' + AdminApp.escapeHtml(trade.to_user) + '</div><div class="meal-offered">🍽 ' + AdminApp.escapeHtml(trade.to_meal) + '</div></div>' +
|
|
'</div>' +
|
|
'<div style="margin-top:var(--gf-space-6)">' +
|
|
'<div class="trade-meta-row"><span class="trade-meta-label">Trade ID</span><span class="trade-meta-value font-mono">#' + trade.id + '</span></div>' +
|
|
'<div class="trade-meta-row"><span class="trade-meta-label">Status</span><span class="trade-meta-value">' + statusBadge(trade.status) + '</span></div>' +
|
|
'<div class="trade-meta-row"><span class="trade-meta-label">Initiated</span><span class="trade-meta-value">' + AdminApp.formatDate(trade.created_at) + ' at ' + AdminApp.formatTime(trade.created_at) + '</span></div>' +
|
|
(trade.completed_at ? '<div class="trade-meta-row"><span class="trade-meta-label">Completed</span><span class="trade-meta-value">' + AdminApp.formatDate(trade.completed_at) + ' at ' + AdminApp.formatTime(trade.completed_at) + '</span></div>' : '') +
|
|
'</div>';
|
|
|
|
tradeModal.classList.add('visible');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeTradeModal() {
|
|
tradeModal.classList.remove('visible');
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
document.getElementById('tradeModalClose').addEventListener('click', closeTradeModal);
|
|
document.getElementById('tradeModalDone').addEventListener('click', closeTradeModal);
|
|
tradeModal.addEventListener('click', function(e) { if (e.target === tradeModal) closeTradeModal(); });
|
|
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeTradeModal(); });
|
|
|
|
// --- Init ---
|
|
loadTrades();
|
|
</script>
|
|
</body>
|
|
</html>
|