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