feat: add Scan New Asset buttons to barcode and OCR results
- handleBarcode now shows result in-place via showScannedAsset instead of navigating to Assets tab - showScannedAsset gets a 'Scan New' button that calls resetBarcodeScanner() - resetBarcodeScanner(): clears scan result, restarts barcode camera - lookupOcrAsset gets a 'Scan New' button that calls resetOcrScanner() - resetOcrScanner(): clears OCR result, restarts OCR camera - Retake still works for re-capturing the same sticker
This commit is contained in:
+98
-28
@@ -2826,8 +2826,9 @@
|
||||
// BARCODE MODE
|
||||
// ═══════════════════════════════════════════════════════════════════════
|
||||
function _barcodeHints() {
|
||||
// All formats — 1D (Code 128, EAN, UPC, etc.) + 2D (QR, DataMatrix,
|
||||
// PDF417, Aztec) for serial number stickers with 2D barcodes.
|
||||
// Focused 1D formats for fast scanning on mobile.
|
||||
// Removed TRY_HARDER (too slow on mobile) and 2D formats (QR, DataMatrix,
|
||||
// PDF417, Aztec) which are rare for canteen machine barcodes.
|
||||
const hints = new Map();
|
||||
const fmts = [
|
||||
ZXing.BarcodeFormat.CODE_128,
|
||||
@@ -2836,31 +2837,41 @@
|
||||
ZXing.BarcodeFormat.EAN_8,
|
||||
ZXing.BarcodeFormat.UPC_A,
|
||||
ZXing.BarcodeFormat.UPC_E,
|
||||
ZXing.BarcodeFormat.CODE_93,
|
||||
ZXing.BarcodeFormat.ITF,
|
||||
ZXing.BarcodeFormat.CODABAR,
|
||||
ZXing.BarcodeFormat.QR_CODE,
|
||||
ZXing.BarcodeFormat.DATA_MATRIX,
|
||||
ZXing.BarcodeFormat.PDF_417,
|
||||
ZXing.BarcodeFormat.AZTEC,
|
||||
];
|
||||
hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, fmts);
|
||||
hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
|
||||
return hints;
|
||||
}
|
||||
|
||||
function _barcodeOptions() {
|
||||
return {
|
||||
delayBetweenScanAttempts: 300, // scan every 300ms (was 50ms — too fast
|
||||
// for TRY_HARDER with 13 formats, caused
|
||||
// decoder thrashing and missed barcodes)
|
||||
delayBetweenScanAttempts: 300,
|
||||
delayBetweenScanSuccess: 2000, // 2s cooldown after a successful scan
|
||||
};
|
||||
}
|
||||
|
||||
let zxingLoadAttempted = false;
|
||||
|
||||
function checkZxingLoaded() {
|
||||
if (typeof ZXing !== 'undefined') return true;
|
||||
if (!zxingLoadAttempted) {
|
||||
zxingLoadAttempted = true;
|
||||
console.warn('ZXing library failed to load from CDN');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function startScanning() {
|
||||
if (addAssetMode !== 'barcode') return;
|
||||
if (scanningActive) return;
|
||||
|
||||
// Check ZXing loaded from CDN
|
||||
if (!checkZxingLoaded()) {
|
||||
setScanStatus('Barcode library not loaded — enter ID manually below', 'error');
|
||||
showFallbackCameraError('Barcode scanner library failed to load.<br>Enter the ID manually instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setScanStatus('Starting camera...', 'working');
|
||||
|
||||
@@ -2887,7 +2898,6 @@
|
||||
deviceId = rear ? rear.deviceId : devices[0].deviceId;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: pass null to let decodeFromVideoDevice pick default
|
||||
console.warn('listVideoInputDevices failed, using default camera:', e);
|
||||
}
|
||||
|
||||
@@ -2897,11 +2907,9 @@
|
||||
|
||||
setScanStatus('Point camera at a barcode', 'success');
|
||||
|
||||
// Camera readiness check: if the video never starts streaming
|
||||
// (no media attached within 5s), treat as camera failure
|
||||
// Camera readiness check — give it 10s (was 5s, too tight for some mobile browsers)
|
||||
const cameraFailTimer = setTimeout(() => {
|
||||
if (video.readyState === 0 && scanningActive) {
|
||||
console.warn('Camera stream never started — showing manual fallback');
|
||||
scanningActive = false;
|
||||
video.style.display = 'none';
|
||||
if (codeReader) {
|
||||
@@ -2909,15 +2917,9 @@
|
||||
}
|
||||
handleCameraError(new Error('No camera stream — connect a camera or enter ID manually'));
|
||||
}
|
||||
}, 5000);
|
||||
}, 10000);
|
||||
|
||||
// decodeFromVideoDevice handles camera + scanning in one call.
|
||||
// The callback fires on every video frame (~30fps). ZXing passes
|
||||
// "No MultiFormat Readers..." as an error when no barcode is in view
|
||||
// — that's NORMAL, not a hardware failure. Only treat real device
|
||||
// errors (permission denied, camera not found, stream broken) as fatal.
|
||||
codeReader.decodeFromVideoDevice(deviceId, video, (result, err) => {
|
||||
// Clear the timeout once we get any callback (stream started or error)
|
||||
clearTimeout(cameraFailTimer);
|
||||
if (result && scanningActive) {
|
||||
const val = result.getText();
|
||||
@@ -2925,7 +2927,6 @@
|
||||
handleBarcode(val.trim());
|
||||
}
|
||||
} else if (err && scanningActive) {
|
||||
// Filter out "no barcode in frame" — that's normal, not a failure
|
||||
const msg = (err.message || err || '').toLowerCase();
|
||||
if (!msg.includes('multiformat') && !msg.includes('unable to detect')) {
|
||||
console.error('ZXing camera error:', err);
|
||||
@@ -3036,14 +3037,13 @@
|
||||
|
||||
try {
|
||||
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
|
||||
// Asset exists — go directly to its detail view
|
||||
showToast(`${esc(asset.name)} — showing details`, false);
|
||||
// Asset exists — show result in-place with scan new button
|
||||
showToast(`${esc(asset.name)} — found!`, false);
|
||||
// Auto-checkin if GPS available (background, non-blocking)
|
||||
if (AppState.gpsLat != null) {
|
||||
doAutoCheckin(asset.id);
|
||||
}
|
||||
switchTab('tabAssets');
|
||||
viewAsset(asset.id);
|
||||
showScannedAsset(asset);
|
||||
} catch (e) {
|
||||
if (e.message === 'Asset not found' || e.message.includes('404')) {
|
||||
showNewAssetForm(machineId);
|
||||
@@ -3073,6 +3073,7 @@
|
||||
${hasGps ? '✓ GPS captured' : '📍 GPS needed'}
|
||||
</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="resetBarcodeScanner()" style="flex:1;">Scan New</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3082,6 +3083,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetBarcodeScanner() {
|
||||
// Reset the scan result UI and restart barcode scanner
|
||||
document.getElementById('scanResult').style.display = 'none';
|
||||
document.getElementById('scanResult').innerHTML = '';
|
||||
document.getElementById('checkinCard').style.display = 'none';
|
||||
setScanStatus('Ready — scan another barcode', 'success');
|
||||
stopScanning();
|
||||
// Re-show the camera placeholder
|
||||
const ph = document.getElementById('cameraPlaceholder');
|
||||
if (ph) {
|
||||
ph.style.display = 'block';
|
||||
ph.innerHTML = '<span class="cam-icon">' + gi('📷') + '</span><div class="cam-hint">Tap to start camera</div>';
|
||||
}
|
||||
document.getElementById('cameraVideo').style.display = 'none';
|
||||
const fallback = document.getElementById('manualSearchFallback');
|
||||
if (fallback) fallback.style.display = 'none';
|
||||
scanningActive = false;
|
||||
currentScannedMachineId = null;
|
||||
// Auto-start the camera
|
||||
setTimeout(startScanning, 300);
|
||||
}
|
||||
|
||||
function resetOcrScanner() {
|
||||
// Reset the OCR result UI and restart OCR camera
|
||||
stopOcrCamera();
|
||||
document.getElementById('ocrResult').style.display = 'none';
|
||||
document.getElementById('ocrResult').innerHTML = '';
|
||||
document.getElementById('ocrGpsBadge').style.display = 'none';
|
||||
document.getElementById('ocrCameraArea').style.display = 'block';
|
||||
setOcrStatus('Point camera at the machine sticker and tap capture', 'success');
|
||||
const ph = document.getElementById('ocrPlaceholder');
|
||||
if (ph) {
|
||||
ph.style.display = 'block';
|
||||
ph.innerHTML = '<span class="cam-icon">' + gi('📷') + '</span><div class="cam-hint" style="margin-top:4px;">Tap to start camera</div>';
|
||||
}
|
||||
AppState.ocrGpsSaved = false;
|
||||
// Auto-start the camera
|
||||
setTimeout(startOcrCamera, 300);
|
||||
}
|
||||
|
||||
async function doAutoCheckin(assetId) {
|
||||
try {
|
||||
await api('/api/checkins', {
|
||||
@@ -3318,6 +3359,25 @@
|
||||
const el = document.getElementById('ocrResult');
|
||||
el.style.display = 'block';
|
||||
|
||||
// ── Helper: render matched_assets list ──────────────────────────────
|
||||
const hasMatched = data.matched_assets && data.matched_assets.length > 0;
|
||||
let matchedHtml = '';
|
||||
if (hasMatched) {
|
||||
matchedHtml = '<div style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px;">' +
|
||||
'<div style="font-size:12px;font-weight:600;color:var(--text2);margin-bottom:6px;">📋 DB matches found by serial/ID:</div>';
|
||||
data.matched_assets.forEach(function(m) {
|
||||
matchedHtml += '<div style="background:var(--card2);border-radius:6px;padding:8px;margin-bottom:4px;">' +
|
||||
'<div style="font-weight:600;font-size:14px;">' + esc(m.name || 'Unnamed') + '</div>' +
|
||||
'<div style="font-size:11px;color:var(--text2);margin-top:2px;">' +
|
||||
'ID: ' + esc(m.machine_id) + (m.serial_number ? ' · SN: ' + esc(m.serial_number) : '') +
|
||||
' · matched: <code style="background:var(--accent-bg);padding:1px 4px;border-radius:3px;font-size:10px;">' + esc(m.matched_on) + '</code>' +
|
||||
'</div>' +
|
||||
'<button class="btn btn-outline btn-sm" onclick="switchTab(\'tabAssets\');viewAsset(' + m.asset_id + ');" style="margin-top:4px;width:100%;">View Details</button>' +
|
||||
'</div>';
|
||||
});
|
||||
matchedHtml += '</div>';
|
||||
}
|
||||
|
||||
if (data.machine_id) {
|
||||
const methodBadge = data.method === 'client'
|
||||
? '<span class="offline-badge">📡 Offline OCR</span>'
|
||||
@@ -3328,12 +3388,21 @@
|
||||
<div class="or-id">${esc(data.machine_id)}${methodBadge}</div>
|
||||
<div class="or-meta">${confLabel}</div>
|
||||
${data.raw_text ? `<div class="or-raw">OCR text: ${esc(data.raw_text)}</div>` : ''}
|
||||
${matchedHtml}
|
||||
<div class="or-searching" style="margin-top:8px;font-size:13px;color:var(--text2);">🔍 Searching for asset...</div>
|
||||
<div style="margin-top:8px;"><button class="btn btn-outline btn-sm" onclick="retakeOcr()" style="width:100%;">🔄 Retake</button></div>`;
|
||||
setOcrStatus('Machine ID found! Searching...', 'working');
|
||||
|
||||
// Auto-search the asset
|
||||
lookupOcrAsset(data.machine_id);
|
||||
} else if (hasMatched) {
|
||||
// Machine ID not found, but we have DB matches from serial/ID cross-reference
|
||||
el.innerHTML = `
|
||||
<div style="font-size:16px;font-weight:600;color:var(--green);">✅ Asset found via serial/ID match</div>
|
||||
${data.raw_text ? `<div class="or-raw">OCR text: ${esc(data.raw_text)}</div>` : ''}
|
||||
${matchedHtml}
|
||||
<div style="margin-top:8px;"><button class="btn btn-outline btn-sm" onclick="retakeOcr()" style="width:100%;">🔄 Retake</button></div>`;
|
||||
setOcrStatus('Asset found via serial/ID match!', 'success');
|
||||
} else {
|
||||
el.innerHTML = `
|
||||
<div style="font-size:16px;font-weight:600;color:var(--red);">No machine ID detected</div>
|
||||
@@ -3414,6 +3483,7 @@
|
||||
}
|
||||
el.innerHTML += `<div style="margin-top:8px;display:flex;gap:6px;">
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
|
||||
<button class="btn btn-primary btn-sm" onclick="resetOcrScanner()" style="flex:1;">Scan New</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="retakeOcr()" style="flex:1;">🔄 Retake</button>
|
||||
</div>`;
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user