4 Commits

13 changed files with 45 additions and 284 deletions

View File

@@ -659,7 +659,6 @@ async def devices_all_partial(request: Request):
"last_seen": unit_data.get("last", "Never"), "last_seen": unit_data.get("last", "Never"),
"deployed": True, "deployed": True,
"retired": False, "retired": False,
"out_for_calibration": False,
"ignored": False, "ignored": False,
"note": unit_data.get("note", ""), "note": unit_data.get("note", ""),
"device_type": unit_data.get("device_type", "seismograph"), "device_type": unit_data.get("device_type", "seismograph"),
@@ -684,32 +683,6 @@ async def devices_all_partial(request: Request):
"last_seen": unit_data.get("last", "Never"), "last_seen": unit_data.get("last", "Never"),
"deployed": False, "deployed": False,
"retired": False, "retired": False,
"out_for_calibration": False,
"ignored": False,
"note": unit_data.get("note", ""),
"device_type": unit_data.get("device_type", "seismograph"),
"address": unit_data.get("address", ""),
"coordinates": unit_data.get("coordinates", ""),
"project_id": unit_data.get("project_id", ""),
"last_calibrated": unit_data.get("last_calibrated"),
"next_calibration_due": unit_data.get("next_calibration_due"),
"deployed_with_modem_id": unit_data.get("deployed_with_modem_id"),
"deployed_with_unit_id": unit_data.get("deployed_with_unit_id"),
"ip_address": unit_data.get("ip_address"),
"phone_number": unit_data.get("phone_number"),
"hardware_model": unit_data.get("hardware_model"),
})
# Add out-for-calibration units
for unit_id, unit_data in snapshot["out_for_calibration"].items():
units_list.append({
"id": unit_id,
"status": "Out for Calibration",
"age": "N/A",
"last_seen": "N/A",
"deployed": False,
"retired": False,
"out_for_calibration": True,
"ignored": False, "ignored": False,
"note": unit_data.get("note", ""), "note": unit_data.get("note", ""),
"device_type": unit_data.get("device_type", "seismograph"), "device_type": unit_data.get("device_type", "seismograph"),
@@ -734,7 +707,6 @@ async def devices_all_partial(request: Request):
"last_seen": "N/A", "last_seen": "N/A",
"deployed": False, "deployed": False,
"retired": True, "retired": True,
"out_for_calibration": False,
"ignored": False, "ignored": False,
"note": unit_data.get("note", ""), "note": unit_data.get("note", ""),
"device_type": unit_data.get("device_type", "seismograph"), "device_type": unit_data.get("device_type", "seismograph"),
@@ -759,7 +731,6 @@ async def devices_all_partial(request: Request):
"last_seen": "N/A", "last_seen": "N/A",
"deployed": False, "deployed": False,
"retired": False, "retired": False,
"out_for_calibration": False,
"ignored": True, "ignored": True,
"note": unit_data.get("note", unit_data.get("reason", "")), "note": unit_data.get("note", unit_data.get("reason", "")),
"device_type": unit_data.get("device_type", "unknown"), "device_type": unit_data.get("device_type", "unknown"),
@@ -777,17 +748,15 @@ async def devices_all_partial(request: Request):
# Sort by status category, then by ID # Sort by status category, then by ID
def sort_key(unit): def sort_key(unit):
# Priority: deployed (active) -> benched -> out_for_calibration -> retired -> ignored # Priority: deployed (active) -> benched -> retired -> ignored
if unit["deployed"]: if unit["deployed"]:
return (0, unit["id"]) return (0, unit["id"])
elif not unit["retired"] and not unit["out_for_calibration"] and not unit["ignored"]: elif not unit["retired"] and not unit["ignored"]:
return (1, unit["id"]) return (1, unit["id"])
elif unit["out_for_calibration"]:
return (2, unit["id"])
elif unit["retired"]: elif unit["retired"]:
return (3, unit["id"]) return (2, unit["id"])
else: else:
return (4, unit["id"]) return (3, unit["id"])
units_list.sort(key=sort_key) units_list.sort(key=sort_key)

View File

@@ -1,54 +0,0 @@
"""
Database Migration: Add out_for_calibration field to roster table
Changes:
- Adds out_for_calibration BOOLEAN column (default FALSE) to roster table
- Safe to run multiple times (idempotent)
- No data loss
Usage:
python backend/migrate_add_out_for_calibration.py
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///./data/seismo_fleet.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def migrate():
db = SessionLocal()
try:
print("=" * 60)
print("Migration: Add out_for_calibration to roster")
print("=" * 60)
# Check if column already exists
result = db.execute(text("PRAGMA table_info(roster)")).fetchall()
columns = [row[1] for row in result]
if "out_for_calibration" in columns:
print("Column out_for_calibration already exists. Skipping.")
else:
db.execute(text("ALTER TABLE roster ADD COLUMN out_for_calibration BOOLEAN DEFAULT FALSE"))
db.commit()
print("Added out_for_calibration column to roster table.")
print("Migration complete.")
except Exception as e:
db.rollback()
print(f"Error: {e}")
raise
finally:
db.close()
if __name__ == "__main__":
migrate()

View File

@@ -32,7 +32,6 @@ class RosterUnit(Base):
device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "slm" device_type = Column(String, default="seismograph") # "seismograph" | "modem" | "slm"
deployed = Column(Boolean, default=True) deployed = Column(Boolean, default=True)
retired = Column(Boolean, default=False) retired = Column(Boolean, default=False)
out_for_calibration = Column(Boolean, default=False)
note = Column(String, nullable=True) note = Column(String, nullable=True)
project_id = Column(String, nullable=True) project_id = Column(String, nullable=True)
location = Column(String, nullable=True) # Legacy field - use address/coordinates instead location = Column(String, nullable=True) # Legacy field - use address/coordinates instead

View File

@@ -146,7 +146,6 @@ async def add_roster_unit(
unit_type: str = Form("series3"), unit_type: str = Form("series3"),
deployed: str = Form(None), deployed: str = Form(None),
retired: str = Form(None), retired: str = Form(None),
out_for_calibration: str = Form(None),
note: str = Form(""), note: str = Form(""),
project_id: str = Form(None), project_id: str = Form(None),
location: str = Form(None), location: str = Form(None),
@@ -178,7 +177,6 @@ async def add_roster_unit(
# Convert boolean strings to actual booleans # Convert boolean strings to actual booleans
deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False
retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False
out_for_calibration_bool = out_for_calibration in ['true', 'True', '1', 'yes'] if out_for_calibration else False
# Convert port strings to integers # Convert port strings to integers
slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None
@@ -213,7 +211,6 @@ async def add_roster_unit(
unit_type=unit_type, unit_type=unit_type,
deployed=deployed_bool, deployed=deployed_bool,
retired=retired_bool, retired=retired_bool,
out_for_calibration=out_for_calibration_bool,
note=note, note=note,
project_id=project_id, project_id=project_id,
location=location, location=location,
@@ -466,7 +463,6 @@ def get_roster_unit(unit_id: str, db: Session = Depends(get_db)):
"unit_type": unit.unit_type, "unit_type": unit.unit_type,
"deployed": unit.deployed, "deployed": unit.deployed,
"retired": unit.retired, "retired": unit.retired,
"out_for_calibration": unit.out_for_calibration or False,
"note": unit.note or "", "note": unit.note or "",
"project_id": unit.project_id or "", "project_id": unit.project_id or "",
"location": unit.location or "", "location": unit.location or "",
@@ -498,7 +494,6 @@ async def edit_roster_unit(
unit_type: str = Form("series3"), unit_type: str = Form("series3"),
deployed: str = Form(None), deployed: str = Form(None),
retired: str = Form(None), retired: str = Form(None),
out_for_calibration: str = Form(None),
note: str = Form(""), note: str = Form(""),
project_id: str = Form(None), project_id: str = Form(None),
location: str = Form(None), location: str = Form(None),
@@ -540,7 +535,6 @@ async def edit_roster_unit(
# Convert boolean strings to actual booleans # Convert boolean strings to actual booleans
deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False deployed_bool = deployed in ['true', 'True', '1', 'yes'] if deployed else False
retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False retired_bool = retired in ['true', 'True', '1', 'yes'] if retired else False
out_for_calibration_bool = out_for_calibration in ['true', 'True', '1', 'yes'] if out_for_calibration else False
# Convert port strings to integers # Convert port strings to integers
slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None slm_tcp_port_int = int(slm_tcp_port) if slm_tcp_port and slm_tcp_port.strip() else None
@@ -570,14 +564,12 @@ async def edit_roster_unit(
old_note = unit.note old_note = unit.note
old_deployed = unit.deployed old_deployed = unit.deployed
old_retired = unit.retired old_retired = unit.retired
old_out_for_calibration = unit.out_for_calibration
# Update all fields # Update all fields
unit.device_type = device_type unit.device_type = device_type
unit.unit_type = unit_type unit.unit_type = unit_type
unit.deployed = deployed_bool unit.deployed = deployed_bool
unit.retired = retired_bool unit.retired = retired_bool
unit.out_for_calibration = out_for_calibration_bool
unit.note = note unit.note = note
unit.project_id = project_id unit.project_id = project_id
unit.location = location unit.location = location
@@ -685,11 +677,6 @@ async def edit_roster_unit(
old_status_text = "retired" if old_retired else "active" old_status_text = "retired" if old_retired else "active"
record_history(db, unit_id, "retired_change", "retired", old_status_text, status_text, "manual") record_history(db, unit_id, "retired_change", "retired", old_status_text, status_text, "manual")
if old_out_for_calibration != out_for_calibration_bool:
status_text = "out_for_calibration" if out_for_calibration_bool else "available"
old_status_text = "out_for_calibration" if old_out_for_calibration else "available"
record_history(db, unit_id, "calibration_status_change", "out_for_calibration", old_status_text, status_text, "manual")
# Handle cascade to paired device # Handle cascade to paired device
cascaded_unit_id = None cascaded_unit_id = None
if cascade_to_unit_id and cascade_to_unit_id.strip(): if cascade_to_unit_id and cascade_to_unit_id.strip():

View File

@@ -28,8 +28,7 @@ async def get_seismo_stats(request: Request, db: Session = Depends(get_db)):
total = len(seismos) total = len(seismos)
deployed = sum(1 for s in seismos if s.deployed) deployed = sum(1 for s in seismos if s.deployed)
benched = sum(1 for s in seismos if not s.deployed and not s.out_for_calibration) benched = sum(1 for s in seismos if not s.deployed)
out_for_calibration = sum(1 for s in seismos if s.out_for_calibration)
# Count modems assigned to deployed seismographs # Count modems assigned to deployed seismographs
with_modem = sum(1 for s in seismos if s.deployed and s.deployed_with_modem_id) with_modem = sum(1 for s in seismos if s.deployed and s.deployed_with_modem_id)
@@ -42,7 +41,6 @@ async def get_seismo_stats(request: Request, db: Session = Depends(get_db)):
"total": total, "total": total,
"deployed": deployed, "deployed": deployed,
"benched": benched, "benched": benched,
"out_for_calibration": out_for_calibration,
"with_modem": with_modem, "with_modem": with_modem,
"without_modem": without_modem "without_modem": without_modem
} }
@@ -79,9 +77,7 @@ async def get_seismo_units(
if status == "deployed": if status == "deployed":
query = query.filter(RosterUnit.deployed == True) query = query.filter(RosterUnit.deployed == True)
elif status == "benched": elif status == "benched":
query = query.filter(RosterUnit.deployed == False, RosterUnit.out_for_calibration == False) query = query.filter(RosterUnit.deployed == False)
elif status == "out_for_calibration":
query = query.filter(RosterUnit.out_for_calibration == True)
# Apply modem filter # Apply modem filter
if modem == "with": if modem == "with":

View File

@@ -80,12 +80,6 @@ def emit_status_snapshot():
age = "N/A" age = "N/A"
last_seen = None last_seen = None
fname = "" fname = ""
elif r.out_for_calibration:
# Out for calibration units get separated later
status = "Out for Calibration"
age = "N/A"
last_seen = None
fname = ""
else: else:
if e: if e:
last_seen = ensure_utc(e.last_seen) last_seen = ensure_utc(e.last_seen)
@@ -109,7 +103,6 @@ def emit_status_snapshot():
"deployed": r.deployed, "deployed": r.deployed,
"note": r.note or "", "note": r.note or "",
"retired": r.retired, "retired": r.retired,
"out_for_calibration": r.out_for_calibration or False,
# Device type and type-specific fields # Device type and type-specific fields
"device_type": r.device_type or "seismograph", "device_type": r.device_type or "seismograph",
"last_calibrated": r.last_calibrated.isoformat() if r.last_calibrated else None, "last_calibrated": r.last_calibrated.isoformat() if r.last_calibrated else None,
@@ -140,7 +133,6 @@ def emit_status_snapshot():
"deployed": False, # default "deployed": False, # default
"note": "", "note": "",
"retired": False, "retired": False,
"out_for_calibration": False,
# Device type and type-specific fields (defaults for unknown units) # Device type and type-specific fields (defaults for unknown units)
"device_type": "seismograph", # default "device_type": "seismograph", # default
"last_calibrated": None, "last_calibrated": None,
@@ -187,12 +179,12 @@ def emit_status_snapshot():
# Separate buckets for UI # Separate buckets for UI
active_units = { active_units = {
uid: u for uid, u in units.items() uid: u for uid, u in units.items()
if not u["retired"] and not u["out_for_calibration"] and u["deployed"] and uid not in ignored if not u["retired"] and u["deployed"] and uid not in ignored
} }
benched_units = { benched_units = {
uid: u for uid, u in units.items() uid: u for uid, u in units.items()
if not u["retired"] and not u["out_for_calibration"] and not u["deployed"] and uid not in ignored if not u["retired"] and not u["deployed"] and uid not in ignored
} }
retired_units = { retired_units = {
@@ -200,11 +192,6 @@ def emit_status_snapshot():
if u["retired"] if u["retired"]
} }
out_for_calibration_units = {
uid: u for uid, u in units.items()
if u["out_for_calibration"]
}
# Unknown units - emitters that aren't in the roster and aren't ignored # Unknown units - emitters that aren't in the roster and aren't ignored
unknown_units = { unknown_units = {
uid: u for uid, u in units.items() uid: u for uid, u in units.items()
@@ -217,14 +204,12 @@ def emit_status_snapshot():
"active": active_units, "active": active_units,
"benched": benched_units, "benched": benched_units,
"retired": retired_units, "retired": retired_units,
"out_for_calibration": out_for_calibration_units,
"unknown": unknown_units, "unknown": unknown_units,
"summary": { "summary": {
"total": len(active_units) + len(benched_units), "total": len(active_units) + len(benched_units),
"active": len(active_units), "active": len(active_units),
"benched": len(benched_units), "benched": len(benched_units),
"retired": len(retired_units), "retired": len(retired_units),
"out_for_calibration": len(out_for_calibration_units),
"unknown": len(unknown_units), "unknown": len(unknown_units),
# Status counts only for deployed units (active_units) # Status counts only for deployed units (active_units)
"ok": sum(1 for u in active_units.values() if u["status"] == "OK"), "ok": sum(1 for u in active_units.values() if u["status"] == "OK"),

View File

@@ -397,14 +397,13 @@
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()"> <div class="bg-white dark:bg-slate-800 rounded-xl shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()">
<div class="p-6"> <div class="p-6">
<div class="flex items-center justify-between mb-6"> <div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white" id="modal-title">New Reservation</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-white">New Reservation</h2>
<button onclick="closeReservationModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"> <button onclick="closeReservationModal()" 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"> <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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg> </svg>
</button> </button>
</div> </div>
<input type="hidden" id="editing-reservation-id" value="">
<form id="reservation-form" onsubmit="submitReservation(event)"> <form id="reservation-form" onsubmit="submitReservation(event)">
<!-- Name --> <!-- Name -->
@@ -523,7 +522,7 @@
class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"> class="px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
Cancel Cancel
</button> </button>
<button type="submit" id="modal-submit-btn" <button type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"> class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Create Reservation Create Reservation
</button> </button>
@@ -601,83 +600,27 @@ function closeDayPanel() {
let reservationModeActive = false; let reservationModeActive = false;
function openReservationModal() { function openReservationModal() {
// Reset to "create" mode
document.getElementById('modal-title').textContent = 'New Reservation';
document.getElementById('modal-submit-btn').textContent = 'Create Reservation';
document.getElementById('editing-reservation-id').value = '';
document.getElementById('reservation-form').reset();
document.getElementById('reservation-modal').classList.remove('hidden'); document.getElementById('reservation-modal').classList.remove('hidden');
reservationModeActive = true; reservationModeActive = true;
// Show reservation legend, hide main legend
document.getElementById('main-legend').classList.add('md:hidden'); document.getElementById('main-legend').classList.add('md:hidden');
document.getElementById('main-legend').classList.remove('md:flex'); document.getElementById('main-legend').classList.remove('md:flex');
document.getElementById('reservation-legend').classList.remove('md:hidden'); document.getElementById('reservation-legend').classList.remove('md:hidden');
document.getElementById('reservation-legend').classList.add('md:flex'); document.getElementById('reservation-legend').classList.add('md:flex');
// Trigger availability update
updateCalendarAvailability(); updateCalendarAvailability();
} }
async function editReservation(id) {
try {
const response = await fetch(`/api/fleet-calendar/reservations/${id}`);
if (!response.ok) { alert('Failed to load reservation'); return; }
const res = await response.json();
// Switch modal to "edit" mode
document.getElementById('modal-title').textContent = 'Edit Reservation';
document.getElementById('modal-submit-btn').textContent = 'Save Changes';
document.getElementById('editing-reservation-id').value = id;
// Populate fields
const form = document.getElementById('reservation-form');
form.querySelector('input[name="name"]').value = res.name;
form.querySelector('select[name="project_id"]').value = res.project_id || '';
form.querySelector('input[name="start_date"]').value = res.start_date;
form.querySelector('textarea[name="notes"]').value = res.notes || '';
// Color radio
const colorRadio = form.querySelector(`input[name="color"][value="${res.color}"]`);
if (colorRadio) colorRadio.checked = true;
// Assignment type
const atRadio = form.querySelector(`input[name="assignment_type"][value="${res.assignment_type}"]`);
if (atRadio) { atRadio.checked = true; toggleAssignmentType(res.assignment_type); }
if (res.assignment_type === 'quantity') {
form.querySelector('input[name="quantity_needed"]').value = res.quantity_needed || 1;
}
// End date / TBD
const tbdCheckbox = document.getElementById('end_date_tbd');
if (res.end_date_tbd) {
tbdCheckbox.checked = true;
form.querySelector('input[name="estimated_end_date"]').value = res.estimated_end_date || '';
} else {
tbdCheckbox.checked = false;
document.getElementById('end_date_input').value = res.end_date || '';
}
toggleEndDateTBD();
document.getElementById('reservation-modal').classList.remove('hidden');
reservationModeActive = true;
document.getElementById('main-legend').classList.add('md:hidden');
document.getElementById('main-legend').classList.remove('md:flex');
document.getElementById('reservation-legend').classList.remove('md:hidden');
document.getElementById('reservation-legend').classList.add('md:flex');
updateCalendarAvailability();
} catch (error) {
console.error('Error loading reservation:', error);
alert('Error loading reservation');
}
}
function closeReservationModal() { function closeReservationModal() {
document.getElementById('reservation-modal').classList.add('hidden'); document.getElementById('reservation-modal').classList.add('hidden');
document.getElementById('reservation-form').reset(); document.getElementById('reservation-form').reset();
document.getElementById('editing-reservation-id').value = '';
reservationModeActive = false; reservationModeActive = false;
// Restore main legend
document.getElementById('main-legend').classList.remove('md:hidden'); document.getElementById('main-legend').classList.remove('md:hidden');
document.getElementById('main-legend').classList.add('md:flex'); document.getElementById('main-legend').classList.add('md:flex');
document.getElementById('reservation-legend').classList.add('md:hidden'); document.getElementById('reservation-legend').classList.add('md:hidden');
document.getElementById('reservation-legend').classList.remove('md:flex'); document.getElementById('reservation-legend').classList.remove('md:flex');
// Reset calendar colors
resetCalendarColors(); resetCalendarColors();
} }
@@ -809,7 +752,6 @@ async function submitReservation(event) {
const form = event.target; const form = event.target;
const formData = new FormData(form); const formData = new FormData(form);
const endDateTbd = formData.get('end_date_tbd') === 'on'; const endDateTbd = formData.get('end_date_tbd') === 'on';
const editingId = document.getElementById('editing-reservation-id').value;
const data = { const data = {
name: formData.get('name'), name: formData.get('name'),
@@ -824,6 +766,7 @@ async function submitReservation(event) {
notes: formData.get('notes') || null notes: formData.get('notes') || null
}; };
// Validate: need either end_date or TBD checked
if (!data.end_date && !data.end_date_tbd) { if (!data.end_date && !data.end_date_tbd) {
alert('Please enter an end date or check "TBD / Ongoing"'); alert('Please enter an end date or check "TBD / Ongoing"');
return; return;
@@ -835,15 +778,9 @@ async function submitReservation(event) {
data.unit_ids = formData.getAll('unit_ids'); data.unit_ids = formData.getAll('unit_ids');
} }
const isEdit = editingId !== '';
const url = isEdit
? `/api/fleet-calendar/reservations/${editingId}`
: '/api/fleet-calendar/reservations';
const method = isEdit ? 'PUT' : 'POST';
try { try {
const response = await fetch(url, { const response = await fetch('/api/fleet-calendar/reservations', {
method, method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data) body: JSON.stringify(data)
}); });
@@ -852,13 +789,14 @@ async function submitReservation(event) {
if (result.success) { if (result.success) {
closeReservationModal(); closeReservationModal();
// Reload the page to refresh calendar
window.location.reload(); window.location.reload();
} else { } else {
alert(`Error ${isEdit ? 'saving' : 'creating'} reservation: ` + (result.detail || 'Unknown error')); alert('Error creating reservation: ' + (result.detail || 'Unknown error'));
} }
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);
alert(`Error ${isEdit ? 'saving' : 'creating'} reservation`); alert('Error creating reservation');
} }
} }

View File

@@ -51,7 +51,7 @@
{% for unit in units %} {% for unit in units %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" <tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
data-device-type="{{ unit.device_type }}" data-device-type="{{ unit.device_type }}"
data-status="{% if unit.deployed %}deployed{% elif unit.out_for_calibration %}out_for_calibration{% elif unit.retired %}retired{% elif unit.ignored %}ignored{% else %}benched{% endif %}" data-status="{% if unit.deployed %}deployed{% elif unit.retired %}retired{% elif unit.ignored %}ignored{% else %}benched{% endif %}"
data-health="{{ unit.status }}" data-health="{{ unit.status }}"
data-id="{{ unit.id }}" data-id="{{ unit.id }}"
data-type="{{ unit.device_type }}" data-type="{{ unit.device_type }}"
@@ -60,9 +60,7 @@
data-note="{{ unit.note if unit.note else '' }}"> data-note="{{ unit.note if unit.note else '' }}">
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
{% if unit.out_for_calibration %} {% if not unit.deployed %}
<span class="w-3 h-3 rounded-full bg-purple-500" title="Out for Calibration"></span>
{% elif not unit.deployed %}
<span class="w-3 h-3 rounded-full bg-gray-400 dark:bg-gray-500" title="Benched"></span> <span class="w-3 h-3 rounded-full bg-gray-400 dark:bg-gray-500" title="Benched"></span>
{% elif unit.status == 'OK' %} {% elif unit.status == 'OK' %}
<span class="w-3 h-3 rounded-full bg-green-500" title="OK"></span> <span class="w-3 h-3 rounded-full bg-green-500" title="OK"></span>
@@ -74,8 +72,6 @@
{% if unit.deployed %} {% if unit.deployed %}
<span class="w-2 h-2 rounded-full bg-blue-500" title="Deployed"></span> <span class="w-2 h-2 rounded-full bg-blue-500" title="Deployed"></span>
{% elif unit.out_for_calibration %}
<span class="w-2 h-2 rounded-full bg-purple-400" title="Out for Calibration"></span>
{% else %} {% else %}
<span class="w-2 h-2 rounded-full bg-gray-300 dark:bg-gray-600" title="Benched"></span> <span class="w-2 h-2 rounded-full bg-gray-300 dark:bg-gray-600" title="Benched"></span>
{% endif %} {% endif %}
@@ -207,16 +203,14 @@
<div class="unit-card device-card" <div class="unit-card device-card"
onclick="openUnitModal('{{ unit.id }}', '{{ unit.status }}', '{{ unit.age }}')" onclick="openUnitModal('{{ unit.id }}', '{{ unit.status }}', '{{ unit.age }}')"
data-device-type="{{ unit.device_type }}" data-device-type="{{ unit.device_type }}"
data-status="{% if unit.deployed %}deployed{% elif unit.out_for_calibration %}out_for_calibration{% elif unit.retired %}retired{% elif unit.ignored %}ignored{% else %}benched{% endif %}" data-status="{% if unit.deployed %}deployed{% elif unit.retired %}retired{% elif unit.ignored %}ignored{% else %}benched{% endif %}"
data-health="{{ unit.status }}" data-health="{{ unit.status }}"
data-unit-id="{{ unit.id }}" data-unit-id="{{ unit.id }}"
data-age="{{ unit.age }}"> data-age="{{ unit.age }}">
<!-- Header: Status Dot + Unit ID + Status Badge --> <!-- Header: Status Dot + Unit ID + Status Badge -->
<div class="flex items-center justify-between mb-2"> <div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{% if unit.out_for_calibration %} {% if not unit.deployed %}
<span class="w-4 h-4 rounded-full bg-purple-500" title="Out for Calibration"></span>
{% elif not unit.deployed %}
<span class="w-4 h-4 rounded-full bg-gray-400 dark:bg-gray-500" title="Benched"></span> <span class="w-4 h-4 rounded-full bg-gray-400 dark:bg-gray-500" title="Benched"></span>
{% elif unit.status == 'OK' %} {% elif unit.status == 'OK' %}
<span class="w-4 h-4 rounded-full bg-green-500" title="OK"></span> <span class="w-4 h-4 rounded-full bg-green-500" title="OK"></span>
@@ -230,13 +224,12 @@
<span class="font-bold text-lg text-seismo-orange dark:text-seismo-orange">{{ unit.id }}</span> <span class="font-bold text-lg text-seismo-orange dark:text-seismo-orange">{{ unit.id }}</span>
</div> </div>
<span class="px-3 py-1 rounded-full text-xs font-medium <span class="px-3 py-1 rounded-full text-xs font-medium
{% if unit.out_for_calibration %}bg-purple-100 dark:bg-purple-900/30 text-purple-800 dark:text-purple-300 {% if unit.status == 'OK' %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300
{% elif unit.status == 'OK' %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-300
{% elif unit.status == 'Pending' %}bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300 {% elif unit.status == 'Pending' %}bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-300
{% elif unit.status == 'Missing' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300 {% elif unit.status == 'Missing' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-300
{% else %}bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 {% else %}bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400
{% endif %}"> {% endif %}">
{% if unit.out_for_calibration %}Out for Cal{% elif unit.status in ['N/A', 'Unknown'] %}Benched{% else %}{{ unit.status }}{% endif %} {% if unit.status in ['N/A', 'Unknown'] %}Benched{% else %}{{ unit.status }}{% endif %}
</span> </span>
</div> </div>
@@ -286,10 +279,6 @@
<span class="text-xs text-blue-600 dark:text-blue-400"> <span class="text-xs text-blue-600 dark:text-blue-400">
⚡ Deployed ⚡ Deployed
</span> </span>
{% elif unit.out_for_calibration %}
<span class="text-xs text-purple-600 dark:text-purple-400">
🔧 Out for Calibration
</span>
{% else %} {% else %}
<span class="text-xs text-gray-500 dark:text-gray-500"> <span class="text-xs text-gray-500 dark:text-gray-500">
📦 Benched 📦 Benched

View File

@@ -87,7 +87,10 @@ async function deleteReservation(id, name) {
} }
} }
// editReservation is defined in fleet_calendar.html function editReservation(id) {
// For now, just show alert - can implement edit modal later
alert('Edit functionality coming soon. For now, delete and recreate the reservation.');
}
</script> </script>
{% else %} {% else %}
<div class="text-center py-8"> <div class="text-center py-8">

View File

@@ -31,9 +31,6 @@
<div> <div>
<p class="text-gray-600 dark:text-gray-400 text-sm">Benched</p> <p class="text-gray-600 dark:text-gray-400 text-sm">Benched</p>
<p class="text-3xl font-bold text-gray-600 dark:text-gray-400 mt-2">{{ benched }}</p> <p class="text-3xl font-bold text-gray-600 dark:text-gray-400 mt-2">{{ benched }}</p>
{% if out_for_calibration > 0 %}
<p class="text-xs text-purple-600 dark:text-purple-400 mt-1">{{ out_for_calibration }} out for calibration</p>
{% endif %}
</div> </div>
<svg class="w-12 h-12 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-12 h-12 text-gray-500" 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> <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>

View File

@@ -106,13 +106,6 @@
</svg> </svg>
Deployed Deployed
</span> </span>
{% elif unit.out_for_calibration %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path>
</svg>
Out for Cal
</span>
{% else %} {% else %}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20"> <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">

View File

@@ -56,7 +56,6 @@
<option value="">All Status</option> <option value="">All Status</option>
<option value="deployed">Deployed</option> <option value="deployed">Deployed</option>
<option value="benched">Benched</option> <option value="benched">Benched</option>
<option value="out_for_calibration">Out for Calibration</option>
</select> </select>
<!-- Modem Filter --> <!-- Modem Filter -->

View File

@@ -92,7 +92,7 @@
<p id="deployedStatus" class="font-medium text-gray-900 dark:text-white">--</p> <p id="deployedStatus" class="font-medium text-gray-900 dark:text-white">--</p>
</div> </div>
<div> <div>
<span class="text-sm text-gray-500 dark:text-gray-400">Unit Status</span> <span class="text-sm text-gray-500 dark:text-gray-400">Retired</span>
<p id="retiredStatus" class="font-medium text-gray-900 dark:text-white">--</p> <p id="retiredStatus" class="font-medium text-gray-900 dark:text-white">--</p>
</div> </div>
</div> </div>
@@ -497,28 +497,18 @@
</div> </div>
</div> </div>
<!-- Status Checkboxes --> <!-- Checkboxes -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-4 space-y-3"> <div class="flex items-center gap-6 border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="flex items-center gap-6"> <label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-2 cursor-pointer"> <input type="checkbox" name="deployed" id="deployed" value="true"
<input type="checkbox" name="deployed" id="deployed" value="true" class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
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>
<span class="text-sm text-gray-700 dark:text-gray-300">Deployed</span> </label>
</label> <label class="flex items-center gap-2 cursor-pointer">
<label class="flex items-center gap-2 cursor-pointer"> <input type="checkbox" name="retired" id="retired" value="true"
<input type="checkbox" name="out_for_calibration" id="outForCalibration" value="true" class="w-4 h-4 text-seismo-orange focus:ring-seismo-orange rounded">
class="w-4 h-4 text-purple-600 focus:ring-purple-500 rounded"> <span class="text-sm text-gray-700 dark:text-gray-300">Retired</span>
<span class="text-sm text-gray-700 dark:text-gray-300">Out for Calibration</span> </label>
</label>
</div>
<!-- Hidden field for retired — controlled by the Retire button below -->
<input type="hidden" name="retired" id="retired" value="">
<div id="retireButtonSection">
<button type="button" id="retireBtn"
class="px-4 py-2 text-sm font-medium rounded-lg border transition-colors"
onclick="toggleRetired()">
</button>
</div>
</div> </div>
<!-- Notes --> <!-- Notes -->
@@ -827,16 +817,7 @@ function populateViewMode() {
} }
document.getElementById('deployedStatus').textContent = currentUnit.deployed ? 'Yes' : 'No'; document.getElementById('deployedStatus').textContent = currentUnit.deployed ? 'Yes' : 'No';
if (currentUnit.retired) { document.getElementById('retiredStatus').textContent = currentUnit.retired ? 'Yes' : 'No';
document.getElementById('retiredStatus').textContent = 'Retired';
document.getElementById('retiredStatus').className = 'font-medium text-red-600 dark:text-red-400';
} else if (currentUnit.out_for_calibration) {
document.getElementById('retiredStatus').textContent = 'Out for Calibration';
document.getElementById('retiredStatus').className = 'font-medium text-purple-600 dark:text-purple-400';
} else {
document.getElementById('retiredStatus').textContent = 'Active';
document.getElementById('retiredStatus').className = 'font-medium text-gray-900 dark:text-white';
}
// Basic info // Basic info
document.getElementById('viewDeviceType').textContent = currentUnit.device_type || '--'; document.getElementById('viewDeviceType').textContent = currentUnit.device_type || '--';
@@ -1028,9 +1009,7 @@ function populateEditForm() {
document.getElementById('address').value = currentUnit.address || ''; document.getElementById('address').value = currentUnit.address || '';
document.getElementById('coordinates').value = currentUnit.coordinates || ''; document.getElementById('coordinates').value = currentUnit.coordinates || '';
document.getElementById('deployed').checked = currentUnit.deployed; document.getElementById('deployed').checked = currentUnit.deployed;
document.getElementById('outForCalibration').checked = currentUnit.out_for_calibration || false; document.getElementById('retired').checked = currentUnit.retired;
document.getElementById('retired').value = currentUnit.retired ? 'true' : '';
updateRetireButton(currentUnit.retired);
document.getElementById('note').value = currentUnit.note || ''; document.getElementById('note').value = currentUnit.note || '';
// Seismograph fields // Seismograph fields
@@ -1174,25 +1153,6 @@ function cancelEdit() {
populateEditForm(); populateEditForm();
} }
function updateRetireButton(isRetired) {
const btn = document.getElementById('retireBtn');
if (!btn) return;
if (isRetired) {
btn.textContent = 'Un-Retire Unit';
btn.className = 'px-4 py-2 text-sm font-medium rounded-lg border transition-colors bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600';
} else {
btn.textContent = 'Retire Unit';
btn.className = 'px-4 py-2 text-sm font-medium rounded-lg border transition-colors bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-700 hover:bg-red-100 dark:hover:bg-red-900/40';
}
}
function toggleRetired() {
const hiddenInput = document.getElementById('retired');
const isCurrentlyRetired = hiddenInput.value === 'true';
hiddenInput.value = isCurrentlyRetired ? '' : 'true';
updateRetireButton(!isCurrentlyRetired);
}
// Handle form submission // Handle form submission
document.getElementById('editForm').addEventListener('submit', async function(e) { document.getElementById('editForm').addEventListener('submit', async function(e) {
e.preventDefault(); e.preventDefault();