UX fixes: admin link gating, GPS pulse timeout, ? shortcut overlay, offline queue, native confirm→modal
This commit is contained in:
+153
-3
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user