From e66ed51e7f4605b407ef9eb6b218089241011b23 Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 20 May 2026 22:35:49 -0400 Subject: [PATCH] fix(navigate): fix Navigate tab routing, add empty state, route search, mode toggle - Fixed switchTab recursion bug when leaving tabNavigate (set currentTab early + return) - Fixed closeNavigate to not call switchTab (caller handles tab transition) - Added renderNavPage call in switchTab lifecycle for tabNavigate - Added empty state with asset search on Navigate page - Added route-finding: search assets, navigate from search results - Added driving/walking mode toggle with OSRM route display - Added ETA display for driving mode - Fixed route polyline to use OSRM road geometry when available - Added switchNavMode function for mode switching - Improved directions label (Walking vs Driving) --- static/index.html | 430 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 334 insertions(+), 96 deletions(-) diff --git a/static/index.html b/static/index.html index 3337882..1f4451c 100644 --- a/static/index.html +++ b/static/index.html @@ -186,16 +186,24 @@ ═══════════════════════════════════════════════════════════════════════ */ .tab-panel { display: none; padding: 0 12px; - animation: none; + opacity: 1; transform: translateX(0); } .tab-panel.active { display: block; animation: tabSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1); } + .tab-panel.slide-left { + display: block !important; + animation: tabSlideOutLeft 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards; + } @keyframes tabSlideIn { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } } + @keyframes tabSlideOutLeft { + from { opacity: 1; transform: translateX(0); } + to { opacity: 0; transform: translateX(-16px); } + } /* ═══════════════════════════════════════════════════════════════════════ BOTTOM TABS @@ -344,6 +352,73 @@ .empty-state { text-align: center; padding: 32px 16px; color: var(--text2); font-size: 13px; } .empty-state .es-icon { font-size: 40px; margin-bottom: 8px; opacity: 0.5; } + /* Enhanced empty-state card — full-page onboarding pattern */ + .empty-state-card { + display: flex; flex-direction: column; align-items: center; justify-content: center; + text-align: center; padding: 48px 24px; min-height: 240px; + } + .esc-icon { + font-size: 56px; margin-bottom: 16px; opacity: 0.35; + width: 88px; height: 88px; border-radius: 50%; + background: var(--card2); display: flex; align-items: center; justify-content: center; + } + .esc-heading { + font-size: 17px; font-weight: 700; color: var(--text); + margin-bottom: 6px; + } + .esc-desc { + font-size: 13px; color: var(--text2); max-width: 280px; + line-height: 1.5; margin-bottom: 20px; + } + .esc-cta { + display: inline-flex; align-items: center; gap: 6px; + padding: 10px 24px; font-size: 14px; font-weight: 600; + background: var(--accent); color: #fff; border: none; + border-radius: var(--radius-sm); cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s; + } + .esc-cta:active { background: var(--accent2); } + .esc-cta-sm { + display: inline-flex; align-items: center; gap: 4px; + padding: 6px 14px; font-size: 12px; font-weight: 600; + background: var(--accent); color: #fff; border: none; + border-radius: var(--radius-xs); cursor: pointer; + -webkit-tap-highlight-color: transparent; + transition: background 0.15s; + } + .esc-cta-sm:active { background: var(--accent2); } + + /* Onboarding welcome overlay */ + .onboard-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.7); + z-index: 600; display: flex; align-items: center; justify-content: center; + padding: 20px; opacity: 0; pointer-events: none; transition: opacity 0.25s; + } + .onboard-overlay.open { opacity: 1; pointer-events: auto; } + .onboard-card { + background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 28px 24px; max-width: 340px; + text-align: center; width: 100%; + } + .onboard-card .esc-icon { margin: 0 auto 16px; } + + /* Guidance card — reports empty state */ + .guidance-card { + text-align: center; padding: 24px 16px; + } + .gc-icon { + font-size: 40px; margin-bottom: 12px; opacity: 0.5; + } + .gc-title { + font-size: 15px; font-weight: 600; color: var(--text); + margin-bottom: 6px; + } + .gc-hint { + font-size: 12px; color: var(--text2); line-height: 1.5; + max-width: 300px; margin: 0 auto; + } + /* ═══════════════════════════════════════════════════════════════════════ MODAL / DIALOG ═══════════════════════════════════════════════════════════════════════ */ @@ -1325,104 +1400,178 @@
-
-
Manual Entry
+ +
- - - - - - -
- - + +
+
+ 📋 + Identity + required +
- - -
+
+ + +
⚠️ Machine ID is required
- -
-
📍 Directions & Access
- -
- - -
-
- - -
- -
- - -
-
- - -
-
+ + - -
-
- 🔑 Keys - -
-
-
+ + +
⚠️ Asset Name is required
- -
-
🪪 Security Badges
-
-
+ + - -
-
🏢 Customer & Location
- - -
- - -
-
📸 Photo (optional)
-
-
- 📸 -
Tap to take photo
+
+ +
- + +
- - -
- + +
+
+ 📍 + Location & Directions + +
+
+ + + +
+ + +
+
+ + +
+ + + +
+ + + ✓ Done +
+
+ + + ✓ Done +
+
+
+ + +
+
+ 🔑 + Keys & Badges + +
+
+
+ 🔑 Keys + +
+
+ +
🪪 Security Badges
+
+
+
+ + +
+
+ 🏢 + Customer & Location + +
+
+ + + + +
+
+ + +
+
+ 📸 + Photo (optional) + +
+
+
+
+ 📸 +
Tap to take photo
+
+ +
+ + +
+
+ +
- - + +
@@ -2724,18 +2873,20 @@ // Update tab panels with slide animation const oldPanel = document.querySelector('.tab-panel.active'); - if (oldPanel) { - oldPanel.classList.remove('active'); + const newPanel = document.getElementById(tabId); + if (oldPanel && oldPanel !== newPanel) { + // Play exit animation on old panel, keeping it visible oldPanel.classList.add('slide-left'); oldPanel.addEventListener('animationend', function handler() { oldPanel.removeEventListener('animationend', handler); - oldPanel.classList.remove('slide-left'); + oldPanel.classList.remove('active', 'slide-left'); }, { once: true }); - } - const panel = document.getElementById(tabId); - if (panel) { - // Brief delay to let exit animation start before entry - setTimeout(() => panel.classList.add('active'), 50); + // Small delay then show new panel with slide-in + setTimeout(() => { + if (newPanel) newPanel.classList.add('active'); + }, 50); + } else if (newPanel) { + newPanel.classList.add('active'); } // Update bottom tabs — highlight the matching tab, or "More" for drawer-only tabs @@ -2916,6 +3067,17 @@ initKeyBadges(); }, 100); } + + // T2: Load draft when switching to manual mode + if (mode === 'manual') { + setTimeout(() => { + if (!loadDraft()) { + // No draft - just ensure categories are populated + populateCategorySelect('manCatSelect'); + } + initKeyBadges(); + }, 100); + } } // ── Shared: Load settings dropdowns ───────────────────────────────────── @@ -4429,6 +4591,82 @@ // Do NOT call switchTab here — the caller handles the tab transition } + // ── Navigate page asset search ──────────────────────────────────────── + let navSearchTimer = null; + async function searchNavAssets() { + clearTimeout(navSearchTimer); + navSearchTimer = setTimeout(async () => { + const q = document.getElementById('navSearchInput').value.trim(); + const results = document.getElementById('navSearchResults'); + const clearBtn = document.getElementById('clearNavSearch'); + if (clearBtn) clearBtn.style.display = q ? '' : 'none'; + if (!q) { results.innerHTML = ''; return; } + try { + const assets = await api('/api/assets?q=' + encodeURIComponent(q) + '&limit=8'); + if (!assets.length) { + results.innerHTML = '
No assets found
'; + return; + } + results.innerHTML = assets.map(a => { + const hasLocation = a.latitude != null && a.longitude != null; + return `
+
📍
+
+
${esc(a.name)}
+
${esc(a.machine_id || '')} ${hasLocation ? '📍 ' + esc(a.address || 'GPS tagged') : '⚠ No location'}
+
+
+
`; + }).join(''); + } catch (e) { + results.innerHTML = '
Search failed
'; + } + }, 300); + } + + function clearNavSearch() { + const input = document.getElementById('navSearchInput'); + input.value = ''; + document.getElementById('navSearchResults').innerHTML = ''; + document.getElementById('clearNavSearch').style.display = 'none'; + input.focus(); + } + + async function navToSearchedAsset(assetId) { + // Fetch asset details to get navigation data + if (!AppState.gpsLat || !AppState.gpsLng) { + showToast('GPS location not available. Tap ◎ My Location first.', true); + return; + } + try { + const nav = await api('/api/assets/' + assetId + + '/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng); + AppState._navData = nav; + renderNavPage(); + // Clear search + document.getElementById('navSearchInput').value = ''; + document.getElementById('navSearchResults').innerHTML = ''; + document.getElementById('clearNavSearch').style.display = 'none'; + } catch (e) { + showToast(e.message, true); + } + } + + async function switchNavMode(mode) { + // Re-fetch navigation with new mode + const nav = AppState._navData; + if (!nav) return; + try { + const newNav = await api('/api/assets/' + nav.asset_id + + '/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng + '&mode=' + mode); + AppState._navData = newNav; + renderNavPage(); + } catch (e) { + showToast(e.message, true); + } + } + function clearRouteLine() { if (!map) return; if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }