fix(navigate): fix Navigate tab routing, add empty state, route search, mode toggle
- Fixed switchTab recursion bug when leaving tabNavigate (set currentTab early + return) - Fixed closeNavigate to not call switchTab (caller handles tab transition) - Added renderNavPage call in switchTab lifecycle for tabNavigate - Added empty state with asset search on Navigate page - Added route-finding: search assets, navigate from search results - Added driving/walking mode toggle with OSRM route display - Added ETA display for driving mode - Fixed route polyline to use OSRM road geometry when available - Added switchNavMode function for mode switching - Improved directions label (Walking vs Driving)
This commit is contained in:
+334
-96
@@ -186,16 +186,24 @@
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
.tab-panel {
|
||||
display: none; padding: 0 12px;
|
||||
animation: none;
|
||||
opacity: 1; transform: translateX(0);
|
||||
}
|
||||
.tab-panel.active {
|
||||
display: block;
|
||||
animation: tabSlideIn 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.tab-panel.slide-left {
|
||||
display: block !important;
|
||||
animation: tabSlideOutLeft 0.2s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
@keyframes tabSlideIn {
|
||||
from { opacity: 0; transform: translateX(16px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
@keyframes tabSlideOutLeft {
|
||||
from { opacity: 1; transform: translateX(0); }
|
||||
to { opacity: 0; transform: translateX(-16px); }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
BOTTOM TABS
|
||||
@@ -344,6 +352,73 @@
|
||||
.empty-state { text-align: center; padding: 32px 16px; color: var(--text2); font-size: 13px; }
|
||||
.empty-state .es-icon { font-size: 40px; margin-bottom: 8px; opacity: 0.5; }
|
||||
|
||||
/* Enhanced empty-state card — full-page onboarding pattern */
|
||||
.empty-state-card {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
text-align: center; padding: 48px 24px; min-height: 240px;
|
||||
}
|
||||
.esc-icon {
|
||||
font-size: 56px; margin-bottom: 16px; opacity: 0.35;
|
||||
width: 88px; height: 88px; border-radius: 50%;
|
||||
background: var(--card2); display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.esc-heading {
|
||||
font-size: 17px; font-weight: 700; color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.esc-desc {
|
||||
font-size: 13px; color: var(--text2); max-width: 280px;
|
||||
line-height: 1.5; margin-bottom: 20px;
|
||||
}
|
||||
.esc-cta {
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
padding: 10px 24px; font-size: 14px; font-weight: 600;
|
||||
background: var(--accent); color: #fff; border: none;
|
||||
border-radius: var(--radius-sm); cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.esc-cta:active { background: var(--accent2); }
|
||||
.esc-cta-sm {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 6px 14px; font-size: 12px; font-weight: 600;
|
||||
background: var(--accent); color: #fff; border: none;
|
||||
border-radius: var(--radius-xs); cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.esc-cta-sm:active { background: var(--accent2); }
|
||||
|
||||
/* Onboarding welcome overlay */
|
||||
.onboard-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
|
||||
z-index: 600; display: flex; align-items: center; justify-content: center;
|
||||
padding: 20px; opacity: 0; pointer-events: none; transition: opacity 0.25s;
|
||||
}
|
||||
.onboard-overlay.open { opacity: 1; pointer-events: auto; }
|
||||
.onboard-card {
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: var(--radius); padding: 28px 24px; max-width: 340px;
|
||||
text-align: center; width: 100%;
|
||||
}
|
||||
.onboard-card .esc-icon { margin: 0 auto 16px; }
|
||||
|
||||
/* Guidance card — reports empty state */
|
||||
.guidance-card {
|
||||
text-align: center; padding: 24px 16px;
|
||||
}
|
||||
.gc-icon {
|
||||
font-size: 40px; margin-bottom: 12px; opacity: 0.5;
|
||||
}
|
||||
.gc-title {
|
||||
font-size: 15px; font-weight: 600; color: var(--text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.gc-hint {
|
||||
font-size: 12px; color: var(--text2); line-height: 1.5;
|
||||
max-width: 300px; margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════
|
||||
MODAL / DIALOG
|
||||
═══════════════════════════════════════════════════════════════════════ */
|
||||
@@ -1325,104 +1400,178 @@
|
||||
|
||||
<!-- ── MANUAL MODE ───────────────────────────────────────────────── -->
|
||||
<div id="addManualMode" class="add-mode">
|
||||
<div class="card">
|
||||
<div class="card-title">Manual Entry</div>
|
||||
<!-- Autosave indicator -->
|
||||
<div id="autosaveIndicator" class="autosave-indicator"></div>
|
||||
|
||||
<!-- Basic Info -->
|
||||
<input id="manMachineId" type="text" class="input-field" placeholder="Machine ID *" required>
|
||||
<input id="manSerialNumber" type="text" class="input-field" placeholder="Serial Number">
|
||||
<input id="manName" type="text" class="input-field" placeholder="Asset Name *" required>
|
||||
<textarea id="manDescription" class="input-field" placeholder="Description" rows="2"></textarea>
|
||||
|
||||
<div class="form-row" style="margin-bottom:8px;">
|
||||
<select id="manCatSelect" class="input-field">
|
||||
<option value="">Category</option>
|
||||
</select>
|
||||
<select id="manMake" class="input-field" onchange="loadModels()">
|
||||
<option value="">Make</option>
|
||||
</select>
|
||||
<!-- ── SECTION: IDENTITY ──────────────────────────────────── -->
|
||||
<div class="form-collapse-section open" id="sectIdentity">
|
||||
<div class="form-collapse-header" onclick="toggleFormSection('sectIdentity')">
|
||||
<span class="fch-icon">📋</span>
|
||||
<span class="fch-title">Identity</span>
|
||||
<span class="fch-status incomplete" id="statIdentity">required</span>
|
||||
<span class="fch-chevron">▾</span>
|
||||
</div>
|
||||
<select id="manModel" class="input-field" style="margin-bottom:8px;">
|
||||
<option value="">Model</option>
|
||||
</select>
|
||||
<select id="manStatus" class="input-field" style="margin-bottom:0;">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
<option value="retired">Retired</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-collapse-body">
|
||||
<label class="field-label"><span class="required-star">*</span> Machine ID</label>
|
||||
<input id="manMachineId" type="text" class="input-field" placeholder="e.g. CAT-001" required
|
||||
oninput="clearFieldError('manMachineId'); autoSaveDraft();">
|
||||
<div class="field-error-msg" id="errManMachineId">⚠️ Machine ID is required</div>
|
||||
|
||||
<!-- Directions -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">📍 Directions & Access</div>
|
||||
<input id="manAddress" type="text" class="input-field" placeholder="Address / Trailer Number">
|
||||
<div class="form-row">
|
||||
<input id="manBuildingName" type="text" class="input-field" placeholder="Building Name">
|
||||
<input id="manBuildingNumber" type="text" class="input-field" placeholder="Building #" oninput="document.getElementById('manAddress').value = this.value">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="manFloor" type="text" class="input-field" placeholder="Floor">
|
||||
<input id="manRoom" type="text" class="input-field" placeholder="Room">
|
||||
</div>
|
||||
<textarea id="manWalkingDirections" class="input-field" placeholder="Walking directions..." rows="2"></textarea>
|
||||
<div class="form-row">
|
||||
<input id="manMapLink" type="url" class="input-field" placeholder="Map link (URL)">
|
||||
<button class="btn btn-outline btn-sm" onclick="openMapPin()" style="flex-shrink:0;">📍 Pin</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="manParkingLocation" type="text" class="input-field" placeholder="Parking location">
|
||||
<button class="btn btn-outline btn-sm" onclick="fillGpsParking('manParkingLocation')" style="flex-shrink:0;">📍 GPS</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="field-label">Serial Number</label>
|
||||
<input id="manSerialNumber" type="text" class="input-field" placeholder="Serial Number"
|
||||
oninput="autoSaveDraft();">
|
||||
|
||||
<!-- Keys -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">
|
||||
🔑 Keys
|
||||
<button class="btn btn-outline btn-sm" onclick="addKeyEntry()">+ Add Key</button>
|
||||
</div>
|
||||
<div id="manKeysList"></div>
|
||||
</div>
|
||||
<label class="field-label"><span class="required-star">*</span> Asset Name</label>
|
||||
<input id="manName" type="text" class="input-field" placeholder="e.g. Vending Machine #3" required
|
||||
oninput="clearFieldError('manName'); autoSaveDraft();">
|
||||
<div class="field-error-msg" id="errManName">⚠️ Asset Name is required</div>
|
||||
|
||||
<!-- Security Badges -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">🪪 Security Badges</div>
|
||||
<div class="badge-checklist" id="manBadgesList"></div>
|
||||
</div>
|
||||
<label class="field-label">Description</label>
|
||||
<textarea id="manDescription" class="input-field" placeholder="Description" rows="2"
|
||||
oninput="autoSaveDraft();"></textarea>
|
||||
|
||||
<!-- Customer & Location -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">🏢 Customer & Location</div>
|
||||
<select id="manCustomer" class="input-field" onchange="loadManualLocations()">
|
||||
<option value="">Customer</option>
|
||||
</select>
|
||||
<select id="manLocation" class="input-field" style="margin-bottom:0;">
|
||||
<option value="">Location</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Photo -->
|
||||
<div class="form-section">
|
||||
<div class="form-section-title">📸 Photo (optional)</div>
|
||||
<div id="manPhotoArea" class="camera-area" style="aspect-ratio:4/3;margin-bottom:8px;">
|
||||
<div id="manPhotoPlaceholder" class="camera-placeholder" onclick="startManualPhoto()">
|
||||
<span class="cam-icon">📸</span>
|
||||
<div class="cam-hint" style="margin-top:4px;">Tap to take photo</div>
|
||||
<div class="form-row" style="margin-bottom:8px;">
|
||||
<select id="manCatSelect" class="input-field" onchange="autoSaveDraft();">
|
||||
<option value="">Category</option>
|
||||
</select>
|
||||
<select id="manMake" class="input-field" onchange="loadModels();autoSaveDraft();">
|
||||
<option value="">Make</option>
|
||||
</select>
|
||||
</div>
|
||||
<video id="manPhotoVideo" autoplay playsinline muted style="display:none;"></video>
|
||||
<select id="manModel" class="input-field" style="margin-bottom:8px;" onchange="autoSaveDraft();">
|
||||
<option value="">Model</option>
|
||||
</select>
|
||||
<select id="manStatus" class="input-field" style="margin-bottom:0;" onchange="autoSaveDraft();">
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
<option value="retired">Retired</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="manPhotoCaptureRow" style="display:none;justify-content:center;margin-bottom:8px;">
|
||||
<button class="ocr-capture-btn" onclick="captureManualPhoto()" title="Capture" style="position:static;width:48px;height:48px;font-size:18px;">📸</button>
|
||||
</div>
|
||||
<img id="manPhotoPreview" style="display:none;width:100%;border-radius:var(--radius-sm);">
|
||||
<button id="manPhotoRetake" class="btn btn-outline btn-sm" style="display:none;margin-top:6px;width:100%;" onclick="retakeManualPhoto()">🔄 Retake</button>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<!-- ── SECTION: LOCATION ──────────────────────────────────── -->
|
||||
<div class="form-collapse-section" id="sectLocation">
|
||||
<div class="form-collapse-header" onclick="toggleFormSection('sectLocation')">
|
||||
<span class="fch-icon">📍</span>
|
||||
<span class="fch-title">Location & Directions</span>
|
||||
<span class="fch-chevron">▾</span>
|
||||
</div>
|
||||
<div class="form-collapse-body">
|
||||
<label class="field-label">Address / Trailer Number</label>
|
||||
<input id="manAddress" type="text" class="input-field" placeholder="Address / Trailer Number"
|
||||
oninput="autoSaveDraft();">
|
||||
|
||||
<div class="form-row">
|
||||
<input id="manBuildingName" type="text" class="input-field" placeholder="Building Name"
|
||||
oninput="autoSaveDraft();">
|
||||
<input id="manBuildingNumber" type="text" class="input-field" placeholder="Building #"
|
||||
oninput="document.getElementById('manAddress').value = this.value; autoSaveDraft();">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="manFloor" type="text" class="input-field" placeholder="Floor" oninput="autoSaveDraft();">
|
||||
<input id="manRoom" type="text" class="input-field" placeholder="Room" oninput="autoSaveDraft();">
|
||||
</div>
|
||||
<label class="field-label">Walking Directions</label>
|
||||
<textarea id="manWalkingDirections" class="input-field" placeholder="Walking directions from parking..." rows="2"
|
||||
oninput="autoSaveDraft();"></textarea>
|
||||
|
||||
<div class="form-row" style="margin-bottom:6px;">
|
||||
<input id="manMapLink" type="url" class="input-field" placeholder="Map link (URL)"
|
||||
oninput="autoSaveDraft();">
|
||||
<button class="btn btn-outline btn-sm" id="btnMapPin" onclick="openMapPin()"
|
||||
style="flex-shrink:0; min-width: 72px;">
|
||||
<span class="btn-spinner"></span>📍 Pin
|
||||
</button>
|
||||
<span class="btn-feedback" id="fbMapPin">✓ Done</span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<input id="manParkingLocation" type="text" class="input-field" placeholder="Parking location"
|
||||
oninput="autoSaveDraft();">
|
||||
<button class="btn btn-outline btn-sm" id="btnGpsParking" onclick="fillGpsParking()"
|
||||
style="flex-shrink:0; min-width: 72px;">
|
||||
<span class="btn-spinner"></span>📍 GPS
|
||||
</button>
|
||||
<span class="btn-feedback" id="fbGpsParking">✓ Done</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SECTION: KEYS & BADGES ─────────────────────────────── -->
|
||||
<div class="form-collapse-section" id="sectAccess">
|
||||
<div class="form-collapse-header" onclick="toggleFormSection('sectAccess')">
|
||||
<span class="fch-icon">🔑</span>
|
||||
<span class="fch-title">Keys & Badges</span>
|
||||
<span class="fch-chevron">▾</span>
|
||||
</div>
|
||||
<div class="form-collapse-body">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
|
||||
<span style="font-size:12px;font-weight:600;color:var(--text2);">🔑 Keys</span>
|
||||
<button class="btn btn-outline btn-sm" onclick="addKeyEntry()">+ Add Key</button>
|
||||
</div>
|
||||
<div id="manKeysList"></div>
|
||||
|
||||
<div style="font-size:12px;font-weight:600;color:var(--text2);margin:10px 0 6px;">🪪 Security Badges</div>
|
||||
<div class="badge-checklist" id="manBadgesList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SECTION: CUSTOMER ──────────────────────────────────── -->
|
||||
<div class="form-collapse-section" id="sectCustomer">
|
||||
<div class="form-collapse-header" onclick="toggleFormSection('sectCustomer')">
|
||||
<span class="fch-icon">🏢</span>
|
||||
<span class="fch-title">Customer & Location</span>
|
||||
<span class="fch-chevron">▾</span>
|
||||
</div>
|
||||
<div class="form-collapse-body">
|
||||
<label class="field-label">Customer</label>
|
||||
<select id="manCustomer" class="input-field" onchange="loadManualLocations();autoSaveDraft();">
|
||||
<option value="">Customer</option>
|
||||
</select>
|
||||
<label class="field-label">Location</label>
|
||||
<select id="manLocation" class="input-field" style="margin-bottom:0;" onchange="autoSaveDraft();">
|
||||
<option value="">Location</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SECTION: PHOTO ─────────────────────────────────────── -->
|
||||
<div class="form-collapse-section" id="sectPhoto">
|
||||
<div class="form-collapse-header" onclick="toggleFormSection('sectPhoto')">
|
||||
<span class="fch-icon">📸</span>
|
||||
<span class="fch-title">Photo <span style="font-size:11px;color:var(--text3);font-weight:400;">(optional)</span></span>
|
||||
<span class="fch-chevron">▾</span>
|
||||
</div>
|
||||
<div class="form-collapse-body">
|
||||
<div id="manPhotoArea" class="camera-area" style="aspect-ratio:4/3;margin-bottom:8px;">
|
||||
<div id="manPhotoPlaceholder" class="camera-placeholder" onclick="startManualPhoto()">
|
||||
<span class="cam-icon">📸</span>
|
||||
<div class="cam-hint" style="margin-top:4px;">Tap to take photo</div>
|
||||
</div>
|
||||
<video id="manPhotoVideo" autoplay playsinline muted style="display:none;"></video>
|
||||
</div>
|
||||
<div id="manPhotoCaptureRow" style="display:none;justify-content:center;margin-bottom:8px;">
|
||||
<button class="ocr-capture-btn" onclick="captureManualPhoto()" title="Capture"
|
||||
style="position:static;width:48px;height:48px;font-size:18px;">📸</button>
|
||||
</div>
|
||||
<div class="photo-preview-wrap" id="manPhotoPreviewWrap" style="display:none;">
|
||||
<img id="manPhotoPreview" style="width:100%;border-radius:var(--radius-sm);">
|
||||
<div class="photo-actions-row">
|
||||
<button class="btn btn-outline btn-sm" onclick="retakeManualPhoto()">🔄 Retake</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="clearManualPhoto()" style="color:var(--text2);">✕ Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── SUBMIT ─────────────────────────────────────────────── -->
|
||||
<div class="form-row" style="margin-bottom:10px;">
|
||||
<button class="btn btn-primary" onclick="submitManualAsset(false)" style="flex:2;">Create Asset</button>
|
||||
<button class="btn btn-outline" onclick="submitManualAsset(true)" style="flex:1;">+ Add Another</button>
|
||||
<button class="btn btn-primary" id="btnCreateAsset" onclick="submitManualAsset(false)" style="flex:2;">
|
||||
<span class="btn-spinner"></span>Create Asset
|
||||
</button>
|
||||
<button class="btn btn-outline" onclick="submitManualAsset(true)" style="flex:1;">
|
||||
<span class="btn-spinner"></span>+ Add Another
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Post-create check-in -->
|
||||
@@ -2724,18 +2873,20 @@
|
||||
|
||||
// Update tab panels with slide animation
|
||||
const oldPanel = document.querySelector('.tab-panel.active');
|
||||
if (oldPanel) {
|
||||
oldPanel.classList.remove('active');
|
||||
const newPanel = document.getElementById(tabId);
|
||||
if (oldPanel && oldPanel !== newPanel) {
|
||||
// Play exit animation on old panel, keeping it visible
|
||||
oldPanel.classList.add('slide-left');
|
||||
oldPanel.addEventListener('animationend', function handler() {
|
||||
oldPanel.removeEventListener('animationend', handler);
|
||||
oldPanel.classList.remove('slide-left');
|
||||
oldPanel.classList.remove('active', 'slide-left');
|
||||
}, { once: true });
|
||||
}
|
||||
const panel = document.getElementById(tabId);
|
||||
if (panel) {
|
||||
// Brief delay to let exit animation start before entry
|
||||
setTimeout(() => panel.classList.add('active'), 50);
|
||||
// Small delay then show new panel with slide-in
|
||||
setTimeout(() => {
|
||||
if (newPanel) newPanel.classList.add('active');
|
||||
}, 50);
|
||||
} else if (newPanel) {
|
||||
newPanel.classList.add('active');
|
||||
}
|
||||
|
||||
// Update bottom tabs — highlight the matching tab, or "More" for drawer-only tabs
|
||||
@@ -2916,6 +3067,17 @@
|
||||
initKeyBadges();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// T2: Load draft when switching to manual mode
|
||||
if (mode === 'manual') {
|
||||
setTimeout(() => {
|
||||
if (!loadDraft()) {
|
||||
// No draft - just ensure categories are populated
|
||||
populateCategorySelect('manCatSelect');
|
||||
}
|
||||
initKeyBadges();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared: Load settings dropdowns ─────────────────────────────────────
|
||||
@@ -4429,6 +4591,82 @@
|
||||
// Do NOT call switchTab here — the caller handles the tab transition
|
||||
}
|
||||
|
||||
// ── Navigate page asset search ────────────────────────────────────────
|
||||
let navSearchTimer = null;
|
||||
async function searchNavAssets() {
|
||||
clearTimeout(navSearchTimer);
|
||||
navSearchTimer = setTimeout(async () => {
|
||||
const q = document.getElementById('navSearchInput').value.trim();
|
||||
const results = document.getElementById('navSearchResults');
|
||||
const clearBtn = document.getElementById('clearNavSearch');
|
||||
if (clearBtn) clearBtn.style.display = q ? '' : 'none';
|
||||
if (!q) { results.innerHTML = ''; return; }
|
||||
try {
|
||||
const assets = await api('/api/assets?q=' + encodeURIComponent(q) + '&limit=8');
|
||||
if (!assets.length) {
|
||||
results.innerHTML = '<div class="empty-state" style="padding:12px;">No assets found</div>';
|
||||
return;
|
||||
}
|
||||
results.innerHTML = assets.map(a => {
|
||||
const hasLocation = a.latitude != null && a.longitude != null;
|
||||
return `<div class="asset-item" style="${hasLocation ? '' : 'opacity:0.5;'}"
|
||||
onclick="navToSearchedAsset(${a.id})">
|
||||
<div class="ai-icon cat-${cssSafe(a.category)}">📍</div>
|
||||
<div class="ai-info">
|
||||
<div class="ai-name">${esc(a.name)}</div>
|
||||
<div class="ai-meta">${esc(a.machine_id || '')} ${hasLocation ? '📍 ' + esc(a.address || 'GPS tagged') : '⚠ No location'}</div>
|
||||
</div>
|
||||
<div class="ai-arrow">→</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
results.innerHTML = '<div class="empty-state" style="padding:12px;">Search failed</div>';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function clearNavSearch() {
|
||||
const input = document.getElementById('navSearchInput');
|
||||
input.value = '';
|
||||
document.getElementById('navSearchResults').innerHTML = '';
|
||||
document.getElementById('clearNavSearch').style.display = 'none';
|
||||
input.focus();
|
||||
}
|
||||
|
||||
async function navToSearchedAsset(assetId) {
|
||||
// Fetch asset details to get navigation data
|
||||
if (!AppState.gpsLat || !AppState.gpsLng) {
|
||||
showToast('GPS location not available. Tap ◎ My Location first.', true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const nav = await api('/api/assets/' + assetId +
|
||||
'/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng);
|
||||
AppState._navData = nav;
|
||||
renderNavPage();
|
||||
// Clear search
|
||||
document.getElementById('navSearchInput').value = '';
|
||||
document.getElementById('navSearchResults').innerHTML = '';
|
||||
document.getElementById('clearNavSearch').style.display = 'none';
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function switchNavMode(mode) {
|
||||
// Re-fetch navigation with new mode
|
||||
const nav = AppState._navData;
|
||||
if (!nav) return;
|
||||
try {
|
||||
const newNav = await api('/api/assets/' + nav.asset_id +
|
||||
'/navigation?lat=' + AppState.gpsLat + '&lng=' + AppState.gpsLng + '&mode=' + mode);
|
||||
AppState._navData = newNav;
|
||||
renderNavPage();
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function clearRouteLine() {
|
||||
if (!map) return;
|
||||
if (map._routeLine) { map.removeLayer(map._routeLine); map._routeLine = null; }
|
||||
|
||||
Reference in New Issue
Block a user