grubflip/admin/trades.html
Sarah 960391c4f4 Add admin restaurant profile, settings, and update trades/orders pages
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>
2026-03-27 15:41:45 +00:00

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">&#x1f4ca;</span> Dashboard
</a>
<a href="meals.html" class="sidebar-link">
<span class="icon">&#x1f354;</span> Meals
</a>
<a href="orders.html" class="sidebar-link">
<span class="icon">&#x1f4e6;</span> Orders
</a>
<a href="trades.html" class="sidebar-link active">
<span class="icon">&#x1f504;</span> Trades
</a>
<div class="sidebar-section-title">Settings</div>
<a href="profile.html" class="sidebar-link">
<span class="icon">&#x1f3ea;</span> Restaurant Profile
</a>
<a href="settings.html" class="sidebar-link">
<span class="icon">&#x2699;&#xfe0f;</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">&#9776;</button>
<h1 class="topbar-title">Trades</h1>
</div>
<div class="topbar-right">
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">&#x23fb;</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">&#x1f504;</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">&#x2705;</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">&#x23f3;</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">&#x1f4c8;</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">&#x1f50d;</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">&times;</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">&#x1f504;</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">&#x21c4;</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">&#x1f37d; ' + 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">&#x1f37d; ' + 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>