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 **Current:** `building_number` and `trailer_number` are separate fields
**Target:** When building_number is entered, auto-populate trailer_number with same value **Target:** When building_number is entered, auto-populate trailer_number with same value
**Files:** **Files:**
- `static/index.html` — manual form event listeners, auto-checkin logic - `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 re
import secrets import secrets
import sqlite3 import sqlite3
import urllib.error
import urllib.request
import uuid import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path 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); } } @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-name { font-size: 17px; font-weight: 700; }
.scan-result .sr-meta { font-size: 12px; color: var(--text2); margin-top: 2px; } .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) MODE TOGGLES (Add Asset tab)
@@ -2717,12 +2718,10 @@
<span class="status-tag ${asset.status}">${asset.status || 'active'}</span> <span class="status-tag ${asset.status}">${asset.status || 'active'}</span>
</div> </div>
<div class="sr-actions"> <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> <button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
</div> </div>
`; `;
document.getElementById('checkinCard').style.display = 'block';
} }
function showNewAssetForm(machineId) { function showNewAssetForm(machineId) {
@@ -3085,6 +3084,18 @@
name: name, name: name,
serial_number: document.getElementById('manSerialNumber').value.trim(), serial_number: document.getElementById('manSerialNumber').value.trim(),
description: document.getElementById('manDescription').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', category: document.getElementById('manCatSelect').value || 'Other',
make: makeName, make: makeName,
model: modelName, model: modelName,
@@ -3493,9 +3504,148 @@
if (!AppState.currentAssetId) return; if (!AppState.currentAssetId) return;
api('/api/assets/' + AppState.currentAssetId).then(a => { api('/api/assets/' + AppState.currentAssetId).then(a => {
if (a.map_link) window.open(a.map_link, '_blank'); 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(() => {}); }).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() { async function quickCheckin() {
if (!AppState.currentAssetId) return; if (!AppState.currentAssetId) return;
const payload = { const payload = {