diff --git a/templates/vibration_location_detail.html b/templates/vibration_location_detail.html index 6afc9d8..6512948 100644 --- a/templates/vibration_location_detail.html +++ b/templates/vibration_location_detail.html @@ -249,22 +249,36 @@ -
+ +
- - + + +
+
Loading...
+
+
+
- - + + +
+
Loading...
+
+
@@ -350,6 +364,10 @@ async function openSwapModal() { document.getElementById('swap-submit-btn').textContent = hasAssignment ? 'Swap' : 'Assign'; document.getElementById('swap-error').classList.add('hidden'); document.getElementById('swap-notes').value = ''; + document.getElementById('swap-unit-search').value = ''; + document.getElementById('swap-modem-search').value = ''; + document.getElementById('swap-unit-id').value = ''; + document.getElementById('swap-modem-id').value = ''; await Promise.all([loadSwapUnits(), loadSwapModems()]); } @@ -357,26 +375,94 @@ function closeSwapModal() { document.getElementById('swap-modal').classList.add('hidden'); } +let _swapUnits = []; +let _swapModems = []; + +function _fuzzyMatch(query, text) { + if (!query) return true; + const q = query.toLowerCase(); + const t = text.toLowerCase(); + // Substring match first (fast), then character-sequence fuzzy + if (t.includes(q)) return true; + let qi = 0; + for (let i = 0; i < t.length && qi < q.length; i++) { + if (t[i] === q[qi]) qi++; + } + return qi === q.length; +} + +function _renderSwapList(type, items, selectedId, noSelectionLabel) { + const listEl = document.getElementById(`swap-${type}-list`); + if (!items.length) { + listEl.innerHTML = `
No results
`; + return; + } + listEl.innerHTML = items.map(item => { + const isSelected = item.value === selectedId; + return ``; + }).join(''); +} + +function selectSwapItem(type, value, btn) { + document.getElementById(`swap-${type}-id`).value = value; + // Update visual state + const list = document.getElementById(`swap-${type}-list`); + list.querySelectorAll('button').forEach(b => { + b.classList.remove('bg-orange-50', 'dark:bg-orange-900/20'); + b.querySelector('.rounded-full').className = 'w-4 h-4 rounded-full border-2 shrink-0 ml-3 border-gray-400 dark:border-gray-500'; + }); + btn.classList.add('bg-orange-50', 'dark:bg-orange-900/20'); + btn.querySelector('.rounded-full').className = 'w-4 h-4 rounded-full border-2 shrink-0 ml-3 border-seismo-orange bg-seismo-orange'; +} + +function filterSwapList(type) { + const query = document.getElementById(`swap-${type}-search`).value; + const items = type === 'unit' ? _swapUnits : _swapModems; + const selectedId = document.getElementById(`swap-${type}-id`).value; + const filtered = items.filter(item => + _fuzzyMatch(query, item.primary + ' ' + (item.secondary || '') + ' ' + (item.searchText || '')) + ); + _renderSwapList(type, filtered, selectedId, type === 'modem' ? 'No modem' : null); + // Re-add "No modem" option for modems + if (type === 'modem') { + const listEl = document.getElementById('swap-modem-list'); + const noModemBtn = ``; + listEl.insertAdjacentHTML('afterbegin', noModemBtn); + } +} + async function loadSwapUnits() { try { const response = await fetch(`/api/projects/${projectId}/available-units?location_type=vibration`); if (!response.ok) throw new Error('Failed to load units'); const data = await response.json(); - const select = document.getElementById('swap-unit-id'); - select.innerHTML = ''; if (!data.length) { document.getElementById('swap-units-empty').classList.remove('hidden'); - } else { - document.getElementById('swap-units-empty').classList.add('hidden'); + document.getElementById('swap-unit-list').innerHTML = '
No available seismographs.
'; + return; } - - data.forEach(unit => { - const option = document.createElement('option'); - option.value = unit.id; - option.textContent = unit.id + (unit.model ? ` \u2022 ${unit.model}` : '') + (unit.location ? ` \u2014 ${unit.location}` : ''); - select.appendChild(option); - }); + document.getElementById('swap-units-empty').classList.add('hidden'); + _swapUnits = data.map(u => ({ + value: u.id, + primary: u.id, + secondary: [u.model, u.location].filter(Boolean).join(' — '), + searchText: u.model + ' ' + u.location, + })); + _renderSwapList('unit', _swapUnits, document.getElementById('swap-unit-id').value); } catch (err) { document.getElementById('swap-error').textContent = 'Failed to load seismographs.'; document.getElementById('swap-error').classList.remove('hidden'); @@ -388,21 +474,16 @@ async function loadSwapModems() { const response = await fetch(`/api/projects/${projectId}/available-modems`); if (!response.ok) throw new Error('Failed to load modems'); const data = await response.json(); - const select = document.getElementById('swap-modem-id'); - select.innerHTML = ''; - - data.forEach(modem => { - const option = document.createElement('option'); - option.value = modem.id; - let label = modem.id; - if (modem.hardware_model) label += ` \u2022 ${modem.hardware_model}`; - if (modem.ip_address) label += ` \u2014 ${modem.ip_address}`; - option.textContent = label; - select.appendChild(option); - }); + _swapModems = data.map(m => ({ + value: m.id, + primary: m.id, + secondary: [m.hardware_model, m.ip_address].filter(Boolean).join(' — '), + searchText: (m.hardware_model || '') + ' ' + (m.ip_address || ''), + })); + filterSwapList('modem'); // renders with "No modem" prepended } catch (err) { - // Modem list failure is non-fatal — just leave blank console.warn('Failed to load modems:', err); + document.getElementById('swap-modem-list').innerHTML = '
Could not load modems.
'; } }