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:
@@ -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 ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user