diff --git a/server.py b/server.py
index 4812811..a901ea7 100644
--- a/server.py
+++ b/server.py
@@ -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)}
diff --git a/static/index.html b/static/index.html
index 296bb40..7342719 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1124,22 +1124,6 @@
-
-
-
-
-
-
Check In
-
- ๐ Tap for GPS
- Capture GPS
-
-
-
-
- โ Check In
-
-
@@ -1166,8 +1150,6 @@
-
-
+
+
+
๐ Your location is needed for navigation
+
๐ Get My Location
+
+
+
๐ Select an asset to navigate to
+
+
+
+
+
๐ Driving
@@ -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 @@
${hasGps ? 'โ GPS captured' : '๐ GPS needed'}
-
- ${hasGps ? 'Add Notes' : 'Check In'}
-
View Details
`;
@@ -2803,11 +2801,41 @@
${esc(catLabel(asset.category))} ยท ${esc(asset.status)}
`;
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 += `โ Auto check-in recorded
`;
+
+ // 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 += `๐ GPS location saved to asset
`;
+ } catch (_) {}
+ }
+ } else {
+ el.innerHTML += `๐ GPS not available โ check-in skipped
`;
}
if (AppState.ocrGpsSaved) {
el.innerHTML += `๐ GPS saved to asset
`;
@@ -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 = 'No assets found
';
+ return;
+ }
+ container.innerHTML = results.slice(0, 10).map(a =>
+ `
+ ${esc(a.name)}
+ ${esc(a.machine_id)}
+
`
+ ).join('');
+ } catch (e) {
+ document.getElementById('navAssetResults').innerHTML =
+ 'Search failed
';
+ }
+ }, 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;