Refactor: dedicated navigation page (tabNavigate)

- New tabNavigate panel with its own map, route info, walking directions, Google Maps link
- Added to bottom nav bar (🧭 Nav) and drawer nav (🧭 Navigate)
- navigateToAsset() now switches to tabNavigate instead of cluttering the map tab
- Back button returns to previous tab
- Auto-cleanup navMap when leaving the tab
This commit is contained in:
2026-05-20 19:24:54 -04:00
parent ffe9090668
commit dd02fb36d1
+151 -82
View File
@@ -1103,6 +1103,9 @@
<span class="dn-icon">⚙️</span> Settings
</button>
<div style="border-top:1px solid var(--border);margin:6px 16px;"></div>
<button class="dn-item" data-tab="tabNavigate" onclick="navFromDrawer('tabNavigate')">
<span class="dn-icon">🧭</span> Navigate
</button>
<button class="dn-item" id="logoutBtn" onclick="doLogout()" style="display:none;">
<span class="dn-icon">🚪</span> Logout
</button>
@@ -1465,6 +1468,42 @@
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════
TAB: NAVIGATE
═══════════════════════════════════════════════════════════════════════ -->
<div id="tabNavigate" class="tab-panel">
<div class="detail-header">
<button class="back-btn" onclick="closeNavigate()">← Back</button>
<div class="dh-info">
<div id="navDestName" style="font-size:18px;font-weight:700;">Navigate</div>
<div id="navDestAddr" style="font-size:12px;color:var(--text2);"></div>
</div>
</div>
<div id="navMapContainer" style="height:280px;border-radius:12px;overflow:hidden;margin-bottom:12px;"></div>
<div class="card" id="navInfoCard" style="display:none;">
<div class="card-title">📏 Route Info</div>
<div style="display:flex;gap:12px;flex-wrap:wrap;">
<div style="flex:1;min-width:100px;">
<div style="font-size:11px;color:var(--text3);">Distance</div>
<div id="navDistance" style="font-size:20px;font-weight:700;"></div>
</div>
<div style="flex:1;min-width:100px;">
<div style="font-size:11px;color:var(--text3);">Direction</div>
<div id="navCardinal" style="font-size:20px;font-weight:700;"></div>
</div>
<div style="flex:1;min-width:100px;">
<div style="font-size:11px;color:var(--text3);">From You</div>
<div id="navBearing" style="font-size:20px;font-weight:700;"></div>
</div>
</div>
</div>
<div id="navDirectionsCard" class="card" style="display:none;">
<div class="card-title">🚶 Walking Directions</div>
<div id="navDirections" style="font-size:13px;color:var(--text2);line-height:1.6;white-space:pre-wrap;"></div>
</div>
<a id="navGoogleMapsLink" href="#" target="_blank" rel="noopener" class="btn btn-primary" style="display:none;text-decoration:none;text-align:center;margin-top:8px;">🧭 Open in Google Maps</a>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════
TAB: CUSTOMERS & LOCATIONS
═══════════════════════════════════════════════════════════════════════ -->
@@ -1824,6 +1863,9 @@
<button class="tab-btn" data-tab="tabMap" onclick="switchTab('tabMap')">
<span class="tab-icon">🗺️</span> Map
</button>
<button class="tab-btn" data-tab="tabNavigate" onclick="switchTab('tabNavigate')">
<span class="tab-icon">🧭</span> Nav
</button>
<button class="tab-btn" data-tab="tabDashboard" onclick="switchTab('tabDashboard')">
<span class="tab-icon">📊</span> Dash
</button>
@@ -2297,6 +2339,14 @@
// TAB SWITCHING
// =========================================================================
function switchTab(tabId) {
// Track previous tab for back navigation
if (AppState.currentTab !== tabId && AppState.currentTab !== 'tabNavigate') {
AppState._prevTab = AppState.currentTab;
}
// Cleanup nav page when leaving
if (AppState.currentTab === 'tabNavigate' && tabId !== 'tabNavigate') {
closeNavigate();
}
AppState.currentTab = tabId;
// Update tab panels
@@ -2309,6 +2359,7 @@
tabAddAsset: 'tabAddAsset',
tabAssets: 'tabAssets',
tabMap: 'tabMap',
tabNavigate: 'tabNavigate',
tabDashboard: 'tabDashboard',
};
const bottomTab = tabMap[tabId] || 'tabMore';
@@ -3528,94 +3579,112 @@
const nav = await api('/api/assets/' + AppState.currentAssetId +
'/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng);
// Switch to map tab
switchTab('tabMap');
// Initialize map if needed
if (!map) { initMap(); }
// Wait for map to be ready
setTimeout(() => {
if (!map) return;
// Clear previous route line
clearRouteLine();
// Draw route line from origin (user GPS) to destination (asset)
const routeCoords = [
[nav.origin_lat, nav.origin_lng],
[nav.asset_lat, nav.asset_lng]
];
map._routeLine = L.polyline(routeCoords, {
color: '#5b6ef7',
weight: 4,
opacity: 0.8,
dashArray: '10, 6',
}).addTo(map);
// Add arrow markers at both ends
const originIcon = L.divIcon({
html: '<div style="width:16px;height:16px;border-radius:50%;background:#5b6ef7;border:2px solid #fff;box-shadow:0 0 8px rgba(91,110,247,0.6);"></div>',
className: '',
iconSize: [16, 16],
iconAnchor: [8, 8],
});
const destIcon = L.divIcon({
html: '<div style="width:20px;height:20px;border-radius:50%;background:#4ade80;border:2px solid #fff;box-shadow:0 0 10px rgba(74,222,128,0.6);font-size:14px;line-height:20px;text-align:center;">📍</div>',
className: '',
iconSize: [20, 20],
iconAnchor: [10, 10],
});
if (map._routeOriginMarker) map.removeLayer(map._routeOriginMarker);
map._routeOriginMarker = L.marker([nav.origin_lat, nav.origin_lng], { icon: originIcon }).addTo(map);
if (map._routeDestMarker) map.removeLayer(map._routeDestMarker);
map._routeDestMarker = L.marker([nav.asset_lat, nav.asset_lng], { icon: destIcon }).addTo(map);
// Distance label at midpoint
const midLat = (nav.origin_lat + nav.asset_lat) / 2;
const midLng = (nav.origin_lng + nav.asset_lng) / 2;
const distText = nav.distance_km >= 1
? nav.distance_km + ' km'
: nav.distance_meters + ' m';
if (map._routeDistLabel) map.removeLayer(map._routeDistLabel);
map._routeDistLabel = L.marker([midLat, midLng], {
icon: L.divIcon({
html: '<div style="background:var(--card);color:var(--text);padding:4px 10px;border-radius:12px;font-size:12px;font-weight:700;white-space:nowrap;border:1px solid var(--border);box-shadow:0 2px 8px rgba(0,0,0,0.3);">' + distText + ' ' + nav.cardinal + '</div>',
className: '',
iconSize: null,
iconAnchor: null,
}),
}).addTo(map);
// Fit bounds to show the whole route
const bounds = L.latLngBounds(routeCoords);
map.fitBounds(bounds, { padding: [60, 60], maxZoom: 16 });
// Show info toast
showToast('📏 ' + distText + ' ' + nav.cardinal + ' to ' + nav.asset_name);
// If asset has address, add popup to destination marker
if (nav.asset_address) {
map._routeDestMarker.bindPopup(
'<div style="min-width:160px;">' +
'<div style="font-weight:700;margin-bottom:4px;">' + esc(nav.asset_name) + '</div>' +
'<div style="font-size:11px;color:var(--text2);margin-bottom:6px;">📍 ' + esc(nav.asset_address) + '</div>' +
'<a href="' + nav.google_maps_url + '" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;padding:6px 12px;border-radius:8px;background:var(--accent);color:#fff;text-decoration:none;">🧭 Open in Google Maps</a>' +
'</div>'
).openPopup();
}
}, 300);
// Store nav data for dedicated page
AppState._navData = nav;
// Switch to dedicated navigate tab
switchTab('tabNavigate');
renderNavPage();
} catch (e) {
showToast(e.message, true);
}
}
let navMap = null;
function initNavMap(lat, lng) {
if (navMap) { navMap.remove(); navMap = null; }
const container = document.getElementById('navMapContainer');
navMap = L.map(container, {
attributionControl: false,
zoomControl: false,
dragging: true,
tap: true,
}).setView([lat, lng], 15);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
}).addTo(navMap);
// Invalidate size after container is visible
setTimeout(() => { if (navMap) navMap.invalidateSize(); }, 100);
}
function renderNavPage() {
const nav = AppState._navData;
if (!nav) return;
// Set header info
document.getElementById('navDestName').textContent = nav.asset_name || 'Navigate';
document.getElementById('navDestAddr').textContent = nav.asset_address || '';
// Init dedicated map centered on route midpoint
const midLat = (nav.origin_lat + nav.asset_lat) / 2;
const midLng = (nav.origin_lng + nav.asset_lng) / 2;
initNavMap(midLat, midLng);
// Give map time to init, then draw
setTimeout(() => {
if (!navMap) return;
// Origin (user) marker — blue
const userIcon = L.divIcon({
html: '<div style="width:14px;height:14px;border-radius:50%;background:#5b6ef7;border:2px solid #fff;box-shadow:0 0 6px rgba(91,110,247,0.6);"></div>',
className: '', iconSize: [14, 14], iconAnchor: [7, 7],
});
L.marker([nav.origin_lat, nav.origin_lng], { icon: userIcon })
.addTo(navMap).bindPopup('<b>You are here</b>');
// Destination marker — green pin
const destIcon = L.divIcon({
html: '<div style="width:20px;height:20px;border-radius:50%;background:#4ade80;border:2px solid #fff;box-shadow:0 0 10px rgba(74,222,128,0.6);font-size:14px;line-height:20px;text-align:center;">📍</div>',
className: '', iconSize: [20, 20], iconAnchor: [10, 10],
});
const destMarker = L.marker([nav.asset_lat, nav.asset_lng], { icon: destIcon }).addTo(navMap);
if (nav.asset_address) {
destMarker.bindPopup('<div style="min-width:140px;"><b>' + esc(nav.asset_name) + '</b><br><span style="font-size:11px;color:var(--text2);">📍 ' + esc(nav.asset_address) + '</span></div>');
}
// Route line
L.polyline([[nav.origin_lat, nav.origin_lng], [nav.asset_lat, nav.asset_lng]], {
color: '#5b6ef7', weight: 4, opacity: 0.8, dashArray: '10, 6',
}).addTo(navMap);
// Fit bounds
navMap.fitBounds([[nav.origin_lat, nav.origin_lng], [nav.asset_lat, nav.asset_lng]], { padding: [40, 40], maxZoom: 16 });
}, 200);
// Populate info card
const distText = nav.distance_km >= 1
? nav.distance_km + ' km' : nav.distance_meters + ' m';
document.getElementById('navDistance').textContent = distText;
document.getElementById('navCardinal').textContent = nav.cardinal || '—';
document.getElementById('navBearing').textContent = nav.bearing ? nav.bearing + '°' : '—';
document.getElementById('navInfoCard').style.display = '';
// Walking directions
if (nav.walking_directions) {
document.getElementById('navDirections').textContent = nav.walking_directions;
document.getElementById('navDirectionsCard').style.display = '';
} else {
document.getElementById('navDirectionsCard').style.display = 'none';
}
// Google Maps link
if (nav.google_maps_url) {
const link = document.getElementById('navGoogleMapsLink');
link.href = nav.google_maps_url;
link.style.display = '';
} else {
document.getElementById('navGoogleMapsLink').style.display = 'none';
}
}
function closeNavigate() {
if (navMap) { navMap.remove(); navMap = null; }
AppState._navData = null;
// Go back to previous tab or asset list
switchTab(AppState._prevTab || 'tabAssets');
}
function clearRouteLine() {
if (!map) return;
if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }