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:
+146
-38
@@ -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
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user