Built out the full restaurant owner admin dashboard: - Dashboard overview with stats, recent trades, and top meals - Meals management with card grid, add/edit/delete modals, search & filter - Orders page with status tabs, accept/ready/complete workflow - Trades activity page with search and stats overview - Shared admin-app.js module (sidebar, API client, toast, auth helpers) - All pages use existing design tokens, mobile-first responsive layout - Demo data fallback when API endpoints aren't ready yet Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
180 lines
4.5 KiB
JavaScript
180 lines
4.5 KiB
JavaScript
/* ============================================
|
|
Grubflip Admin — Core Application Module
|
|
Sidebar, API client, toast, auth helpers
|
|
============================================ */
|
|
|
|
const AdminApp = (() => {
|
|
'use strict';
|
|
|
|
const API_BASE = '/api';
|
|
|
|
// --- Auth ---
|
|
function getToken() {
|
|
return localStorage.getItem('gf_token');
|
|
}
|
|
|
|
function getUser() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem('gf_user') || '{}');
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function requireAuth() {
|
|
if (!getToken()) {
|
|
window.location.href = 'index.html';
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('gf_token');
|
|
localStorage.removeItem('gf_user');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
// --- API Client ---
|
|
async function api(endpoint, options = {}) {
|
|
const url = `${API_BASE}${endpoint}`;
|
|
const headers = {
|
|
'Accept': 'application/json',
|
|
...options.headers,
|
|
};
|
|
|
|
const token = getToken();
|
|
if (token) {
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
}
|
|
|
|
if (options.body && !(options.body instanceof FormData)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
if (typeof options.body === 'object') {
|
|
options.body = JSON.stringify(options.body);
|
|
}
|
|
}
|
|
|
|
const resp = await fetch(url, { ...options, headers });
|
|
|
|
if (resp.status === 401) {
|
|
logout();
|
|
return null;
|
|
}
|
|
|
|
if (!resp.ok) {
|
|
const data = await resp.json().catch(() => ({}));
|
|
throw new Error(data.message || `Request failed (${resp.status})`);
|
|
}
|
|
|
|
if (resp.status === 204) return null;
|
|
return resp.json();
|
|
}
|
|
|
|
// --- Sidebar ---
|
|
function initSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.getElementById('sidebarOverlay');
|
|
const toggle = document.getElementById('menuToggle');
|
|
|
|
if (!sidebar || !toggle) return;
|
|
|
|
toggle.addEventListener('click', () => {
|
|
sidebar.classList.toggle('open');
|
|
if (overlay) overlay.classList.toggle('visible');
|
|
});
|
|
|
|
if (overlay) {
|
|
overlay.addEventListener('click', () => {
|
|
sidebar.classList.remove('open');
|
|
overlay.classList.remove('visible');
|
|
});
|
|
}
|
|
|
|
// Close on Escape
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && sidebar.classList.contains('open')) {
|
|
sidebar.classList.remove('open');
|
|
if (overlay) overlay.classList.remove('visible');
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- Toast System ---
|
|
function toast(message, type = 'info', duration = 4000) {
|
|
const container = document.getElementById('toastContainer');
|
|
if (!container) return;
|
|
|
|
const icons = {
|
|
success: '✅',
|
|
error: '❌',
|
|
warning: '⚠️',
|
|
info: 'ℹ️'
|
|
};
|
|
|
|
const el = document.createElement('div');
|
|
el.className = `toast toast-${type}`;
|
|
el.innerHTML = `<span>${icons[type] || ''}</span> <span>${escapeHtml(message)}</span>`;
|
|
container.appendChild(el);
|
|
|
|
setTimeout(() => {
|
|
el.classList.add('removing');
|
|
el.addEventListener('animationend', () => el.remove());
|
|
}, duration);
|
|
}
|
|
|
|
// --- Utilities ---
|
|
function escapeHtml(str) {
|
|
const div = document.createElement('div');
|
|
div.textContent = str;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '--';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
|
}
|
|
|
|
function formatTime(dateStr) {
|
|
if (!dateStr) return '--';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
|
}
|
|
|
|
function timeAgo(dateStr) {
|
|
if (!dateStr) return '--';
|
|
const now = Date.now();
|
|
const then = new Date(dateStr).getTime();
|
|
const diff = Math.floor((now - then) / 1000);
|
|
|
|
if (diff < 60) return 'just now';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)} min ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)} hr ago`;
|
|
if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`;
|
|
return formatDate(dateStr);
|
|
}
|
|
|
|
// --- Confirm Dialog ---
|
|
function confirm(message) {
|
|
return new Promise((resolve) => {
|
|
// Use native confirm for now — can replace with modal later
|
|
resolve(window.confirm(message));
|
|
});
|
|
}
|
|
|
|
return {
|
|
api,
|
|
getToken,
|
|
getUser,
|
|
requireAuth,
|
|
logout,
|
|
initSidebar,
|
|
toast,
|
|
escapeHtml,
|
|
formatDate,
|
|
formatTime,
|
|
timeAgo,
|
|
confirm,
|
|
};
|
|
})();
|