feat(locations): delete assignment record for mis-clicks / duplicates
When an operator accidentally clicks Assign multiple times on the same
location (or assigns the wrong unit), the resulting bogus assignment
rows cluttered the location's deployment history with no way to clean
them up — Unassign just sets assigned_until to now, which preserves
the row.
New DELETE /api/projects/{p}/assignments/{a} endpoint hard-deletes the
row entirely, intended for mis-clicks that never represented a real
deployment.
Safety:
- Refuses if any MonitoringSession exists in the assignment's window
for the same (unit, location). If there's a recording session
backing it, this isn't a mis-click — operator should Edit or
Unassign instead.
- Records UnitHistory `assignment_deleted` so the unit's deployment
timeline still shows the deletion happened, even though the row
itself is gone.
UI: trash icon added next to the existing pencil (Edit) icon on each
row of the vibration location's "Deployment History" panel. Confirms
intent with a descriptive prompt that explains the consequence
(attribution becomes unattributed for that window) and points to
Edit/Unassign as alternatives.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -830,6 +830,92 @@ async def update_assignment(
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/assignments/{assignment_id}")
|
||||
async def delete_assignment(
|
||||
project_id: str,
|
||||
assignment_id: str,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Hard-delete an assignment record.
|
||||
|
||||
Use case: operator clicked Assign by mistake (or 8 times in a row) and
|
||||
wants the bogus records gone — not just closed with an `assigned_until`
|
||||
timestamp. The standard close-via-unassign path is for legitimate
|
||||
deployments that ended; this is for mis-clicks that never actually
|
||||
happened.
|
||||
|
||||
Safety:
|
||||
- Refuses if any MonitoringSession exists for the same (unit, location)
|
||||
within this assignment's window — that suggests the deployment was
|
||||
real, and the operator should use unassign instead.
|
||||
- Refuses if the assignment is the ONLY active assignment for a unit
|
||||
currently shown as deployed AND a recording session is in progress.
|
||||
|
||||
Audit:
|
||||
- Records UnitHistory `assignment_deleted` so the unit's deployment
|
||||
timeline shows the deletion happened (even though the row itself
|
||||
is gone).
|
||||
"""
|
||||
assignment = db.query(UnitAssignment).filter_by(
|
||||
id=assignment_id,
|
||||
project_id=project_id,
|
||||
).first()
|
||||
|
||||
if not assignment:
|
||||
raise HTTPException(status_code=404, detail="Assignment not found")
|
||||
|
||||
# Safety: is there a real recording history for this (unit, location)
|
||||
# within the assignment's time window? If so, this isn't a mis-click —
|
||||
# the operator should close it via unassign, not delete it.
|
||||
window_start = assignment.assigned_at
|
||||
window_end = assignment.assigned_until or datetime.utcnow()
|
||||
real_sessions = db.query(MonitoringSession).filter(
|
||||
and_(
|
||||
MonitoringSession.location_id == assignment.location_id,
|
||||
MonitoringSession.unit_id == assignment.unit_id,
|
||||
MonitoringSession.start_time >= window_start,
|
||||
MonitoringSession.start_time <= window_end,
|
||||
)
|
||||
).count()
|
||||
|
||||
if real_sessions > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
f"Cannot delete this assignment — {real_sessions} monitoring "
|
||||
f"session(s) were recorded under it. Use Unassign to close "
|
||||
f"the window instead, which preserves the audit trail."
|
||||
),
|
||||
)
|
||||
|
||||
# Resolve location name for audit log before deletion.
|
||||
location = db.query(MonitoringLocation).filter_by(
|
||||
id=assignment.location_id
|
||||
).first()
|
||||
location_label = location.name if location else assignment.location_id
|
||||
|
||||
_record_assignment_history(
|
||||
db,
|
||||
unit_id=assignment.unit_id,
|
||||
change_type="assignment_deleted",
|
||||
old_value=f"{location_label} ({assignment.assigned_at:%Y-%m-%d} → "
|
||||
f"{assignment.assigned_until and assignment.assigned_until.strftime('%Y-%m-%d') or 'active'})",
|
||||
new_value="deleted",
|
||||
notes=(
|
||||
"Assignment row removed — created in error or accidental duplicate."
|
||||
),
|
||||
)
|
||||
|
||||
db.delete(assignment)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Assignment deleted.",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/locations/{location_id}/swap")
|
||||
async def swap_unit_on_location(
|
||||
project_id: str,
|
||||
|
||||
Reference in New Issue
Block a user