@@ -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.
';
}
}