feat: add swap functionality for unit and modem assignments in vibration monitoring locations
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
{{ location.name }}
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Monitoring Location • {{ project.name }}
|
||||
Monitoring Location • {{ project.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@@ -116,20 +116,36 @@
|
||||
<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">Assigned Unit</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>
|
||||
{% if assigned_unit.device_type %}
|
||||
<!-- Modem row -->
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Device Type</div>
|
||||
<div class="text-gray-900 dark:text-white">{{ assigned_unit.device_type|capitalize }}</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>
|
||||
{% endif %}
|
||||
{% if assignment %}
|
||||
<div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Assigned Since</div>
|
||||
@@ -142,10 +158,14 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<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">
|
||||
Unassign Unit
|
||||
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>
|
||||
@@ -155,7 +175,7 @@
|
||||
<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="openAssignModal()"
|
||||
<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>
|
||||
@@ -214,47 +234,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Assign Unit Modal -->
|
||||
<div id="assign-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center">
|
||||
<!-- 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 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">Attach a seismograph to this location</p>
|
||||
<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="closeAssignModal()" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||
<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="assign-form" class="p-6 space-y-4">
|
||||
<form id="swap-form" class="p-6 space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Available Units</label>
|
||||
<select id="assign-unit-id" name="unit_id"
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Seismograph <span class="text-red-500">*</span></label>
|
||||
<select id="swap-unit-id" name="unit_id"
|
||||
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>
|
||||
<option value="">Loading units...</option>
|
||||
</select>
|
||||
<p id="assign-empty" class="hidden text-xs text-gray-500 mt-2">No available seismographs for this project.</p>
|
||||
<p id="swap-units-empty" class="hidden text-xs text-gray-500 mt-1">No available seismographs.</p>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<select id="swap-modem-id" name="modem_id"
|
||||
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">
|
||||
<option value="">No modem</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
|
||||
<textarea id="assign-notes" name="notes" rows="2"
|
||||
<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="assign-error" class="hidden text-sm text-red-600"></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="closeAssignModal()"
|
||||
<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"
|
||||
<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 Unit
|
||||
Assign
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -264,6 +292,7 @@
|
||||
<script>
|
||||
const projectId = "{{ project_id }}";
|
||||
const locationId = "{{ location_id }}";
|
||||
const hasAssignment = {{ 'true' if assigned_unit else 'false' }};
|
||||
|
||||
// Tab switching
|
||||
function switchTab(tabName) {
|
||||
@@ -314,60 +343,89 @@ document.getElementById('location-settings-form').addEventListener('submit', asy
|
||||
}
|
||||
});
|
||||
|
||||
// Assign modal
|
||||
function openAssignModal() {
|
||||
document.getElementById('assign-modal').classList.remove('hidden');
|
||||
loadAvailableUnits();
|
||||
// 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 = '';
|
||||
await Promise.all([loadSwapUnits(), loadSwapModems()]);
|
||||
}
|
||||
|
||||
function closeAssignModal() {
|
||||
document.getElementById('assign-modal').classList.add('hidden');
|
||||
function closeSwapModal() {
|
||||
document.getElementById('swap-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function loadAvailableUnits() {
|
||||
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 available units');
|
||||
if (!response.ok) throw new Error('Failed to load units');
|
||||
const data = await response.json();
|
||||
const select = document.getElementById('assign-unit-id');
|
||||
select.innerHTML = '<option value="">Select a unit</option>';
|
||||
const select = document.getElementById('swap-unit-id');
|
||||
select.innerHTML = '<option value="">Select a seismograph</option>';
|
||||
|
||||
if (!data.length) {
|
||||
document.getElementById('assign-empty').classList.remove('hidden');
|
||||
return;
|
||||
document.getElementById('swap-units-empty').classList.remove('hidden');
|
||||
} else {
|
||||
document.getElementById('swap-units-empty').classList.add('hidden');
|
||||
}
|
||||
|
||||
data.forEach(unit => {
|
||||
const option = document.createElement('option');
|
||||
option.value = unit.id;
|
||||
option.textContent = `${unit.id} • ${unit.model || unit.device_type}`;
|
||||
option.textContent = unit.id + (unit.model ? ` \u2022 ${unit.model}` : '') + (unit.location ? ` \u2014 ${unit.location}` : '');
|
||||
select.appendChild(option);
|
||||
});
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('assign-error');
|
||||
errorEl.textContent = err.message || 'Failed to load units.';
|
||||
errorEl.classList.remove('hidden');
|
||||
document.getElementById('swap-error').textContent = 'Failed to load seismographs.';
|
||||
document.getElementById('swap-error').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('assign-form').addEventListener('submit', async function(e) {
|
||||
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();
|
||||
const select = document.getElementById('swap-modem-id');
|
||||
select.innerHTML = '<option value="">No modem</option>';
|
||||
|
||||
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);
|
||||
});
|
||||
} catch (err) {
|
||||
// Modem list failure is non-fatal — just leave blank
|
||||
console.warn('Failed to load modems:', err);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('swap-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const unitId = document.getElementById('assign-unit-id').value;
|
||||
const notes = document.getElementById('assign-notes').value.trim();
|
||||
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('assign-error').textContent = 'Select a unit to assign.';
|
||||
document.getElementById('assign-error').classList.remove('hidden');
|
||||
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);
|
||||
formData.append('notes', notes);
|
||||
if (modemId) formData.append('modem_id', modemId);
|
||||
if (notes) formData.append('notes', notes);
|
||||
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}/assign`, {
|
||||
const response = await fetch(`/api/projects/${projectId}/locations/${locationId}/swap`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
@@ -379,9 +437,8 @@ document.getElementById('assign-form').addEventListener('submit', async function
|
||||
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
const errorEl = document.getElementById('assign-error');
|
||||
errorEl.textContent = err.message || 'Failed to assign unit.';
|
||||
errorEl.classList.remove('hidden');
|
||||
document.getElementById('swap-error').textContent = err.message || 'Failed to assign unit.';
|
||||
document.getElementById('swap-error').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -405,11 +462,11 @@ async function unassignUnit(assignmentId) {
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closeAssignModal();
|
||||
if (e.key === 'Escape') closeSwapModal();
|
||||
});
|
||||
|
||||
document.getElementById('assign-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) closeAssignModal();
|
||||
document.getElementById('swap-modal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) closeSwapModal();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user