Add GPS Map Editor to asset detail view
Add a GPS map editor card at the top of asset details, allowing users to manually set an asset's GPS coordinates via an interactive Leaflet map. - GPS card with Leaflet map and draggable marker - 🔒 Lock/unlock toggle (locked by default when GPS exists) - 💾 Save GPS button (visible only when unlocked) - Saves via PUT /api/assets/{id} with lat/lng - Click on map or drag marker to set position - Fallback to Orlando coords (28.5383, -81.3792) when no GPS - Auto-refresh detail view after save Closes #61
This commit is contained in:
@@ -1020,6 +1020,21 @@
|
||||
.no-print { display: none !important; }
|
||||
.stop-num { background: #333 !important; }
|
||||
}
|
||||
|
||||
/* GPS Map Editor */
|
||||
.gps-lock-btn {
|
||||
font-size: 16px; cursor: pointer; user-select: none;
|
||||
padding: 2px 6px; border-radius: 4px; transition: background 0.15s;
|
||||
float: right; line-height: 1;
|
||||
}
|
||||
.gps-lock-btn:hover { background: rgba(255,255,255,0.08); }
|
||||
.gps-lock-btn.unlocked { background: var(--amber-bg); border-radius: 4px; }
|
||||
#detailGpsMap {
|
||||
z-index: 1; overflow: hidden;
|
||||
}
|
||||
#detailGpsMap .leaflet-container {
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1267,6 +1282,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPS Map Editor -->
|
||||
<div class="card" id="detailGpsCard" style="display:none;">
|
||||
<div class="card-title">
|
||||
📍 Location
|
||||
<span id="detailGpsLockBtn" class="gps-lock-btn" title="GPS is locked — click to edit">🔒</span>
|
||||
</div>
|
||||
<div id="detailGpsInfo" style="font-size:12px;color:var(--text2);margin-bottom:6px;"></div>
|
||||
<div id="detailGpsMap" style="height:300px;border-radius:8px;margin-bottom:8px;border:1px solid var(--border);"></div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
<button id="detailGpsSaveBtn" class="btn btn-green btn-sm" onclick="saveGpsCoords()" style="display:none;">💾 Save GPS</button>
|
||||
<span id="detailGpsCoords" style="font-size:12px;color:var(--text2);font-family:monospace;"></span>
|
||||
<span id="detailGpsStatus" style="font-size:11px;color:var(--text3);"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info card -->
|
||||
<div class="card">
|
||||
<div class="card-title">Asset Details</div>
|
||||
@@ -3891,6 +3921,9 @@
|
||||
photoCard.style.display = 'none';
|
||||
}
|
||||
|
||||
// Render GPS Map Editor card (at top of detail view)
|
||||
renderGpsCard(a);
|
||||
|
||||
// Location / Directions
|
||||
const hasDir = a.address || a.building_name || a.building_number || a.floor || a.room || a.trailer_number || a.walking_directions || a.map_link || a.parking_location;
|
||||
const dirCard = document.getElementById('detailDirections');
|
||||
@@ -4053,6 +4086,188 @@
|
||||
return `<div class="detail-field"><div class="df-label">${esc(label)}</div><div class="df-value"${cls}>${value}</div></div>`;
|
||||
}
|
||||
|
||||
// ── GPS Map Editor ──────────────────────────────────────────────────
|
||||
let _gpsState = { map: null, marker: null, unlocked: false, asset: null };
|
||||
|
||||
function renderGpsCard(a) {
|
||||
const card = document.getElementById('detailGpsCard');
|
||||
const mapDiv = document.getElementById('detailGpsMap');
|
||||
const infoEl = document.getElementById('detailGpsInfo');
|
||||
const coordsEl = document.getElementById('detailGpsCoords');
|
||||
const lockBtn = document.getElementById('detailGpsLockBtn');
|
||||
const saveBtn = document.getElementById('detailGpsSaveBtn');
|
||||
const statusEl = document.getElementById('detailGpsStatus');
|
||||
|
||||
// Clean up previous map instance
|
||||
if (_gpsState.map) {
|
||||
_gpsState.map.remove();
|
||||
_gpsState.map = null;
|
||||
_gpsState.marker = null;
|
||||
}
|
||||
|
||||
_gpsState.asset = a;
|
||||
const hasGps = a.latitude != null && a.longitude != null;
|
||||
|
||||
// If no GPS, show card as unlocked and editable
|
||||
_gpsState.unlocked = !hasGps;
|
||||
card.style.display = 'block';
|
||||
|
||||
lockBtn.onclick = toggleGpsLock;
|
||||
|
||||
if (hasGps) {
|
||||
infoEl.textContent = '📍 GPS coordinates on file — click 🔒 to unlock and edit';
|
||||
lockBtn.textContent = '🔒';
|
||||
lockBtn.className = 'gps-lock-btn';
|
||||
lockBtn.title = 'GPS is locked — click to edit';
|
||||
saveBtn.style.display = 'none';
|
||||
statusEl.textContent = '';
|
||||
} else {
|
||||
infoEl.textContent = '📍 No GPS data — tap the map to set coordinates';
|
||||
lockBtn.textContent = '🔓';
|
||||
lockBtn.className = 'gps-lock-btn unlocked';
|
||||
lockBtn.title = 'Editable — tap lock to disable editing';
|
||||
statusEl.textContent = 'Editable';
|
||||
}
|
||||
|
||||
const defaultLat = hasGps ? a.latitude : 28.5383;
|
||||
const defaultLng = hasGps ? a.longitude : -81.3792;
|
||||
|
||||
// Update coords display
|
||||
coordsEl.textContent = `${defaultLat.toFixed(6)}, ${defaultLng.toFixed(6)}`;
|
||||
|
||||
// Initialize map next tick so the div is in DOM
|
||||
requestAnimationFrame(() => {
|
||||
if (!mapDiv.offsetParent && mapDiv.parentNode) {
|
||||
// Map may not be visible yet — use setTimeout
|
||||
setTimeout(() => initGpsEditorMap(defaultLat, defaultLng, a), 100);
|
||||
} else {
|
||||
initGpsEditorMap(defaultLat, defaultLng, a);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initGpsEditorMap(lat, lng, a) {
|
||||
const mapDiv = document.getElementById('detailGpsMap');
|
||||
if (!mapDiv || mapDiv.offsetHeight === 0) {
|
||||
// Retry if map isn't visible yet (hidden tab, not yet scrolled)
|
||||
setTimeout(() => initGpsEditorMap(lat, lng, a), 300);
|
||||
return;
|
||||
}
|
||||
|
||||
const map = L.map(mapDiv, {
|
||||
zoomControl: true,
|
||||
attributionControl: false,
|
||||
}).setView([lat, lng], 15);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
}).addTo(map);
|
||||
|
||||
const marker = L.marker([lat, lng], {
|
||||
draggable: _gpsState.unlocked,
|
||||
}).addTo(map);
|
||||
|
||||
marker.on('dragend', function() {
|
||||
const pos = marker.getLatLng();
|
||||
const latRounded = Math.round(pos.lat * 1000000) / 1000000;
|
||||
const lngRounded = Math.round(pos.lng * 1000000) / 1000000;
|
||||
marker.setLatLng([latRounded, lngRounded]);
|
||||
document.getElementById('detailGpsCoords').textContent =
|
||||
`${latRounded.toFixed(6)}, ${lngRounded.toFixed(6)}`;
|
||||
});
|
||||
|
||||
// When map is clicked (not dragging), move marker there
|
||||
map.on('click', function(e) {
|
||||
if (!_gpsState.unlocked) return;
|
||||
const latRounded = Math.round(e.latlng.lat * 1000000) / 1000000;
|
||||
const lngRounded = Math.round(e.latlng.lng * 1000000) / 1000000;
|
||||
marker.setLatLng([latRounded, lngRounded]);
|
||||
document.getElementById('detailGpsCoords').textContent =
|
||||
`${latRounded.toFixed(6)}, ${lngRounded.toFixed(6)}`;
|
||||
});
|
||||
|
||||
_gpsState.map = map;
|
||||
_gpsState.marker = marker;
|
||||
|
||||
// Invalidate size after a short delay to fix rendering in hidden containers
|
||||
setTimeout(() => map.invalidateSize(), 350);
|
||||
}
|
||||
|
||||
function toggleGpsLock() {
|
||||
const lockBtn = document.getElementById('detailGpsLockBtn');
|
||||
const saveBtn = document.getElementById('detailGpsSaveBtn');
|
||||
const statusEl = document.getElementById('detailGpsStatus');
|
||||
const infoEl = document.getElementById('detailGpsInfo');
|
||||
|
||||
_gpsState.unlocked = !_gpsState.unlocked;
|
||||
|
||||
if (_gpsState.unlocked) {
|
||||
lockBtn.textContent = '🔓';
|
||||
lockBtn.className = 'gps-lock-btn unlocked';
|
||||
lockBtn.title = 'Click to lock GPS';
|
||||
saveBtn.style.display = '';
|
||||
statusEl.textContent = 'Editable — drag the marker or tap the map';
|
||||
infoEl.textContent = '📍 Drag the marker or click the map to set GPS position';
|
||||
} else {
|
||||
lockBtn.textContent = '🔒';
|
||||
lockBtn.className = 'gps-lock-btn';
|
||||
lockBtn.title = 'GPS is locked — click to edit';
|
||||
saveBtn.style.display = 'none';
|
||||
statusEl.textContent = 'Locked';
|
||||
infoEl.textContent = '📍 GPS position is locked — click 🔒 to edit';
|
||||
}
|
||||
|
||||
// Update marker draggability
|
||||
if (_gpsState.marker) {
|
||||
_gpsState.marker.dragging[_gpsState.unlocked ? 'enable' : 'disable']();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGpsCoords() {
|
||||
if (!_gpsState.marker || !_gpsState.asset) return;
|
||||
const pos = _gpsState.marker.getLatLng();
|
||||
const lat = Math.round(pos.lat * 1000000) / 1000000;
|
||||
const lng = Math.round(pos.lng * 1000000) / 1000000;
|
||||
|
||||
const saveBtn = document.getElementById('detailGpsSaveBtn');
|
||||
saveBtn.textContent = '⏳ Saving...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
try {
|
||||
await api('/api/assets/' + _gpsState.asset.id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ latitude: lat, longitude: lng }),
|
||||
});
|
||||
showToast('✅ GPS coordinates saved!', false);
|
||||
|
||||
// Update asset data and re-lock
|
||||
_gpsState.asset.latitude = lat;
|
||||
_gpsState.asset.longitude = lng;
|
||||
|
||||
// Re-lock
|
||||
_gpsState.unlocked = false;
|
||||
const lockBtn = document.getElementById('detailGpsLockBtn');
|
||||
lockBtn.textContent = '🔒';
|
||||
lockBtn.className = 'gps-lock-btn';
|
||||
lockBtn.title = 'GPS is locked — click to edit';
|
||||
document.getElementById('detailGpsSaveBtn').style.display = 'none';
|
||||
document.getElementById('detailGpsStatus').textContent = 'Locked';
|
||||
document.getElementById('detailGpsInfo').textContent = '📍 GPS coordinates saved';
|
||||
if (_gpsState.marker) {
|
||||
_gpsState.marker.dragging.disable();
|
||||
}
|
||||
|
||||
// Refresh detail to show updated data
|
||||
setTimeout(() => viewAsset(_gpsState.asset.id), 1200);
|
||||
} catch (e) {
|
||||
showToast('Failed to save GPS: ' + e.message, true);
|
||||
} finally {
|
||||
saveBtn.textContent = '💾 Save GPS';
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
return '';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user