feat: adds deployment records for seismographs.
This commit is contained in:
@@ -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, '"')})"
|
||||
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 =====
|
||||
|
||||
Reference in New Issue
Block a user