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:
|
try:
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""SELECT DISTINCT name AS technician FROM bookableresource WHERE name IS NOT NULL AND name != ''
|
||||||
SELECT DISTINCT b."resource!name" AS technician
|
UNION
|
||||||
FROM bookableresourcebooking b
|
SELECT DISTINCT b."resource!name" AS technician FROM bookableresourcebooking b
|
||||||
WHERE b."resource!name" IS NOT NULL
|
WHERE b."resource!name" IS NOT NULL AND b."resource!name" != ''
|
||||||
AND b."resource!name" != ''
|
ORDER BY technician"""
|
||||||
ORDER BY technician
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
techs = [r["technician"] for r in cur.fetchall()]
|
techs = [r["technician"] for r in cur.fetchall()]
|
||||||
return {"technicians": techs, "total": len(techs)}
|
return {"technicians": techs, "total": len(techs)}
|
||||||
|
|||||||
+161
-25
@@ -1124,22 +1124,6 @@
|
|||||||
<div id="scanResult" class="scan-result" style="display:none;"></div>
|
<div id="scanResult" class="scan-result" style="display:none;"></div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- ── OCR MODE ──────────────────────────────────────────────────── -->
|
<!-- ── OCR MODE ──────────────────────────────────────────────────── -->
|
||||||
@@ -1166,8 +1150,6 @@
|
|||||||
<!-- OCR GPS badge -->
|
<!-- OCR GPS badge -->
|
||||||
<div id="ocrGpsBadge" class="gps-badge ok" style="display:none; margin-bottom:8px;"></div>
|
<div id="ocrGpsBadge" class="gps-badge ok" style="display:none; margin-bottom:8px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- ── PHOTO BADGE ── -->
|
|
||||||
<div id="ocrGpsBadge" class="gps-badge ok" style="display:none; margin-bottom:8px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════════════════════════════════════════════════════════════════
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -1407,6 +1389,20 @@
|
|||||||
<div id="navDestAddr" style="font-size:12px;color:var(--text2);"></div>
|
<div id="navDestAddr" style="font-size:12px;color:var(--text2);"></div>
|
||||||
</div>
|
</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 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;">
|
<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>
|
<button class="btn btn-sm btn-outline" onclick="navigateToAsset('driving')" style="flex:1;">🚗 Driving</button>
|
||||||
@@ -2118,6 +2114,11 @@
|
|||||||
document.dispatchEvent(new CustomEvent('rpTabActivated'));
|
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
|
// Dispatch event so child tab implementations can hook in
|
||||||
document.dispatchEvent(new CustomEvent('tabChange', { detail: { tabId } }));
|
document.dispatchEvent(new CustomEvent('tabChange', { detail: { tabId } }));
|
||||||
}
|
}
|
||||||
@@ -2507,9 +2508,6 @@
|
|||||||
<span class="sr-checkin-done" style="color:${hasGps ? '#22c55e' : 'var(--amber)'}">
|
<span class="sr-checkin-done" style="color:${hasGps ? '#22c55e' : 'var(--amber)'}">
|
||||||
${hasGps ? '✓ GPS captured' : '📍 GPS needed'}
|
${hasGps ? '✓ GPS captured' : '📍 GPS needed'}
|
||||||
</span>
|
</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>
|
<button class="btn btn-outline btn-sm" onclick="switchTab('tabAssets');viewAsset(${asset.id});" style="flex:1;">View Details</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -2803,11 +2801,41 @@
|
|||||||
<div class="or-meta">${esc(catLabel(asset.category))} · ${esc(asset.status)}</div>`;
|
<div class="or-meta">${esc(catLabel(asset.category))} · ${esc(asset.status)}</div>`;
|
||||||
setOcrStatus('Asset found!', 'success');
|
setOcrStatus('Asset found!', 'success');
|
||||||
|
|
||||||
// Auto-checkin if GPS available
|
// Try to capture GPS if not available
|
||||||
AppState.currentAssetId = asset.id;
|
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) {
|
if (AppState.gpsLat != null) {
|
||||||
doAutoCheckin(asset.id);
|
doAutoCheckin(asset.id);
|
||||||
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--green);">✓ Auto check-in recorded</div>`;
|
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) {
|
if (AppState.ocrGpsSaved) {
|
||||||
el.innerHTML += `<div style="margin-top:8px;font-size:12px;color:var(--green);">📍 GPS saved to asset</div>`;
|
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);
|
canvas.getContext('2d').drawImage(video, 0, 0);
|
||||||
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
const blob = await new Promise(r => canvas.toBlob(r, 'image/jpeg', 0.85));
|
||||||
|
|
||||||
|
// Stop camera after capturing photo
|
||||||
|
stopOcrCamera();
|
||||||
|
|
||||||
setOcrStatus('Processing OCR...', 'working');
|
setOcrStatus('Processing OCR...', 'working');
|
||||||
document.getElementById('ocrResult').style.display = 'none';
|
document.getElementById('ocrResult').style.display = 'none';
|
||||||
|
|
||||||
@@ -2879,7 +2910,7 @@
|
|||||||
// Try server-side OCR first with 8s timeout
|
// Try server-side OCR first with 8s timeout
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const serverTimeout = setTimeout(() => controller.abort(), 8000);
|
const serverTimeout = setTimeout(() => controller.abort(), 15000);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', blob, 'sticker.jpg');
|
formData.append('file', blob, 'sticker.jpg');
|
||||||
const res = await fetch('/api/ocr', {
|
const res = await fetch('/api/ocr', {
|
||||||
@@ -2920,6 +2951,19 @@
|
|||||||
setOcrStatus('Processing OCR...', 'working');
|
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);
|
displayOcrResult(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3940,9 +3984,14 @@
|
|||||||
|
|
||||||
async function navigateToAsset(mode) {
|
async function navigateToAsset(mode) {
|
||||||
mode = mode || 'driving';
|
mode = mode || 'driving';
|
||||||
if (!AppState.currentAssetId) return;
|
if (!AppState.currentAssetId) {
|
||||||
|
updateNavSetup();
|
||||||
|
showToast('Select an asset first', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!AppState.gpsLat || !AppState.gpsLng) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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() {
|
function closeNavigate() {
|
||||||
if (navMap) { navMap.remove(); navMap = null; }
|
if (navMap) { navMap.remove(); navMap = null; }
|
||||||
AppState._navData = null;
|
AppState._navData = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user