feat: adds deployment records for seismographs.

This commit is contained in:
2026-03-25 17:36:51 +00:00
parent 57a85f565b
commit 4f56dea4f3
7 changed files with 594 additions and 4 deletions

View File

@@ -278,6 +278,22 @@
<p id="viewNote" class="mt-1 text-gray-900 dark:text-white whitespace-pre-wrap">--</p>
</div>
<!-- Deployment History -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Deployment History</h3>
<button onclick="openNewDeploymentModal()" class="px-3 py-1.5 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg text-sm transition-colors flex items-center gap-1.5">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
Log Deployment
</button>
</div>
<div id="deploymentHistory" class="space-y-3">
<p class="text-sm text-gray-500 dark:text-gray-400">Loading...</p>
</div>
</div>
<!-- Unit History Timeline -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Timeline</h3>
@@ -320,6 +336,53 @@
</div>
</div>
<!-- Deployment Modal -->
<div id="deploymentModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl w-full max-w-lg">
<div class="flex justify-between items-center p-6 border-b border-gray-200 dark:border-gray-700">
<h3 id="deploymentModalTitle" class="text-lg font-semibold text-gray-900 dark:text-white">Log Deployment</h3>
<button onclick="closeDeploymentModal()" 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 class="p-6 space-y-4">
<input type="hidden" id="deploymentModalId">
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Deployed Date</label>
<input type="date" id="deploymentDeployedDate" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Est. Removal Date</label>
<input type="date" id="deploymentEstRemovalDate" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange">
</div>
</div>
<div id="actualRemovalRow">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Actual Removal Date <span class="text-gray-400 font-normal">(fill when returned)</span></label>
<input type="date" id="deploymentActualRemovalDate" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Job / Project</label>
<input type="text" id="deploymentProjectRef" placeholder="e.g. Fay I-80, CMU Campus" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Location Name</label>
<input type="text" id="deploymentLocationName" placeholder="e.g. North Gate, VP-001" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange">
</div>
<div>
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">Notes</label>
<textarea id="deploymentNotes" rows="2" class="mt-1 w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-slate-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-seismo-orange resize-none"></textarea>
</div>
</div>
<div class="flex justify-end gap-3 p-6 border-t border-gray-200 dark:border-gray-700">
<button onclick="closeDeploymentModal()" class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg text-sm transition-colors">Cancel</button>
<button onclick="saveDeployment()" class="px-4 py-2 bg-seismo-orange hover:bg-orange-600 text-white rounded-lg text-sm transition-colors">Save</button>
</div>
</div>
</div>
<!-- Edit Mode: Unit Information Form (Hidden by default) -->
<div id="editMode" class="hidden rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<div class="flex justify-between items-center mb-6">
@@ -1631,12 +1694,173 @@ async function pingModem() {
btn.disabled = false;
}
// ============================================================
// Deployment History
// ============================================================
async function loadDeploymentHistory() {
try {
const res = await fetch(`/api/deployments/${unitId}`);
const data = await res.json();
const container = document.getElementById('deploymentHistory');
const deployments = data.deployments || [];
if (deployments.length === 0) {
container.innerHTML = '<p class="text-sm text-gray-500 dark:text-gray-400">No deployment records yet.</p>';
return;
}
container.innerHTML = '';
deployments.forEach(d => {
container.appendChild(createDeploymentRow(d));
});
} catch (e) {
document.getElementById('deploymentHistory').innerHTML =
'<p class="text-sm text-red-500">Failed to load deployment history.</p>';
}
}
function formatDateDisplay(iso) {
if (!iso) return '—';
const [y, m, d] = iso.split('-');
return `${parseInt(m)}/${parseInt(d)}/${y}`;
}
function createDeploymentRow(d) {
const div = document.createElement('div');
div.className = 'flex items-start gap-3 p-3 rounded-lg ' +
(d.is_active
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-gray-50 dark:bg-slate-700/50');
const statusDot = d.is_active
? '<span class="mt-1 flex-shrink-0 w-2.5 h-2.5 rounded-full bg-green-500"></span>'
: '<span class="mt-1 flex-shrink-0 w-2.5 h-2.5 rounded-full bg-gray-400 dark:bg-gray-500"></span>';
const jobLabel = d.project_ref || d.project_id || 'Unspecified job';
const locLabel = d.location_name ? `<span class="text-gray-500 dark:text-gray-400"> · ${d.location_name}</span>` : '';
const deployedStr = formatDateDisplay(d.deployed_date);
const estStr = d.estimated_removal_date ? formatDateDisplay(d.estimated_removal_date) : 'TBD';
const actualStr = d.actual_removal_date ? formatDateDisplay(d.actual_removal_date) : null;
const dateRange = actualStr
? `${deployedStr}${actualStr}`
: `${deployedStr} → <span class="font-medium ${d.is_active ? 'text-green-700 dark:text-green-400' : 'text-gray-600 dark:text-gray-300'}">Est. ${estStr}</span>`;
const activeTag = d.is_active
? '<span class="ml-2 px-1.5 py-0.5 text-xs font-medium bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 rounded">In Field</span>'
: '';
div.innerHTML = `
${statusDot}
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900 dark:text-white">
${jobLabel}${activeTag}${locLabel}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">${dateRange}</div>
${d.notes ? `<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5 italic">${d.notes}</div>` : ''}
</div>
<div class="flex gap-1 flex-shrink-0">
<button onclick="openEditDeploymentModal(${JSON.stringify(d).replace(/"/g, '&quot;')})"
class="p-1.5 text-gray-400 hover:text-seismo-orange rounded transition-colors" title="Edit">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
</svg>
</button>
<button onclick="deleteDeployment('${d.id}')"
class="p-1.5 text-gray-400 hover:text-red-500 rounded transition-colors" title="Delete">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</div>
`;
return div;
}
function openNewDeploymentModal() {
document.getElementById('deploymentModalTitle').textContent = 'Log Deployment';
document.getElementById('deploymentModalId').value = '';
document.getElementById('deploymentDeployedDate').value = '';
document.getElementById('deploymentEstRemovalDate').value = '';
document.getElementById('deploymentActualRemovalDate').value = '';
document.getElementById('deploymentProjectRef').value = '';
document.getElementById('deploymentLocationName').value = '';
document.getElementById('deploymentNotes').value = '';
document.getElementById('deploymentModal').classList.remove('hidden');
}
function openEditDeploymentModal(d) {
document.getElementById('deploymentModalTitle').textContent = 'Edit Deployment';
document.getElementById('deploymentModalId').value = d.id;
document.getElementById('deploymentDeployedDate').value = d.deployed_date || '';
document.getElementById('deploymentEstRemovalDate').value = d.estimated_removal_date || '';
document.getElementById('deploymentActualRemovalDate').value = d.actual_removal_date || '';
document.getElementById('deploymentProjectRef').value = d.project_ref || '';
document.getElementById('deploymentLocationName').value = d.location_name || '';
document.getElementById('deploymentNotes').value = d.notes || '';
document.getElementById('deploymentModal').classList.remove('hidden');
}
function closeDeploymentModal() {
document.getElementById('deploymentModal').classList.add('hidden');
}
async function saveDeployment() {
const id = document.getElementById('deploymentModalId').value;
const payload = {
deployed_date: document.getElementById('deploymentDeployedDate').value || null,
estimated_removal_date: document.getElementById('deploymentEstRemovalDate').value || null,
actual_removal_date: document.getElementById('deploymentActualRemovalDate').value || null,
project_ref: document.getElementById('deploymentProjectRef').value || null,
location_name: document.getElementById('deploymentLocationName').value || null,
notes: document.getElementById('deploymentNotes').value || null,
};
try {
let res;
if (id) {
res = await fetch(`/api/deployments/${unitId}/${id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
} else {
res = await fetch(`/api/deployments/${unitId}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
}
if (!res.ok) throw new Error(await res.text());
closeDeploymentModal();
loadDeploymentHistory();
} catch (e) {
alert('Failed to save deployment: ' + e.message);
}
}
async function deleteDeployment(deploymentId) {
if (!confirm('Delete this deployment record?')) return;
try {
const res = await fetch(`/api/deployments/${unitId}/${deploymentId}`, { method: 'DELETE' });
if (!res.ok) throw new Error(await res.text());
loadDeploymentHistory();
} catch (e) {
alert('Failed to delete: ' + e.message);
}
}
// Load data when page loads
loadCalibrationInterval();
setupCalibrationAutoCalc();
loadUnitData().then(() => {
loadPhotos();
loadUnitHistory();
loadDeploymentHistory();
});
// ===== Pair Device Modal Functions =====