Add clear asset GPS endpoint + UI
- DELETE /api/assets/{id}/gps: clears lat/lng to NULL
- GET /api/assets/{id}: asset lookup by ID
- GET /api/assets/search?machine_id=: asset lookup by machine ID
- Export & Admin page: Clear Asset GPS card with lookup + confirmation
- Tests: 204 success + 404 not found
This commit is contained in:
@@ -1583,6 +1583,51 @@ def export_checkins_csv(asset_id: Optional[int] = Query(None)):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Asset GPS Management ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/assets/{asset_id}")
|
||||||
|
def get_asset_admin(asset_id: int):
|
||||||
|
"""Get a single asset by ID (admin)."""
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute("SELECT * FROM assets WHERE id = ?", (asset_id,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
return row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/assets/search")
|
||||||
|
def search_assets_admin(machine_id: str = Query(...)):
|
||||||
|
"""Search for an asset by machine_id (admin)."""
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM assets WHERE machine_id = ?", (machine_id,)
|
||||||
|
).fetchone()
|
||||||
|
conn.close()
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
return row_to_dict(row)
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/assets/{asset_id}/gps", status_code=204)
|
||||||
|
def clear_asset_gps(asset_id: int):
|
||||||
|
"""Clear GPS coordinates from an asset (sets latitude/longitude to NULL)."""
|
||||||
|
conn = get_db()
|
||||||
|
existing = conn.execute("SELECT id, name FROM assets WHERE id = ?", (asset_id,)).fetchone()
|
||||||
|
if existing is None:
|
||||||
|
conn.close()
|
||||||
|
raise HTTPException(status_code=404, detail="Asset not found")
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE assets SET latitude = NULL, longitude = NULL, updated_at = datetime('now') WHERE id = ?",
|
||||||
|
(asset_id,),
|
||||||
|
)
|
||||||
|
_log_activity(conn, "updated", "asset", asset_id,
|
||||||
|
f"GPS data cleared for '{existing['name']}'")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ─── Icon Upload API ────────────────────────────────────────────────────────
|
# ─── Icon Upload API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -878,6 +878,17 @@
|
|||||||
<a href="/api/export/checkins" class="btn btn-outline" download>📄 Export Check-Ins CSV</a>
|
<a href="/api/export/checkins" class="btn btn-outline" download>📄 Export Check-Ins CSV</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title">Clear Asset GPS</div>
|
||||||
|
<p style="font-size:13px;color:var(--text2);margin-bottom:10px;">
|
||||||
|
Clear GPS coordinates from an asset. Enter the asset ID or machine ID.
|
||||||
|
</p>
|
||||||
|
<div style="display:flex;gap:8px;margin-bottom:10px;">
|
||||||
|
<input id="clearGpsAssetId" type="text" class="input-field" placeholder="Asset ID or Machine ID" style="flex:1;">
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="clearAssetGps()">📍 Clear GPS</button>
|
||||||
|
</div>
|
||||||
|
<div id="clearGpsResult" style="display:none;"></div>
|
||||||
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-title">Database</div>
|
<div class="card-title">Database</div>
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
<div style="display:flex;flex-wrap:wrap;gap:8px;">
|
||||||
@@ -2110,6 +2121,58 @@ async function confirmResetDb() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Clear Asset GPS ──────────────────────────────────────────────────────
|
||||||
|
async function clearAssetGps() {
|
||||||
|
const input = document.getElementById('clearGpsAssetId').value.trim();
|
||||||
|
if (!input) {
|
||||||
|
showToast('Enter an asset ID or machine ID', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const resultEl = document.getElementById('clearGpsResult');
|
||||||
|
resultEl.style.display = 'none';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try numeric asset ID first, otherwise search by machine_id
|
||||||
|
let asset;
|
||||||
|
const idNum = parseInt(input);
|
||||||
|
if (!isNaN(idNum) && idNum > 0) {
|
||||||
|
try {
|
||||||
|
asset = await api('/api/assets/' + idNum);
|
||||||
|
} catch (e) {
|
||||||
|
// Fall through to machine_id search
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!asset) {
|
||||||
|
asset = await api('/api/assets/search?machine_id=' + encodeURIComponent(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!asset || !asset.id) {
|
||||||
|
resultEl.style.display = '';
|
||||||
|
resultEl.innerHTML = '<span style="color:var(--red);">Asset not found</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await showModal(
|
||||||
|
'Clear GPS Data',
|
||||||
|
`<strong>${esc(asset.name)}</strong> (#${asset.id})<br>
|
||||||
|
Machine ID: ${esc(asset.machine_id)}<br>
|
||||||
|
Current: lat=${asset.latitude || 'none'}, lng=${asset.longitude || 'none'}<br><br>
|
||||||
|
Remove GPS coordinates from this asset?`,
|
||||||
|
'Clear GPS', 'btn-danger'
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await api('/api/assets/' + asset.id + '/gps', { method: 'DELETE' });
|
||||||
|
resultEl.style.display = '';
|
||||||
|
resultEl.innerHTML = '<span style="color:var(--green);">✅ GPS data cleared for ' + esc(asset.name) + ' (#' + asset.id + ')</span>';
|
||||||
|
document.getElementById('clearGpsAssetId').value = '';
|
||||||
|
showToast('GPS data cleared');
|
||||||
|
} catch (e) {
|
||||||
|
resultEl.style.display = '';
|
||||||
|
resultEl.innerHTML = '<span style="color:var(--red);">' + esc(e.message) + '</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
// CANTALOUPE SYNC
|
// CANTALOUPE SYNC
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -288,3 +288,32 @@ class TestSettings:
|
|||||||
cid = _assert_created(client.post("/api/settings/categories", json={"name": name}), "category")
|
cid = _assert_created(client.post("/api/settings/categories", json={"name": name}), "category")
|
||||||
resp = client.delete(f"/api/settings/categories/{cid}")
|
resp = client.delete(f"/api/settings/categories/{cid}")
|
||||||
assert resp.status_code == 204
|
assert resp.status_code == 204
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Asset GPS ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class TestClearAssetGps:
|
||||||
|
def test_clear_gps_returns_204(self, client):
|
||||||
|
"""Clear GPS returns 204 and sets lat/lng to null."""
|
||||||
|
# Create an asset with GPS coords via direct DB insert
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO assets (machine_id, name, latitude, longitude) VALUES (?, ?, ?, ?)",
|
||||||
|
("GPS-ADMIN-TEST", "Test GPS Asset", 28.5, -81.3),
|
||||||
|
)
|
||||||
|
asset_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
resp = client.delete(f"/api/assets/{asset_id}/gps")
|
||||||
|
assert resp.status_code == 204
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
data = _assert_ok(client.get(f"/api/assets/{asset_id}"), "asset")
|
||||||
|
assert data["latitude"] is None
|
||||||
|
assert data["longitude"] is None
|
||||||
|
|
||||||
|
def test_clear_gps_not_found_returns_404(self, client):
|
||||||
|
resp = client.delete("/api/assets/99999/gps")
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|||||||
Reference in New Issue
Block a user