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
|
<span class="dn-icon">⚙️</span> Settings
|
||||||
</button>
|
</button>
|
||||||
<div style="border-top:1px solid var(--border);margin:6px 16px;"></div>
|
<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;">
|
<button class="dn-item" id="logoutBtn" onclick="doLogout()" style="display:none;">
|
||||||
<span class="dn-icon">🚪</span> Logout
|
<span class="dn-icon">🚪</span> Logout
|
||||||
</button>
|
</button>
|
||||||
@@ -1465,6 +1468,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</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
|
TAB: CUSTOMERS & LOCATIONS
|
||||||
═══════════════════════════════════════════════════════════════════════ -->
|
═══════════════════════════════════════════════════════════════════════ -->
|
||||||
@@ -1824,6 +1863,9 @@
|
|||||||
<button class="tab-btn" data-tab="tabMap" onclick="switchTab('tabMap')">
|
<button class="tab-btn" data-tab="tabMap" onclick="switchTab('tabMap')">
|
||||||
<span class="tab-icon">🗺️</span> Map
|
<span class="tab-icon">🗺️</span> Map
|
||||||
</button>
|
</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')">
|
<button class="tab-btn" data-tab="tabDashboard" onclick="switchTab('tabDashboard')">
|
||||||
<span class="tab-icon">📊</span> Dash
|
<span class="tab-icon">📊</span> Dash
|
||||||
</button>
|
</button>
|
||||||
@@ -2297,6 +2339,14 @@
|
|||||||
// TAB SWITCHING
|
// TAB SWITCHING
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
function switchTab(tabId) {
|
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;
|
AppState.currentTab = tabId;
|
||||||
|
|
||||||
// Update tab panels
|
// Update tab panels
|
||||||
@@ -2309,6 +2359,7 @@
|
|||||||
tabAddAsset: 'tabAddAsset',
|
tabAddAsset: 'tabAddAsset',
|
||||||
tabAssets: 'tabAssets',
|
tabAssets: 'tabAssets',
|
||||||
tabMap: 'tabMap',
|
tabMap: 'tabMap',
|
||||||
|
tabNavigate: 'tabNavigate',
|
||||||
tabDashboard: 'tabDashboard',
|
tabDashboard: 'tabDashboard',
|
||||||
};
|
};
|
||||||
const bottomTab = tabMap[tabId] || 'tabMore';
|
const bottomTab = tabMap[tabId] || 'tabMore';
|
||||||
@@ -3528,94 +3579,112 @@
|
|||||||
const nav = await api('/api/assets/' + AppState.currentAssetId +
|
const nav = await api('/api/assets/' + AppState.currentAssetId +
|
||||||
'/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng);
|
'/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng);
|
||||||
|
|
||||||
// Switch to map tab
|
// Store nav data for dedicated page
|
||||||
switchTab('tabMap');
|
AppState._navData = nav;
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
|
// Switch to dedicated navigate tab
|
||||||
|
switchTab('tabNavigate');
|
||||||
|
renderNavPage();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showToast(e.message, true);
|
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() {
|
function clearRouteLine() {
|
||||||
if (!map) return;
|
if (!map) return;
|
||||||
if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }
|
if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }
|
||||||
|
|||||||
Reference in New Issue
Block a user