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:
parent
a3331881d4
commit
960391c4f4
4 changed files with 913 additions and 300 deletions
|
|
@ -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">📊</span> Dashboard</a>
|
||||||
<span class="icon">📊</span> Dashboard
|
<a href="meals.html" class="sidebar-link"><span class="icon">🍔</span> Meals</a>
|
||||||
</a>
|
<a href="orders.html" class="sidebar-link active"><span class="icon">📦</span> Orders</a>
|
||||||
<a href="meals.html" class="sidebar-link">
|
<a href="trades.html" class="sidebar-link"><span class="icon">🔄</span> Trades</a>
|
||||||
<span class="icon">🍔</span> Meals
|
|
||||||
</a>
|
|
||||||
<a href="orders.html" class="sidebar-link active">
|
|
||||||
<span class="icon">📦</span> Orders
|
|
||||||
</a>
|
|
||||||
<a href="trades.html" class="sidebar-link">
|
|
||||||
<span class="icon">🔄</span> Trades
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="sidebar-section-title">Settings</div>
|
<div class="sidebar-section-title">Settings</div>
|
||||||
<a href="profile.html" class="sidebar-link">
|
<a href="profile.html" class="sidebar-link"><span class="icon">🏪</span> Restaurant Profile</a>
|
||||||
<span class="icon">🏪</span> Restaurant Profile
|
<a href="settings.html" class="sidebar-link"><span class="icon">⚙️</span> Settings</a>
|
||||||
</a>
|
|
||||||
<a href="settings.html" class="sidebar-link">
|
|
||||||
<span class="icon">⚙️</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">📦</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">🍳</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">✅</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">💰</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">🔍</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">×</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() {
|
||||||
function renderOrders() {
|
document.getElementById('statTotal').textContent = orders.length;
|
||||||
const container = document.getElementById('ordersContainer');
|
document.getElementById('statPreparing').textContent = orders.filter(function(o){ return o.status === 'preparing' || o.status === 'new'; }).length;
|
||||||
let filtered = orders;
|
document.getElementById('statFulfilled').textContent = orders.filter(function(o){ return o.status === 'fulfilled'; }).length;
|
||||||
if (currentTab !== 'all') {
|
var todayStr = new Date().toDateString();
|
||||||
filtered = orders.filter(o => o.status === currentTab);
|
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() {
|
||||||
|
var container = document.getElementById('ordersContent');
|
||||||
|
var search = document.getElementById('searchInput').value.toLowerCase();
|
||||||
|
var statusFilter = document.getElementById('filterStatus').value;
|
||||||
|
|
||||||
|
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">📦</div><h3>No orders found</h3><p>Try adjusting your filters.</p></div>';
|
||||||
<div class="empty-state">
|
|
||||||
<div class="icon">📦</div>
|
|
||||||
<h3>No ${currentTab === 'all' ? '' : currentTab + ' '}orders</h3>
|
|
||||||
<p>Orders will appear here when users trade for your meals.</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
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)} · ${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
282
admin/profile.html
Normal 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">📊</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"><span class="icon">🔄</span> Trades</a>
|
||||||
|
<div class="sidebar-section-title">Settings</div>
|
||||||
|
<a href="profile.html" class="sidebar-link active"><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">Restaurant Profile</h1>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">⏻</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>🖼</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
312
admin/settings.html
Normal 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">📊</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"><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 active"><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">Settings</h1>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-right">
|
||||||
|
<button class="topbar-btn" aria-label="Sign out" id="logoutBtn">⏻</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 · 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>
|
||||||
|
|
@ -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">🔄</span> Trades
|
<span class="icon">🔄</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">🏪</span> Restaurant Profile
|
<span class="icon">🏪</span> Restaurant Profile
|
||||||
|
|
@ -43,7 +41,6 @@
|
||||||
<span class="icon">⚙️</span> Settings
|
<span class="icon">⚙️</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">🔄</div>
|
<div class="stat-icon orange">🔄</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">🔥</div>
|
<div class="stat-icon green">✅</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">⭐</div>
|
<div class="stat-icon yellow">⏳</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">📈</div>
|
<div class="stat-icon blue">📈</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">🔍</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">🔍</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">×</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;
|
||||||
let filtered = trades;
|
const today = new Date().toDateString();
|
||||||
if (search) {
|
document.getElementById('statToday').textContent = trades.filter(t => new Date(t.created_at).toDateString() === today).length;
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (filtered.length === 0) {
|
||||||
container.innerHTML = `
|
container.innerHTML = '<div class="empty-state"><div class="icon">🔄</div><h3>No trades found</h3><p>Try adjusting your search or filters.</p></div>';
|
||||||
<div class="empty-state">
|
|
||||||
<div class="icon">🔄</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">⇄</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">🍽 ' + 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();
|
loadTrades();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue