Files
terra-view/templates/roster.html
2026-01-06 07:50:58 +00:00

948 lines
47 KiB
HTML

{% extends "base.html" %}
{% block title %}Fleet Roster - Seismo Fleet Manager{% endblock %}
{% block content %}
<div class="mb-8">
<div class="flex justify-between items-start">
<div>
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Fleet Roster</h1>
<p class="text-gray-600 dark:text-gray-400 mt-1">Real-time status of all seismograph units</p>
</div>
<div class="flex gap-3">
<button onclick="openAddUnitModal()" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg flex items-center gap-2 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
Add Unit
</button>
<button onclick="openImportModal()" class="px-4 py-2 bg-seismo-navy hover:bg-blue-800 text-white rounded-lg flex items-center gap-2 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Import CSV
</button>
</div>
</div>
</div>
<!-- Unknown Emitters Section -->
<div hx-get="/partials/unknown-emitters" hx-trigger="load, every 10s" hx-swap="innerHTML">
<!-- Loading placeholder -->
</div>
<!-- Fleet Roster with Tabs -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<!-- Search Bar -->
<div class="mb-4">
<div class="relative">
<input
type="text"
id="roster-search"
placeholder="Search by Unit ID, Type, or Note..."
class="w-full px-4 py-2 pl-10 pr-4 text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-seismo-orange"
onkeyup="filterRosterTable()">
<svg class="absolute left-3 top-3 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
</div>
</div>
<!-- Tab Bar -->
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<button
class="px-4 py-2 text-sm font-medium roster-tab-button active-roster-tab"
data-endpoint="/partials/roster-deployed"
hx-get="/partials/roster-deployed"
hx-target="#roster-content"
hx-swap="innerHTML">
Deployed
</button>
<button
class="px-4 py-2 text-sm font-medium roster-tab-button"
data-endpoint="/partials/roster-benched"
hx-get="/partials/roster-benched"
hx-target="#roster-content"
hx-swap="innerHTML">
Benched
</button>
<button
class="px-4 py-2 text-sm font-medium roster-tab-button"
data-endpoint="/partials/roster-retired"
hx-get="/partials/roster-retired"
hx-target="#roster-content"
hx-swap="innerHTML">
Retired
</button>
<button
class="px-4 py-2 text-sm font-medium roster-tab-button"
data-endpoint="/partials/roster-ignored"
hx-get="/partials/roster-ignored"
hx-target="#roster-content"
hx-swap="innerHTML">
Ignored
</button>
</div>
<!-- Tab Content Target -->
<div id="roster-content"
hx-get="/partials/roster-deployed"
hx-trigger="load"
hx-swap="innerHTML">
<p class="text-gray-500 dark:text-gray-400">Loading roster data...</p>
</div>
</div>
<!-- Add Unit Modal -->
<div id="addUnitModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Add Unit</h2>
<button onclick="closeAddUnitModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<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>
</div>
<form id="addUnitForm" hx-post="/api/roster/add" hx-swap="none" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit ID *</label>
<input type="text" name="id" required
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
placeholder="BE1234">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Device Type *</label>
<select name="device_type" id="deviceTypeSelect" onchange="toggleDeviceFields()"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<option value="seismograph">Seismograph</option>
<option value="modem">Modem</option>
<option value="sound_level_meter">Sound Level Meter</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit Type</label>
<input type="text" name="unit_type" value="series3"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project ID</label>
<input type="text" name="project_id"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
placeholder="PROJ-001">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Location</label>
<input type="text" name="location"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
placeholder="San Francisco, CA">
</div>
<!-- Seismograph-specific fields -->
<div id="seismographFields" class="space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Seismograph Information</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Last Calibrated</label>
<input type="date" name="last_calibrated"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Next Calibration Due</label>
<input type="date" name="next_calibration_due"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Typically 1 year after last calibration</p>
</div>
<div id="modemPairingField" class="hidden">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Modem</label>
<input type="text" name="deployed_with_modem_id" placeholder="Modem ID"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Only needed when deployed</p>
</div>
</div>
<!-- Modem-specific fields -->
<div id="modemFields" class="hidden space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Modem Information</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">IP Address</label>
<input type="text" name="ip_address" placeholder="192.168.1.100"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone Number</label>
<input type="text" name="phone_number" placeholder="+1-555-0123"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Hardware Model</label>
<input type="text" name="hardware_model" placeholder="e.g., Raven XTV"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
</div>
<!-- Sound Level Meter-specific fields -->
<div id="slmFields" class="hidden space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Sound Level Meter Information</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">SLM Model</label>
<input type="text" name="slm_model" placeholder="NL-43"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Host/IP Address</label>
<input type="text" name="slm_host" placeholder="192.168.1.100"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">TCP Port</label>
<input type="number" name="slm_tcp_port" placeholder="2255"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">FTP Port</label>
<input type="number" name="slm_ftp_port" placeholder="21"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Serial Number</label>
<input type="text" name="slm_serial_number" placeholder="SN123456"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Frequency Weighting</label>
<select name="slm_frequency_weighting"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<option value="A">A-weighting</option>
<option value="C">C-weighting</option>
<option value="Z">Z-weighting (Flat)</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Time Weighting</label>
<select name="slm_time_weighting"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<option value="F">F (Fast)</option>
<option value="S">S (Slow)</option>
<option value="I">I (Impulse)</option>
</select>
</div>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="deployed" id="deployedCheckbox" value="true" checked onchange="toggleModemPairing()"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="retired" id="retiredCheckbox" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Retired</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
<textarea name="note" rows="3"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"
placeholder="Additional notes..."></textarea>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors">
Add Unit
</button>
<button type="button" onclick="closeAddUnitModal()" class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg transition-colors">
Cancel
</button>
</div>
</form>
</div>
</div>
<!-- Edit Unit Modal -->
<div id="editUnitModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Edit Unit</h2>
<button onclick="closeEditUnitModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<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>
</div>
<form id="editUnitForm" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit ID</label>
<input type="text" name="id" id="editUnitId" readonly
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Device Type *</label>
<select name="device_type" id="editDeviceTypeSelect" onchange="toggleEditDeviceFields()"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<option value="seismograph">Seismograph</option>
<option value="modem">Modem</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Unit Type</label>
<input type="text" name="unit_type" id="editUnitType"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Project ID</label>
<input type="text" name="project_id" id="editProjectId"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Address</label>
<input type="text" name="address" id="editAddress" placeholder="123 Main St, City, State"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Coordinates</label>
<input type="text" name="coordinates" id="editCoordinates" placeholder="34.0522,-118.2437"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange font-mono">
</div>
<!-- Seismograph-specific fields -->
<div id="editSeismographFields" class="space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Seismograph Information</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Last Calibrated</label>
<input type="date" name="last_calibrated" id="editLastCalibrated"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Next Calibration Due</label>
<input type="date" name="next_calibration_due" id="editNextCalibrationDue"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div id="editModemPairingField" class="hidden">
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Deployed With Modem</label>
<input type="text" name="deployed_with_modem_id" id="editDeployedWithModemId" placeholder="Modem ID"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
</div>
<!-- Modem-specific fields -->
<div id="editModemFields" class="hidden space-y-4 border-t border-gray-200 dark:border-gray-700 pt-4">
<p class="text-sm font-semibold text-gray-700 dark:text-gray-300">Modem Information</p>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">IP Address</label>
<input type="text" name="ip_address" id="editIpAddress" placeholder="192.168.1.100"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Phone Number</label>
<input type="text" name="phone_number" id="editPhoneNumber" placeholder="+1-555-0123"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Hardware Model</label>
<input type="text" name="hardware_model" id="editHardwareModel" placeholder="e.g., Raven XTV"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
</div>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="deployed" id="editDeployedCheckbox" value="true" onchange="toggleEditModemPairing()"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" name="retired" id="editRetiredCheckbox" value="true"
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<span class="text-sm text-gray-700 dark:text-gray-300">Retired</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Notes</label>
<textarea name="note" id="editNote" rows="3"
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange"></textarea>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg transition-colors">
Save Changes
</button>
<button type="button" onclick="closeEditUnitModal()" class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg transition-colors">
Cancel
</button>
</div>
</form>
</div>
</div>
<!-- Import CSV Modal -->
<div id="importModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full mx-4">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">Import CSV</h2>
<button onclick="closeImportModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<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>
</div>
<form id="importForm" class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">CSV File *</label>
<input type="file" name="file" accept=".csv" required
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-seismo-orange">
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Format: unit_id,unit_type,deployed,retired,note,project_id,location</p>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" name="update_existing" id="updateExisting" checked
class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
<label for="updateExisting" class="text-sm text-gray-700 dark:text-gray-300 cursor-pointer">Update existing units</label>
</div>
<div class="flex gap-3 pt-4">
<button type="submit" class="flex-1 px-4 py-2 bg-seismo-navy hover:bg-blue-800 text-white rounded-lg transition-colors">
Import
</button>
<button type="button" onclick="closeImportModal()" class="px-4 py-2 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-700 dark:text-white rounded-lg transition-colors">
Cancel
</button>
</div>
<div id="importResult" class="hidden mt-4 p-4 rounded-lg"></div>
</form>
</div>
</div>
<script>
// Add Unit Modal
function openAddUnitModal() {
document.getElementById('addUnitModal').classList.remove('hidden');
toggleDeviceFields(); // Initialize field visibility
}
function closeAddUnitModal() {
document.getElementById('addUnitModal').classList.add('hidden');
document.getElementById('addUnitForm').reset();
}
// Toggle device-specific fields based on device type selection
function toggleDeviceFields() {
const deviceType = document.getElementById('deviceTypeSelect').value;
const seismoFields = document.getElementById('seismographFields');
const modemFields = document.getElementById('modemFields');
const slmFields = document.getElementById('slmFields');
if (deviceType === 'seismograph') {
seismoFields.classList.remove('hidden');
modemFields.classList.add('hidden');
slmFields.classList.add('hidden');
// Enable seismograph fields, disable others
setFieldsDisabled(seismoFields, false);
setFieldsDisabled(modemFields, true);
setFieldsDisabled(slmFields, true);
toggleModemPairing(); // Check if modem pairing should be shown
} else if (deviceType === 'modem') {
seismoFields.classList.add('hidden');
modemFields.classList.remove('hidden');
slmFields.classList.add('hidden');
// Enable modem fields, disable others
setFieldsDisabled(seismoFields, true);
setFieldsDisabled(modemFields, false);
setFieldsDisabled(slmFields, true);
} else if (deviceType === 'sound_level_meter') {
seismoFields.classList.add('hidden');
modemFields.classList.add('hidden');
slmFields.classList.remove('hidden');
// Enable SLM fields, disable others
setFieldsDisabled(seismoFields, true);
setFieldsDisabled(modemFields, true);
setFieldsDisabled(slmFields, false);
}
}
// Helper function to disable/enable all inputs in a container
function setFieldsDisabled(container, disabled) {
if (!container) return;
const inputs = container.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
input.disabled = disabled;
});
}
// Toggle modem pairing field visibility (only for deployed seismographs)
function toggleModemPairing() {
const deviceType = document.getElementById('deviceTypeSelect').value;
const deployedCheckbox = document.getElementById('deployedCheckbox');
const modemPairingField = document.getElementById('modemPairingField');
if (deviceType === 'seismograph' && deployedCheckbox.checked) {
modemPairingField.classList.remove('hidden');
} else {
modemPairingField.classList.add('hidden');
}
}
// Add unknown unit to roster
function addUnknownUnit(unitId) {
openAddUnitModal();
// Pre-fill the unit ID
document.querySelector('#addUnitForm input[name="id"]').value = unitId;
// Set deployed to true by default
document.querySelector('#addUnitForm input[name="deployed"]').checked = true;
// Trigger field visibility updates
toggleModemPairing(); // Show modem pairing field for deployed seismographs
}
// Ignore unknown unit
async function ignoreUnknownUnit(unitId) {
if (!confirm(`Ignore unit ${unitId}? It will no longer appear in the unknown emitters list.`)) {
return;
}
try {
const formData = new FormData();
formData.append('reason', 'Ignored from unknown emitters');
const response = await fetch(`/api/roster/ignore/${unitId}`, {
method: 'POST',
body: formData
});
if (response.ok) {
// Trigger refresh of unknown emitters
htmx.trigger(document.querySelector('[hx-get="/partials/unknown-emitters"]'), 'load');
} else {
const result = await response.json();
alert(`Error ignoring unit: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error ignoring unit: ${error.message}`);
}
}
// Import Modal
function openImportModal() {
document.getElementById('importModal').classList.remove('hidden');
}
function closeImportModal() {
document.getElementById('importModal').classList.add('hidden');
document.getElementById('importForm').reset();
document.getElementById('importResult').classList.add('hidden');
}
// Handle Add Unit form submission
document.getElementById('addUnitForm').addEventListener('htmx:afterRequest', function(event) {
if (event.detail.successful) {
closeAddUnitModal();
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
// Show success message
alert('Unit added successfully!');
} else {
alert('Error adding unit. Please check the form and try again.');
}
});
// Edit Unit Modal Functions
function openEditUnitModal() {
document.getElementById('editUnitModal').classList.remove('hidden');
}
function closeEditUnitModal() {
document.getElementById('editUnitModal').classList.add('hidden');
document.getElementById('editUnitForm').reset();
}
// Toggle device-specific fields in edit modal
function toggleEditDeviceFields() {
const deviceType = document.getElementById('editDeviceTypeSelect').value;
const seismoFields = document.getElementById('editSeismographFields');
const modemFields = document.getElementById('editModemFields');
if (deviceType === 'seismograph') {
seismoFields.classList.remove('hidden');
modemFields.classList.add('hidden');
// Enable seismograph fields, disable modem fields
setFieldsDisabled(seismoFields, false);
setFieldsDisabled(modemFields, true);
toggleEditModemPairing();
} else {
seismoFields.classList.add('hidden');
modemFields.classList.remove('hidden');
// Enable modem fields, disable seismograph fields
setFieldsDisabled(seismoFields, true);
setFieldsDisabled(modemFields, false);
}
}
// Toggle modem pairing field in edit modal
function toggleEditModemPairing() {
const deviceType = document.getElementById('editDeviceTypeSelect').value;
const deployedCheckbox = document.getElementById('editDeployedCheckbox');
const modemPairingField = document.getElementById('editModemPairingField');
if (deviceType === 'seismograph' && deployedCheckbox.checked) {
modemPairingField.classList.remove('hidden');
} else {
modemPairingField.classList.add('hidden');
}
}
// Edit Unit - Fetch data and populate form
async function editUnit(unitId) {
try {
const response = await fetch(`/api/roster/${unitId}`);
if (!response.ok) {
throw new Error('Failed to fetch unit data');
}
const unit = await response.json();
// Populate form fields
document.getElementById('editUnitId').value = unit.id;
document.getElementById('editDeviceTypeSelect').value = unit.device_type;
document.getElementById('editUnitType').value = unit.unit_type;
document.getElementById('editProjectId').value = unit.project_id;
document.getElementById('editAddress').value = unit.address;
document.getElementById('editCoordinates').value = unit.coordinates;
document.getElementById('editNote').value = unit.note;
// Checkboxes
document.getElementById('editDeployedCheckbox').checked = unit.deployed;
document.getElementById('editRetiredCheckbox').checked = unit.retired;
// Seismograph fields
document.getElementById('editLastCalibrated').value = unit.last_calibrated;
document.getElementById('editNextCalibrationDue').value = unit.next_calibration_due;
document.getElementById('editDeployedWithModemId').value = unit.deployed_with_modem_id;
// Modem fields
document.getElementById('editIpAddress').value = unit.ip_address;
document.getElementById('editPhoneNumber').value = unit.phone_number;
document.getElementById('editHardwareModel').value = unit.hardware_model;
// Store unit ID for form submission
document.getElementById('editUnitForm').dataset.unitId = unitId;
// Show/hide fields based on device type
toggleEditDeviceFields();
// Open modal
openEditUnitModal();
} catch (error) {
alert(`Error loading unit data: ${error.message}`);
}
}
// Handle Edit Unit form submission
document.getElementById('editUnitForm').addEventListener('submit', async function(event) {
event.preventDefault();
const unitId = this.dataset.unitId;
if (!unitId) {
alert('Error: Unit ID not found');
return;
}
const formData = new FormData(this);
try {
const response = await fetch(`/api/roster/edit/${unitId}`, {
method: 'POST',
body: formData
});
if (response.ok) {
closeEditUnitModal();
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
alert('Unit updated successfully!');
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error updating unit: ${error.message}`);
}
});
// Toggle Deployed Status
async function toggleDeployed(unitId, deployed) {
const action = deployed ? 'deploy' : 'bench';
if (!confirm(`Are you sure you want to ${action} unit ${unitId}?`)) {
return;
}
try {
const formData = new FormData();
formData.append('deployed', deployed);
const response = await fetch(`/api/roster/set-deployed/${unitId}`, {
method: 'POST',
body: formData
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
alert(`Unit ${deployed ? 'deployed' : 'benched'} successfully!`);
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Move to Ignore List
async function moveToIgnore(unitId) {
const reason = prompt(`Why are you ignoring unit ${unitId}?`, '');
if (reason === null) {
return; // User cancelled
}
try {
const formData = new FormData();
formData.append('reason', reason);
const response = await fetch(`/api/roster/ignore/${unitId}`, {
method: 'POST',
body: formData
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
alert(`Unit ${unitId} moved to ignore list`);
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Delete Unit
async function deleteUnit(unitId) {
if (!confirm(`Are you sure you want to PERMANENTLY delete unit ${unitId}?\n\nThis action cannot be undone!`)) {
return;
}
try {
const response = await fetch(`/api/roster/${unitId}`, {
method: 'DELETE'
});
if (response.ok) {
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
alert(`Unit ${unitId} deleted successfully`);
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Handle CSV Import
document.getElementById('importForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const resultDiv = document.getElementById('importResult');
try {
const response = await fetch('/api/roster/import-csv', {
method: 'POST',
body: formData
});
const result = await response.json();
if (response.ok) {
resultDiv.className = 'mt-4 p-4 rounded-lg bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200';
resultDiv.innerHTML = `
<p class="font-semibold mb-2">Import Successful!</p>
<ul class="text-sm space-y-1">
<li>✅ Added: ${result.summary.added}</li>
<li>🔄 Updated: ${result.summary.updated}</li>
<li>⏭️ Skipped: ${result.summary.skipped}</li>
<li>❌ Errors: ${result.summary.errors}</li>
</ul>
`;
resultDiv.classList.remove('hidden');
// Trigger roster refresh for current active tab
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
// Close modal after 2 seconds
setTimeout(() => closeImportModal(), 2000);
} else {
resultDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
resultDiv.innerHTML = `<p class="font-semibold">Import Failed</p><p class="text-sm">${result.detail || 'Unknown error'}</p>`;
resultDiv.classList.remove('hidden');
}
} catch (error) {
resultDiv.className = 'mt-4 p-4 rounded-lg bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200';
resultDiv.innerHTML = `<p class="font-semibold">Import Failed</p><p class="text-sm">${error.message}</p>`;
resultDiv.classList.remove('hidden');
}
});
// Handle roster tab switching with auto-refresh
let currentRosterEndpoint = '/partials/roster-deployed'; // Default to deployed tab
document.addEventListener('DOMContentLoaded', function() {
const tabButtons = document.querySelectorAll('.roster-tab-button');
tabButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active-roster-tab class from all buttons
tabButtons.forEach(btn => btn.classList.remove('active-roster-tab'));
// Add active-roster-tab class to clicked button
this.classList.add('active-roster-tab');
// Update current endpoint for auto-refresh
currentRosterEndpoint = this.getAttribute('data-endpoint');
});
});
// Auto-refresh the current active tab every 10 seconds
setInterval(() => {
const rosterContent = document.getElementById('roster-content');
if (rosterContent) {
// Use HTMX to trigger a refresh of the current endpoint
htmx.ajax('GET', currentRosterEndpoint, {
target: '#roster-content',
swap: 'innerHTML'
});
}
}, 10000); // 10 seconds
});
// Un-ignore Unit (remove from ignored list)
async function unignoreUnit(unitId) {
if (!confirm(`Remove unit ${unitId} from ignore list?`)) {
return;
}
try {
const response = await fetch(`/api/roster/ignore/${unitId}`, {
method: 'DELETE'
});
if (response.ok) {
// Trigger ignored tab refresh
htmx.trigger(document.querySelector('[hx-get="/partials/roster-ignored"]'), 'load');
alert(`Unit ${unitId} removed from ignore list`);
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Delete ignored unit completely (from emitters table)
async function deleteIgnoredUnit(unitId) {
if (!confirm(`Are you sure you want to PERMANENTLY delete unit ${unitId}?\n\nThis will remove it from the ignore list and delete all records.`)) {
return;
}
try {
// First remove from ignore list
await fetch(`/api/roster/ignore/${unitId}`, {
method: 'DELETE'
});
// Then delete the unit
const response = await fetch(`/api/roster/${unitId}`, {
method: 'DELETE'
});
if (response.ok) {
// Trigger ignored tab refresh
htmx.trigger(document.querySelector('[hx-get="/partials/roster-ignored"]'), 'load');
alert(`Unit ${unitId} deleted successfully`);
} else {
const result = await response.json();
alert(`Error: ${result.detail || 'Unknown error'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Filter roster table based on search input
function filterRosterTable() {
const searchInput = document.getElementById('roster-search').value.toLowerCase();
const table = document.querySelector('#roster-content table tbody');
if (!table) return;
const rows = table.getElementsByTagName('tr');
for (let row of rows) {
const cells = row.getElementsByTagName('td');
if (cells.length === 0) continue; // Skip header or empty rows
const unitId = cells[1]?.textContent?.toLowerCase() || '';
const unitType = cells[2]?.textContent?.toLowerCase() || '';
const note = cells[6]?.textContent?.toLowerCase() || '';
const matches = unitId.includes(searchInput) ||
unitType.includes(searchInput) ||
note.includes(searchInput);
row.style.display = matches ? '' : 'none';
}
}
</script>
<style>
.roster-tab-button {
color: #6b7280; /* gray-500 */
border-bottom: 2px solid transparent;
}
.roster-tab-button:hover {
color: #374151; /* gray-700 */
}
.active-roster-tab {
color: #f48b1c !important; /* seismo orange */
border-bottom: 2px solid #f48b1c !important;
}
</style>
{% endblock %}