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
This commit is contained in:
2026-05-20 17:57:04 -04:00
parent c1020ac702
commit b69bffe44a
3 changed files with 160 additions and 6 deletions
+4 -2
View File
@@ -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
---
+2
View File
@@ -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
+154 -4
View File
@@ -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 @@
<span class="status-tag ${asset.status}">${asset.status || 'active'}</span>
</div>
<div class="sr-actions">
<button class="btn btn-green btn-sm" onclick="showCheckinForm()" style="flex:1;">✓ Check In</button>
<span class="sr-checkin-done">✓ Auto checked in</span>
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
</div>
`;
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: '<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);
} 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 = {