diff --git a/backend/main.py b/backend/main.py index 4d9cc1e..2d414f3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -355,8 +355,11 @@ async def nrl_detail_page( ).first() assigned_unit = None + assigned_modem = None if assignment: assigned_unit = db.query(RosterUnit).filter_by(id=assignment.unit_id).first() + if assigned_unit and assigned_unit.deployed_with_modem_id: + assigned_modem = db.query(RosterUnit).filter_by(id=assigned_unit.deployed_with_modem_id).first() # Get session count session_count = db.query(MonitoringSession).filter_by(location_id=location_id).count() @@ -393,6 +396,7 @@ async def nrl_detail_page( "location": location, "assignment": assignment, "assigned_unit": assigned_unit, + "assigned_modem": assigned_modem, "session_count": session_count, "file_count": file_count, "active_session": active_session, diff --git a/backend/routers/project_locations.py b/backend/routers/project_locations.py index 44fcdd5..e3dff52 100644 --- a/backend/routers/project_locations.py +++ b/backend/routers/project_locations.py @@ -353,18 +353,18 @@ async def assign_unit_to_location( detail=f"Unit type '{unit.device_type}' does not match location type '{location.location_type}'", ) - # Check if location already has an active assignment + # Check if location already has an active assignment (active = assigned_until IS NULL) existing_assignment = db.query(UnitAssignment).filter( and_( UnitAssignment.location_id == location_id, - UnitAssignment.status == "active", + UnitAssignment.assigned_until == None, ) ).first() if existing_assignment: raise HTTPException( status_code=400, - detail=f"Location already has an active unit assignment ({existing_assignment.unit_id}). Unassign first.", + detail=f"Location already has an active unit assignment ({existing_assignment.unit_id}). Use swap to replace it.", ) # Create new assignment @@ -433,10 +433,120 @@ async def unassign_unit( return {"success": True, "message": "Unit unassigned successfully"} +@router.post("/locations/{location_id}/swap") +async def swap_unit_on_location( + project_id: str, + location_id: str, + request: Request, + db: Session = Depends(get_db), +): + """ + Swap the unit assigned to a vibration monitoring location. + Ends the current active assignment (if any), creates a new one, + and optionally updates modem pairing on the seismograph. + Works for first-time assignments too (no current assignment = just create). + """ + location = db.query(MonitoringLocation).filter_by( + id=location_id, + project_id=project_id, + ).first() + if not location: + raise HTTPException(status_code=404, detail="Location not found") + + form_data = await request.form() + unit_id = form_data.get("unit_id") + modem_id = form_data.get("modem_id") or None + notes = form_data.get("notes") or None + + if not unit_id: + raise HTTPException(status_code=400, detail="unit_id is required") + + # Validate new unit + unit = db.query(RosterUnit).filter_by(id=unit_id).first() + if not unit: + raise HTTPException(status_code=404, detail="Unit not found") + + expected_device_type = "slm" if location.location_type == "sound" else "seismograph" + if unit.device_type != expected_device_type: + raise HTTPException( + status_code=400, + detail=f"Unit type '{unit.device_type}' does not match location type '{location.location_type}'", + ) + + # End current active assignment if one exists (active = assigned_until IS NULL) + current = db.query(UnitAssignment).filter( + and_( + UnitAssignment.location_id == location_id, + UnitAssignment.assigned_until == None, + ) + ).first() + if current: + current.assigned_until = datetime.utcnow() + current.status = "completed" + + # Create new assignment + new_assignment = UnitAssignment( + id=str(uuid.uuid4()), + unit_id=unit_id, + location_id=location_id, + project_id=project_id, + device_type=unit.device_type, + assigned_until=None, + status="active", + notes=notes, + ) + db.add(new_assignment) + + # Update modem pairing on the seismograph if modem provided + if modem_id: + modem = db.query(RosterUnit).filter_by(id=modem_id, device_type="modem").first() + if not modem: + raise HTTPException(status_code=404, detail=f"Modem '{modem_id}' not found") + unit.deployed_with_modem_id = modem_id + modem.deployed_with_unit_id = unit_id + else: + # Clear modem pairing if not provided + unit.deployed_with_modem_id = None + + db.commit() + + return JSONResponse({ + "success": True, + "assignment_id": new_assignment.id, + "message": f"Unit '{unit_id}' assigned to '{location.name}'" + (f" with modem '{modem_id}'" if modem_id else ""), + }) + + # ============================================================================ # Available Units for Assignment # ============================================================================ +@router.get("/available-modems", response_class=JSONResponse) +async def get_available_modems( + project_id: str, + db: Session = Depends(get_db), +): + """ + Get all deployed, non-retired modems for the modem assignment dropdown. + """ + modems = db.query(RosterUnit).filter( + and_( + RosterUnit.device_type == "modem", + RosterUnit.deployed == True, + RosterUnit.retired == False, + ) + ).order_by(RosterUnit.id).all() + + return [ + { + "id": m.id, + "hardware_model": m.hardware_model, + "ip_address": m.ip_address, + } + for m in modems + ] + + @router.get("/available-units", response_class=JSONResponse) async def get_available_units( project_id: str, diff --git a/templates/projects/detail.html b/templates/projects/detail.html index 5a4a8cf..d559017 100644 --- a/templates/projects/detail.html +++ b/templates/projects/detail.html @@ -838,6 +838,7 @@ async function loadProjectDetails() { // Update tab labels and visibility based on project type const isSoundProject = projectTypeId === 'sound_monitoring'; + const isVibrationProject = projectTypeId === 'vibration_monitoring'; if (isSoundProject) { document.getElementById('locations-tab-label').textContent = 'NRLs'; document.getElementById('locations-header').textContent = 'Noise Recording Locations'; @@ -848,9 +849,9 @@ async function loadProjectDetails() { const isRemote = mode === 'remote'; document.getElementById('sessions-tab-btn').classList.toggle('hidden', !isSoundProject); document.getElementById('data-tab-btn').classList.toggle('hidden', !isSoundProject); - // Schedules and Assigned Units are remote-only (manual projects collect data by hand) - document.getElementById('schedules-tab-btn')?.classList.toggle('hidden', isSoundProject && !isRemote); - document.getElementById('units-tab-btn')?.classList.toggle('hidden', isSoundProject && !isRemote); + // Schedules and Assigned Units: hidden for vibration; for sound, only show if remote + document.getElementById('schedules-tab-btn')?.classList.toggle('hidden', isVibrationProject || (isSoundProject && !isRemote)); + document.getElementById('units-tab-btn')?.classList.toggle('hidden', isVibrationProject || (isSoundProject && !isRemote)); // FTP browser within Data Files tab document.getElementById('ftp-browser')?.classList.toggle('hidden', !isRemote); diff --git a/templates/vibration_location_detail.html b/templates/vibration_location_detail.html index acb41dd..6afc9d8 100644 --- a/templates/vibration_location_detail.html +++ b/templates/vibration_location_detail.html @@ -37,7 +37,7 @@ {{ location.name }}
- Monitoring Location • {{ project.name }} + Monitoring Location • {{ project.name }}
No unit currently assigned
- @@ -214,47 +234,55 @@Attach a seismograph to this location
+Select a seismograph and optionally a modem for this location