Fix mode structure: Scan (barcode+OCR+gallery) + Manual only
- Removed separate OCR and Connect mode toggles — merged into Scan - Renamed Barcode mode to Scan with OCR fallback + gallery upload - Added captureScanOcr() — tap to capture frame for OCR when barcode fails - handleBarcode() now compares barcode vs OCR result, picks valid ID - Gallery photo picker integrated into Scan mode (OCR + EXIF GPS) - Removed old photoPreviewCard and connect-mode HTML sections - Removed orphaned functions referencing removed elements
This commit is contained in:
+182
-214
@@ -1217,26 +1217,39 @@
|
|||||||
<input type="file" id="photoPicker" accept="image/*" capture="environment" style="display:none;">
|
<input type="file" id="photoPicker" accept="image/*" capture="environment" style="display:none;">
|
||||||
<!-- Mode Toggles -->
|
<!-- Mode Toggles -->
|
||||||
<div class="mode-toggles">
|
<div class="mode-toggles">
|
||||||
<button class="mode-toggle active" data-mode="barcode" onclick="setAddAssetMode('barcode')">📷 Barcode</button>
|
<button class="mode-toggle active" data-mode="barcode" onclick="setAddAssetMode('barcode')">📷 Scan</button>
|
||||||
<button class="mode-toggle" data-mode="ocr" onclick="setAddAssetMode('ocr')">🔍 OCR</button>
|
|
||||||
<button class="mode-toggle" data-mode="manual" onclick="setAddAssetMode('manual')">✏️ Manual</button>
|
<button class="mode-toggle" data-mode="manual" onclick="setAddAssetMode('manual')">✏️ Manual</button>
|
||||||
<button class="mode-toggle" data-mode="connect" onclick="setAddAssetMode('connect')">🏷️ Connect</button>
|
|
||||||
<button class="photo-picker-btn" onclick="document.getElementById('photoPicker').click()">📱 Pick from Gallery</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── BARCODE MODE ──────────────────────────────────────────────── -->
|
<!-- ── SCAN MODE (barcode + OCR + gallery upload) ────────────────── -->
|
||||||
<div id="addBarcodeMode" class="add-mode active">
|
<div id="addBarcodeMode" class="add-mode active">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Barcode Scanner</div>
|
<div class="card-title">Scan</div>
|
||||||
<div id="cameraArea" class="camera-area">
|
<div id="cameraArea" class="camera-area">
|
||||||
<div id="cameraPlaceholder" class="camera-placeholder">
|
<div id="cameraPlaceholder" class="camera-placeholder">
|
||||||
<span class="cam-icon">📷</span>
|
<span class="cam-icon">📷</span>
|
||||||
<div class="cam-hint">Tap to start camera</div>
|
<div class="cam-hint">Tap to start camera</div>
|
||||||
</div>
|
</div>
|
||||||
<video id="cameraVideo" autoplay playsinline muted></video>
|
<video id="cameraVideo" autoplay playsinline muted></video>
|
||||||
|
<div class="ocr-capture-overlay" id="scanCaptureOverlay" style="display:none;">
|
||||||
|
<button class="ocr-capture-btn" onclick="captureScanOcr()" title="Capture for OCR">📸</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="scanStatus" class="status-bar">Point camera at a barcode</div>
|
<div id="scanStatus" class="status-bar">Point camera at a barcode or sticker</div>
|
||||||
<div id="scanResult" class="scan-result" style="display:none;"></div>
|
<div id="scanResult" class="scan-result" style="display:none;"></div>
|
||||||
|
<div style="display:flex;gap:6px;margin-top:6px;">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="document.getElementById('photoPicker').click()" style="flex:1;">📱 Pick from Gallery</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Photo preview (from gallery) -->
|
||||||
|
<div id="scanPhotoPreview" class="card" style="display:none;">
|
||||||
|
<div class="card-title">📷 Photo Preview</div>
|
||||||
|
<img id="scanPhotoThumb" style="width:100%;border-radius:8px;margin-bottom:8px;">
|
||||||
|
<input type="hidden" id="scanMachineId" value="">
|
||||||
|
<div id="scanGpsResult" class="gps-result" style="display:none;"></div>
|
||||||
|
<div id="scanOcrResult" style="font-size:13px;color:var(--text2);"></div>
|
||||||
|
<button class="btn btn-primary" onclick="createFromScanPhoto()" style="margin-top:6px;">➕ Create Asset</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Asset form (shown when machine_id not found) -->
|
<!-- New Asset form (shown when machine_id not found) -->
|
||||||
@@ -1268,45 +1281,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── OCR MODE ──────────────────────────────────────────────────── -->
|
<!-- ── MANUAL MODE ──────────────────────────────────────────────── -->
|
||||||
<div id="addOcrMode" class="add-mode">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">OCR Scanner — Photograph Sticker</div>
|
|
||||||
<div id="ocrCameraArea" class="camera-area">
|
|
||||||
<div id="ocrPlaceholder" class="camera-placeholder" onclick="startOcrCamera()">
|
|
||||||
<span class="cam-icon">📸</span>
|
|
||||||
<div class="cam-hint" style="margin-top:4px;">Tap to start camera</div>
|
|
||||||
</div>
|
|
||||||
<video id="ocrVideo" autoplay playsinline muted style="display:none;"></video>
|
|
||||||
<div class="ocr-capture-overlay" id="ocrCaptureOverlay" style="display:none;">
|
|
||||||
<button class="ocr-capture-btn" onclick="captureOcr()" title="Capture">📸</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="ocrStatus" class="status-bar">Point camera at the machine sticker and tap capture</div>
|
|
||||||
<div id="ocrProgressWrap" class="ocr-progress-wrap">
|
|
||||||
<div id="ocrProgressBar" class="ocr-progress-bar"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- OCR result -->
|
|
||||||
<div id="ocrResult" class="ocr-result" style="display:none;"></div>
|
|
||||||
<!-- Quick create from OCR -->
|
|
||||||
<div id="ocrCreateCard" class="card" style="display:none;">
|
|
||||||
<div class="card-title">Create from OCR</div>
|
|
||||||
<input id="ocrMachineId" type="text" class="input-field" placeholder="Machine ID" readonly>
|
|
||||||
<input id="ocrName" type="text" class="input-field" placeholder="Asset name *" required>
|
|
||||||
<select id="ocrCatSelect" class="input-field">
|
|
||||||
<option value="">Category</option>
|
|
||||||
</select>
|
|
||||||
<select id="ocrStatus" class="input-field">
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="maintenance">Maintenance</option>
|
|
||||||
<option value="retired">Retired</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-primary" onclick="createOcrAsset()">Create Asset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── MANUAL MODE ───────────────────────────────────────────────── -->
|
|
||||||
<div id="addManualMode" class="add-mode">
|
<div id="addManualMode" class="add-mode">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Manual Entry</div>
|
<div class="card-title">Manual Entry</div>
|
||||||
@@ -1414,75 +1389,8 @@
|
|||||||
<textarea id="manCheckinNotes" class="input-field" placeholder="Notes (optional)" rows="2"></textarea>
|
<textarea id="manCheckinNotes" class="input-field" placeholder="Notes (optional)" rows="2"></textarea>
|
||||||
<button class="btn btn-green" onclick="quickCheckinManual()" style="width:100%;">✓ Check In Now</button>
|
<button class="btn btn-green" onclick="quickCheckinManual()" style="width:100%;">✓ Check In Now</button>
|
||||||
</div>
|
</div>
|
||||||
</div> </div>
|
|
||||||
|
|
||||||
<!-- ── CONNECT LABEL MODE (T4: unified photo + OCR + GPS) ──────────── -->
|
|
||||||
<div id="addConnectMode" class="add-mode">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-title">🏷️ Scan Connect Label — Photo + OCR + GPS</div>
|
|
||||||
<p style="font-size:12px;color:var(--text2);margin-bottom:10px;">
|
|
||||||
Take or pick a photo of a machine sticker. OCR reads the ID, GPS is extracted automatically.
|
|
||||||
</p>
|
|
||||||
<div style="display:flex;gap:8px;margin-bottom:4px;">
|
|
||||||
<button class="btn btn-primary" onclick="connectFromCamera()" style="flex:1;">
|
|
||||||
📷 Camera
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline" onclick="connectFromGallery()" style="flex:1;">
|
|
||||||
🖼️ From Gallery
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden camera for connect mode -->
|
|
||||||
<div id="connectCameraArea" class="camera-area" style="display:none;">
|
|
||||||
<video id="connectVideo" autoplay playsinline muted style="display:none;width:100%;height:100%;object-fit:cover;position:absolute;inset:0;"></video>
|
|
||||||
<div id="connectCameraPlaceholder" class="camera-placeholder" style="display:none;">
|
|
||||||
<span class="cam-icon">📸</span>
|
|
||||||
<div class="cam-hint">Tap to start camera</div>
|
|
||||||
</div>
|
|
||||||
<div class="ocr-capture-overlay" id="connectCaptureOverlay" style="display:none;">
|
|
||||||
<button class="ocr-capture-btn" onclick="captureConnectPhoto()" title="Capture">📸</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Processing status -->
|
|
||||||
<div id="connectStatus" class="status-bar" style="display:none;"></div>
|
|
||||||
|
|
||||||
<!-- Results card -->
|
|
||||||
<div id="connectResults" class="card" style="display:none;">
|
|
||||||
<div class="card-title">📋 Scan Results</div>
|
|
||||||
<img id="connectThumb" src="" alt="Scanned photo" style="width:100%;max-height:200px;object-fit:cover;border-radius:var(--radius-sm);margin-bottom:10px;">
|
|
||||||
<div id="connectOcrResult" style="margin-bottom:8px;"></div>
|
|
||||||
<div id="connectGpsResult" style="margin-bottom:8px;"></div>
|
|
||||||
<div id="connectMiniMap" style="display:none;width:100%;height:160px;border-radius:var(--radius-sm);overflow:hidden;margin-bottom:8px;"></div>
|
|
||||||
|
|
||||||
<!-- Editable fields -->
|
|
||||||
<input id="connectMachineId" type="text" class="input-field" placeholder="Machine ID *">
|
|
||||||
<input id="connectName" type="text" class="input-field" placeholder="Asset Name *">
|
|
||||||
<div class="form-row" style="margin-bottom:8px;">
|
|
||||||
<input id="connectLat" type="number" step="any" class="input-field" placeholder="Latitude">
|
|
||||||
<input id="connectLng" type="number" step="any" class="input-field" placeholder="Longitude">
|
|
||||||
</div>
|
|
||||||
<select id="connectCatSelect" class="input-field"></select>
|
|
||||||
|
|
||||||
<div style="display:flex;gap:8px;">
|
|
||||||
<button class="btn btn-primary" onclick="createConnectAsset()" style="flex:2;">➕ Create Asset</button>
|
|
||||||
<button class="btn btn-outline" onclick="resetConnectMode()" style="flex:1;">🔄 Reset</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Photo Preview Card (shown when a photo is picked from gallery) ── -->
|
|
||||||
<div id="photoPreviewCard" class="photo-preview-card" style="display:none;">
|
|
||||||
<img id="pickedPhotoPreview" src="" alt="Picked photo">
|
|
||||||
<div class="photo-preview-toolbar">
|
|
||||||
<button onclick="ocrPickedPhoto()">🔍 OCR this photo</button>
|
|
||||||
<button onclick="extractGpsFromPicked()">📍 Extract GPS</button>
|
|
||||||
<button onclick="createAssetFromPicked()">➕ Create Asset</button>
|
|
||||||
</div>
|
|
||||||
<div class="exif-data" id="pickedExifData" style="display:none;"></div>
|
|
||||||
<button class="btn btn-outline btn-sm" style="margin-top:8px;width:100%;" onclick="clearPickedPhoto()">✕ Clear</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
TAB: ASSETS LIST
|
TAB: ASSETS LIST
|
||||||
@@ -2667,9 +2575,7 @@
|
|||||||
|
|
||||||
// Stop any running scanners
|
// Stop any running scanners
|
||||||
stopScanning();
|
stopScanning();
|
||||||
stopOcrCamera();
|
|
||||||
stopManualPhoto();
|
stopManualPhoto();
|
||||||
stopConnectCamera();
|
|
||||||
|
|
||||||
if (mode === 'barcode') {
|
if (mode === 'barcode') {
|
||||||
startScanning();
|
startScanning();
|
||||||
@@ -2695,63 +2601,88 @@
|
|||||||
function handlePickedPhoto(file) {
|
function handlePickedPhoto(file) {
|
||||||
pickedPhotoFile = file;
|
pickedPhotoFile = file;
|
||||||
pickedPhotoGps = null;
|
pickedPhotoGps = null;
|
||||||
|
|
||||||
|
// Switch to Scan mode
|
||||||
|
setAddAssetMode('barcode');
|
||||||
|
|
||||||
|
// Show thumbnail in scan photo preview
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function(e) {
|
reader.onload = async function(e) {
|
||||||
document.getElementById('pickedPhotoPreview').src = e.target.result;
|
const thumb = document.getElementById('scanPhotoThumb');
|
||||||
document.getElementById('photoPreviewCard').style.display = 'block';
|
if (thumb) thumb.src = e.target.result;
|
||||||
|
|
||||||
// Extract EXIF / file metadata
|
const preview = document.getElementById('scanPhotoPreview');
|
||||||
extractExifData(file);
|
if (preview) preview.style.display = 'block';
|
||||||
|
|
||||||
// Try EXIF GPS extraction
|
// Run OCR on the photo
|
||||||
tryExtractGpsFromPicked();
|
const ocrEl = document.getElementById('scanOcrResult');
|
||||||
|
if (ocrEl) ocrEl.textContent = '⏳ Running OCR...';
|
||||||
|
|
||||||
// Scroll to preview
|
try {
|
||||||
document.getElementById('photoPreviewCard').scrollIntoView({ behavior: 'smooth' });
|
const ocrData = await ocrImageClient(file);
|
||||||
|
if (ocrData && ocrData.machine_id) {
|
||||||
|
if (ocrEl) ocrEl.innerHTML = `✅ Machine ID: <strong>${esc(ocrData.machine_id)}</strong>` +
|
||||||
|
(ocrData.confidence === 'low' ? ' ⚠️ Low confidence' : '');
|
||||||
|
} else {
|
||||||
|
if (ocrEl) ocrEl.innerHTML = '❌ No machine ID detected. <button class="btn btn-outline btn-sm" onclick="document.getElementById(\'scanMachineId\').value=prompt(\'Enter machine ID:\')">Enter manually</button>';
|
||||||
|
}
|
||||||
|
} catch (ocrErr) {
|
||||||
|
if (ocrEl) ocrEl.textContent = '❌ OCR failed: ' + ocrErr.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract GPS from EXIF
|
||||||
|
const gpsEl = document.getElementById('scanGpsResult');
|
||||||
|
try {
|
||||||
|
const gps = await extractGpsFromPhoto(file);
|
||||||
|
if (gps && gps.lat != null && gps.lng != null) {
|
||||||
|
pickedPhotoGps = gps;
|
||||||
|
if (gpsEl) {
|
||||||
|
gpsEl.style.display = 'block';
|
||||||
|
gpsEl.innerHTML = `📍 GPS: ${gps.lat.toFixed(6)}, ${gps.lng.toFixed(6)}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (gpsEl) {
|
||||||
|
gpsEl.style.display = 'block';
|
||||||
|
gpsEl.innerHTML = '📍 No GPS data in photo';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (gpsErr) {
|
||||||
|
if (gpsEl) {
|
||||||
|
gpsEl.style.display = 'block';
|
||||||
|
gpsEl.innerHTML = '📍 GPS error: ' + gpsErr.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractExifData(file) {
|
function createFromScanPhoto() {
|
||||||
const exifDiv = document.getElementById('pickedExifData');
|
// Gather OCR + GPS results from scan photo and create asset
|
||||||
const rows = [];
|
const midEl = document.getElementById('scanMachineId');
|
||||||
rows.push({ label: 'File name', value: file.name });
|
const mid = midEl ? midEl.value : '';
|
||||||
rows.push({ label: 'File size', value: formatFileSize(file.size) });
|
const name = prompt('Asset name:', '');
|
||||||
rows.push({ label: 'Type', value: file.type || 'unknown' });
|
if (!mid || !name) return showToast('Machine ID and name required', true);
|
||||||
rows.push({ label: 'Last modified', value: formatDate(new Date(file.lastModified).toISOString()) });
|
|
||||||
|
|
||||||
exifDiv.innerHTML = rows.map(r =>
|
const payload = { machine_id: mid, name };
|
||||||
`<div class="exif-row"><span class="exif-label">${esc(r.label)}</span><span class="exif-value">${esc(r.value)}</span></div>`
|
if (pickedPhotoGps) {
|
||||||
).join('');
|
payload.latitude = pickedPhotoGps.lat;
|
||||||
exifDiv.style.display = 'block';
|
payload.longitude = pickedPhotoGps.lng;
|
||||||
|
}
|
||||||
|
|
||||||
|
api('/api/connect-label', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).then(() => {
|
||||||
|
showToast('✅ Asset created');
|
||||||
|
document.getElementById('scanPhotoPreview').style.display = 'none';
|
||||||
|
pickedPhotoFile = null;
|
||||||
|
pickedPhotoGps = null;
|
||||||
|
}).catch(e => showToast('Error: ' + e.message, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes) {
|
|
||||||
if (bytes < 1024) return bytes + ' B';
|
|
||||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
|
||||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearPickedPhoto() {
|
// ▶ Core EXIF GPS extraction using exifr.js
|
||||||
pickedPhotoFile = null;
|
|
||||||
pickedPhotoGps = null;
|
|
||||||
document.getElementById('photoPreviewCard').style.display = 'none';
|
|
||||||
document.getElementById('pickedPhotoPreview').src = '';
|
|
||||||
document.getElementById('pickedExifData').style.display = 'none';
|
|
||||||
document.getElementById('pickedExifData').innerHTML = '';
|
|
||||||
// Reset the file input so the same file can be picked again
|
|
||||||
const pp = document.getElementById('photoPicker');
|
|
||||||
if (pp) pp.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function ocrPickedPhoto() {
|
|
||||||
if (!pickedPhotoFile) return;
|
|
||||||
// Switch to OCR mode with the picked photo available
|
|
||||||
setAddAssetMode('ocr');
|
|
||||||
showToast('Photo loaded in preview — OCR processing ready');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ▶ Core EXIF GPS extraction using exifr.js (loaded in <head>)
|
|
||||||
async function extractGpsFromPhoto(file) {
|
async function extractGpsFromPhoto(file) {
|
||||||
try {
|
try {
|
||||||
if (typeof exifr === 'undefined') {
|
if (typeof exifr === 'undefined') {
|
||||||
@@ -2772,61 +2703,9 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ▶ Try to extract GPS from the currently picked photo and render results
|
|
||||||
async function tryExtractGpsFromPicked() {
|
|
||||||
if (!pickedPhotoFile) return;
|
|
||||||
const exifEl = document.getElementById('pickedExifData');
|
|
||||||
|
|
||||||
// Append GPS status to existing EXIF display
|
|
||||||
let gpsSection = document.getElementById('pickedGpsSection');
|
|
||||||
if (!gpsSection) {
|
|
||||||
gpsSection = document.createElement('div');
|
|
||||||
gpsSection.id = 'pickedGpsSection';
|
|
||||||
gpsSection.style.marginTop = '8px';
|
|
||||||
gpsSection.style.paddingTop = '8px';
|
|
||||||
gpsSection.style.borderTop = '1px solid var(--border)';
|
|
||||||
exifEl.appendChild(gpsSection);
|
|
||||||
}
|
|
||||||
gpsSection.innerHTML = '<span style="color:var(--amber);">⏳ Reading GPS from photo...</span>';
|
|
||||||
|
|
||||||
const gps = await extractGpsFromPhoto(pickedPhotoFile);
|
|
||||||
pickedPhotoGps = gps;
|
|
||||||
|
|
||||||
if (gps) {
|
|
||||||
gpsSection.innerHTML = `
|
|
||||||
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
|
||||||
<span class="gps-badge ok">📍 GPS from photo</span>
|
|
||||||
</div>
|
|
||||||
<div style="font-size:13px;color:var(--text2);margin-bottom:4px;">
|
|
||||||
${gps.lat.toFixed(6)}, ${gps.lng.toFixed(6)}
|
|
||||||
${gps.accuracy ? ` (±${gps.accuracy.toFixed(1)}m)` : ''}
|
|
||||||
</div>
|
|
||||||
<div id="gpsMiniMap"></div>
|
|
||||||
<div style="display:flex;gap:6px;margin-top:8px;">
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="fillManualFromGps()" style="flex:1;font-size:12px;">
|
|
||||||
📝 Fill Form
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="openInMaps()" style="flex:1;font-size:12px;">
|
|
||||||
🗺️ Maps
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
// Render mini-map
|
|
||||||
const miniMapEl = document.getElementById('gpsMiniMap');
|
|
||||||
if (miniMapEl) {
|
|
||||||
setTimeout(() => renderGpsPreview(gps.lat, gps.lng, miniMapEl), 100);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gpsSection.innerHTML = `
|
|
||||||
<span class="gps-badge err">📍 No GPS in photo</span>
|
|
||||||
<div style="font-size:11px;color:var(--text3);margin-top:4px;">
|
|
||||||
Phones often strip EXIF GPS when sharing. You can enter coordinates manually.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ▶ Render a mini-map preview using Leaflet
|
// ▶ Render a mini-map preview using Leaflet
|
||||||
function renderGpsPreview(lat, lng, containerEl) {
|
function renderGpsPreview(lat, lng, containerEl) {
|
||||||
if (!containerEl) return;
|
if (!containerEl) return;
|
||||||
@@ -3164,6 +3043,55 @@
|
|||||||
if (el) { el.textContent = msg; el.className = 'status-bar ' + cls; }
|
if (el) { el.textContent = msg; el.className = 'status-bar ' + cls; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function captureScanOcr() {
|
||||||
|
const video = document.getElementById('cameraVideo');
|
||||||
|
if (!video || video.readyState < 2) {
|
||||||
|
setScanStatus('Camera not ready — tap to start it first', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||||
|
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
||||||
|
setScanStatus('Processing OCR...', 'working');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try server first with short timeout
|
||||||
|
const controller = new AbortController();
|
||||||
|
const t = setTimeout(() => controller.abort(), 6000);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', blob, 'sticker.jpg');
|
||||||
|
const res = await fetch('/api/ocr', {
|
||||||
|
method: 'POST', body: fd, signal: controller.signal,
|
||||||
|
headers: AppState.authToken
|
||||||
|
? { 'Authorization': 'Bearer ' + AppState.authToken } : {},
|
||||||
|
});
|
||||||
|
clearTimeout(t);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
if (data && data.machine_id) {
|
||||||
|
handleBarcode(data.machine_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Server OCR failed');
|
||||||
|
} catch (e) {
|
||||||
|
// Fall back to client-side Tesseract.js
|
||||||
|
try {
|
||||||
|
const data = await ocrImageClient(blob);
|
||||||
|
if (data && data.machine_id) {
|
||||||
|
handleBarcode(data.machine_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (clientErr) {
|
||||||
|
setScanStatus('OCR failed: ' + clientErr.message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setScanStatus('No machine ID detected in frame', 'error');
|
||||||
|
}
|
||||||
|
|
||||||
async function handleBarcode(machineId) {
|
async function handleBarcode(machineId) {
|
||||||
if (barcodeDebounce) return;
|
if (barcodeDebounce) return;
|
||||||
barcodeDebounce = machineId;
|
barcodeDebounce = machineId;
|
||||||
@@ -3174,6 +3102,46 @@
|
|||||||
document.getElementById('newAssetCard').style.display = 'none';
|
document.getElementById('newAssetCard').style.display = 'none';
|
||||||
document.getElementById('checkinCard').style.display = 'none';
|
document.getElementById('checkinCard').style.display = 'none';
|
||||||
|
|
||||||
|
// Also OCR the current frame to compare with barcode result
|
||||||
|
try {
|
||||||
|
const video = document.getElementById('cameraVideo');
|
||||||
|
if (video && video.readyState >= 2) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = video.videoWidth;
|
||||||
|
canvas.height = video.videoHeight;
|
||||||
|
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||||
|
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.8));
|
||||||
|
if (blob && blob.size > 0) {
|
||||||
|
const ocrData = await ocrImageClient(blob);
|
||||||
|
if (ocrData && ocrData.machine_id) {
|
||||||
|
console.log(`Barcode: ${machineId}, OCR: ${ocrData.machine_id}`);
|
||||||
|
// Prefer the one that looks more like a valid machine ID
|
||||||
|
// Machine ID pattern: 5 digits (last 5 of XXXXX-XXXXXX)
|
||||||
|
const barcodeDigits = machineId.replace(/\D/g, '');
|
||||||
|
const ocrDigits = ocrData.machine_id.replace(/\D/g, '');
|
||||||
|
const barcodeValid = barcodeDigits.length >= 5;
|
||||||
|
const ocrValid = ocrDigits.length >= 5 && ocrData.confidence !== 'low';
|
||||||
|
if (ocrValid && !barcodeValid) {
|
||||||
|
console.log('Using OCR result over barcode');
|
||||||
|
machineId = ocrData.machine_id;
|
||||||
|
currentScannedMachineId = machineId;
|
||||||
|
} else if (ocrValid && barcodeValid) {
|
||||||
|
// Both valid — check which matches the XXXXX-XXXXXX pattern better
|
||||||
|
const barcodeIsId = machineId.includes('-') || /^\d{4,6}$/.test(machineId.trim());
|
||||||
|
const ocrIsId = ocrData.machine_id.includes('-') || /^\d{4,6}$/.test(ocrData.machine_id);
|
||||||
|
if (ocrIsId && !barcodeIsId) {
|
||||||
|
console.log('OCR result matches ID format better');
|
||||||
|
machineId = ocrData.machine_id;
|
||||||
|
currentScannedMachineId = machineId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ocrErr) {
|
||||||
|
console.warn('OCR comparison failed:', ocrErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
|
const asset = await api(`/api/assets/search?machine_id=${encodeURIComponent(machineId)}`);
|
||||||
autoCheckin(asset.id);
|
autoCheckin(asset.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user