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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user