fix: add/find tab now lookup-only, nav fix, route techs fix

- Add/Find tab: removed check-in card and add-asset form, now pure lookup
- OCR: stop camera after capture, added GPS capture at photo time
- OCR: increased timeout from 8s to 15s to reduce 500 errors
- Nav tab: added GPS prompt + asset search when navigating directly
- Route: include all 26 techs from bookableresource table (not just Shawn)
This commit is contained in:
2026-05-29 00:10:30 -04:00
parent 1db6475f83
commit 9ee8de8578
2 changed files with 166 additions and 32 deletions
+5 -7
View File
@@ -2972,13 +2972,11 @@ async def workorders_technicians():
try:
cur = conn.cursor()
cur.execute(
"""
SELECT DISTINCT b."resource!name" AS technician
FROM bookableresourcebooking b
WHERE b."resource!name" IS NOT NULL
AND b."resource!name" != ''
ORDER BY technician
"""
"""SELECT DISTINCT name AS technician FROM bookableresource WHERE name IS NOT NULL AND name != ''
UNION
SELECT DISTINCT b."resource!name" AS technician FROM bookableresourcebooking b
WHERE b."resource!name" IS NOT NULL AND b."resource!name" != ''
ORDER BY technician"""
)
techs = [r["technician"] for r in cur.fetchall()]
return {"technicians": techs, "total": len(techs)}
+161 -25
View File
@@ -1124,22 +1124,6 @@
<div id="scanResult" class="scan-result" style="display:none;"></div>
</div>
<!-- Lookup result shown here when asset found -->
<!-- Check-in form (shown when barcode found) -->
<div id="checkinCard" class="card" style="display:none;">
<div class="card-title">Check In</div>
<div id="checkinGpsRow" style="display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:13px;">
<span id="checkinGpsStatus" class="gps-badge waiting" style="margin:0;">📍 Tap for GPS</span>
<button class="btn btn-sm btn-outline" onclick="captureCheckinGps()" style="white-space:nowrap;">Capture GPS</button>
<span id="checkinGpsCoords" style="color:var(--text3);font-family:monospace;font-size:11px;display:none;"></span>
</div>
<textarea id="checkinNotes" class="input-field" placeholder="Notes (optional)" rows="2"></textarea>
<div class="form-row">
<button class="btn btn-green" onclick="submitCheckin()" style="flex:1;">✓ Check In</button>
</div>
</div>
</div>
<!-- ── OCR MODE ──────────────────────────────────────────────────── -->
@@ -1166,8 +1150,6 @@
<!-- OCR GPS badge -->
<div id="ocrGpsBadge" class="gps-badge ok" style="display:none; margin-bottom:8px;"></div>
</div>
<!-- ── PHOTO BADGE ── -->
<div id="ocrGpsBadge" class="gps-badge ok" style="display:none; margin-bottom:8px;"></div>
</div>
<!-- ═══════════════════════════════════════════════════════════════════════
@@ -1407,6 +1389,20 @@
<div id="navDestAddr" style="font-size:12px;color:var(--text2);"></div>
</div>
</div>
<!-- Setup prompt: shown when missing GPS or asset -->
<div id="navSetup" class="card" style="margin-bottom:10px;">
<div id="navSetupGps" style="display:none;margin-bottom:8px;">
<div style="font-size:13px;color:var(--text2);margin-bottom:6px;">📍 Your location is needed for navigation</div>
<button class="btn btn-sm btn-primary" onclick="captureNavGps()" style="width:100%;">📍 Get My Location</button>
</div>
<div id="navSetupAsset" style="display:none;margin-bottom:8px;">
<div style="font-size:13px;color:var(--text2);margin-bottom:6px;">🔍 Select an asset to navigate to</div>
<input id="navAssetSearch" type="text" class="input-field" placeholder="Search asset by name or machine ID..." oninput="searchNavAssets()" style="margin-bottom:6px;">
<div id="navAssetResults" style="font-size:13px;"></div>
</div>
</div>
<div id="navMapContainer" style="height:280px;border-radius:12px;overflow:hidden;margin-bottom:12px;"></div>
<div style="display:flex;gap:6px;margin-bottom:8px;flex-wrap:wrap;">
<button class="btn btn-sm btn-outline" onclick="navigateToAsset('driving')" style="flex:1;">🚗 Driving</button>
@@ -2118,6 +2114,11 @@
document.dispatchEvent(new CustomEvent('rpTabActivated'));
}
// Navigate tab — check GPS and asset setup
if (tabId === 'tabNavigate') {
updateNavSetup();
}
// Dispatch event so child tab implementations can hook in
document.dispatchEvent(new CustomEvent('tabChange', { detail: { tabId } }));
}
@@ -2507,9 +2508,6 @@
<span class="sr-checkin-done" style="color:${hasGps ? '#22c55e' : 'var(--amber)'}">
${hasGps ? '✓ GPS captured' : '📍 GPS needed'}
</span>
<button class="btn btn-outline btn-sm" onclick="openCheckinForAsset(${asset.id});" style="flex:1;">
${hasGps ? 'Add Notes' : 'Check In'}
</button>
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
</div>
`;
@@ -2803,11 +2801,41 @@
<div class="or-meta">${esc(catLabel(asset.category))} · ${esc(asset.status)}</div>`;
setOcrStatus('Asset found!', 'success');
// Auto-checkin if GPS available
// Try to capture GPS if not available
AppState.currentAssetId = asset.id;
if (AppState.gpsLat == null) {
try {
const pos = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject,
{ enableHighAccuracy: true, timeout: 8000 });
});
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
} catch (_) { /* GPS not available */ }
}
// Auto-checkin if GPS available
if (AppState.gpsLat != null) {
doAutoCheckin(asset.id);
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--green);">✓ Auto check-in recorded</div>`;
// Save GPS to asset if it's missing GPS coords
if (!asset.latitude || !asset.longitude) {
try {
await api('/api/assets/' + asset.id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
latitude: AppState.gpsLat,
longitude: AppState.gpsLng,
}),
});
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--green);">📍 GPS location saved to asset</div>`;
} catch (_) {}
}
} else {
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--amber);">📍 GPS not available — check-in skipped</div>`;
}
if (AppState.ocrGpsSaved) {
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--green);">📍 GPS saved to asset</div>`;
@@ -2870,6 +2898,9 @@
canvas.getContext('2d').drawImage(video, 0, 0);
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.85));
// Stop camera after capturing photo
stopOcrCamera();
setOcrStatus('Processing OCR...', 'working');
document.getElementById('ocrResult').style.display = 'none';
@@ -2879,7 +2910,7 @@
// Try server-side OCR first with 8s timeout
try {
const controller = new AbortController();
const serverTimeout = setTimeout(() => controller.abort(), 8000);
const serverTimeout = setTimeout(() => controller.abort(), 15000);
const formData = new FormData();
formData.append('file', blob, 'sticker.jpg');
const res = await fetch('/api/ocr', {
@@ -2920,6 +2951,19 @@
setOcrStatus('Processing OCR...', 'working');
}
// Capture device GPS for this photo (fires async — saved to AppState)
if (AppState.gpsLat == null) {
navigator.geolocation.getCurrentPosition(
(pos) => {
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
},
() => {},
{ enableHighAccuracy: true, timeout: 10000 }
);
}
displayOcrResult(data);
}
@@ -3940,9 +3984,14 @@
async function navigateToAsset(mode) {
mode = mode || 'driving';
if (!AppState.currentAssetId) return;
if (!AppState.currentAssetId) {
updateNavSetup();
showToast('Select an asset first', true);
return;
}
if (!AppState.gpsLat || !AppState.gpsLng) {
showToast('GPS location not available. Tap ◎ My Location first.', true);
updateNavSetup();
showToast('GPS location not available. Tap 📍 Get My Location.', true);
return;
}
try {
@@ -4090,6 +4139,93 @@
}
}
function updateNavSetup() {
const hasGps = AppState.gpsLat != null;
const hasAsset = AppState.currentAssetId != null;
const setupEl = document.getElementById('navSetup');
const gpsEl = document.getElementById('navSetupGps');
const assetEl = document.getElementById('navSetupAsset');
if (!setupEl) return;
if (!hasGps || !hasAsset) {
setupEl.style.display = 'block';
gpsEl.style.display = hasGps ? 'none' : 'block';
assetEl.style.display = hasAsset ? 'none' : 'block';
} else {
setupEl.style.display = 'none';
}
}
function captureNavGps() {
const btn = document.querySelector('#navSetupGps button');
if (btn) { btn.textContent = '⏳ Acquiring...'; btn.disabled = true; }
navigator.geolocation.getCurrentPosition(
(pos) => {
AppState.gpsLat = pos.coords.latitude;
AppState.gpsLng = pos.coords.longitude;
AppState.gpsAcc = pos.coords.accuracy;
if (btn) { btn.textContent = '📍 Got location! ✓'; btn.disabled = false; }
setTimeout(updateNavSetup, 500);
showToast('📍 Location acquired');
},
() => {
if (btn) { btn.textContent = '📍 Get My Location'; btn.disabled = false; }
showToast('GPS failed — check permissions', true);
},
{ enableHighAccuracy: true, timeout: 15000 }
);
}
let _navSearchTimeout = null;
function searchNavAssets() {
clearTimeout(_navSearchTimeout);
const q = document.getElementById('navAssetSearch')?.value.trim();
if (!q || q.length < 2) {
document.getElementById('navAssetResults').innerHTML = '';
return;
}
_navSearchTimeout = setTimeout(async () => {
try {
const res = await api('/api/assets?search=' + encodeURIComponent(q));
const results = Array.isArray(res) ? res : (res.assets || res.data || []);
const container = document.getElementById('navAssetResults');
if (results.length === 0) {
container.innerHTML = '<div style="color:var(--text3);font-size:12px;">No assets found</div>';
return;
}
container.innerHTML = results.slice(0, 10).map(a =>
`<div style="padding:6px 8px;cursor:pointer;border-radius:6px;background:var(--card2);margin-bottom:3px;"
onclick="selectNavAsset(${a.id})">
<strong>${esc(a.name)}</strong>
<span style="color:var(--text3);font-size:11px;margin-left:4px;">${esc(a.machine_id)}</span>
</div>`
).join('');
} catch (e) {
document.getElementById('navAssetResults').innerHTML =
'<div style="color:var(--red);font-size:12px;">Search failed</div>';
}
}, 300);
}
function selectNavAsset(id) {
AppState.currentAssetId = id;
document.getElementById('navDestName').textContent = '→ Asset #' + id;
document.getElementById('navAssetSearch').value = '';
document.getElementById('navAssetResults').innerHTML = '';
// Fetch the asset name
api('/api/assets/' + id).then(a => {
document.getElementById('navDestName').textContent = '→ ' + a.name;
}).catch(() => {});
updateNavSetup();
// If GPS is also set, navigate immediately
if (AppState.gpsLat != null) {
navigateToAsset('driving');
} else {
showToast('Asset selected. Now get your location.');
}
}
function closeNavigate() {
if (navMap) { navMap.remove(); navMap = null; }
AppState._navData = null;