feat: Asset Cleanup tool with bulk edit + name generator

Adds a new Cleanup page in the admin Dashboard for reviewing
and sanitizing asset records.

Features:
- Filter by Company, Place/City, or Make with live counts
- Inline edit for make, model, building, floor on each asset
- Bulk edit bar: set any field for ALL matching assets at once
- Auto-generates names in '<make> <model> @ <company> - <Building> <Floor>' format
- Paginated results (50/page) with prev/next navigation
- Backend: POST /api/assets/batch-update for efficient batch updates
- Backend: company/place/make filter params on GET /api/assets
This commit is contained in:
Leo
2026-06-02 22:07:37 -04:00
parent 4ab08ca7bd
commit 25aec1490a
2 changed files with 467 additions and 0 deletions
+66
View File
@@ -1662,6 +1662,9 @@ def list_assets_admin(
status: Optional[str] = Query(None),
category: Optional[str] = Query(None),
customer_id: Optional[int] = Query(None),
company: Optional[str] = Query(None),
place: Optional[str] = Query(None),
make: Optional[str] = Query(None),
limit: int = Query(500, ge=1, le=5000),
offset: int = Query(0, ge=0),
):
@@ -1683,6 +1686,15 @@ def list_assets_admin(
if customer_id is not None:
conditions.append("customer_id = ?")
params.append(customer_id)
if company:
conditions.append("company = ?")
params.append(company)
if place:
conditions.append("place = ?")
params.append(place)
if make:
conditions.append("make = ?")
params.append(make)
where = " AND ".join(conditions)
sql = "SELECT * FROM assets"
@@ -1855,6 +1867,60 @@ def batch_update_status(body: BatchStatusUpdate):
return {"updated": len(body.ids), "status": body.status}
class BatchAssetUpdate(BaseModel):
ids: list[int]
fields: dict # { "column_name": value, ... }
@app.post("/api/assets/batch-update")
def batch_update_assets(body: BatchAssetUpdate):
"""Update the same fields on multiple assets at once. Used by Cleanup tool.
Body: { ids: [1,2,3], fields: { make: "Crane", building_name: "B7", floor: "FL 1" } }
Only updates non-null field values.
"""
if not body.ids:
raise HTTPException(status_code=422, detail="No asset IDs provided")
if not body.fields:
raise HTTPException(status_code=422, detail="No fields to update")
ALLOWED_FIELDS = {
"make", "model", "name", "company", "place", "building_name",
"building_number", "floor", "room", "location_area",
"address", "category", "status", "customer_name",
}
# Filter to allowed fields with non-null/empty values
updates = {k: v for k, v in body.fields.items()
if k in ALLOWED_FIELDS and v is not None and v != ""}
if not updates:
raise HTTPException(status_code=422, detail="No valid fields to update")
conn = get_db()
placeholders = ",".join("?" * len(body.ids))
# Verify all IDs exist
existing = conn.execute(
f"SELECT COUNT(*) FROM assets WHERE id IN ({placeholders})",
body.ids,
).fetchone()[0]
if existing != len(body.ids):
conn.close()
raise HTTPException(status_code=404, detail="One or more asset IDs not found")
set_clause = ", ".join(f"{col} = ?" for col in updates)
values = list(updates.values())
conn.execute(
f"UPDATE assets SET {set_clause}, updated_at = datetime('now') "
f"WHERE id IN ({placeholders})",
values + body.ids,
)
_log_activity(conn, "updated", "asset", 0,
f"Batch update: {len(body.ids)} assets — {', '.join(updates.keys())}")
conn.commit()
conn.close()
return {"updated": len(body.ids), "fields": list(updates.keys())}
# ─── Icon Upload API ────────────────────────────────────────────────────────