fix: GPS chip visibility and Leaflet Draw geofencing

- Removed hardcoded display:none from GPS center chip on map tab
- Chip now shows when GPS position is obtained in startVisitTracking
- Added leaflet-draw CSS and JS to HTML head
- Added draw control (polygon, rectangle, circle) to map init
- Emits geofenceDrawn custom event for external hooks
This commit is contained in:
2026-05-22 11:50:44 -04:00
parent f9f6d9d5ce
commit 72003e2742
5 changed files with 455 additions and 124 deletions
+146 -38
View File
@@ -8,7 +8,9 @@
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
<!-- Leaflet Map -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
<script src="https://cdn.jsdelivr.net/npm/exifr@7/dist/lite.umd.js"></script>
<style>
@@ -862,6 +864,11 @@
<!-- Check-in form (shown when barcode found) -->
<div id="checkinCard" class="card" style="display:none;">
<div class="card-title">Check In</div>
<div id="checkinGpsRow" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;">
<span id="checkinGpsStatus" class="gps-badge waiting" style="margin:0;">📍 Tap for GPS</span>
<button class="btn btn-sm btn-outline" onclick="captureCheckinGps()" style="white-space:nowrap;">Capture GPS</button>
<span id="checkinGpsCoords" style="color:var(--text3);font-family:monospace;font-size:11px;display:none;"></span>
</div>
<textarea id="checkinNotes" class="input-field" placeholder="Notes (optional)" rows="2"></textarea>
<div class="form-row">
<button class="btn btn-green" onclick="submitCheckin()" style="flex:1;">✓ Check In</button>
@@ -1165,7 +1172,7 @@
<div id="tabMap" class="tab-panel">
<div class="map-controls">
<span class="map-chip" id="chipHeat" onclick="toggleHeatmap()">🔥 Heatmap</span>
<span class="map-chip" id="chipGeoGPS" onclick="if(navigator.geolocation)navigator.geolocation.getCurrentPosition(p=>{map.setView([p.coords.latitude,p.coords.longitude],15)})" style="display:none;">◎ My GPS</span>
<span class="map-chip" id="chipGeoGPS" onclick="if(navigator.geolocation)navigator.geolocation.getCurrentPosition(p=>{map.setView([p.coords.latitude,p.coords.longitude],15)})">◎ My GPS</span>
</div>
<div id="mapContainer"></div>
<!-- Auto-visit status indicator -->
@@ -1507,17 +1514,18 @@
badge.style.cursor = 'default';
return;
}
// Show a tappable hint so user knows to interact
badge.className = 'gps-badge waiting';
badge.textContent = '📍 Tap for GPS';
// Always tappable — never one-shot, user can re-capture anytime
updateGpsBadge();
badge.style.cursor = 'pointer';
badge.onclick = function() {
badge.textContent = '📍 Locating...';
badge.onclick = null; // one-shot
requestLocation().catch(() => {
badge.onclick = function() { requestLocation().catch(() => {}); };
badge.style.cursor = 'pointer';
requestLocation().then(() => {
updateGpsBadge();
// Re-enable for next re-capture
badge.onclick = arguments.callee;
}).catch(() => {
badge.textContent = '📍 Tap to retry';
badge.onclick = arguments.callee;
});
};
// Also try silently once in case browser allows it without gesture
@@ -1526,6 +1534,17 @@
});
}
function updateGpsBadge() {
const badge = document.getElementById('gpsBadge');
if (AppState.gpsLat != null) {
badge.className = 'gps-badge ok';
badge.textContent = `📍 ${AppState.gpsLat.toFixed(4)}, ${AppState.gpsLng.toFixed(4)}`;
} else {
badge.className = 'gps-badge waiting';
badge.textContent = '📍 Tap for GPS';
}
}
// =========================================================================
// TOAST
// =========================================================================
@@ -1648,6 +1667,21 @@
zoomControl: true,
});
L.tileLayer(tileUrl, { attribution: tileAttr, maxZoom: 19 }).addTo(map);
// Geofence drawing — only if L.Draw loaded
if (typeof L.Draw !== 'undefined') {
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
edit: { featureGroup: drawnItems },
draw: { polygon: true, polyline: false, rectangle: true, circle: true, marker: false, circlemarker: false }
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function(e) {
drawnItems.addLayer(e.layer);
// Dispatch event for any external hooks
document.dispatchEvent(new CustomEvent('geofenceDrawn', { detail: { layer: e.layerType, coords: e.layer.toGeoJSON() } }));
});
}
// Restore heatmap layer if it was active
if (heatVisible) setTimeout(() => loadHeatmapData(), 300);
}
@@ -2184,7 +2218,6 @@
try {
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
autoCheckin(asset.id);
showScannedAsset(asset);
} catch (e) {
if (e.message === 'Asset not found' || e.message.includes('404')) {
@@ -2201,6 +2234,7 @@
AppState.currentAssetId = asset.id;
setScanStatus('Asset found!', 'success');
const hasGps = AppState.gpsLat != null;
const el = document.getElementById('scanResult');
el.style.display = 'block';
el.innerHTML = `
@@ -2210,10 +2244,43 @@
<span class="status-tag ${asset.status}">${asset.status || 'active'}</span>
</div>
<div class="sr-actions">
<span class="sr-checkin-done">✓ Auto checked in</span>
<span class="sr-checkin-done" style="color:${hasGps ? '#22c55e' : 'var(--amber)'}">
${hasGps ? '✓ GPS captured' : '📍 GPS needed'}
</span>
<button class="btn btn-outline btn-sm" onclick="openCheckinForAsset(${asset.id});" style="flex:1;">
${hasGps ? 'Add Notes' : 'Check In'}
</button>
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
</div>
`;
// If we have GPS, auto-checkin silently
if (hasGps) {
doAutoCheckin(asset.id);
}
}
async function doAutoCheckin(assetId) {
try {
await api('/api/checkins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
asset_id: assetId,
latitude: AppState.gpsLat,
longitude: AppState.gpsLng,
accuracy: AppState.gpsAcc,
notes: 'Auto check-in on scan',
}),
});
} catch (e) {
console.warn('Auto check-in failed:', e);
}
}
function openCheckinForAsset(assetId) {
AppState.currentAssetId = assetId;
showCheckinForm();
}
function showNewAssetForm(machineId) {
@@ -2234,7 +2301,57 @@
function showCheckinForm() {
document.getElementById('checkinNotes').value = '';
document.getElementById('checkinCard').style.display = 'block';
document.getElementById('checkinNotes').focus();
updateCheckinGpsDisplay();
}
function updateCheckinGpsDisplay() {
const status = document.getElementById('checkinGpsStatus');
const coords = document.getElementById('checkinGpsCoords');
if (AppState.gpsLat != null) {
status.className = 'gps-badge ok';
status.textContent = '📍 GPS locked';
coords.textContent = `${AppState.gpsLat.toFixed(4)}, ${AppState.gpsLng.toFixed(4)}${Math.round(AppState.gpsAcc || 0)}m)`;
coords.style.display = 'inline';
} else {
status.className = 'gps-badge waiting';
status.textContent = '📍 Tap for GPS';
coords.style.display = 'none';
}
}
function captureCheckinGps() {
const status = document.getElementById('checkinGpsStatus');
status.textContent = '📍 Locating...';
status.className = 'gps-badge waiting';
if (!navigator.geolocation) {
status.className = 'gps-badge err';
status.textContent = '📍 Unavailable';
showToast('GPS not available on this device', true);
return;
}
navigator.geolocation.getCurrentPosition(
pos => {
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
updateCheckinGpsDisplay();
updateGpsBadge();
showToast('📍 GPS location captured');
},
err => {
status.className = 'gps-badge err';
if (err.code === 1) {
status.textContent = '📍 Permission denied — tap to retry';
showToast('Please allow GPS access in your browser settings', true);
} else {
status.textContent = '📍 ' + err.message;
showToast('GPS error: ' + err.message, true);
}
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }
);
}
async function createScannedAsset() {
@@ -2269,7 +2386,6 @@
showToast('Asset created!');
document.getElementById('newAssetCard').style.display = 'none';
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
showScannedAsset(asset);
} catch (e) {
showToast(e.message, true);
@@ -2281,6 +2397,12 @@
showToast('No asset selected', true);
return;
}
// If GPS is missing, warn but still let them check in
if (AppState.gpsLat == null) {
if (!confirm('No GPS location captured. Check in without location data? Press Cancel to go back and tap "Capture GPS" first.')) {
return;
}
}
const payload = {
asset_id: AppState.currentAssetId,
latitude: AppState.gpsLat,
@@ -2303,28 +2425,6 @@
}
}
// ── Auto check-in on asset creation (GPS grab) ──────────────────────────
async function autoCheckin(assetId) {
if (!AppState.gpsLat) return; // No GPS — skip silently
try {
await api('/api/checkins', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
asset_id: assetId,
latitude: AppState.gpsLat,
longitude: AppState.gpsLng,
accuracy: AppState.gpsAcc,
notes: 'Auto check-in on creation',
}),
});
// Don't show toast — just silently grab the location
} catch (e) {
// GPS check-in is best-effort; never block creation for it
console.warn('Auto check-in failed:', e);
}
}
// ── Reverse geocode GPS → address (Nominatim) ─────────────────────────
async function reverseGeocode(lat, lng) {
if (!lat || !lng) return null;
@@ -2598,9 +2698,8 @@
document.getElementById('ocrResult').style.display = 'none';
setOcrStatus('Ready — take another photo', 'success');
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
// Show check-in option
document.getElementById('checkinCard').style.display = 'block';
doAutoCheckin(asset.id);
showCheckinForm();
} catch (e) {
showToast(e.message, true);
}
@@ -2761,7 +2860,7 @@
});
showToast('Asset created!');
AppState.currentAssetId = asset.id;
autoCheckin(asset.id);
doAutoCheckin(asset.id);
if (addAnother) {
// Clear form
@@ -3691,6 +3790,9 @@
// AUTO-VISIT LOGGING (Client-side GPS tracking)
// ═══════════════════════════════════════════════════════════════════════
let geoWatchId = null;
let visitTimers = {};
function startVisitTracking() {
if (!navigator.geolocation) return;
if (geoWatchId !== null) return; // already tracking
@@ -3701,6 +3803,9 @@
const lng = pos.coords.longitude;
AppState.gpsLat = lat;
AppState.gpsLng = lng;
// Show GPS center chip on map
const chip = document.getElementById('chipGeoGPS');
if (chip) chip.style.display = '';
checkProximityToAssets(lat, lng);
},
err => { /* silently fail — GPS permission may change */ },
@@ -3978,7 +4083,10 @@
}
// Boot auth at page load
document.addEventListener('DOMContentLoaded', () => { initAuth(); });
document.addEventListener('DOMContentLoaded', () => {
initAuth();
initGPS();
});
</script>
</body>
+1 -1
View File
@@ -3,7 +3,7 @@
// Caches app shell + CDN deps. Network-first for API, cache fallback for static.
// ═══════════════════════════════════════════════════════════════════════════
const CACHE_NAME = 'canteen-v2';
const CACHE_NAME = 'canteen-v3';
// App shell — core resources needed to boot the PWA
const APP_SHELL = [