grubflip/admin/js/admin-app.js
Sarah a3331881d4 Add GrubFlip admin dashboard — meals, orders, trades pages
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>
2026-03-27 15:38:49 +00:00

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: '&#x2705;',
error: '&#x274c;',
warning: '&#x26a0;&#xfe0f;',
info: '&#x2139;&#xfe0f;'
};
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,
};
})();