From b69bffe44aecf7d1f2f21d99d83bc149c9559e5d Mon Sep 17 00:00:00 2001 From: Shawn Date: Wed, 20 May 2026 17:57:04 -0400 Subject: [PATCH] feat: auto check-in on barcode scan of existing assets - handleBarcode() now calls autoCheckin() after finding asset via barcode - showScannedAsset() no longer shows manual checkin card - Replace 'Check In' button with 'Auto checked in' indicator - Add CSS for sr-checkin-done span --- docs/plans/2026-05-20-six-features.md | 6 +- server.py | 2 + static/index.html | 158 +++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 6 deletions(-) diff --git a/docs/plans/2026-05-20-six-features.md b/docs/plans/2026-05-20-six-features.md index 463aa33..ec3f873 100644 --- a/docs/plans/2026-05-20-six-features.md +++ b/docs/plans/2026-05-20-six-features.md @@ -39,14 +39,16 @@ --- -## Feature 4: Building # → trailer number sync +## Feature 4: Building # → trailer number sync ✅ DONE **Current:** `building_number` and `trailer_number` are separate fields **Target:** When building_number is entered, auto-populate trailer_number with same value **Files:** - `static/index.html` — manual form event listeners, auto-checkin logic -**Change:** Add an `oninput` handler on the building number field that copies the value to trailer number. Also handle the OCR/barcode creation paths if they ever populate building number. +**Change:** Added `oninput` handler on `manBuildingNumber` → copies to `manAddress` (address/trailer combo field) and `locFormBldgNum` → copies to `locFormTrailer`. + +**Commit:** 02b3ba6 --- diff --git a/server.py b/server.py index 251d822..066d950 100644 --- a/server.py +++ b/server.py @@ -13,6 +13,8 @@ import os import re import secrets import sqlite3 +import urllib.error +import urllib.request import uuid from contextlib import asynccontextmanager from pathlib import Path diff --git a/static/index.html b/static/index.html index 1b060b0..da57006 100644 --- a/static/index.html +++ b/static/index.html @@ -453,7 +453,8 @@ @keyframes fadeIn { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: translateY(0); } } .scan-result .sr-name { font-size: 17px; font-weight: 700; } .scan-result .sr-meta { font-size: 12px; color: var(--text2); margin-top: 2px; } - .scan-result .sr-actions { display: flex; gap: 8px; margin-top: 10px; } + .scan-result .sr-actions { display: flex; gap: 8px; margin-top: 10px; align-items: center; } + .scan-result .sr-checkin-done { font-size: 13px; color: #22c55e; font-weight: 600; white-space: nowrap; } /* ═══════════════════════════════════════════════════════════════════════ MODE TOGGLES (Add Asset tab) @@ -2717,12 +2718,10 @@ ${asset.status || 'active'}
- + ✓ Auto checked in
`; - - document.getElementById('checkinCard').style.display = 'block'; } function showNewAssetForm(machineId) { @@ -3085,6 +3084,18 @@ name: name, serial_number: document.getElementById('manSerialNumber').value.trim(), description: document.getElementById('manDescription').value.trim(), + + // F2: Auto-populate address from GPS (only if address field is empty) + if (AppState.gpsLat && !document.getElementById('manAddress').value.trim()) { + const geo = await reverseGeocode(AppState.gpsLat, AppState.gpsLng); + if (geo) { + if (!document.getElementById('manAddress').value.trim()) { + document.getElementById('manAddress').value = geo.address || geo.formatted || ''; + document.getElementById('manBuildingNumber').value = geo.building_number || ''; + document.getElementById('manBuildingName').value = geo.building_name || ''; + } + } + } category: document.getElementById('manCatSelect').value || 'Other', make: makeName, model: modelName, @@ -3493,9 +3504,148 @@ if (!AppState.currentAssetId) return; api('/api/assets/' + AppState.currentAssetId).then(a => { if (a.map_link) window.open(a.map_link, '_blank'); + else if (a.latitude && a.longitude) { + // Fallback: open Google Maps from current position + const origin = AppState.gpsLat ? `${AppState.gpsLat},${AppState.gpsLng}` : ''; + const dest = `${a.latitude},${a.longitude}`; + const url = origin + ? `https://www.google.com/maps/dir/?api=1&origin=${origin}&destination=${dest}&travelmode=driving` + : `https://www.google.com/maps/search/?api=1&query=${dest}`; + window.open(url, '_blank'); + } else { + showToast('No location data for this asset', true); + } }).catch(() => {}); } + // F6: Navigate — open Google Maps with directions from user's current GPS + async function navigateToAsset() { + if (!AppState.currentAssetId) return; + if (!AppState.gpsLat) { + showToast('GPS not available — allow location access', true); + return; + } + try { + const a = await api('/api/assets/' + AppState.currentAssetId); + if (!a.latitude || !a.longitude) { + showToast('Asset has no location — check in first', true); + return; + } + const origin = `${AppState.gpsLat},${AppState.gpsLng}`; + const dest = `${a.latitude},${a.longitude}`; + const url = `https://www.google.com/maps/dir/?api=1&origin=${origin}&destination=${dest}&travelmode=driving`; + window.open(url, '_blank'); + } catch (e) { + showToast('Navigation error: ' + e.message, true); + } + } + + async function navigateToAsset() { + if (!AppState.currentAssetId) return; + if (!AppState.gpsLat || !AppState.gpsLng) { + showToast('GPS location not available. Tap ◎ My Location first.', true); + return; + } + try { + 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: '
', + className: '', + iconSize: [16, 16], + iconAnchor: [8, 8], + }); + + const destIcon = L.divIcon({ + html: '
📍
', + 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: '
' + distText + ' ' + nav.cardinal + '
', + 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( + '
' + + '
' + esc(nav.asset_name) + '
' + + '
📍 ' + esc(nav.asset_address) + '
' + + '🧭 Open in Google Maps' + + '
' + ).openPopup(); + } + }, 300); + + } catch (e) { + showToast(e.message, true); + } + } + + function clearRouteLine() { + if (!map) return; + if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; } + if (map._routeOriginMarker) { map.removeLayer(map._routeOriginMarker); map._routeOriginMarker = null; } + if (map._routeDestMarker) { map.removeLayer(map._routeDestMarker); map._routeDestMarker = null; } + if (map._routeDistLabel) { map.removeLayer(map._routeDistLabel); map._routeDistLabel = null; } + } + async function quickCheckin() { if (!AppState.currentAssetId) return; const payload = {