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>
This commit is contained in:
Sarah 2026-03-27 15:41:45 +00:00
parent a3331881d4
commit 960391c4f4
4 changed files with 913 additions and 300 deletions

View file

@ -19,31 +19,16 @@
<div class="logo-icon">GF</div> <div class="logo-icon">GF</div>
<span>Grubflip</span> <span>Grubflip</span>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<div class="sidebar-section-title">Main</div> <div class="sidebar-section-title">Main</div>
<a href="dashboard.html" class="sidebar-link"> <a href="dashboard.html" class="sidebar-link"><span class="icon">&#x1f4ca;</span> Dashboard</a>
<span class="icon">&#x1f4ca;</span> Dashboard <a href="meals.html" class="sidebar-link"><span class="icon">&#x1f354;</span> Meals</a>
</a> <a href="orders.html" class="sidebar-link active"><span class="icon">&#x1f4e6;</span> Orders</a>
<a href="meals.html" class="sidebar-link"> <a href="trades.html" class="sidebar-link"><span class="icon">&#x1f504;</span> Trades</a>
<span class="icon">&#x1f354;</span> Meals
</a>
<a href="orders.html" class="sidebar-link active">
<span class="icon">&#x1f4e6;</span> Orders
</a>
<a href="trades.html" class="sidebar-link">
<span class="icon">&#x1f504;</span> Trades
</a>
<div class="sidebar-section-title">Settings</div> <div class="sidebar-section-title">Settings</div>
<a href="profile.html" class="sidebar-link"> <a href="profile.html" class="sidebar-link"><span class="icon">&#x1f3ea;</span> Restaurant Profile</a>
<span class="icon">&#x1f3ea;</span> Restaurant Profile <a href="settings.html" class="sidebar-link"><span class="icon">&#x2699;&#xfe0f;</span> Settings</a>
</a>
<a href="settings.html" class="sidebar-link">
<span class="icon">&#x2699;&#xfe0f;</span> Settings
</a>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<div class="sidebar-user"> <div class="sidebar-user">
<div class="avatar" id="userAvatar">DR</div> <div class="avatar" id="userAvatar">DR</div>
@ -69,21 +54,60 @@
<div class="page-content"> <div class="page-content">
<div class="page-header"> <div class="page-header">
<h1>Orders</h1> <h1>Orders</h1>
<p class="subtitle">Track and manage incoming meal orders from trades.</p> <p class="subtitle">Manage incoming meal orders from customers.</p>
</div> </div>
<!-- Tabs --> <!-- Stats -->
<div class="tabs"> <div class="stats-grid" style="margin-bottom:var(--gf-space-6)">
<button class="tab active" data-tab="all">All Orders</button> <div class="stat-card">
<button class="tab" data-tab="pending">Pending</button> <div class="stat-icon orange">&#x1f4e6;</div>
<button class="tab" data-tab="preparing">Preparing</button> <div class="stat-info">
<button class="tab" data-tab="ready">Ready</button> <h3>Total Orders</h3>
<button class="tab" data-tab="completed">Completed</button> <div class="stat-value" id="statTotal">--</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon yellow">&#x1f373;</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">&#x2705;</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">&#x1f4b0;</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">&#x1f50d;</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> </div>
<!-- Orders Table --> <!-- Orders Table -->
<div class="card"> <div class="card">
<div id="ordersContainer"> <div id="ordersContent">
<div class="loading-overlay"><div class="spinner"></div></div> <div class="loading-overlay"><div class="spinner"></div></div>
</div> </div>
</div> </div>
@ -91,219 +115,104 @@
</main> </main>
</div> </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">&times;</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> <div class="toast-container" id="toastContainer"></div>
<script src="js/admin-app.js"></script> <script src="js/admin-app.js"></script>
<script> <script>
if (!AdminApp.requireAuth()) throw new Error('Not authenticated'); if (!AdminApp.requireAuth()) throw new Error('Not authenticated');
const user = AdminApp.getUser(); var user = AdminApp.getUser();
if (user.name) { if (user.name) {
document.getElementById('userName').textContent = user.name; document.getElementById('userName').textContent = user.name;
document.getElementById('userAvatar').textContent = user.name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); 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);
} }
if (user.role) document.getElementById('userRole').textContent = user.role.charAt(0).toUpperCase() + user.role.slice(1);
AdminApp.initSidebar(); AdminApp.initSidebar();
document.getElementById('logoutBtn').addEventListener('click', AdminApp.logout); document.getElementById('logoutBtn').addEventListener('click', AdminApp.logout);
// --- State --- var orders = [];
let orders = [];
let currentTab = 'all';
const DEMO_ORDERS = [ var DEMO_ORDERS = [
{ id: 1001, user: 'Jamie L.', meal: 'Spicy Ramen Bowl', status: 'completed', time: '2026-03-27T15:10:00Z', notes: '' }, { 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: 1002, user: 'Alex K.', meal: 'Chicken Burrito', status: 'pending', time: '2026-03-27T15:15:00Z', notes: 'No onions please' }, { id: 'ORD-002', customer: 'Alex K.', meal: 'Chicken Burrito', qty: 2, total: 25.98, status: 'preparing', created_at: '2026-03-27T15:15:00Z' },
{ id: 1003, user: 'Sam R.', meal: 'Veggie Power Bowl', status: 'completed', time: '2026-03-27T14:50:00Z', notes: '' }, { 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: 1004, user: 'Morgan T.', meal: 'BBQ Pulled Pork', status: 'preparing', time: '2026-03-27T14:35:00Z', notes: 'Extra sauce' }, { 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: 1005, user: 'Riley W.', meal: 'Greek Salad Wrap', status: 'ready', time: '2026-03-27T14:20:00Z', notes: '' }, { 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: 1006, user: 'Casey P.', meal: 'Spicy Ramen Bowl', status: 'pending', time: '2026-03-27T15:25:00Z', notes: 'Mild spice level' }, { 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: 1007, user: 'Jordan B.', meal: 'Chicken Burrito', status: 'preparing', time: '2026-03-27T14:55:00Z', notes: '' }, { id: 'ORD-007', customer: 'Drew N.', meal: 'Chicken Burrito', qty: 1, total: 12.99, status: 'cancelled', created_at: '2026-03-27T10:45:00Z' },
{ id: 1008, user: 'Taylor M.', meal: 'Veggie Power Bowl', status: 'completed', time: '2026-03-27T13:30:00Z', notes: 'Extra avocado' }, { id: 'ORD-008', customer: 'Taylor P.', meal: 'Veggie Power Bowl', qty: 2, total: 26.98, status: 'fulfilled', created_at: '2026-03-26T16:00:00Z' },
]; ];
// --- 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() { async function loadOrders() {
try { try {
const data = await AdminApp.api('/admin/orders'); var data = await AdminApp.api('/admin/orders');
orders = data.orders || data; orders = data.orders || data;
} catch { } catch(e) {
orders = [...DEMO_ORDERS]; orders = DEMO_ORDERS.slice();
} }
updateStats();
renderOrders(); renderOrders();
} }
// --- Render --- 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() { function renderOrders() {
const container = document.getElementById('ordersContainer'); var container = document.getElementById('ordersContent');
let filtered = orders; var search = document.getElementById('searchInput').value.toLowerCase();
if (currentTab !== 'all') { var statusFilter = document.getElementById('filterStatus').value;
filtered = orders.filter(o => o.status === currentTab);
} 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) { if (filtered.length === 0) {
container.innerHTML = ` container.innerHTML = '<div class="empty-state"><div class="icon">&#x1f4e6;</div><h3>No orders found</h3><p>Try adjusting your filters.</p></div>';
<div class="empty-state">
<div class="icon">&#x1f4e6;</div>
<h3>No ${currentTab === 'all' ? '' : currentTab + ' '}orders</h3>
<p>Orders will appear here when users trade for your meals.</p>
</div>
`;
return; return;
} }
const statusBadge = (s) => { var statusMap = { 'new': 'badge-info', preparing: 'badge-warning', ready: 'badge-primary', fulfilled: 'badge-success', cancelled: 'badge-default' };
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 = ` 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>' +
<div class="table-wrapper"> filtered.map(function(o) {
<table class="data-table"> var canUpdate = o.status !== 'fulfilled' && o.status !== 'cancelled';
<thead> return '<tr>' +
<tr> '<td class="font-mono text-muted">' + o.id + '</td>' +
<th>Order #</th> '<td>' + AdminApp.escapeHtml(o.customer) + '</td>' +
<th>User</th> '<td>' + AdminApp.escapeHtml(o.meal) + '</td>' +
<th>Meal</th> '<td>' + o.qty + '</td>' +
<th>Status</th> '<td class="font-mono">$' + o.total.toFixed(2) + '</td>' +
<th>Time</th> '<td><span class="badge ' + (statusMap[o.status] || 'badge-default') + '">' + o.status + '</span></td>' +
<th>Actions</th> '<td class="text-muted">' + AdminApp.timeAgo(o.created_at) + '</td>' +
</tr> '<td>' + (canUpdate ? '<button class="btn btn-ghost btn-sm" onclick="advanceOrder(\'' + o.id + '\')">Advance</button>' : '') + '</td>' +
</thead> '</tr>';
<tbody> }).join('') +
${filtered.map(o => ` '</tbody></table></div>';
<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 --- function advanceOrder(id) {
async function updateStatus(id, newStatus) { var order = orders.find(function(o){ return o.id === id; });
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; if (!order) return;
var flow = { 'new': 'preparing', 'preparing': 'ready', 'ready': 'fulfilled' };
const modal = document.getElementById('orderModal'); var next = flow[order.status];
document.getElementById('orderModalBody').innerHTML = ` if (!next) return;
<div style="display:flex;flex-direction:column;gap:var(--gf-space-4)"> order.status = next;
<div class="flex justify-between items-center"> updateStats();
<span class="font-mono text-muted">Order #${order.id}</span> renderOrders();
${(() => { AdminApp.toast('Order ' + id + ' → ' + next, 'success');
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)} &middot; ${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() { document.getElementById('searchInput').addEventListener('input', renderOrders);
const modal = document.getElementById('orderModal'); document.getElementById('filterStatus').addEventListener('change', renderOrders);
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(); loadOrders();
</script> </script>

282
admin/profile.html Normal file
View file

@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grubflip Admin — Restaurant Profile</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"><span class="icon">&#x1f504;</span> Trades</a>
<div class="sidebar-section-title">Settings</div>
<a href="profile.html" class="sidebar-link active"><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">Restaurant Profile</h1>
</div>
<div class="topbar-right">
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">&#x23fb;</button>
</div>
</header>
<div class="page-content" style="max-width:48rem">
<div class="page-header">
<h1>Restaurant Profile</h1>
<p class="subtitle">This is how your restaurant appears to customers on Grubflip.</p>
</div>
<!-- Cover Photo -->
<div class="card mb-6">
<div class="card-header">
<h2>Cover Photo</h2>
</div>
<div class="img-preview-wrapper" id="coverPreview" style="height:12rem">
<div class="upload-label">
<span>&#x1f5bc;</span>
Click to upload a cover photo
</div>
</div>
<input type="file" id="coverInput" accept="image/*" class="hidden">
<p class="form-hint mt-2">Recommended: 1200x400px. Max 5MB.</p>
</div>
<!-- Basic Info -->
<div class="card mb-6">
<div class="card-header">
<h2>Basic Information</h2>
</div>
<form id="profileForm">
<div class="form-group">
<label for="restName" class="form-label">Restaurant Name *</label>
<input type="text" id="restName" class="form-input" placeholder="e.g. Downtown Grill">
</div>
<div class="form-group">
<label for="restDesc" class="form-label">Description</label>
<textarea id="restDesc" class="form-textarea" placeholder="Tell customers about your restaurant, your style of food, what makes you unique..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="restCuisine" class="form-label">Cuisine Type</label>
<select id="restCuisine" class="form-select">
<option value="">Select cuisine</option>
<option value="american">American</option>
<option value="mexican">Mexican</option>
<option value="italian">Italian</option>
<option value="asian">Asian</option>
<option value="japanese">Japanese</option>
<option value="indian">Indian</option>
<option value="mediterranean">Mediterranean</option>
<option value="fusion">Fusion</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="restPrice" class="form-label">Price Range</label>
<select id="restPrice" class="form-select">
<option value="$">$ — Budget</option>
<option value="$$" selected>$$ — Moderate</option>
<option value="$$$">$$$ — Upscale</option>
<option value="$$$$">$$$$ — Fine Dining</option>
</select>
</div>
</div>
<div class="form-group">
<label for="restAddress" class="form-label">Address</label>
<input type="text" id="restAddress" class="form-input" placeholder="123 Main St, City, State ZIP">
</div>
<div class="form-row">
<div class="form-group">
<label for="restPhone" class="form-label">Phone</label>
<input type="tel" id="restPhone" class="form-input" placeholder="(555) 123-4567">
</div>
<div class="form-group">
<label for="restWebsite" class="form-label">Website</label>
<input type="url" id="restWebsite" class="form-input" placeholder="https://yourrestaurant.com">
</div>
</div>
<button type="submit" class="btn btn-primary">Save Profile</button>
</form>
</div>
<!-- Hours -->
<div class="card mb-6">
<div class="card-header">
<h2>Business Hours</h2>
</div>
<div id="hoursEditor">
</div>
<button class="btn btn-ghost btn-sm mt-4" id="saveHoursBtn">Save Hours</button>
</div>
<!-- Social Links -->
<div class="card">
<div class="card-header">
<h2>Social Links</h2>
</div>
<div class="form-group">
<label for="socialInsta" class="form-label">Instagram</label>
<input type="text" id="socialInsta" class="form-input" placeholder="@yourrestaurant">
</div>
<div class="form-group">
<label for="socialFacebook" class="form-label">Facebook</label>
<input type="url" id="socialFacebook" class="form-input" placeholder="https://facebook.com/yourpage">
</div>
<div class="form-group">
<label for="socialTwitter" class="form-label">X (Twitter)</label>
<input type="text" id="socialTwitter" class="form-input" placeholder="@yourrestaurant">
</div>
<button class="btn btn-primary btn-sm" id="saveSocialBtn">Save Social Links</button>
</div>
</div>
</main>
</div>
<div class="toast-container" id="toastContainer"></div>
<style>
.hours-row {
display: flex;
align-items: center;
gap: var(--gf-space-3);
padding: var(--gf-space-2) 0;
border-bottom: 1px solid var(--gf-neutral-100);
}
.hours-row:last-child { border-bottom: none; }
.hours-day {
width: 5rem;
font-size: var(--gf-text-sm);
font-weight: var(--gf-weight-semibold);
color: var(--gf-neutral-700);
}
.hours-inputs {
display: flex;
align-items: center;
gap: var(--gf-space-2);
flex: 1;
}
.hours-inputs input {
width: 6rem;
padding: 0.5rem 0.75rem;
font-size: var(--gf-text-sm);
}
.hours-closed {
font-size: var(--gf-text-sm);
color: var(--gf-neutral-400);
}
</style>
<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();
document.getElementById('restName').value = user.name;
}
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);
// --- Cover Photo ---
document.getElementById('coverPreview').addEventListener('click', function() {
document.getElementById('coverInput').click();
});
document.getElementById('coverInput').addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) { AdminApp.toast('Image must be under 5MB', 'error'); return; }
var reader = new FileReader();
reader.onload = function(ev) {
document.getElementById('coverPreview').innerHTML = '<img src="' + ev.target.result + '" alt="Cover">';
};
reader.readAsDataURL(file);
});
// --- Hours Editor ---
var days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
var defaultHours = { open: '11:00', close: '21:00' };
var hoursEditor = document.getElementById('hoursEditor');
hoursEditor.innerHTML = days.map(function(day, i) {
var isClosed = (i >= 6); // Sunday closed by default
return '<div class="hours-row">' +
'<div class="hours-day">' + day.slice(0, 3) + '</div>' +
'<div class="hours-inputs">' +
'<label class="toggle" style="flex-shrink:0"><input type="checkbox" data-day="' + i + '"' + (isClosed ? '' : ' checked') + '><span class="toggle-slider"></span></label>' +
'<input type="time" class="form-input" data-field="open-' + i + '" value="' + (isClosed ? '' : defaultHours.open) + '"' + (isClosed ? ' disabled' : '') + '>' +
'<span class="text-muted text-sm">to</span>' +
'<input type="time" class="form-input" data-field="close-' + i + '" value="' + (isClosed ? '' : defaultHours.close) + '"' + (isClosed ? ' disabled' : '') + '>' +
'</div>' +
'</div>';
}).join('');
// Toggle hours
hoursEditor.addEventListener('change', function(e) {
if (e.target.dataset.day !== undefined) {
var i = e.target.dataset.day;
var open = document.querySelector('[data-field="open-' + i + '"]');
var close = document.querySelector('[data-field="close-' + i + '"]');
open.disabled = !e.target.checked;
close.disabled = !e.target.checked;
if (!e.target.checked) { open.value = ''; close.value = ''; }
else { open.value = defaultHours.open; close.value = defaultHours.close; }
}
});
// --- Save Profile ---
document.getElementById('profileForm').addEventListener('submit', function(e) {
e.preventDefault();
var name = document.getElementById('restName').value.trim();
if (!name) { AdminApp.toast('Restaurant name is required', 'error'); return; }
AdminApp.toast('Profile saved!', 'success');
});
// --- Save Hours ---
document.getElementById('saveHoursBtn').addEventListener('click', function() {
AdminApp.toast('Business hours saved!', 'success');
});
// --- Save Social ---
document.getElementById('saveSocialBtn').addEventListener('click', function() {
AdminApp.toast('Social links saved!', 'success');
});
</script>
</body>
</html>

312
admin/settings.html Normal file
View file

@ -0,0 +1,312 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grubflip Admin — Settings</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"><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 active"><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">Settings</h1>
</div>
<div class="topbar-right">
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">&#x23fb;</button>
</div>
</header>
<div class="page-content" style="max-width:48rem">
<div class="page-header">
<h1>Settings</h1>
<p class="subtitle">Manage your account and notification preferences.</p>
</div>
<!-- Tabs -->
<div class="tabs" id="settingsTabs">
<button class="tab active" data-tab="account">Account</button>
<button class="tab" data-tab="notifications">Notifications</button>
<button class="tab" data-tab="security">Security</button>
</div>
<!-- Account Tab -->
<div class="settings-panel" id="panel-account">
<div class="card mb-6">
<div class="card-header">
<h2>Account Information</h2>
</div>
<form id="accountForm">
<div class="form-group">
<label for="accName" class="form-label">Full Name</label>
<input type="text" id="accName" class="form-input" placeholder="Your name">
</div>
<div class="form-group">
<label for="accEmail" class="form-label">Email Address</label>
<input type="email" id="accEmail" class="form-input" placeholder="you@restaurant.com">
</div>
<div class="form-group">
<label for="accPhone" class="form-label">Phone Number</label>
<input type="tel" id="accPhone" class="form-input" placeholder="(555) 123-4567">
</div>
<button type="submit" class="btn btn-primary">Save Changes</button>
</form>
</div>
<div class="card">
<div class="card-header">
<h2 style="color:var(--gf-error)">Danger Zone</h2>
</div>
<p class="text-sm text-muted mb-4">Permanently delete your account and all associated data. This action cannot be undone.</p>
<button class="btn btn-danger btn-sm" id="deleteAccountBtn">Delete Account</button>
</div>
</div>
<!-- Notifications Tab -->
<div class="settings-panel hidden" id="panel-notifications">
<div class="card">
<div class="card-header">
<h2>Notification Preferences</h2>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>New Trade Requests</strong>
<p class="text-sm text-muted">Get notified when someone proposes a trade for one of your meals.</p>
</div>
<label class="toggle">
<input type="checkbox" checked data-pref="trade_requests">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>Trade Completed</strong>
<p class="text-sm text-muted">Notification when a trade is successfully completed.</p>
</div>
<label class="toggle">
<input type="checkbox" checked data-pref="trade_completed">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>New Reviews</strong>
<p class="text-sm text-muted">Get notified when someone leaves a review on your meal.</p>
</div>
<label class="toggle">
<input type="checkbox" checked data-pref="new_reviews">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>Weekly Summary</strong>
<p class="text-sm text-muted">Receive a weekly email with your trade stats and popular meals.</p>
</div>
<label class="toggle">
<input type="checkbox" data-pref="weekly_summary">
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>Marketing Emails</strong>
<p class="text-sm text-muted">Tips, feature updates, and promotional content.</p>
</div>
<label class="toggle">
<input type="checkbox" data-pref="marketing">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Security Tab -->
<div class="settings-panel hidden" id="panel-security">
<div class="card mb-6">
<div class="card-header">
<h2>Change Password</h2>
</div>
<form id="passwordForm">
<div class="form-group">
<label for="currentPass" class="form-label">Current Password</label>
<input type="password" id="currentPass" class="form-input" placeholder="Enter current password" autocomplete="current-password">
</div>
<div class="form-group">
<label for="newPass" class="form-label">New Password</label>
<input type="password" id="newPass" class="form-input" placeholder="Enter new password" autocomplete="new-password">
<span class="form-hint">At least 8 characters with a mix of letters and numbers.</span>
</div>
<div class="form-group">
<label for="confirmPass" class="form-label">Confirm New Password</label>
<input type="password" id="confirmPass" class="form-input" placeholder="Confirm new password" autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary">Update Password</button>
</form>
</div>
<div class="card">
<div class="card-header">
<h2>Active Sessions</h2>
</div>
<div class="setting-row">
<div class="setting-info">
<strong>Current Session</strong>
<p class="text-sm text-muted">This device &middot; Last active now</p>
</div>
<span class="badge badge-success">Active</span>
</div>
</div>
</div>
</div>
</main>
</div>
<div class="toast-container" id="toastContainer"></div>
<style>
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--gf-space-4);
padding: var(--gf-space-4) 0;
border-bottom: 1px solid var(--gf-neutral-100);
}
.setting-row:last-child { border-bottom: none; }
.setting-info { flex: 1; }
.setting-info strong {
display: block;
font-size: var(--gf-text-sm);
font-weight: var(--gf-weight-semibold);
color: var(--gf-neutral-800);
margin-bottom: var(--gf-space-1);
}
.settings-panel { animation: fadeIn 0.2s ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
<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();
document.getElementById('accName').value = user.name;
}
if (user.email) document.getElementById('accEmail').value = user.email;
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);
// --- Tabs ---
document.getElementById('settingsTabs').addEventListener('click', function(e) {
var tab = e.target.closest('.tab');
if (!tab) return;
document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
document.querySelectorAll('.settings-panel').forEach(function(p) { p.classList.add('hidden'); });
var panel = document.getElementById('panel-' + tab.dataset.tab);
if (panel) panel.classList.remove('hidden');
});
// --- Account Form ---
document.getElementById('accountForm').addEventListener('submit', function(e) {
e.preventDefault();
var name = document.getElementById('accName').value.trim();
var email = document.getElementById('accEmail').value.trim();
if (!name || !email) {
AdminApp.toast('Please fill in all required fields', 'error');
return;
}
// Update local storage for demo
var u = AdminApp.getUser();
u.name = name;
u.email = email;
u.phone = document.getElementById('accPhone').value.trim();
localStorage.setItem('gf_user', JSON.stringify(u));
document.getElementById('userName').textContent = name;
document.getElementById('userAvatar').textContent = name.split(' ').map(function(w) { return w[0]; }).join('').slice(0, 2).toUpperCase();
AdminApp.toast('Account updated', 'success');
});
// --- Password Form ---
document.getElementById('passwordForm').addEventListener('submit', function(e) {
e.preventDefault();
var current = document.getElementById('currentPass').value;
var newPass = document.getElementById('newPass').value;
var confirm = document.getElementById('confirmPass').value;
if (!current || !newPass || !confirm) {
AdminApp.toast('Please fill in all fields', 'error');
return;
}
if (newPass.length < 8) {
AdminApp.toast('Password must be at least 8 characters', 'error');
return;
}
if (newPass !== confirm) {
AdminApp.toast('Passwords do not match', 'error');
return;
}
AdminApp.toast('Password updated successfully', 'success');
document.getElementById('passwordForm').reset();
});
// --- Delete Account ---
document.getElementById('deleteAccountBtn').addEventListener('click', function() {
if (window.confirm('Are you sure? This will permanently delete your account.')) {
AdminApp.toast('Account deletion is not available in demo mode', 'warning');
}
});
// --- Notification Toggles ---
document.querySelectorAll('[data-pref]').forEach(function(toggle) {
toggle.addEventListener('change', function() {
AdminApp.toast('Preference updated', 'success');
});
});
</script>
</body>
</html>

View file

@ -19,7 +19,6 @@
<div class="logo-icon">GF</div> <div class="logo-icon">GF</div>
<span>Grubflip</span> <span>Grubflip</span>
</div> </div>
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<div class="sidebar-section-title">Main</div> <div class="sidebar-section-title">Main</div>
<a href="dashboard.html" class="sidebar-link"> <a href="dashboard.html" class="sidebar-link">
@ -34,7 +33,6 @@
<a href="trades.html" class="sidebar-link active"> <a href="trades.html" class="sidebar-link active">
<span class="icon">&#x1f504;</span> Trades <span class="icon">&#x1f504;</span> Trades
</a> </a>
<div class="sidebar-section-title">Settings</div> <div class="sidebar-section-title">Settings</div>
<a href="profile.html" class="sidebar-link"> <a href="profile.html" class="sidebar-link">
<span class="icon">&#x1f3ea;</span> Restaurant Profile <span class="icon">&#x1f3ea;</span> Restaurant Profile
@ -43,7 +41,6 @@
<span class="icon">&#x2699;&#xfe0f;</span> Settings <span class="icon">&#x2699;&#xfe0f;</span> Settings
</a> </a>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">
<div class="sidebar-user"> <div class="sidebar-user">
<div class="avatar" id="userAvatar">DR</div> <div class="avatar" id="userAvatar">DR</div>
@ -68,54 +65,68 @@
<div class="page-content"> <div class="page-content">
<div class="page-header"> <div class="page-header">
<h1>Trade Activity</h1> <h1>Trade History</h1>
<p class="subtitle">See how your meals are being traded on Grubflip.</p> <p class="subtitle">Track all meal trades happening at your restaurant.</p>
</div> </div>
<!-- Stats row --> <!-- Stats Row -->
<div class="stats-grid" style="margin-bottom: var(--gf-space-6);"> <div class="stats-grid" style="margin-bottom:var(--gf-space-6)">
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon green">&#x1f504;</div> <div class="stat-icon orange">&#x1f504;</div>
<div class="stat-info"> <div class="stat-info">
<h3>Total Trades</h3> <h3>Total Trades</h3>
<div class="stat-value" id="totalTrades">159</div> <div class="stat-value" id="statTotal">--</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon orange">&#x1f525;</div> <div class="stat-icon green">&#x2705;</div>
<div class="stat-info"> <div class="stat-info">
<h3>This Week</h3> <h3>Completed</h3>
<div class="stat-value" id="weekTrades">47</div> <div class="stat-value" id="statCompleted">--</div>
<div class="stat-change up">+23% vs last week</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon yellow">&#x2b50;</div> <div class="stat-icon yellow">&#x23f3;</div>
<div class="stat-info"> <div class="stat-info">
<h3>Most Traded</h3> <h3>Pending</h3>
<div class="stat-value" id="topMeal" style="font-size:var(--gf-text-lg)">Spicy Ramen</div> <div class="stat-value" id="statPending">--</div>
</div> </div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-icon blue">&#x1f4c8;</div> <div class="stat-icon blue">&#x1f4c8;</div>
<div class="stat-info"> <div class="stat-info">
<h3>Trade Rate</h3> <h3>Today</h3>
<div class="stat-value" id="tradeRate">87%</div> <div class="stat-value" id="statToday">--</div>
<div class="stat-change up">Acceptance rate</div>
</div> </div>
</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 --> <!-- Trades Table -->
<div class="card"> <div class="card">
<div class="card-header"> <div id="tradesContent">
<h2>Recent Trades</h2>
<div class="search-box" style="max-width: 14rem;">
<span class="search-icon">&#x1f50d;</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 class="loading-overlay"><div class="spinner"></div></div>
</div> </div>
</div> </div>
@ -123,10 +134,69 @@
</main> </main>
</div> </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> <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 src="js/admin-app.js"></script>
<script> <script>
// --- Auth ---
if (!AdminApp.requireAuth()) throw new Error('Not authenticated'); if (!AdminApp.requireAuth()) throw new Error('Not authenticated');
const user = AdminApp.getUser(); const user = AdminApp.getUser();
if (user.name) { if (user.name) {
@ -140,19 +210,23 @@
AdminApp.initSidebar(); AdminApp.initSidebar();
document.getElementById('logoutBtn').addEventListener('click', AdminApp.logout); document.getElementById('logoutBtn').addEventListener('click', AdminApp.logout);
const DEMO_TRADES = [ // --- State ---
{ 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 = []; 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() { async function loadTrades() {
try { try {
const data = await AdminApp.api('/admin/trades'); const data = await AdminApp.api('/admin/trades');
@ -160,73 +234,109 @@
} catch { } catch {
trades = [...DEMO_TRADES]; trades = [...DEMO_TRADES];
} }
updateStats();
renderTrades(); renderTrades();
} }
function renderTrades() { function updateStats() {
const container = document.getElementById('tradesContainer'); document.getElementById('statTotal').textContent = trades.length;
const search = document.getElementById('searchTrades').value.toLowerCase(); 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;
}
let filtered = trades; function renderTrades() {
if (search) { const container = document.getElementById('tradesContent');
filtered = trades.filter(t => const search = document.getElementById('searchInput').value.toLowerCase();
t.from_user.toLowerCase().includes(search) || const statusFilter = document.getElementById('filterStatus').value;
t.to_user.toLowerCase().includes(search) || const dateFilter = document.getElementById('filterDate').value;
t.meal.toLowerCase().includes(search) ||
t.offered.toLowerCase().includes(search) 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) { if (filtered.length === 0) {
container.innerHTML = ` 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>';
<div class="empty-state">
<div class="icon">&#x1f504;</div>
<h3>No trades found</h3>
<p>Trades involving your meals will appear here.</p>
</div>
`;
return; return;
} }
const statusBadge = (s) => { const statusBadge = (status) => {
const map = { pending: 'badge-warning', completed: 'badge-success', declined: 'badge-error', cancelled: 'badge-default' }; const map = { completed: 'badge-success', pending: 'badge-warning', cancelled: 'badge-default', disputed: 'badge-error' };
return `<span class="badge ${map[s] || 'badge-default'}">${s}</span>`; return '<span class="badge ' + (map[status] || 'badge-default') + '">' + status + '</span>';
}; };
container.innerHTML = ` 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>' +
<div class="table-wrapper"> filtered.map(t =>
<table class="data-table"> '<tr>' +
<thead> '<td class="font-mono text-muted">#' + t.id + '</td>' +
<tr> '<td>' + AdminApp.escapeHtml(t.from_user) + '</td>' +
<th>Trade #</th> '<td>' + AdminApp.escapeHtml(t.to_user) + '</td>' +
<th>From</th> '<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>' +
<th>To</th> '<td>' + statusBadge(t.status) + '</td>' +
<th>Meal Wanted</th> '<td class="text-muted">' + AdminApp.timeAgo(t.created_at) + '</td>' +
<th>Offered</th> '<td><button class="btn btn-ghost btn-sm" onclick="viewTrade(' + t.id + ')">View</button></td>' +
<th>Status</th> '</tr>'
<th>Time</th> ).join('') +
</tr> '</tbody></table></div>';
</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); // --- 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(); loadTrades();
</script> </script>
</body> </body>