554 lines
28 KiB
HTML
554 lines
28 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ location.name }} - Monitoring Location{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Breadcrumb Navigation -->
|
|
<div class="mb-6">
|
|
<nav class="flex items-center space-x-2 text-sm">
|
|
<a href="/projects" class="text-seismo-orange hover:text-seismo-navy flex items-center">
|
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
|
</svg>
|
|
Projects
|
|
</a>
|
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
<a href="/projects/{{ project_id }}" class="text-seismo-orange hover:text-seismo-navy">
|
|
{{ project.name }}
|
|
</a>
|
|
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
</svg>
|
|
<span class="text-gray-900 dark:text-white font-medium">{{ location.name }}</span>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
<svg class="w-8 h-8 mr-3 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
</svg>
|
|
{{ location.name }}
|
|
</h1>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">
|
|
Monitoring Location • {{ project.name }}
|
|
</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
{% if assigned_unit %}
|
|
<span class="px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
</svg>
|
|
Unit Assigned
|
|
</span>
|
|
{% else %}
|
|
<span class="px-3 py-1 rounded-full text-sm font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">
|
|
No Unit Assigned
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab Navigation -->
|
|
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
|
<nav class="flex space-x-6">
|
|
<button onclick="switchTab('overview')"
|
|
data-tab="overview"
|
|
class="tab-button px-4 py-3 border-b-2 font-medium text-sm transition-colors border-seismo-orange text-seismo-orange">
|
|
Overview
|
|
</button>
|
|
<button onclick="switchTab('settings')"
|
|
data-tab="settings"
|
|
class="tab-button px-4 py-3 border-b-2 border-transparent font-medium text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:border-gray-300 dark:hover:border-gray-600 transition-colors">
|
|
Settings
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div id="tab-content">
|
|
<!-- Overview Tab -->
|
|
<div id="overview-tab" class="tab-panel">
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Location Details Card -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Location Details</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Name</div>
|
|
<div class="text-lg font-medium text-gray-900 dark:text-white">{{ location.name }}</div>
|
|
</div>
|
|
{% if location.description %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Description</div>
|
|
<div class="text-gray-900 dark:text-white">{{ location.description }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if location.address %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Address</div>
|
|
<div class="text-gray-900 dark:text-white">{{ location.address }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if location.coordinates %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Coordinates</div>
|
|
<div class="text-gray-900 dark:text-white font-mono text-sm">{{ location.coordinates }}</div>
|
|
</div>
|
|
{% endif %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Created</div>
|
|
<div class="text-gray-900 dark:text-white">{{ location.created_at|local_datetime if location.created_at else 'N/A' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assignment Card -->
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Unit Assignment</h2>
|
|
{% if assigned_unit %}
|
|
<div class="space-y-4">
|
|
<!-- Seismograph row -->
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Seismograph</div>
|
|
<div class="text-lg font-medium text-gray-900 dark:text-white">
|
|
<a href="/unit/{{ assigned_unit.id }}" class="text-seismo-orange hover:text-seismo-navy">
|
|
{{ assigned_unit.id }}
|
|
</a>
|
|
</div>
|
|
{% if assigned_unit.unit_type %}
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{{ assigned_unit.unit_type }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<!-- Modem row -->
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Modem</div>
|
|
{% if assigned_modem %}
|
|
<div class="text-lg font-medium text-gray-900 dark:text-white">
|
|
<a href="/unit/{{ assigned_modem.id }}" class="text-seismo-orange hover:text-seismo-navy">
|
|
{{ assigned_modem.id }}
|
|
</a>
|
|
</div>
|
|
{% if assigned_modem.hardware_model or assigned_modem.ip_address %}
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
|
{{ assigned_modem.hardware_model or '' }}{% if assigned_modem.hardware_model and assigned_modem.ip_address %} • {% endif %}{{ assigned_modem.ip_address or '' }}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-sm text-gray-400 dark:text-gray-500 italic">No modem paired</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if assignment %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Assigned Since</div>
|
|
<div class="text-gray-900 dark:text-white">{{ assignment.assigned_at|local_datetime if assignment.assigned_at else 'N/A' }}</div>
|
|
</div>
|
|
{% if assignment.notes %}
|
|
<div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400">Notes</div>
|
|
<div class="text-gray-900 dark:text-white text-sm">{{ assignment.notes }}</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
<div class="pt-2 flex gap-2 flex-wrap">
|
|
<button onclick="openSwapModal()"
|
|
class="px-4 py-2 bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors text-sm">
|
|
Swap Unit / Modem
|
|
</button>
|
|
<button onclick="unassignUnit('{{ assignment.id }}')"
|
|
class="px-4 py-2 bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300 rounded-lg hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors text-sm">
|
|
Unassign
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-8">
|
|
<svg class="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
|
</svg>
|
|
<p class="text-gray-500 dark:text-gray-400 mb-4">No unit currently assigned</p>
|
|
<button onclick="openSwapModal()"
|
|
class="px-4 py-2 bg-seismo-orange text-white rounded-lg hover:bg-seismo-navy transition-colors">
|
|
Assign a Unit
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Tab -->
|
|
<div id="settings-tab" class="tab-panel hidden">
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Location Settings</h2>
|
|
|
|
<form id="location-settings-form" class="space-y-6">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Name</label>
|
|
<input type="text" id="settings-name" value="{{ location.name }}"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white" required>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description</label>
|
|
<textarea id="settings-description" rows="3"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">{{ location.description or '' }}</textarea>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address</label>
|
|
<input type="text" id="settings-address" value="{{ location.address or '' }}"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Coordinates</label>
|
|
<input type="text" id="settings-coordinates" value="{{ location.coordinates or '' }}"
|
|
placeholder="40.7128,-74.0060"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
|
<p class="text-xs text-gray-500 mt-1">Format: latitude,longitude</p>
|
|
</div>
|
|
|
|
<div id="settings-error" class="hidden text-sm text-red-600"></div>
|
|
|
|
<div class="flex justify-end gap-3 pt-2">
|
|
<button type="button" onclick="window.location.href='/projects/{{ project_id }}'"
|
|
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
Cancel
|
|
</button>
|
|
<button type="submit"
|
|
class="px-6 py-2 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg font-medium">
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assign / Swap Modal -->
|
|
<div id="swap-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto m-4">
|
|
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
<div>
|
|
<h2 id="swap-modal-title" class="text-2xl font-bold text-gray-900 dark:text-white">Assign Unit</h2>
|
|
<p class="text-gray-600 dark:text-gray-400 mt-1">Select a seismograph and optionally a modem for this location</p>
|
|
</div>
|
|
<button onclick="closeSwapModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<form id="swap-form" class="p-6 space-y-5">
|
|
<!-- Seismograph picker -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Seismograph <span class="text-red-500">*</span>
|
|
</label>
|
|
<input id="swap-unit-search" type="text" placeholder="Search by ID or model..."
|
|
oninput="filterSwapList('unit')"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm mb-2 focus:ring-2 focus:ring-seismo-orange">
|
|
<div id="swap-unit-list"
|
|
class="max-h-48 overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-gray-700">
|
|
<div class="px-3 py-6 text-center text-sm text-gray-400">Loading...</div>
|
|
</div>
|
|
<input type="hidden" id="swap-unit-id" name="unit_id" required>
|
|
<p id="swap-units-empty" class="hidden text-xs text-gray-500 mt-1">No available seismographs.</p>
|
|
</div>
|
|
|
|
<!-- Modem picker -->
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Modem <span class="text-xs text-gray-400">(optional)</span>
|
|
</label>
|
|
<input id="swap-modem-search" type="text" placeholder="Search by ID, model, or IP..."
|
|
oninput="filterSwapList('modem')"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm mb-2 focus:ring-2 focus:ring-seismo-orange">
|
|
<div id="swap-modem-list"
|
|
class="max-h-40 overflow-y-auto rounded-lg border border-gray-200 dark:border-gray-700 divide-y divide-gray-100 dark:divide-gray-700 bg-white dark:bg-gray-700">
|
|
<div class="px-3 py-6 text-center text-sm text-gray-400">Loading...</div>
|
|
</div>
|
|
<input type="hidden" id="swap-modem-id" name="modem_id">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
|
|
<textarea id="swap-notes" name="notes" rows="2"
|
|
class="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white"></textarea>
|
|
</div>
|
|
|
|
<div id="swap-error" class="hidden text-sm text-red-600"></div>
|
|
|
|
<div class="flex justify-end gap-3 pt-2">
|
|
<button type="button" onclick="closeSwapModal()"
|
|
class="px-6 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" id="swap-submit-btn"
|
|
class="px-6 py-2 bg-seismo-orange hover:bg-seismo-navy text-white rounded-lg font-medium">
|
|
Assign
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const projectId = "{{ project_id }}";
|
|
const locationId = "{{ location_id }}";
|
|
const hasAssignment = {{ 'true' if assigned_unit else 'false' }};
|
|
|
|
// Tab switching
|
|
function switchTab(tabName) {
|
|
document.querySelectorAll('.tab-panel').forEach(panel => {
|
|
panel.classList.add('hidden');
|
|
});
|
|
document.querySelectorAll('.tab-button').forEach(button => {
|
|
button.classList.remove('border-seismo-orange', 'text-seismo-orange');
|
|
button.classList.add('border-transparent', 'text-gray-600', 'dark:text-gray-400');
|
|
});
|
|
const panel = document.getElementById(`${tabName}-tab`);
|
|
if (panel) panel.classList.remove('hidden');
|
|
const button = document.querySelector(`[data-tab="${tabName}"]`);
|
|
if (button) {
|
|
button.classList.remove('border-transparent', 'text-gray-600', 'dark:text-gray-400');
|
|
button.classList.add('border-seismo-orange', 'text-seismo-orange');
|
|
}
|
|
}
|
|
|
|
// Location settings form submission
|
|
document.getElementById('location-settings-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const payload = {
|
|
name: document.getElementById('settings-name').value.trim(),
|
|
description: document.getElementById('settings-description').value.trim() || null,
|
|
address: document.getElementById('settings-address').value.trim() || null,
|
|
coordinates: document.getElementById('settings-coordinates').value.trim() || null,
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}`, {
|
|
method: 'PUT',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.detail || 'Failed to update location');
|
|
}
|
|
|
|
window.location.reload();
|
|
} catch (err) {
|
|
const errorEl = document.getElementById('settings-error');
|
|
errorEl.textContent = err.message || 'Failed to update location.';
|
|
errorEl.classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
// Swap / Assign modal
|
|
async function openSwapModal() {
|
|
document.getElementById('swap-modal').classList.remove('hidden');
|
|
document.getElementById('swap-modal-title').textContent = hasAssignment ? 'Swap Unit / Modem' : 'Assign Unit';
|
|
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()]);
|
|
}
|
|
|
|
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 = `<div class="px-3 py-4 text-center text-sm text-gray-400">No results</div>`;
|
|
return;
|
|
}
|
|
listEl.innerHTML = items.map(item => {
|
|
const isSelected = item.value === selectedId;
|
|
return `<button type="button"
|
|
onclick="selectSwapItem('${type}', '${item.value}', this)"
|
|
class="w-full text-left px-3 py-2.5 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors ${isSelected ? 'bg-orange-50 dark:bg-orange-900/20' : ''}">
|
|
<div>
|
|
<span class="font-medium text-gray-900 dark:text-white text-sm">${item.primary}</span>
|
|
${item.secondary ? `<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">${item.secondary}</span>` : ''}
|
|
</div>
|
|
<div class="w-4 h-4 rounded-full border-2 shrink-0 ml-3 ${isSelected ? 'border-seismo-orange bg-seismo-orange' : 'border-gray-400 dark:border-gray-500'}"></div>
|
|
</button>`;
|
|
}).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 = `<button type="button"
|
|
onclick="selectSwapItem('modem', '', this)"
|
|
class="w-full text-left px-3 py-2.5 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors ${!selectedId ? 'bg-orange-50 dark:bg-orange-900/20' : ''}">
|
|
<span class="text-sm text-gray-500 dark:text-gray-400 italic">No modem</span>
|
|
<div class="w-4 h-4 rounded-full border-2 shrink-0 ml-3 ${!selectedId ? 'border-seismo-orange bg-seismo-orange' : 'border-gray-400 dark:border-gray-500'}"></div>
|
|
</button>`;
|
|
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();
|
|
|
|
if (!data.length) {
|
|
document.getElementById('swap-units-empty').classList.remove('hidden');
|
|
document.getElementById('swap-unit-list').innerHTML = '<div class="px-3 py-4 text-center text-sm text-gray-400">No available seismographs.</div>';
|
|
return;
|
|
}
|
|
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');
|
|
}
|
|
}
|
|
|
|
async function loadSwapModems() {
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectId}/available-modems`);
|
|
if (!response.ok) throw new Error('Failed to load modems');
|
|
const data = await response.json();
|
|
_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) {
|
|
console.warn('Failed to load modems:', err);
|
|
document.getElementById('swap-modem-list').innerHTML = '<div class="px-3 py-4 text-center text-sm text-gray-400">Could not load modems.</div>';
|
|
}
|
|
}
|
|
|
|
document.getElementById('swap-form').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
const unitId = document.getElementById('swap-unit-id').value;
|
|
const modemId = document.getElementById('swap-modem-id').value;
|
|
const notes = document.getElementById('swap-notes').value.trim();
|
|
|
|
if (!unitId) {
|
|
document.getElementById('swap-error').textContent = 'Please select a seismograph.';
|
|
document.getElementById('swap-error').classList.remove('hidden');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('unit_id', unitId);
|
|
if (modemId) formData.append('modem_id', modemId);
|
|
if (notes) formData.append('notes', notes);
|
|
|
|
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}/swap`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.detail || 'Failed to assign unit');
|
|
}
|
|
|
|
window.location.reload();
|
|
} catch (err) {
|
|
document.getElementById('swap-error').textContent = err.message || 'Failed to assign unit.';
|
|
document.getElementById('swap-error').classList.remove('hidden');
|
|
}
|
|
});
|
|
|
|
async function unassignUnit(assignmentId) {
|
|
if (!confirm('Unassign this unit from the location?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/projects/${projectId}/assignments/${assignmentId}/unassign`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json().catch(() => ({}));
|
|
throw new Error(data.detail || 'Failed to unassign unit');
|
|
}
|
|
|
|
window.location.reload();
|
|
} catch (err) {
|
|
alert(err.message || 'Failed to unassign unit.');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') closeSwapModal();
|
|
});
|
|
|
|
document.getElementById('swap-modal')?.addEventListener('click', function(e) {
|
|
if (e.target === this) closeSwapModal();
|
|
});
|
|
</script>
|
|
{% endblock %}
|