fix: OCR last-5-digits extraction, barcode 1D-only scanning, auto-checkin on create
- OCR: Extract only last 5 digits from Connect ID (XXXXX-XXXXXX → last 5) - Barcode: Switch to decodeFromVideoDevice with 1D-only format hints (Code 128/39/EAN/UPC/ITF/Codabar), TRY_HARDER, 50ms scan interval - Auto check-in: New assets auto-checkin with GPS on creation via all three entry modes (barcode, OCR, manual) - Tests: Updated e2e tests for login wait and API flow
This commit is contained in:
+75
-15
@@ -2568,31 +2568,69 @@
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// BARCODE MODE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
function _barcodeHints() {
|
||||
// Restrict to 1D barcode formats only — much faster and more accurate
|
||||
// than scanning all formats (QR, DataMatrix, PDF417, Aztec, etc.)
|
||||
const hints = new Map();
|
||||
const fmts = [
|
||||
ZXing.BarcodeFormat.CODE_128,
|
||||
ZXing.BarcodeFormat.CODE_39,
|
||||
ZXing.BarcodeFormat.EAN_13,
|
||||
ZXing.BarcodeFormat.EAN_8,
|
||||
ZXing.BarcodeFormat.UPC_A,
|
||||
ZXing.BarcodeFormat.UPC_E,
|
||||
ZXing.BarcodeFormat.CODE_93,
|
||||
ZXing.BarcodeFormat.ITF,
|
||||
ZXing.BarcodeFormat.CODABAR,
|
||||
];
|
||||
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, fmts);
|
||||
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
|
||||
return hints;
|
||||
}
|
||||
|
||||
function _barcodeOptions() {
|
||||
return {
|
||||
delayBetweenScanAttempts: 50, // scan every 50ms (was default ~500ms)
|
||||
delayBetweenScanSuccess: 2000, // 2s cooldown after a successful scan
|
||||
};
|
||||
}
|
||||
|
||||
async function startScanning() {
|
||||
if (addAssetMode !== 'barcode') return;
|
||||
if (scanningActive) return;
|
||||
try {
|
||||
setScanStatus('Starting camera...', 'working');
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
stream = null;
|
||||
|
||||
// Stop any previous reader
|
||||
if (codeReader) {
|
||||
try { codeReader.reset(); } catch (e) { /* ignore */ }
|
||||
codeReader = null;
|
||||
}
|
||||
stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 }, height: { ideal: 1080 } },
|
||||
audio: false,
|
||||
});
|
||||
|
||||
const video = document.getElementById('cameraVideo');
|
||||
if (!video) return;
|
||||
video.srcObject = stream;
|
||||
await video.play();
|
||||
|
||||
// Create reader with 1D-only hints for fast, accurate scanning
|
||||
codeReader = new ZXing.BrowserMultiFormatReader(_barcodeHints(), _barcodeOptions());
|
||||
|
||||
document.getElementById('cameraPlaceholder').style.display = 'none';
|
||||
video.style.display = 'block';
|
||||
scanningActive = true;
|
||||
|
||||
if (!codeReader) codeReader = new ZXing.BrowserMultiFormatReader();
|
||||
// Pick the rear-facing camera (environment-facing on phones)
|
||||
const devices = await ZXing.BrowserMultiFormatReader.listVideoInputDevices();
|
||||
let deviceId = null;
|
||||
if (devices && devices.length > 0) {
|
||||
const rear = devices.find(d =>
|
||||
d.label && /back|rear|environment/i.test(d.label)
|
||||
);
|
||||
deviceId = rear ? rear.deviceId : devices[0].deviceId;
|
||||
}
|
||||
|
||||
setScanStatus('Point camera at a barcode', 'success');
|
||||
|
||||
codeReader.decodeFromVideoElement(video, (result, err) => {
|
||||
// decodeFromVideoDevice handles camera + scanning in one call
|
||||
codeReader.decodeFromVideoDevice(deviceId, video, (result, err) => {
|
||||
if (result && scanningActive) {
|
||||
const val = result.getText();
|
||||
if (val && val !== currentScannedMachineId) {
|
||||
@@ -2612,10 +2650,7 @@
|
||||
scanningActive = false;
|
||||
if (codeReader) {
|
||||
try { codeReader.reset(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
if (stream) {
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
stream = null;
|
||||
codeReader = null;
|
||||
}
|
||||
const video = document.getElementById('cameraVideo');
|
||||
if (video) video.style.display = 'none';
|
||||
@@ -2723,6 +2758,7 @@
|
||||
showToast('Asset created!');
|
||||
document.getElementById('newAssetCard').style.display = 'none';
|
||||
AppState.currentAssetId = asset.id;
|
||||
autoCheckin(asset.id);
|
||||
showScannedAsset(asset);
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
@@ -2756,6 +2792,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
// OCR MODE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
@@ -2870,6 +2928,7 @@
|
||||
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';
|
||||
} catch (e) {
|
||||
@@ -2997,6 +3056,7 @@
|
||||
});
|
||||
showToast('Asset created!');
|
||||
AppState.currentAssetId = asset.id;
|
||||
autoCheckin(asset.id);
|
||||
|
||||
if (addAnother) {
|
||||
// Clear form
|
||||
|
||||
Reference in New Issue
Block a user