UX fixes: admin link gating, GPS pulse timeout, ? shortcut overlay, offline queue, native confirm→modal

This commit is contained in:
2026-06-01 22:01:23 -04:00
parent a7b1887274
commit d092abd91a
+153 -3
View File
@@ -1228,7 +1228,7 @@
<div style="font-size:11px;line-height:1.5;margin-bottom:6px;">
<kbd>Esc</kbd> to close · <kbd>Ctrl</kbd>+<kbd>/</kbd> to search · <svg class="gi" aria-hidden="true"><use href="#gi-help"/></svg> for help
</div>
<a href="https://admin.canteen.ourpad.casa" target="_blank" rel="noopener" style="display:block;font-size:11px;color:var(--accent2);text-decoration:none;"><svg class="gi" aria-hidden="true"><use href="#gi-gear"/></svg> Admin Panel</a>
<a href="https://admin.canteen.ourpad.casa" target="_blank" rel="noopener" class="admin-only" style="display:none;font-size:11px;color:var(--accent2);text-decoration:none;"><svg class="gi" aria-hidden="true"><use href="#gi-gear"/></svg> Admin Panel</a>
</div>
</div>
@@ -1255,6 +1255,11 @@
</div>
<div id="scanStatus" class="status-bar">Point camera at a barcode</div>
<div id="scanResult" class="scan-result" style="display:none;"></div>
<div id="manualSearchFallback" style="margin-top:12px;display:none;">
<div style="font-size:12px;color:var(--text2);margin-bottom:6px;">Or enter ID manually:</div>
<input type="text" id="manualBarcodeInput" class="input-field" placeholder="Connect ID or barcode..." onkeypress="if(event.key==='Enter')handleManualBarcode()">
<button class="btn btn-primary btn-sm" onclick="handleManualBarcode()" style="margin-top:6px;">Search</button>
</div>
</div>
</div>
@@ -1953,6 +1958,7 @@
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
<li><kbd>Ctrl</kbd>+<kbd>/</kbd> — Focus search bar</li>
<li><kbd>Escape</kbd> — Close drawers, modals, or go back</li>
<li><kbd>?</kbd> — Show this help modal (anywhere)</li>
</ul>
<p style="margin-bottom:10px;"><strong>Navigation</strong></p>
<ul style="margin:0 0 12px 20px;padding:0;list-style:disc;">
@@ -2058,6 +2064,7 @@
function isLoggedIn() { return !!AppState.authToken; }
function isTechnician() { return AppState.currentUser && AppState.currentUser.role === 'technician'; }
function isAdmin() { return AppState.currentUser && AppState.currentUser.role === 'admin'; }
// ── Dropdown helper ────────────────────────────────────────────────────
let _activeDropdown = null;
@@ -2113,6 +2120,11 @@
document.querySelectorAll('.tech-only').forEach(el => {
el.style.display = isTechnician() ? '' : 'none';
});
// Admin-only elements
document.querySelectorAll('.admin-only').forEach(el => {
el.style.display = isAdmin() ? 'block' : 'none';
});
}
// Try to restore session from localStorage or auto-login
@@ -2236,6 +2248,15 @@
if (searchEl) { searchEl.focus(); searchEl.select(); }
return;
}
// ? — show keyboard shortcut overlay (only when not typing in an input)
if (e.key === '?' && !e.ctrlKey && !e.metaKey && !e.altKey) {
var tag = document.activeElement?.tagName || '';
if (!['INPUT', 'TEXTAREA', 'SELECT'].includes(tag)) {
e.preventDefault();
showShortcutOverlay();
return;
}
}
// Escape — close modals, drawers, go back
if (e.key === 'Escape') {
var helpOverlay = document.getElementById('helpModalOverlay');
@@ -2300,6 +2321,7 @@
badge.style.cursor = 'pointer';
badge.onclick = function() {
badge.innerHTML = gi('📍') + ' Locating...';
badge.className = 'gps-badge waiting';
requestLocation().then(() => {
updateGpsBadge();
// Re-enable for next re-capture
@@ -2313,6 +2335,12 @@
requestLocation().catch(() => {
// Silent fail is expected on mobile — badge click handler covers it
});
// Stop pulsing animation after 10s if GPS still hasn't resolved
setTimeout(() => {
if (badge.classList.contains('waiting')) {
badge.style.animation = 'none';
}
}, 10000);
}
function updateGpsBadge() {
@@ -2339,6 +2367,69 @@
toastTimer = setTimeout(() => t.classList.remove('show'), 4000);
}
// =========================================================================
// OFFLINE QUEUE
// =========================================================================
const offlineQueue = [];
function updateOnlineStatus() {
const banner = document.getElementById('offlineBanner');
const indicator = document.getElementById('queueIndicator');
if (!navigator.onLine) {
if (banner) banner.classList.add('show');
if (indicator) indicator.style.display = offlineQueue.length > 0 ? 'flex' : 'none';
showToast('📡 Offline — changes will sync when connected', false);
} else {
if (banner) banner.classList.remove('show');
if (indicator) indicator.style.display = 'none';
if (offlineQueue.length > 0) {
processOfflineQueue();
}
}
}
function processOfflineQueue() {
const count = offlineQueue.length;
if (count === 0) {
showToast('No queued actions to sync', false);
return;
}
showToast('Syncing ' + count + ' queued action(s)...', false);
const queue = [...offlineQueue];
offlineQueue.length = 0;
let synced = 0;
let failed = 0;
queue.forEach((item, i) => {
// Attempt each queued action
const attempt = item.action();
if (attempt && typeof attempt.then === 'function') {
attempt.then(() => synced++).catch(() => {
failed++;
offlineQueue.push(item); // Re-queue on failure
});
} else {
synced++;
}
});
setTimeout(() => {
updateOnlineStatus();
if (failed === 0) {
showToast('✅ Synced ' + synced + ' action(s)', false);
} else {
showToast('⚠️ Synced ' + synced + ', ' + failed + ' failed (will retry)', true);
}
}, 500);
}
function enqueueOffline(actionFn, description) {
offlineQueue.push({ action: actionFn, description: description || 'queued action', timestamp: Date.now() });
const countEl = document.getElementById('queueIndicatorCount');
if (countEl) countEl.textContent = offlineQueue.length;
const indicator = document.getElementById('queueIndicator');
if (indicator) indicator.style.display = 'flex';
showToast('📡 Saved offline: ' + (description || 'action') + ' — will sync when connected', false);
}
// =========================================================================
// MODAL
// =========================================================================
@@ -2370,6 +2461,11 @@
function showHelpModal() {
document.getElementById('helpModalOverlay').classList.add('open');
}
function showShortcutOverlay() {
// Use the help modal as the overlay (it already lists all shortcuts)
document.getElementById('helpModalOverlay').classList.add('open');
showToast('Press ? again or Esc to close', false);
}
function closeHelpModal(e) {
if (e && e.target !== e.currentTarget) return;
document.getElementById('helpModalOverlay').classList.remove('open');
@@ -2811,6 +2907,21 @@
setScanStatus('Camera failed: ' + e.message, 'error');
const ph = document.getElementById('cameraPlaceholder');
if (ph) ph.innerHTML = `<span class="cam-icon">${gi("📷")}</span><div class="cam-hint" style="margin-top:4px;">Camera: ${e.message}</div>`;
// Show manual search fallback
const fallback = document.getElementById('manualSearchFallback');
if (fallback) {
fallback.style.display = 'block';
fallback.innerHTML = `
<div style="background:var(--red-bg);color:var(--red);padding:10px;border-radius:8px;margin-bottom:10px;font-size:13px;">
<strong>Camera unavailable</strong><br>
${getCameraErrorMessage(e)}
</div>
<div style="font-size:12px;color:var(--text2);margin-bottom:6px;">Enter ID manually:</div>
<input type="text" id="manualBarcodeInput" class="input-field" placeholder="Connect ID or barcode..." onkeypress="if(event.key==='Enter')handleManualBarcode()">
<button class="btn btn-primary btn-sm" onclick="handleManualBarcode()" style="margin-top:6px;width:100%;">Search</button>
<button class="btn btn-outline btn-sm" onclick="startScanning()" style="margin-top:6px;width:100%;">Try camera again</button>
`;
}
}
}
@@ -2838,6 +2949,34 @@
if (el) { el.textContent = msg; el.className = 'status-bar ' + cls; }
}
function getCameraErrorMessage(err) {
if (err.name === 'NotAllowedError') {
return 'Camera permission denied. Please allow camera access in your browser settings and reload.';
}
if (err.name === 'NotFoundError') {
return 'No camera found on this device. Enter an ID manually below.';
}
if (err.name === 'NotReadableError') {
return 'Camera is in use by another app. Close other apps using the camera and try again.';
}
if (err.name === 'OverconstrainedError') {
return 'No suitable camera found. Try a different device or enter an ID manually.';
}
return err.message || 'Unknown camera error. Try entering an ID manually.';
}
function handleManualBarcode() {
const input = document.getElementById('manualBarcodeInput');
if (!input) return;
const value = input.value.trim();
if (!value) {
showToast('Please enter a Connect ID or barcode', true);
return;
}
input.value = '';
handleBarcode(value);
}
async function handleBarcode(machineId) {
if (barcodeDebounce) return;
barcodeDebounce = machineId;
@@ -6622,8 +6761,8 @@
'&travelmode=driving';
showToast('→ Next: ' + (nextStop.name || 'Stop ' + (rpState.currentStopIndex + 1)), false);
// Offer to navigate
setTimeout(() => {
if (confirm('Navigate to ' + (nextStop.account_name || nextStop.name) + '?')) {
setTimeout(async () => {
if (await showModal('Navigate', 'Open directions to ' + (nextStop.account_name || nextStop.name) + ' in Google Maps?', 'Navigate', 'btn-primary')) {
window.open(url, '_blank');
}
}, 500);
@@ -6715,6 +6854,17 @@
document.addEventListener('DOMContentLoaded', () => {
initAuth();
initGPS();
// First-visit shortcut discovery toast
if (!localStorage.getItem('canteen_shortcuts_shown')) {
setTimeout(() => {
showToast('💡 Press ? for keyboard shortcuts · Esc to close', false);
localStorage.setItem('canteen_shortcuts_shown', 'true');
}, 3000);
}
// Online/offline detection
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
updateOnlineStatus();
});
</script>