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:
+151
-82
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user