fix(admin): sidebar hamburger menu for mobile, remove auto Cantaloupe sync
- Replace broken icon-only sidebar collapse with off-canvas hamburger menu that slides in from left (hidden by default on <768px) - Add sidebar overlay backdrop, auto-close on nav/logout - Rename 'Cantaloupe Sync' to 'Import' in sidebar and 'Excel Import' in page - Remove 'Sync Now' button and backend subprocess auto-export entirely - Keep manual Excel file upload as the only import path - Update empty-state text and upload handler to work without Sync Now btn
This commit is contained in:
+5
-37
@@ -386,7 +386,7 @@ async def run_sync(
|
|||||||
file_path: Optional[str] = Query(None, description="Server-side path to Excel file"),
|
file_path: Optional[str] = Query(None, description="Server-side path to Excel file"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Run a full Cantaloupe sync from an Excel file.
|
Import an Excel file into a staged batch for review.
|
||||||
|
|
||||||
Accepts either:
|
Accepts either:
|
||||||
- An uploaded Excel file (multipart form)
|
- An uploaded Excel file (multipart form)
|
||||||
@@ -416,42 +416,10 @@ async def run_sync(
|
|||||||
excel_path = str(p)
|
excel_path = str(p)
|
||||||
display_path = file_path
|
display_path = file_path
|
||||||
else:
|
else:
|
||||||
# Try subprocess call to cantaloupe export
|
raise HTTPException(
|
||||||
try:
|
status_code=400,
|
||||||
import subprocess
|
detail="No file provided. Upload an Excel file to import asset data."
|
||||||
import sys
|
)
|
||||||
cantaloupe_dir = os.path.expanduser("~/projects/cantaloupe-downloader")
|
|
||||||
result = subprocess.run(
|
|
||||||
[sys.executable, "-m", "cantaloupe", "export", "--scheduled"],
|
|
||||||
capture_output=True, text=True, timeout=120,
|
|
||||||
cwd=cantaloupe_dir,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f"Cantaloupe export failed: {result.stderr[:500]}"
|
|
||||||
)
|
|
||||||
# Parse output to find the exported file path
|
|
||||||
for line in result.stdout.splitlines() + result.stderr.splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if line and Path(line).exists() and line.endswith((".xlsx", ".xls")):
|
|
||||||
excel_path = line
|
|
||||||
display_path = line
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="Cantaloupe export ran but produced no recognizable file path. Provide file upload or file_path parameter."
|
|
||||||
)
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail="No file provided and cantaloupe module not found. Upload an Excel file or provide file_path."
|
|
||||||
)
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"Cantaloupe export error: {e}")
|
|
||||||
|
|
||||||
conn = _get_db()
|
conn = _get_db()
|
||||||
try:
|
try:
|
||||||
|
|||||||
+68
-46
@@ -514,30 +514,51 @@
|
|||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
.mono { font-family: 'Courier New', monospace; font-size: 12px; }
|
.mono { font-family: 'Courier New', monospace; font-size: 12px; }
|
||||||
|
|
||||||
/* Responsive: collapse sidebar on narrow screens */
|
/* ═══════════════════════════════════════════════════════════════════════
|
||||||
|
HAMBURGER BUTTON & SIDEBAR OVERLAY (mobile)
|
||||||
|
═══════════════════════════════════════════════════════════════════════ */
|
||||||
|
.hamburger {
|
||||||
|
display: none;
|
||||||
|
background: none; border: none; color: var(--text); font-size: 24px;
|
||||||
|
cursor: pointer; padding: 6px; margin-right: 10px;
|
||||||
|
line-height: 1; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.hamburger:hover { color: var(--accent2); }
|
||||||
|
.sidebar-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed; inset: 0; background: rgba(0,0,0,0.5);
|
||||||
|
z-index: 99; opacity: 0; transition: opacity 0.25s;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.sidebar-overlay.open { opacity: 1; pointer-events: auto; }
|
||||||
|
|
||||||
|
/* Responsive: off-canvas sidebar on narrow screens */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
|
.hamburger { display: inline-flex; align-items: center; }
|
||||||
|
.sidebar-overlay { display: block; }
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 56px;
|
transform: translateX(-100%);
|
||||||
overflow: hidden;
|
transition: transform 0.25s ease;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
.sidebar .sidebar-header span,
|
.sidebar.open { transform: translateX(0); }
|
||||||
.sidebar .sidebar-user,
|
|
||||||
.sidebar .nav-item span,
|
.main {
|
||||||
.sidebar .nav-item .ni-badge,
|
margin-left: 0;
|
||||||
.sidebar .sidebar-footer span,
|
padding: 16px;
|
||||||
.sidebar .sidebar-footer .logout-btn { display: none; }
|
max-width: 100vw;
|
||||||
.sidebar .nav-item { justify-content: center; padding: 11px 0; }
|
}
|
||||||
.sidebar .nav-item .ni-icon { font-size: 20px; width: auto; }
|
|
||||||
.sidebar .sidebar-header { justify-content: center; }
|
.page-title .pt-actions .btn .btn-label { display: none; }
|
||||||
.sidebar .sidebar-header .sh-icon { font-size: 22px; }
|
.page-title .pt-actions .btn { min-width: 36px; justify-content: center; }
|
||||||
.main { margin-left: 56px; padding: 16px; max-width: calc(100vw - 56px); }
|
|
||||||
|
|
||||||
/* Stats grid: tighter cards */
|
/* Stats grid: tighter cards */
|
||||||
.stats-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 6px; }
|
.stats-grid { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 6px; }
|
||||||
.stat-card { padding: 10px; }
|
.stat-card { padding: 10px; }
|
||||||
.stat-card .stat-value { font-size: 22px; }
|
.stat-card .stat-value { font-size: 22px; }
|
||||||
|
|
||||||
/* Cantaloupe Sync batch rows stack */
|
/* Import batch rows stack */
|
||||||
.cs-batch-row .csb-status { width: 60px; }
|
.cs-batch-row .csb-status { width: 60px; }
|
||||||
.cs-batch-row .csb-time { width: auto; font-size: 11px; }
|
.cs-batch-row .csb-time { width: auto; font-size: 11px; }
|
||||||
.cs-batch-row .csb-rows { width: auto; text-align: left; margin-left: auto; }
|
.cs-batch-row .csb-rows { width: auto; text-align: left; margin-left: auto; }
|
||||||
@@ -567,7 +588,7 @@
|
|||||||
.page-title .pt-actions { width: 100%; }
|
.page-title .pt-actions { width: 100%; }
|
||||||
.page-title .pt-actions button { flex: 1; }
|
.page-title .pt-actions button { flex: 1; }
|
||||||
|
|
||||||
/* Cantaloupe diff badges stack 2x2 */
|
/* Import diff badges stack 2x2 */
|
||||||
.cs-diff-summary { grid-template-columns: repeat(2, 1fr); }
|
.cs-diff-summary { grid-template-columns: repeat(2, 1fr); }
|
||||||
.cs-diff-badge { padding: 8px; }
|
.cs-diff-badge { padding: 8px; }
|
||||||
.cs-diff-badge .csd-val { font-size: 18px; }
|
.cs-diff-badge .csd-val { font-size: 18px; }
|
||||||
@@ -586,7 +607,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ═══════════════════════════════════════════════════════════════════════
|
/* ═══════════════════════════════════════════════════════════════════════
|
||||||
CANTALOUPE SYNC
|
EXCEL IMPORT
|
||||||
═══════════════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════════════ */
|
||||||
.cs-diff-summary {
|
.cs-diff-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -657,8 +678,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══ SIDEBAR OVERLAY (mobile) ═══ -->
|
||||||
|
<div id="sidebarOverlay" class="sidebar-overlay" onclick="toggleSidebar()"></div>
|
||||||
|
|
||||||
<!-- ═══ SIDEBAR ═══ -->
|
<!-- ═══ SIDEBAR ═══ -->
|
||||||
<div class="sidebar">
|
<div class="sidebar" id="sidebar"> <!-- add hamburger to title bar-->
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<span class="sh-icon">📊</span>
|
<span class="sh-icon">📊</span>
|
||||||
<span>Canteen Admin</span>
|
<span>Canteen Admin</span>
|
||||||
@@ -690,7 +714,7 @@
|
|||||||
<span class="ni-icon">📤</span><span>Export</span>
|
<span class="ni-icon">📤</span><span>Export</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="nav-item" data-page="cantaloupe" onclick="switchPage('cantaloupe')">
|
<button class="nav-item" data-page="cantaloupe" onclick="switchPage('cantaloupe')">
|
||||||
<span class="ni-icon">🔄</span><span>Cantaloupe Sync</span>
|
<span class="ni-icon">📥</span><span>Import</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
@@ -702,6 +726,11 @@
|
|||||||
<!-- ═══ MAIN CONTENT ═══ -->
|
<!-- ═══ MAIN CONTENT ═══ -->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- Hamburger button (mobile) -->
|
||||||
|
<div style="display:flex;align-items:center;margin-bottom:12px;" class="mobile-title-row">
|
||||||
|
<button class="hamburger" onclick="toggleSidebar()" aria-label="Toggle menu">☰</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ── DASHBOARD ── -->
|
<!-- ── DASHBOARD ── -->
|
||||||
<div id="pageDashboard" class="page">
|
<div id="pageDashboard" class="page">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
@@ -858,14 +887,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── CANTALOUPE SYNC ── -->
|
<!-- ── EXCEL IMPORT ── -->
|
||||||
<div id="pageCantaloupe" class="page hidden">
|
<div id="pageCantaloupe" class="page hidden">
|
||||||
<div class="page-title">
|
<div class="page-title">
|
||||||
<span>Cantaloupe Sync</span>
|
<span>Excel Import</span>
|
||||||
<div class="pt-actions">
|
<div class="pt-actions">
|
||||||
<button class="btn btn-sm btn-primary" onclick="runSync()" id="csSyncBtn">🔄 Sync Now</button>
|
|
||||||
<input type="file" id="csFileInput" accept=".xlsx,.xls" style="display:none" onchange="uploadSyncFile(event)">
|
<input type="file" id="csFileInput" accept=".xlsx,.xls" style="display:none" onchange="uploadSyncFile(event)">
|
||||||
<button class="btn btn-sm btn-outline" onclick="document.getElementById('csFileInput').click()">📤 Upload Excel</button>
|
<button class="btn btn-sm btn-primary" onclick="document.getElementById('csFileInput').click()">📤 Upload Excel</button>
|
||||||
<button class="btn btn-sm btn-outline" onclick="loadCantaloupeSync()">↻ Refresh</button>
|
<button class="btn btn-sm btn-outline" onclick="loadCantaloupeSync()">↻ Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -969,6 +997,12 @@ function updateUserUI() {
|
|||||||
if (logoutBtn) logoutBtn.style.display = isLoggedIn() ? 'block' : 'none';
|
if (logoutBtn) logoutBtn.style.display = isLoggedIn() ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sidebar toggle (mobile) ──
|
||||||
|
function toggleSidebar() {
|
||||||
|
document.getElementById('sidebar').classList.toggle('open');
|
||||||
|
document.getElementById('sidebarOverlay').classList.toggle('open');
|
||||||
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
// TOAST & MODAL
|
// TOAST & MODAL
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -1009,6 +1043,9 @@ function switchPage(page) {
|
|||||||
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
||||||
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
|
const navItem = document.querySelector(`.nav-item[data-page="${page}"]`);
|
||||||
if (navItem) navItem.classList.add('active');
|
if (navItem) navItem.classList.add('active');
|
||||||
|
// Close sidebar on mobile after nav
|
||||||
|
document.getElementById('sidebar')?.classList.remove('open');
|
||||||
|
document.getElementById('sidebarOverlay')?.classList.remove('open');
|
||||||
// Lazy load
|
// Lazy load
|
||||||
if (page === 'dashboard') loadDashboard();
|
if (page === 'dashboard') loadDashboard();
|
||||||
else if (page === 'settings') renderAllSettings();
|
else if (page === 'settings') renderAllSettings();
|
||||||
@@ -1062,6 +1099,9 @@ function doLogout() {
|
|||||||
localStorage.removeItem('adminToken');
|
localStorage.removeItem('adminToken');
|
||||||
document.getElementById('loginOverlay').classList.remove('hidden');
|
document.getElementById('loginOverlay').classList.remove('hidden');
|
||||||
updateUserUI();
|
updateUserUI();
|
||||||
|
// Close sidebar on mobile
|
||||||
|
document.getElementById('sidebar')?.classList.remove('open');
|
||||||
|
document.getElementById('sidebarOverlay')?.classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initAuth() {
|
async function initAuth() {
|
||||||
@@ -2099,7 +2139,7 @@ async function loadCantaloupeSync() {
|
|||||||
function renderCsOverview() {
|
function renderCsOverview() {
|
||||||
const el = document.getElementById('csOverviewContent');
|
const el = document.getElementById('csOverviewContent');
|
||||||
if (!csLatestBatch) {
|
if (!csLatestBatch) {
|
||||||
el.innerHTML = '<div class="empty-state"><div class="es-icon">🔄</div>No sync batches yet. Click "Sync Now" to import data.</div>';
|
el.innerHTML = '<div class="empty-state"><div class="es-icon">📥</div>No imports yet. Upload an Excel file to import asset data.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const b = csLatestBatch;
|
const b = csLatestBatch;
|
||||||
@@ -2305,32 +2345,15 @@ async function toggleBatchHistory(batchId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync Now ──
|
|
||||||
async function runSync() {
|
|
||||||
const btn = document.getElementById('csSyncBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<div class="spinner"></div> Syncing...';
|
|
||||||
try {
|
|
||||||
const result = await api('/api/admin/cantaloupe/sync', { method: 'POST' });
|
|
||||||
showToast('Sync completed — batch #' + result.id);
|
|
||||||
await loadCantaloupeSync();
|
|
||||||
} catch (e) {
|
|
||||||
showToast('Sync failed: ' + e.message, true);
|
|
||||||
} finally {
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.innerHTML = '🔄 Sync Now';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Upload Excel File ──
|
// ── Upload Excel File ──
|
||||||
|
let _csUploading = false;
|
||||||
async function uploadSyncFile(event) {
|
async function uploadSyncFile(event) {
|
||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
if (_csUploading) return;
|
||||||
|
_csUploading = true;
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append('file', file);
|
fd.append('file', file);
|
||||||
const btn = document.getElementById('csSyncBtn');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.innerHTML = '<div class="spinner"></div> Uploading...';
|
|
||||||
try {
|
try {
|
||||||
const result = await api('/api/admin/cantaloupe/sync', { method: 'POST', body: fd });
|
const result = await api('/api/admin/cantaloupe/sync', { method: 'POST', body: fd });
|
||||||
showToast('Upload processed — batch #' + result.id);
|
showToast('Upload processed — batch #' + result.id);
|
||||||
@@ -2338,8 +2361,7 @@ async function uploadSyncFile(event) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast('Upload failed: ' + e.message, true);
|
showToast('Upload failed: ' + e.message, true);
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
_csUploading = false;
|
||||||
btn.innerHTML = '🔄 Sync Now';
|
|
||||||
event.target.value = ''; // reset so same file can be re-picked
|
event.target.value = ''; // reset so same file can be re-picked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user