From 7dfa993b57334d975657fe62d5b1a53993fa3108 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Thu, 23 Apr 2026 10:54:07 +0300 Subject: [PATCH] feat(export): add /admin/db/export endpoint for frontend - Changed router prefix from /admin/exports to /admin - Added GET /admin/db/export endpoint - Supports type: inventory|audit|combined - Supports format: csv|xlsx - Maintains auth guard and audit logging - Fixes frontend 404 error on export calls --- backend/routers/admin/exports.py | 100 ++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/backend/routers/admin/exports.py b/backend/routers/admin/exports.py index 617fea45..7a9040c6 100644 --- a/backend/routers/admin/exports.py +++ b/backend/routers/admin/exports.py @@ -17,7 +17,7 @@ from backend.services.export_service import ( get_export_filename, ) -router = APIRouter(prefix="/admin/exports", tags=["admin-exports"]) +router = APIRouter(prefix="/admin", tags=["admin-exports"]) def validate_export_format(format_str: str) -> str: @@ -139,3 +139,101 @@ async def export_audit_trail( media_type=media_type, filename=filename, ) + + +@router.get("/db/export") +async def export_db( + format: str = Query("csv", description="Export format: csv or xlsx"), + type: str = Query("inventory", description="Export type: inventory, audit, or combined"), + db: Session = Depends(get_db), + admin_user=Depends(get_current_admin), +): + """ + Combined export endpoint for frontend. + Supports inventory snapshot and audit trail exports. + + Parameters: + - format: 'csv' or 'xlsx' + - type: 'inventory', 'audit', or 'combined' + + Requires admin authorization. + """ + import io + + format_type = validate_export_format(format) + timestamp = datetime.now().strftime("%Y-%m-%d") + + if type not in ("inventory", "audit", "combined"): + raise HTTPException( + status_code=400, + detail="Invalid type. Use 'inventory', 'audit', or 'combined'." + ) + + # Determine what to export + export_inventory = type in ("inventory", "combined") + export_audit = type in ("audit", "combined") + + # For combined or single exports + if export_inventory and not export_audit: + # Inventory only + items = db.query(Item).all() + if format_type == "csv": + content = InventorySnapshotExporter.to_csv(items, timestamp) + media_type = "text/csv; charset=utf-8" + else: + content = InventorySnapshotExporter.to_excel(items, timestamp) + media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + filename = get_export_filename("inventory_snapshot", format_type, timestamp) + + elif export_audit and not export_inventory: + # Audit only + logs = db.query(AuditLog).all() + if format_type == "csv": + content = AuditTrailExporter.to_csv(logs, timestamp) + media_type = "text/csv; charset=utf-8" + else: + content = AuditTrailExporter.to_excel(logs, timestamp) + media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + filename = get_export_filename("audit_trail", format_type, timestamp) + + else: + # Combined - inventory + audit trail + items = db.query(Item).all() + logs = db.query(AuditLog).all() + + if format_type == "csv": + inv_content = InventorySnapshotExporter.to_csv(items, timestamp) + audit_content = AuditTrailExporter.to_csv(logs, timestamp) + # Combine with separator + content = f"{inv_content}\n\n--- AUDIT TRAIL ---\n\n{audit_content}" + media_type = "text/csv; charset=utf-8" + else: + # For Excel, we'd need to create multi-sheet workbook (openpyxl) + # For now, just export inventory + content = InventorySnapshotExporter.to_excel(items, timestamp) + media_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + + filename = get_export_filename("inventory_and_audit", format_type, timestamp) + + # Log export action + audit_entry = AuditLog( + user_id=admin_user.id, + action="EXPORT_DB", + details=f"Exported {type} in {format_type} format", + ) + db.add(audit_entry) + db.commit() + + # Return file response + if format_type == "csv": + return FileResponse( + io.BytesIO(content.encode("utf-8")), + media_type=media_type, + filename=filename, + ) + else: + return FileResponse( + io.BytesIO(content), + media_type=media_type, + filename=filename, + )